├── BookReaderPlugin.php ├── LICENSE ├── README.md ├── build.xml ├── controllers ├── IndexController.php └── ViewerController.php ├── languages ├── fr.mo ├── fr.po ├── template.base.pot └── template.pot ├── libraries └── BookReader │ ├── Creator.php │ └── Creator │ ├── Default.php │ ├── ExtractOCR.php │ ├── RefnumOCR.php │ └── Simple.php ├── models └── BookReader.php ├── plugin.ini ├── routes.ini └── views ├── admin ├── forms │ └── bookreader-batch-edit.php └── plugins │ └── bookreader-config-form.php ├── helpers └── GetBookReader.php ├── public ├── index │ ├── fulltext.php │ └── image-proxy.php └── viewer │ ├── show.php │ └── view.php └── shared ├── css ├── BookReader.css ├── BookReaderEmbed.css └── BookReaderLending.css ├── images ├── BRicons.png ├── back_cover.png ├── back_pages.png ├── blank.png ├── book_bottom_icon.png ├── book_down_icon.png ├── book_left_icon.png ├── book_leftmost_icon.png ├── book_right_icon.png ├── book_rightmost_icon.png ├── book_top_icon.png ├── book_up_icon.png ├── booksplit.png ├── control_pause_icon.png ├── control_play_icon.png ├── down_arrow.png ├── down_arrow_wt.png ├── embed_icon.png ├── icon_OL-logo-xs.png ├── icon_alert-xs.png ├── icon_close-pop.png ├── icon_home.png ├── icon_indicator.png ├── icon_zoomer.png ├── left_edges.png ├── logo_icon.png ├── marker_chap-off.png ├── marker_chap-on.png ├── marker_srch-off.png ├── marker_srch-on.png ├── marker_srchchap-off.png ├── marker_srchchap-on.png ├── nav_control-dn.png ├── nav_control-lt.png ├── nav_control-rt.png ├── nav_control-up.png ├── nav_control.png ├── one_page_mode_icon.png ├── print_icon.png ├── progressbar.gif ├── right_arrow.png ├── right_arrow_wt.png ├── right_edges.png ├── slider.png ├── thumbnail_mode_icon.png ├── transparent.png ├── two_page_mode_icon.png ├── zoom_in_icon.png └── zoom_out_icon.png └── javascripts ├── BookReader.js ├── ToCmenu.js ├── dragscrollable.js ├── excanvas.compiled.js ├── jquery-1.4.2.min.js ├── jquery-ui-1.8.1.custom.min.js ├── jquery-ui-1.8.5.custom.min.js ├── jquery.bt.min.js ├── jquery.colorbox-min.js ├── jquery.ui.ipad.js └── touch ├── BookReaderTouch.css └── images ├── book_bottom_icon.png ├── book_down_icon.png ├── book_left_icon.png ├── book_leftmost_icon.png ├── book_right_icon.png ├── book_rightmost_icon.png ├── book_top_icon.png ├── book_up_icon.png ├── control_pause_icon.png ├── control_play_icon.png ├── embed_icon.png ├── logo_icon.png ├── one_page_mode_icon.png ├── print_icon.png ├── thumbnail_mode_icon.png ├── two_page_mode_icon.png ├── zoom_in_icon.png └── zoom_out_icon.png /BookReaderPlugin.php: -------------------------------------------------------------------------------- 1 | 'BookReader_Creator_Default', 50 | 'bookreader_sorting_mode' => false, 51 | 'bookreader_mode_page' => '1', 52 | 'bookreader_append_items_show' => true, 53 | 'bookreader_embed_functions' => '0', 54 | 'bookreader_class' => '', 55 | 'bookreader_width' => '100%', 56 | 'bookreader_height' => '480', 57 | ); 58 | 59 | /** 60 | * Installs the plugin. 61 | */ 62 | public function hookInstall() 63 | { 64 | $this->_installOptions(); 65 | } 66 | 67 | /** 68 | * Upgrade the plugin. 69 | */ 70 | public function hookUpgrade($args) 71 | { 72 | $oldVersion = $args['old_version']; 73 | $newVersion = $args['new_version']; 74 | 75 | if (version_compare($oldVersion, '2.1', '<=')) { 76 | set_option('bookreader_custom_css', WEB_PLUGIN . '/BookReader/' . $this->_options['bookreader_custom_css']); 77 | delete_option('bookreader_logo_url'); 78 | delete_option('bookreader_toolbar_color'); 79 | } 80 | 81 | if (version_compare($oldVersion, '2.6', '<=')) { 82 | delete_option('bookreader_custom_library'); 83 | set_option('bookreader_creator', $this->_options['bookreader_creator']); 84 | set_option('bookreader_append_items_show', $this->_options['bookreader_append_items_show']); 85 | } 86 | 87 | if (version_compare($oldVersion, '2.7', '<')) { 88 | delete_option('bookreader_custom_css'); 89 | delete_option('bookreader_favicon_url'); 90 | } 91 | } 92 | 93 | /** 94 | * Uninstalls the plugin. 95 | */ 96 | public function hookUninstall() 97 | { 98 | $this->_uninstallOptions(); 99 | } 100 | 101 | /** 102 | * Initialize the plugin. 103 | */ 104 | public function hookInitialize() 105 | { 106 | add_translation_source(dirname(__FILE__) . '/languages'); 107 | add_shortcode('bookreader', array($this, 'shortcodeBookReader')); 108 | } 109 | 110 | /** 111 | * Shows plugin configuration page. 112 | * 113 | * @return void 114 | */ 115 | public function hookConfigForm($args) 116 | { 117 | $view = get_view(); 118 | echo $view->partial('plugins/bookreader-config-form.php'); 119 | } 120 | 121 | /** 122 | * Processes the configuration form. 123 | * 124 | * @return void 125 | */ 126 | public function hookConfig($args) 127 | { 128 | $post = $args['post']; 129 | foreach ($this->_options as $optionKey => $optionValue) { 130 | if (isset($post[$optionKey])) { 131 | set_option($optionKey, $post[$optionKey]); 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * Defines public routes. 138 | * 139 | * @return void 140 | */ 141 | public function hookDefineRoutes($args) 142 | { 143 | if (is_admin_theme()) { 144 | return; 145 | } 146 | 147 | $args['router']->addConfig(new Zend_Config_Ini(dirname(__FILE__) . '/routes.ini', 'routes')); 148 | } 149 | 150 | /** 151 | * Manages data when an item is saved. 152 | */ 153 | public function hookAfterSaveItem($args) 154 | { 155 | $item = $args['record']; 156 | 157 | // This is done after insert, update or post and only if a function exists 158 | // in the custom library. 159 | $bookreader = new BookReader($item); 160 | $bookreader->saveData(); 161 | } 162 | 163 | /** 164 | * Add a partial batch edit form. 165 | * 166 | * @return void 167 | */ 168 | public function hookAdminItemsBatchEditForm($args) 169 | { 170 | $view = $args['view']; 171 | echo $view->partial( 172 | 'forms/bookreader-batch-edit.php' 173 | ); 174 | } 175 | 176 | /** 177 | * Process the partial batch edit form. 178 | * 179 | * @return void 180 | */ 181 | public function hookItemsBatchEditCustom($args) 182 | { 183 | $item = $args['item']; 184 | $orderByFilename = $args['custom']['bookreader']['orderByFilename']; 185 | $mixFileTypes = $args['custom']['bookreader']['mixFileTypes']; 186 | $checkImageSize = $args['custom']['bookreader']['checkImageSize']; 187 | 188 | if ($orderByFilename) { 189 | $this->_sortFiles($item, (boolean) $mixFileTypes); 190 | } 191 | 192 | if ($checkImageSize) { 193 | $this->_checkImageSize($item); 194 | } 195 | } 196 | 197 | /** 198 | * Sort all files of an item by name. 199 | * 200 | * @param Item $item 201 | * @param boolean $mixFileTypes 202 | * 203 | * @return void 204 | */ 205 | protected function _sortFiles($item, $mixFileTypes = false) 206 | { 207 | if ($item->fileCount() < 2) { 208 | return; 209 | } 210 | 211 | if ($mixFileTypes) { 212 | $list = $item->Files; 213 | BookReader_Creator::sortFilesByOriginalName($list, false); 214 | } 215 | else { 216 | $bookreader = new BookReader($item); 217 | // Get leaves and remove blank ones. 218 | $leaves = array_filter($bookreader->getLeaves()); 219 | $non_leaves = array_filter($bookreader->getNonLeaves()); 220 | // Manage the case where there is no BookReader data. 221 | if (empty($leaves) && empty($non_leaves)) { 222 | $list = $item->Files; 223 | BookReader_Creator::sortFilesByOriginalName($list, false); 224 | } 225 | else { 226 | // Order them separately. 227 | BookReader_Creator::sortFilesByOriginalName($leaves, false); 228 | BookReader_Creator::sortFilesByOriginalName($non_leaves, false); 229 | // Finally, merge them. 230 | $list = array_merge($leaves, $non_leaves); 231 | } 232 | } 233 | 234 | // To avoid issues with unique index when updating (order should be 235 | // unique for each file of an item), all orders are reset to null before 236 | // true process. 237 | $db = $this->_db; 238 | $bind = array( 239 | $item->id, 240 | ); 241 | $sql = " 242 | UPDATE `$db->File` files 243 | SET files.order = NULL 244 | WHERE files.item_id = ? 245 | "; 246 | $db->query($sql, $bind); 247 | 248 | // To avoid multiple updates, a single query is used. 249 | foreach ($list as &$file) { 250 | $file = $file->id; 251 | } 252 | // The array is made unique, because a leaf can be repeated. 253 | $list = implode(',', array_unique($list)); 254 | $sql = " 255 | UPDATE `$db->File` files 256 | SET files.order = FIND_IN_SET(files.id, '$list') 257 | WHERE files.id in ($list) 258 | "; 259 | $db->query($sql); 260 | } 261 | 262 | /** 263 | * Rebuild missing metadata of files. 264 | * 265 | * @param Item $item 266 | * @return void 267 | */ 268 | protected function _checkImageSize($item) 269 | { 270 | foreach ($item->Files as $file) { 271 | if (!$file->hasThumbnail() || strpos($file->mime_type, 'image/') !== 0) { 272 | continue; 273 | } 274 | $metadata = json_decode($file->metadata, true); 275 | if (empty($metadata)) { 276 | $metadata = array(); 277 | } 278 | // Check if resolution is set. 279 | elseif (!empty($metadata['video']['resolution_x']) && !empty($metadata['video']['resolution_y'])) { 280 | continue; 281 | } 282 | 283 | // Set the resolution directly. 284 | $imageType = 'original'; 285 | // The storage adapter should be checked for external storage. 286 | $storageAdapter = $file->getStorage()->getAdapter(); 287 | $filepath = get_class($storageAdapter) == 'Omeka_Storage_Adapter_Filesystem' 288 | ? FILES_DIR . DIRECTORY_SEPARATOR . $file->getStoragePath($imageType) 289 | : $file->getWebPath($imageType); 290 | list($width, $height, $type, $attr) = getimagesize($filepath); 291 | $metadata['video']['resolution_x'] = $width; 292 | $metadata['video']['resolution_y'] = $height; 293 | $file->metadata = version_compare(phpversion(), '5.4.0', '<') 294 | ? json_encode($metadata) 295 | : json_encode($metadata, JSON_UNESCAPED_SLASHES); 296 | $file->save(); 297 | } 298 | } 299 | 300 | /** 301 | * Hook to display viewer. 302 | * 303 | * @param array $args 304 | * 305 | * @return void 306 | */ 307 | public function hookPublicItemsShow($args) 308 | { 309 | if (!get_option('bookreader_append_items_show') && empty($args['direct'])) { 310 | return; 311 | } 312 | 313 | $view = empty($args['view']) ? get_view() : $args['view']; 314 | echo $view->getBookReader($args); 315 | } 316 | 317 | /** 318 | * Shortcode to display viewer. 319 | * 320 | * @param array $args 321 | * @param Omeka_View $view 322 | * @return string 323 | */ 324 | public static function shortcodeBookReader($args, $view) 325 | { 326 | $args['view'] = $view; 327 | return $view->getBookReader($args); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BookReader (plugin for Omeka) 2 | ============================= 3 | 4 | [BookReader] is a plugin for [Omeka] that adds [Internet Archive BookReader]. 5 | The IA BookReader is used to view books from the Internet Archive online and can 6 | also be used to view other books. 7 | 8 | So you can create online flip book from image files constituting an item or from 9 | files listed in a gDoc spreadsheet. 10 | 11 | See demo of the [embedded version] of [Mines ParisTech] or in [fullscreen mode] 12 | with highlighted results of a search in the OCR text ([Université Rennes 2]). 13 | 14 | For the spreasheet use, see [gDoc explanation]. The javascript included comes 15 | from [BookReader spreadsheet] via [Dušan Ranđelović fork]. Main benefit is that item 16 | doesn't need to have all pages/images uploaded to Omeka and that there is no 17 | need for composit PDF, so presentation is completely parted from metadata. 18 | 19 | 20 | Installation 21 | ------------ 22 | 23 | - Upload the BookReader plugin folder into your plugins folder on the server; 24 | - Activate it from the admin dashboad > Settings > Plugins page 25 | - Click the Configure link to add the following 26 | - Name of to custom class (default is `BookReader_Creator_Default`) 27 | - Sorting mode for the viewer (omeka default order or original filename order) 28 | - Number of pages in Embed mode (1 or 2) 29 | - Embed all functions (0 for none or 1 for all) 30 | - The width of the inline frame (Embedded Simple Viewer) 31 | - The height of the inline frame (Embedded Simple Viewer) 32 | 33 | If you want to change more options, copy the file `views/public/viewer/show.php` 34 | in the subdirectory `book-reader/viewer/`of your theme and update it. 35 | 36 | See below the notes for more info. 37 | 38 | 39 | Usage 40 | ----- 41 | 42 | The viewer is always available at `http://www.example.com/items/viewer/{item id}`. 43 | Furthermore, it is automatically embedded in items/show page. This can be 44 | disabled in the config of the plugin. 45 | 46 | The url for the default page `items/viewer/:id` can be replaced by the old one 47 | `viewer/show/:id` or any other url via the file `routes.ini`. 48 | 49 | To embed the BookReader with more control, three mechanisms are provided. So, 50 | according to your needs, you may add this code in the `items/show.php` file of 51 | your theme or anywhere else, as long as the item is defined (as variable or as 52 | current record 'item'). 53 | 54 | * Helper (recommended) 55 | - With no option: 56 | 57 | ``` 58 | echo $this->getBookReader(); 59 | ``` 60 | 61 | * Shortcode 62 | - In a field that can be shortcoded: `[bookreader]`. 63 | - Default in theme: `shortcodes('[bookreader]'); ?>` 64 | - With all options: 65 | 66 | ``` 67 | shortcodes('[bookreader item=1 page=0 embed_functions=0 mode_page=1]'); 69 | ?> 70 | ``` 71 | 72 | * Hook 73 | - With all options: 74 | 75 | ``` 76 | true, 79 | 'view' => $this, 80 | 'item' => $item, 81 | 'page' => '0', 82 | 'embed_functions' => false, 83 | 'mode_page' => 1, 84 | )); 85 | ?> 86 | ``` 87 | 88 | All options are optional. If one is not defined, the parameters set in the 89 | config page will be used. 90 | The image number starts from '0' with default functions. 91 | 92 | 93 | Notes 94 | ----- 95 | 96 | - A batch edit is provided to sort images before other files (pdf, xml...) that 97 | are associated to an item (Items > check box items > edit button). 98 | 99 | *Warning* 100 | 101 | PHP should be installed with the extension "exif" in order to get the size of 102 | images. This is the case for all major distributions and providers. 103 | 104 | If technical metadata are missing for some images, in particular when the 105 | extension "exif" is not installed or when images are not fully compliant with 106 | the standards, they should be rebuilt. A notice is added in the error log. 107 | A form in the batch edit can be used to process them automatically: check the 108 | items in the "admin/items/browse" view, then click the button "Edit", then the 109 | checkbox "Rebuild metadata when missing". The viewer will work without these 110 | metadata, but the display will be slower. 111 | 112 | 113 | Customing 114 | --------- 115 | 116 | There are several ways to store data about items in Omeka, so the BookReader can 117 | be customized via a class that extends `BookReader_Creator`. 118 | 119 | BookReader uses several arrays to get images and infos about them. Take a 120 | document of twelve pages as an example. In Javascript, we have these arrays: 121 | - br.leafMap : mapping of pages, as [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] 122 | - br.pageNums : number of pages, as [,, "i", "ii", "iii", "iv", 1, 2, 3, 4, 5,] 123 | - br.pageLabels : label of pages, as ["Cover", "Blank",,,,, "Page 1 (unnumbered)",,,,, "Back cover"] 124 | - br.pageWidths : width of each image, as [500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500] 125 | - br.pageHeights : height of each image, as [800, 800, 800, 800, 800, 800, 800, 800, 800, 800, 800, 800] 126 | 127 | With the default files of BookReader, all images of an item are displayed, so 128 | the leafMap indexes are always a simple list of numbers like above (starting 129 | from 0 when the first page is a right page, else from 1). Page numbers and/or 130 | page labels can be empty, so in that case the index is used. When the user 131 | leafs through the document, the viewer sends a request with index + 1 as image 132 | parameter. So the controller can send the index - 1 to select image in the 133 | ordered list of images files. 134 | 135 | Some functions of php are used to make relation with this index and to provide 136 | images. They are used in the beginning and via Ajax. During creation of the 137 | viewer, php should provide mapping, numbers and labels via BookReader custom 138 | functions (`getPageIndexes()`, `getPageNumbers()` and `getPageLabels()`). These 139 | functions use one main method, `getLeaves()`, that provides the ordered 140 | list of all images that should be displayed as leaves (saved by default as a 141 | static variable in php). This list is used too to get the selected image when 142 | needed via the index. The `getNonLeaves()` method is used to get links to other 143 | files to share. So,The list of leaves files is a simple array as [File 1, File 2, File 3, File 4...]. 144 | 145 | In case of a non-digitalized blank or forgotten page, in order to keep the 146 | left/right succession of leafs, the mapping and the page numbers and labels 147 | should be the same, and the list of leaves files should be [File 1, null, File 3, File 4...]. 148 | The `transparent.png` image will be displayed if an image is missing, with the 149 | width and the height of the first page. 150 | 151 | In case of multiple files for the same page, for example with a pop-up or with 152 | and without a tracing paper, the mapping can be: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 12, 11]. 153 | Other arrays should be set in accordance: number of pages as [,, "i", "ii", "iii", "iv", 1, 2, 3, 4, 5, 4, 5,], 154 | labels as ["Cover", "Blank",,,,, "Page 1 (unnumbered)",,,, "Page 5 with tracing paper", "Back cover"] 155 | and files as [File 1, null, File 3, File 4..., File 10, File 11a, File 10, File 11b, File 12]. 156 | Any other arrangements can be used. 157 | 158 | To avoid to calculate these data each time an item is displayed, it's 159 | recommanded to save them either in a xml file, or in the database. It's 160 | specially important for widths and heights data, as all of them should be got 161 | before first display. 162 | A batch can be launched in the admin/items/show page if needed. The function 163 | `saveData()` should be customized to your needs. Of course, other functions 164 | should check if these data are available and use them. This function can be used 165 | too the first time a viewer is displayed for an item. 166 | 167 | 168 | Spreadsheet use 169 | --------------- 170 | 171 | To test it, create an item with this value in Dublin Core : Relation: 172 | `https://spreadsheets.google.com/feeds/list/0Ag7PrlWT3aWadDdVODJLVUs0a1AtUVlUWlhnXzdwcGc/od6/public/values`. 173 | This is the public Atom feed of a Google Spreadsheet with five columns. These 174 | columns are `image`, `height`, `width`,`num` and `label`. Only the first one, 175 | that is the url to the image, is required. See [gDoc explanation] for more info. 176 | Then go to `http://example.org/items/view/{item id}`. 177 | 178 | Remarks: 179 | 180 | - Because data are not in the local database, the build of the Book Reader can 181 | take some seconds when the page is loaded. 182 | - Thumbs are not set because files are not in database but on custom server 183 | place listed in gDoc. 184 | - In the default example, the image 182 is set in the spreadsheet, but is empty. 185 | 186 | 187 | Optional plugins 188 | ---------------- 189 | 190 | The extract ocr and pdfToc plugins are highly recommended. 191 | 192 | - [Extract ocr] allows fulltext searching inside a flip book. To enable it in 193 | BookReader, you need to overwrite Bookreader/libraries/BookReader/Creator/Default.php 194 | using Bookreader/libraries/BookReader/Creator/ExtractOCR.php or to set the path 195 | in configuration panel of the extension. 196 | - [PDF Toc] retrieves table of contents from pdf file associated to an item. 197 | 198 | 199 | Troubleshooting 200 | --------------- 201 | 202 | See online issues on the [plugin issues] page on GitHub. 203 | 204 | 205 | License 206 | ------- 207 | 208 | This plugin is published under [GNU/GPL]. 209 | 210 | This program is free software; you can redistribute it and/or modify it under 211 | the terms of the GNU General Public License as published by the Free Software 212 | Foundation; either version 3 of the License, or (at your option) any later 213 | version. 214 | 215 | This program is distributed in the hope that it will be useful, but WITHOUT 216 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 217 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 218 | details. 219 | 220 | You should have received a copy of the GNU General Public License along with 221 | this program; if not, write to the Free Software Foundation, Inc., 222 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 223 | 224 | 225 | Contact 226 | ------- 227 | 228 | See developer documentation on [Internet Archive BookReader] and [source of IA BookReader] 229 | on GitHub. 230 | 231 | Current maintainers: 232 | * [Julien Sicot] 233 | * [Daniel Berthereau] 234 | 235 | First version has been built by Julien Sicot for [Université Rennes 2]. 236 | The upgrade for Omeka 2.0 has been built for [Mines ParisTech]. 237 | 238 | 239 | Copyright 240 | --------- 241 | 242 | The source code of [Internet Archive BookReader] is licensed under AGPL v3, as 243 | described in the LICENSE file. 244 | 245 | * Copyright Internet Archive, 2008-2009 246 | 247 | BookReader Omeka plugin: 248 | 249 | * Copyright Julien Sicot, 2011-2013 250 | * Copyright Daniel Berthereau, 2013-2015 (upgrade for Omeka 2.0) 251 | 252 | BookReader Spreadsheet: 253 | 254 | * Copyright Doug Reside, 2013 255 | * Copyright Dušan Ranđelović, 2013 256 | 257 | 258 | [BookReader]: https://github.com/Daniel-KM/BookReader 259 | [Omeka]: https://omeka.org 260 | [Internet Archive BookReader]: http://openlibrary.org/dev/docs/bookreader 261 | [BookReader spreadsheet]: https://github.com/dougreside/bookreaderspreadsheet 262 | [Dušan Ranđelović fork]: https://github.com/duxan/BookReader-PDF 263 | [gDoc explanation]: http://www.nypl.org/blog/2013/06/25/binding-your-own-ebooks-pt-1-bookreader 264 | [source of IA BookReader]: https://github.com/openlibrary/bookreader 265 | [embedded version]: https://patrimoine.mines-paristech.fr/document/Brochant_MS_39 266 | [fullscreen mode]: http://bibnum.univ-rennes2.fr/viewer/show/566#page/5/mode/1up 267 | [Extract ocr]: https://github.com/symac/Plugin-Extractocr 268 | [PDF Toc]: https://github.com/symac/Plugin-PdfToc 269 | [plugin issues]: https://github.com/jsicot/BookReader/issues 270 | [GNU/GPL]: https://www.gnu.org/licenses/gpl-3.0.html 271 | [Daniel Berthereau]: https://github.com/Daniel-KM 272 | [Julien Sicot]: https://github.com/jsicot 273 | [Université Rennes 2]: http://bibnum.univ-rennes2.fr 274 | [Mines ParisTech]: http://bib.mines-paristech.fr 275 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /controllers/IndexController.php: -------------------------------------------------------------------------------- 1 | _helper->viewRenderer->setNoRender(); 16 | } 17 | 18 | /** 19 | * Returns the answer to a query in order to highlight it via javascript. 20 | * 21 | * A result can contain multiple words, multiple matches, multiple pars and 22 | * multiple boxes, for example when the answer is on two lines or pages. 23 | * 24 | * The resulted javascript/json is echoed and sent via Ajax. 25 | * 26 | * The structure of the json object is: 27 | * ->ia = item id 28 | * ->q = query 29 | * ->page_count = page count (useless) 30 | * ->body_length = body lenght (useless) 31 | * ->leaf0_missing = generally empty 32 | * ->matches = results as array of objects 33 | * ->text = few words to contextualize the result, used in nav bar 34 | * ->par = array of parallel images (currently, only the [0] is used) 35 | * ->t = top limit of global zone 36 | * ->l = left limit of global zone 37 | * ->b = bottom limit of global zone 38 | * ->r = right limit of global zone 39 | * ->page = page number 40 | * ->index = page index 41 | * ->boxes = array of coordinates of boxes to highlight 42 | * ->t = top limit of word zone 43 | * ->l = left limit of word zone 44 | * ->b = bottom limit of word zone 45 | * ->r = right limit of word zone 46 | * ->page = page number 47 | * ->index = page index 48 | * Note that only one of "page" or "index" is needed. Index is prefered, 49 | * because it's generally simpler to manage and more efficient. 50 | */ 51 | public function fulltextAction() 52 | { 53 | $request = $this->getRequest(); 54 | $item_id = $request->getParam('item_id'); 55 | $item = get_record_by_id('Item', $item_id); 56 | if (empty($item)) { 57 | throw new Omeka_Controller_Exception_404; 58 | } 59 | 60 | $part = $request->getParam('part'); 61 | $query = $request->getParam('q'); 62 | $query = utf8_encode($query); 63 | $callback = $request->getParam('callback'); 64 | 65 | $output = array(); 66 | 67 | // Check if there are data for search. 68 | $bookreader = new BookReader($item); 69 | if ($bookreader->hasDataForSearch()) { 70 | $output['id'] = $item_id; 71 | $output['part'] = $part; 72 | $output['q'] = $query; 73 | // TODO Check if these keys are really needed. 74 | // $output['page_count'] = 200; 75 | // $output['body_length'] = 140000; 76 | // TODO Kezako ? 77 | // $output['leaf0_missing'] = false; 78 | 79 | $answer = $bookreader->searchFulltext($query); 80 | $output['matches'] = $bookreader->highlightFiles($answer); 81 | } 82 | 83 | // Send answer. 84 | $this->getResponse()->clearBody(); 85 | $this->getResponse()->setHeader('Content-Type', 'text/html'); 86 | // header('Content-Type: text/javascript; charset=utf8'); 87 | // header('Access-Control-Allow-Methods: GET, POST'); 88 | $tab_json = json_encode($output); 89 | echo $callback . '(' . $tab_json . ')'; 90 | } 91 | 92 | /** 93 | * Returns sized image for the current image. 94 | */ 95 | public function imageProxyAction() 96 | { 97 | $this->_sendImage(); 98 | } 99 | 100 | /** 101 | * Returns thumbnail image of the current image. 102 | */ 103 | public function thumbProxyAction() 104 | { 105 | $this->_sendImage('thumbnail'); 106 | } 107 | 108 | /** 109 | * Helper to return image of the current image. 110 | * 111 | * @param string $type Derivative type of the image. 112 | * @return void 113 | */ 114 | protected function _sendImage($type = null) 115 | { 116 | $request = $this->getRequest(); 117 | $itemId = $request->getParam('id'); 118 | $item = get_record_by_id('item', $itemId); 119 | if (empty($item)) { 120 | throw new Omeka_Controller_Exception_404; 121 | } 122 | 123 | $index = $request->getParam('image'); 124 | // Get the index. 125 | if ($index != '000') { 126 | $index = preg_replace('`^[0]*`', '', $index); 127 | $index--; 128 | } 129 | else { 130 | $index = 0; 131 | } 132 | 133 | $bookreader = new BookReader($item); 134 | 135 | $part = $request->getParam('part'); 136 | $bookreader->setPart($part); 137 | 138 | if (is_null($type)) { 139 | $scale = $request->getParam('scale'); 140 | $type = $bookreader->getSizeType($scale); 141 | // Set a default, even it's normally useless. 142 | $type = $type ?: 'fullsize'; 143 | } 144 | 145 | $imagesFiles = $bookreader->getLeaves(); 146 | $file = $imagesFiles[$index]; 147 | // No file, so a blank image 148 | if (empty($file)) { 149 | $filepath = 'images/blank.png'; 150 | $image = file_get_contents(physical_path_to($filepath)); 151 | $this->getResponse()->clearBody(); 152 | $this->getResponse()->setHeader('Content-Type', 'image/jpeg'); 153 | $this->getResponse()->setBody($image); 154 | } 155 | // Else, redirect (302/307) to the url of the file. 156 | else { 157 | $this->_helper->redirector->gotoUrlAndExit($file->getWebPath($type)); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /controllers/ViewerController.php: -------------------------------------------------------------------------------- 1 | _forward('show'); 19 | } 20 | 21 | public function showAction() 22 | { 23 | $id = $this->getParam('id'); 24 | $item = get_record_by_id('Item', $id); 25 | if (empty($item)) { 26 | throw new Omeka_Controller_Exception_404; 27 | } 28 | 29 | $this->_prepareViewer($item); 30 | } 31 | 32 | public function viewAction() 33 | { 34 | $id = $this->getParam('id'); 35 | $item = get_record_by_id('Item', $id); 36 | if (empty($item)) { 37 | throw new Omeka_Controller_Exception_404; 38 | } 39 | 40 | $relations = metadata($item, array('Dublin Core', 'Relation'), 41 | array('all' => true, 'no_escape' => true, 'no_filter' => true)); 42 | 43 | // Currently, only support gDoc urls. 44 | $tableUrl = ''; 45 | $baseUrl = 'https://spreadsheets.google.com/feeds/list/'; 46 | $endUrl = '/public/values'; 47 | foreach ($relations as $relation) { 48 | if (strpos($relation, $baseUrl) === 0 49 | && substr_compare($relation, $endUrl, -strlen($endUrl), strlen($endUrl)) === 0 50 | ) { 51 | $tableUrl = $relation; 52 | break; 53 | } 54 | } 55 | if (empty($tableUrl)) { 56 | $this->_helper->flashMessenger(__('This item has no table of images.'), 'error'); 57 | return $this->forward('show', 'items', 'default', array( 58 | 'module' => null, 59 | 'controller' => 'items', 60 | 'action' => 'show', 61 | 'id' => $item->id, 62 | )); 63 | } 64 | 65 | $this->_prepareViewer($item); 66 | $this->view->tableUrl = $tableUrl . '?alt=json-in-script&callback=spreadsheetLoaded'; 67 | } 68 | 69 | /** 70 | * Helper to prepare the viewer (only the javascript differs in view). 71 | */ 72 | protected function _prepareViewer($item) 73 | { 74 | $ui = $this->getParam('ui'); 75 | $part = $this->getParam('part'); 76 | 77 | $bookreader = new BookReader($item); 78 | $bookreader->setUI($ui); 79 | $bookreader->setPart($part); 80 | 81 | $this->view->bookreader = $bookreader; 82 | $this->view->item = $item; 83 | // Values have been checked inside BookReader. 84 | $this->view->ui = $bookreader->getUI(); 85 | $this->view->part = $bookreader->getPart(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /languages/fr.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsicot/BookReader/4e98fc1e7a2d86fbff63fb4bbcc8ba4e0da931a5/languages/fr.mo -------------------------------------------------------------------------------- /languages/template.base.pot: -------------------------------------------------------------------------------- 1 | # Translation for the BookReader plugin for Omeka. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the Omeka package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: BookReader\n" 10 | "Report-Msgid-Bugs-To: https://github.com/Daniel-KM/BookReader/issues\n" 11 | "POT-Creation-Date: 2013-07-04 00:00+0000\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | -------------------------------------------------------------------------------- /libraries/BookReader/Creator.php: -------------------------------------------------------------------------------- 1 | setItem($item); 23 | } 24 | 25 | /** 26 | * Set item. 27 | * 28 | * @param integer|Item $item 29 | */ 30 | public function setItem($item) 31 | { 32 | if (empty($item)) { 33 | $this->_item = null; 34 | } 35 | elseif ($item instanceof Item) { 36 | $this->_item = $item; 37 | } 38 | else { 39 | $this->_item = get_record_by_id('Item', (integer) $item); 40 | } 41 | } 42 | 43 | public function setUI($ui) 44 | { 45 | $this->_ui = ($ui == 'embed') ? 'embed' : ''; 46 | } 47 | 48 | public function setPart($part) 49 | { 50 | $this->_part = (integer) $part; 51 | } 52 | 53 | public function getItem() 54 | { 55 | return $this->_item; 56 | } 57 | 58 | public function getUI() 59 | { 60 | return $this->_ui; 61 | } 62 | 63 | public function getPart() 64 | { 65 | return $this->_part; 66 | } 67 | 68 | /** 69 | * Get an array of all images of an item in order to display them with 70 | * BookReader. 71 | * 72 | * @return array 73 | * Array of filenames associated to original filenames. 74 | */ 75 | public function getLeaves() 76 | { 77 | return $this->_get_list_of_leaves(false); 78 | } 79 | 80 | /** 81 | * Get an array of all non-images of an item in order to display them as 82 | * links. 83 | * 84 | * @return array 85 | * Array of filenames associated to original filenames. 86 | */ 87 | public function getNonLeaves() 88 | { 89 | return $this->_get_list_of_leaves(true); 90 | } 91 | 92 | /** 93 | * Get an array of all leaves (or all non-leaves) of an item in order to 94 | * display them with BookReader. 95 | * 96 | * @param boolean $invert 97 | * 98 | * @return array 99 | * Array of files or nulls. 100 | */ 101 | protected function _get_list_of_leaves($invert = false) 102 | { 103 | if (empty($this->_item)) { 104 | return; 105 | } 106 | 107 | if (is_null($this->_leaves)) { 108 | $this->_leaves = array(); 109 | $this->_nonleaves = array(); 110 | 111 | $supportedFormats = array( 112 | 'jpeg' => 'JPEG Joint Photographic Experts Group JFIF format', 113 | 'jpg' => 'Joint Photographic Experts Group JFIF format', 114 | 'png' => 'Portable Network Graphics', 115 | 'gif' => 'Graphics Interchange Format', 116 | 'tif' => 'Tagged Image File Format', 117 | 'tiff' => 'Tagged Image File Format', 118 | ); 119 | // Set the regular expression to match selected/supported formats. 120 | $supportedFormatRegex = '/\.(' . implode('|', array_keys($supportedFormats)) . ')$/i'; 121 | 122 | // Retrieve image files from the item. 123 | set_loop_records('files', $this->_item->getFiles()); 124 | foreach (loop('files') as $file) { 125 | if ($file->hasThumbnail() && preg_match($supportedFormatRegex, $file->filename)) { 126 | $this->_leaves[] = $file; 127 | } 128 | else { 129 | $this->_nonleaves[] = $file; 130 | } 131 | } 132 | 133 | // Sorting by original filename or keep attachment order. 134 | if (get_option('bookreader_sorting_mode')) { 135 | $this->sortFilesByOriginalName($this->_leaves); 136 | $this->sortFilesByOriginalName($this->_nonleaves); 137 | } 138 | // Reset keys, because the important is to get files by order. 139 | $this->_leaves = array_values($this->_leaves); 140 | $this->_nonleaves = array_values($this->_nonleaves); 141 | } 142 | 143 | return $invert 144 | ? $this->_nonleaves 145 | : $this->_leaves; 146 | } 147 | 148 | /** 149 | * Count the number of image files attached to an item. 150 | * 151 | * @return integer 152 | * Number of images attached to an item. 153 | */ 154 | public function itemLeafsCount() 155 | { 156 | if (empty($this->_item)) { 157 | return; 158 | } 159 | return count($this->getLeaves()); 160 | } 161 | 162 | /** 163 | * Get the list of indexes of pages for an item. 164 | * 165 | * This function is used to get quickly all page indexes of an item. First 166 | * page should be 0 if document starts from right, and 1 if document starts 167 | * from left. Use null for a missing page. 168 | * 169 | * By default, indexes are simply a list of numbers starting from 0. 170 | * 171 | * @return array of integers 172 | */ 173 | public function getPageIndexes() 174 | { 175 | if (empty($this->_item)) { 176 | return; 177 | } 178 | 179 | // Start from 0 by default. 180 | $leaves = $this->getLeaves(); 181 | $indexes = array(); 182 | foreach($leaves as $key => $leaf) { 183 | $indexes[] = empty($leaf) ? null : $key; 184 | } 185 | return $indexes; 186 | } 187 | 188 | /** 189 | * Get the list of numbers of pages of an item. 190 | * 191 | * The page number is the name of a page of a file, like "6" or "XIV". 192 | * 193 | * This function is used to get quickly all page numbers of an item. If the 194 | * page number is empty, the label page will be used. If there is no page 195 | * number, a null value or an empty string is used, so the label in viewer 196 | * will be the page index + 1. 197 | * 198 | * No page number by default. 199 | * 200 | * @see getPageLabels() 201 | * 202 | * @return array of strings 203 | */ 204 | public function getPageNumbers() 205 | { 206 | if (empty($this->_item)) { 207 | return; 208 | } 209 | 210 | $leaves = $this->getLeaves(); 211 | return array_fill(0, count($leaves), null); 212 | } 213 | 214 | /** 215 | * Get the list of labels of pages of an item. 216 | * 217 | * This function is used to get quickly all page labels of an item. 218 | * 219 | * A label is used first for pages without pagination, like cover, summary, 220 | * title page, index, inserted page, planches, etc. If there is a page 221 | * number, this label is not needed, but it can be used to add a specific 222 | * information ("Page XIV : Illustration"). 223 | * 224 | * No label by default. 225 | * 226 | * @see getPageNumbers() 227 | * 228 | * @return array of strings 229 | */ 230 | public function getPageLabels() 231 | { 232 | if (empty($this->_item)) { 233 | return; 234 | } 235 | 236 | $leaves = $this->getLeaves(); 237 | return array_fill(0, count($leaves), ''); 238 | } 239 | 240 | /** 241 | * Return the cover file of an item (the leaf to display as a thumbnail). 242 | * 243 | * This data can be saved in the base in order to speed the display. 244 | * 245 | * @return file 246 | * Object file of the cover. 247 | */ 248 | public function getCoverFile() 249 | { 250 | if (empty($this->_item)) { 251 | return; 252 | } 253 | 254 | $leaves = $this->getLeaves(); 255 | $index = $this->getTitleLeaf(); 256 | return isset($leaves[$index]) ? $leaves[$index] : reset($leaves); 257 | } 258 | 259 | /** 260 | * Return index of the first leaf to display by BookReader. 261 | * 262 | * @return integer 263 | * Index for bookreader. 264 | */ 265 | public function getTitleLeaf() 266 | { 267 | if (empty($this->_item)) { 268 | return; 269 | } 270 | 271 | return 0; 272 | } 273 | 274 | /** 275 | * Get the index of a file in the list of leaves. 276 | * 277 | * @return integer|null 278 | */ 279 | public function getLeafIndex($file = null) 280 | { 281 | if (empty($file)) { 282 | $file = get_current_record('file'); 283 | if (empty($file)) { 284 | return null; 285 | } 286 | } 287 | 288 | $leaves = $this->getLeaves(); 289 | foreach($leaves as $key => $leaf) { 290 | if ($leaf && $leaf->id == $file->id) { 291 | return $key; 292 | } 293 | } 294 | 295 | return null; 296 | } 297 | 298 | /** 299 | * Get the page index of a file in the list of images. 300 | * 301 | * @see getPageIndexes() 302 | * 303 | * @return integer 304 | * Index of the page. 305 | */ 306 | public function getPageIndex($file = null) 307 | { 308 | return $this->_getLeafData('PageIndexes', $file); 309 | } 310 | 311 | /** 312 | * Get the page number of a file. 313 | * 314 | * @see getPageNumbers() 315 | * 316 | * @return string 317 | * Number of the page, empty to use the page label, or 'null' if none. 318 | */ 319 | public function getPageNumber($file = null) 320 | { 321 | return $this->_getLeafData('PageNumbers', $file); 322 | } 323 | 324 | /** 325 | * Get the page label of a file, like "4th Cover" or "Faux titre". 326 | * 327 | * @see getPageLabels() 328 | * 329 | * @return string 330 | * Label of the page, if needed. 331 | */ 332 | public function getPageLabel($file = null) 333 | { 334 | return $this->_getLeafData('PageLabels', $file); 335 | } 336 | 337 | /** 338 | * Get a specific data of a file in list of leaves. 339 | * 340 | * @return integer|null 341 | */ 342 | protected function _getLeafData($dataType, $file = null) 343 | { 344 | $key = $this->getLeafIndex($file); 345 | if (is_null($key)) { 346 | return null; 347 | } 348 | $callback = 'get' . $dataType; 349 | $array = $this->$callback(); 350 | return isset($array[$key]) ? $array[$key] : null; 351 | } 352 | 353 | /** 354 | * Get an array of the widths and heights of each image file of an item. 355 | * 356 | * This data can be saved in the base in order to speed the display. 357 | * 358 | * @param string $imageType 359 | * @return array Array of width and height of image files of an item. 360 | */ 361 | public function getImagesSizes($imageType = 'fullsize') 362 | { 363 | if (empty($this->_item)) { 364 | return; 365 | } 366 | 367 | $widths = array(); 368 | $heights = array(); 369 | $leaves = $this->getLeaves(); 370 | foreach ($leaves as $file) { 371 | $imageSize = $this->getImageSize($file, $imageType); 372 | $widths[] = $imageSize['width']; 373 | $heights[] = $imageSize['height']; 374 | } 375 | 376 | return array( 377 | $widths, 378 | $heights, 379 | ); 380 | } 381 | 382 | /** 383 | * Get an array of the width and height of the image file. 384 | * 385 | * @internal The process uses the saved constraints. It they are changed but 386 | * the derivative haven't been rebuilt, the return will be wrong (but 387 | * generally without consequences for BookReader). 388 | * 389 | * @param File $file 390 | * @param string $imageType 391 | * @return array Associative array of width and height of the image file. 392 | * If the file is not an image, the width and the height will be null. 393 | */ 394 | public function getImageSize($file, $imageType = 'fullsize') 395 | { 396 | static $sizeConstraints = array(); 397 | 398 | if (!isset($sizeConstraints[$imageType])) { 399 | $sizeConstraints[$imageType] = get_option($imageType . '_constraint'); 400 | } 401 | $sizeConstraint = $sizeConstraints[$imageType]; 402 | 403 | // Check if this is an image. 404 | if (empty($file) || strpos($file->mime_type, 'image/') !== 0) { 405 | $width = null; 406 | $height = null; 407 | } 408 | 409 | // This is an image. 410 | else { 411 | $metadata = json_decode($file->metadata, true); 412 | if (empty($metadata['video']['resolution_x']) || empty($metadata['video']['resolution_y'])) { 413 | $msg = __('The image #%d ("%s") is not stored correctly.', $file->id, $file->original_filename); 414 | _log($msg, Zend_Log::NOTICE); 415 | 416 | if (isset($metadata['video']['resolution_x']) || isset($metadata['video']['resolution_y'])) { 417 | throw new Exception($msg); 418 | } 419 | 420 | // Get the resolution directly. 421 | // The storage adapter should be checked for external storage. 422 | $storageAdapter = $file->getStorage()->getAdapter(); 423 | $filepath = get_class($storageAdapter) == 'Omeka_Storage_Adapter_Filesystem' 424 | ? FILES_DIR . DIRECTORY_SEPARATOR . $file->getStoragePath($imageType) 425 | : $file->getWebPath($imageType); 426 | list($width, $height, $type, $attr) = getimagesize($filepath); 427 | if (empty($width) || empty($height)) { 428 | throw new Exception($msg); 429 | } 430 | } 431 | 432 | // Calculate the size. 433 | else { 434 | $sourceWidth = $metadata['video']['resolution_x']; 435 | $sourceHeight = $metadata['video']['resolution_y']; 436 | 437 | // Use the original size when possible. 438 | if ($imageType == 'original') { 439 | $width = $sourceWidth; 440 | $height = $sourceHeight; 441 | } 442 | // This supposes that the option has not changed before. 443 | else { 444 | // Source is landscape. 445 | if ($sourceWidth > $sourceHeight) { 446 | $width = $sizeConstraint; 447 | $height = round($sourceHeight * $sizeConstraint / $sourceWidth); 448 | } 449 | // Source is portrait. 450 | elseif ($sourceWidth < $sourceHeight) { 451 | $width = round($sourceWidth * $sizeConstraint / $sourceHeight); 452 | $height = $sizeConstraint; 453 | } 454 | // Source is square. 455 | else { 456 | $width = $sizeConstraint; 457 | $height = $sizeConstraint; 458 | } 459 | } 460 | } 461 | } 462 | 463 | return array( 464 | 'width' => $width, 465 | 'height' => $height, 466 | ); 467 | } 468 | 469 | /** 470 | * Get an array of the number, label, witdh and height of each image file of 471 | * an item. Individual data are json encoded. 472 | * 473 | * @return array 474 | * Array of the json encoded index, number, label, width and height of 475 | * images (leaves) files of an item. 476 | */ 477 | public function imagesData($imageType = 'fullsize') 478 | { 479 | if (empty($this->_item)) { 480 | return; 481 | } 482 | 483 | $indexes = $this->getPageIndexes(); 484 | $numbers = $this->getPageNumbers(); 485 | $labels = $this->getPageLabels(); 486 | list($widths, $heights) = $this->getImagesSizes($imageType); 487 | 488 | return array( 489 | $indexes, 490 | $numbers, 491 | $labels, 492 | $widths, 493 | $heights, 494 | ); 495 | } 496 | 497 | /** 498 | * Return the image in html format of the cover of the item. 499 | * 500 | * @todo Put it in a custom library. 501 | * 502 | * @return string 503 | * Html code of the image of the cover of the item. 504 | */ 505 | public function itemCover($props = array(), $index = 0) 506 | { 507 | if (empty($this->_item)) { 508 | return; 509 | } 510 | 511 | $file = $this->getCoverFile(); 512 | 513 | $img = ''; 514 | if ($file) { 515 | $title = $this->_item->getElementTexts('Dublin Core', 'Title'); 516 | $title = empty($title) ? '' : $title[0]->text; 517 | $defaultProps = array( 518 | 'alt' => html_escape($title), 519 | ); 520 | 521 | $props = array_merge($defaultProps, $props); 522 | 523 | // TODO Currently use automatic width. 524 | $width = @$props['width']; 525 | $height = @$props['height']; 526 | 527 | $img = '_tagAttributes($props) . ' width="auto" height="120" />'; 528 | } 529 | 530 | return $img; 531 | } 532 | 533 | /** 534 | * Returns the derivative size to use for the current image, depending on 535 | * the scale. 536 | * 537 | * This default is correct for all digitalized normal books. 538 | * 539 | * @return string 540 | * Derivative name of the size. 541 | */ 542 | public function getSizeType($scale) 543 | { 544 | switch ($scale) { 545 | case ($scale < 1): return 'original'; 546 | case ($scale < 2): return 'fullsize'; 547 | case ($scale < 4): return 'fullsize'; 548 | case ($scale < 8): return 'fullsize'; 549 | case ($scale < 16): return 'thumbnail'; 550 | case ($scale < 32): return 'thumbnail'; 551 | } 552 | return 'fullsize'; 553 | } 554 | 555 | /** 556 | * Get links to non-images files of the item. 557 | * 558 | * @return string 559 | * Html code of links. 560 | */ 561 | public function linksToNonImages() 562 | { 563 | if (empty($this->_item)) { 564 | return; 565 | } 566 | 567 | $html = ''; 568 | $nonImagesFiles = $this->getNonLeaves(); 569 | foreach ($nonImagesFiles as $file) { 570 | // Set the document's absolute URL. 571 | // Note: file_download_uri($file) does not work here. It results 572 | // in the iPaper error: "Unable to reach provided URL." 573 | //$documentUrl = WEB_FILES . '/' . $file->filename; 574 | //$documentUrl = file_download_uri($file); 575 | $filesize = $this->_formatFileSize($file->size); 576 | $extension = pathinfo($file->original_filename, PATHINFO_EXTENSION); 577 | //$type = $file->mime_browser; 578 | $html .= '
  • '; 579 | $html .= '
    '; 580 | $html .= '' . $file->original_filename . ''; 581 | $html .= '  (' . $extension . ' / ' . $filesize . ')'; 582 | $html .= '
    '; // Bug when PHP_EOL is added. 583 | $html .= '
  • '; 584 | } 585 | 586 | return $html; 587 | } 588 | 589 | /** 590 | * Check if there are data for search. 591 | * 592 | * @return boolean 593 | * True if there are data for search, else false. 594 | */ 595 | public function hasDataForSearch() 596 | { 597 | if (empty($this->_item)) { 598 | return; 599 | } 600 | 601 | return false; 602 | } 603 | 604 | /** 605 | * Save all BookReader data about an item in a file or in database. 606 | * 607 | * @return false|array 608 | * False if an error occur, else array of data. 609 | */ 610 | public function saveData() 611 | { 612 | return null; 613 | } 614 | 615 | /** 616 | * Returns answers to a query. 617 | * 618 | * @return array 619 | * Result can be returned by leaf index or by file id. The custom 620 | * highlightFiles() function should use the same. 621 | * Associative array of leaf indexes or file ids as keys and an array 622 | * values for each result in the page (words and start position): 623 | * array( 624 | * leaf index = array( 625 | * array( 626 | * 'answer' => answer, findable in original text, 627 | * 'position' => position of the answer in original text, 628 | * ), 629 | * ), 630 | * ); 631 | */ 632 | public function searchFulltext($query) 633 | { 634 | return null; 635 | } 636 | 637 | /** 638 | * Prepares data to be highlighted via javascript. 639 | * 640 | * @see BookReader_IndexController::fulltextAction() 641 | * 642 | * @return array 643 | * Array of matches with coordinates. 644 | */ 645 | public function highlightFiles($texts) 646 | { 647 | return null; 648 | } 649 | 650 | /** 651 | * Return the html code of an array of attributes. 652 | * 653 | * @return string 654 | * Html code of the attributes. 655 | * 656 | * @todo Escape value. 657 | */ 658 | protected function _tagAttributes($props) 659 | { 660 | $html = ''; 661 | foreach ($props as $key => $value) { 662 | $html .= $key . '="' . $value . '" '; 663 | } 664 | return $html; 665 | } 666 | 667 | /** 668 | * Return a file size with the appropriate format of unit. 669 | * 670 | * @return string 671 | * String of the file size. 672 | */ 673 | protected function _formatFileSize($size) 674 | { 675 | if ($size < 1024) { 676 | return $size . ' ' . __('bytes'); 677 | } 678 | 679 | foreach (array(__('KB'), __('MB'), __('GB'), __('TB')) as $unit) { 680 | $size /= 1024.0; 681 | if ($size < 10) { 682 | return sprintf("%.1f" . ' ' . $unit, $size); 683 | } 684 | if ($size < 1024) { 685 | return (int) $size . ' ' . $unit; 686 | } 687 | } 688 | } 689 | 690 | /** 691 | * Returns a cleaned string. 692 | * 693 | * Removes trailing spaces and anything else, except letters, numbers and 694 | * symbols. 695 | * 696 | * @param string $string The string to clean. 697 | * 698 | * @return string 699 | * The cleaned string. 700 | */ 701 | protected function _alnumString($string) 702 | { 703 | $string = preg_replace('/[^\p{L}\p{N}\p{S}]/u', ' ', $string); 704 | return trim(preg_replace('/\s+/', ' ', $string)); 705 | } 706 | 707 | /** 708 | * Sort an array of files by name. 709 | * 710 | * @param array $files By reference array of files. 711 | * @param boolean $associative Keep association or not. 712 | * 713 | * @return void 714 | */ 715 | public static function sortFilesByOriginalName(&$files, $associative = true) 716 | { 717 | // The function determines if one variable is greater, equal or lower 718 | // than another one. It returns an integer -1, 0 or 1. 719 | 720 | if ($associative) { 721 | uasort($files, function($file_a, $file_b) { 722 | return strcmp($file_a->original_filename, $file_b->original_filename); 723 | }); 724 | } 725 | else { 726 | usort($files, function($file_a, $file_b) { 727 | return strcmp($file_a->original_filename, $file_b->original_filename); 728 | }); 729 | } 730 | } 731 | } 732 | -------------------------------------------------------------------------------- /libraries/BookReader/Creator/Default.php: -------------------------------------------------------------------------------- 1 | _item également. On peut donc faire la recherche 10 | * et le surlignage directement dans cette fonction et ne rien renvoyer dans la 11 | * fonction searchFulltext(). 12 | * 13 | * @internal Limites de la recherche : 14 | * - La recherche se fait via grep ou regex, alors que c'est du xml. 15 | * - La recherche est ligne par ligne et échoue si les mots sont sur 16 | * plusieurs lignes. 17 | */ 18 | 19 | /** 20 | * Extract OCR helper for BookReader. 21 | * 22 | * @package BookReader 23 | */ 24 | class BookReader_Creator_ExtractOCR extends BookReader_Creator 25 | { 26 | /** 27 | * Get the list of numbers of pages of an item. 28 | * 29 | * The page number is the name of a page of a file, like "6" or "XIV". 30 | * 31 | * This function is used to get quickly all page numbers of an item. If the 32 | * page number is empty, the label page will be used. If there is no page 33 | * number, a null value or an empty string is used, so the label in viewer 34 | * will be the page index + 1. 35 | * 36 | * @see getPageLabels() 37 | * 38 | * @return array of strings 39 | */ 40 | public function getPageNumbers() 41 | { 42 | if (empty($this->_item)) { 43 | return; 44 | } 45 | 46 | $leaves = $this->getLeaves($this->_item); 47 | $numbers = array(); 48 | foreach ($leaves as $leaf) { 49 | if (empty($leaf)) { 50 | $number = ''; 51 | } 52 | else { 53 | $file = &$leaf; 54 | $txt = $file->original_filename; 55 | 56 | $re1 = '.*?'; # Non-greedy match on filler 57 | $re2 = '(page)'; # Word 1 58 | $re3 = '(\\d+)'; # Integer Number 1 59 | $c = preg_match_all('/' . $re1 . $re2 . $re3 . '/is', $txt, $matches); 60 | if ($c) { 61 | $word1 = $matches[1][0]; 62 | $int1 = $matches[2][0]; 63 | $int1 = preg_replace( "/^[0]{0,6}/", '', $int1 ); 64 | $number = $int1; 65 | } 66 | else { 67 | $number = null; 68 | } 69 | } 70 | $numbers[] = $number; 71 | } 72 | return $numbers; 73 | } 74 | 75 | /** 76 | * Returns the derivative size to use for the current image, depending on 77 | * the scale. 78 | * 79 | * @return string 80 | * Derivative name of the size. 81 | */ 82 | public function getSizeType($scale) 83 | { 84 | switch ($scale) { 85 | case ($scale < 1.1): return 'original'; 86 | case ($scale < 1.4): return 'fullsize'; 87 | case ($scale < 6): return 'fullsize'; 88 | case ($scale < 16): return 'thumbnail'; 89 | case ($scale < 32): return 'thumbnail'; 90 | } 91 | return 'fullsize'; 92 | } 93 | 94 | /** 95 | * Check if there are data for search. 96 | * 97 | * @return boolean 98 | * True if there are data for search, else false. 99 | */ 100 | public function hasDataForSearch() 101 | { 102 | if (empty($this->_item)) { 103 | return; 104 | } 105 | 106 | set_loop_records('files', $this->_item->getFiles()); 107 | if (has_loop_records('files')) { 108 | foreach (loop('files') as $file) { 109 | if (strtolower($file->getExtension()) == 'xml') { 110 | return true; 111 | } 112 | } 113 | } 114 | 115 | return false; 116 | } 117 | 118 | /** 119 | * Returns answers to a query. 120 | * 121 | * @return array 122 | * Result can be returned by leaf index or by file id. The custom 123 | * highlightFiles() function should use the same. 124 | * Associative array of leaf indexes or file ids as keys and an array 125 | * values for each result in the page (words and start position): 126 | * array( 127 | * leaf index = array( 128 | * array( 129 | * 'answer' => answer, findable in the original text, 130 | * 'position' => position of the answer in the original text, 131 | * ), 132 | * ), 133 | * ); 134 | */ 135 | public function searchFulltext($query) 136 | { 137 | if (empty($this->_item)) { 138 | return; 139 | } 140 | 141 | $minimumQueryLength = 3; 142 | $maxResult = 10; 143 | 144 | // Simplify checks, because arrays are 0-based. 145 | $maxResult--; 146 | 147 | $results = array(); 148 | 149 | // Normalize query because the search occurs inside a normalized text. 150 | $cleanQuery = $this->_alnumString($query); 151 | if (strlen($cleanQuery) < $minimumQueryLength) { 152 | return $results; 153 | } 154 | 155 | $queryWords = explode(' ', $cleanQuery); 156 | $countQueryWords = count($queryWords); 157 | 158 | if ($countQueryWords > 1) $queryWords[] = $cleanQuery; 159 | 160 | $iResult = 0; 161 | $list = array(); 162 | set_loop_records('files', $this->_item->getFiles()); 163 | 164 | foreach (loop('files') as $file) { 165 | //if ($file->mime_type == 'application/xml') { 166 | if (strtolower($file->getExtension()) == 'xml') { 167 | $xml_file = $file; 168 | } 169 | elseif ($file->hasThumbnail()) { 170 | if (preg_match('/\.(jpg|jpeg|png|gif)$/i', $file->filename)) { 171 | $list[] = $file; 172 | } 173 | } 174 | } 175 | 176 | $widths = array(); 177 | $heights = array(); 178 | foreach ($list as $file) { 179 | $image = $file->getWebPath('fullsize'); 180 | list($width, $height, $type, $attr) = getimagesize($image); 181 | $widths[] = $width; 182 | $heights[] = $height; 183 | } 184 | 185 | if ($xml_file) { 186 | $results = array(); 187 | 188 | // To use the local path is discouraged, because it bypasses the 189 | // storage, so it's not compatible with Amazon S3, etc. 190 | $string = file_get_contents($xml_file->getWebPath('original')); 191 | if (empty($string)) { 192 | throw new Exception('Error:Cannot get XML file!'); 193 | } 194 | 195 | $string = preg_replace('/\s{2,}/ui', ' ', $string); 196 | $string = preg_replace('/<\/?b>/ui', '', $string); 197 | $string = preg_replace('/<\/?i>/ui', '', $string); 198 | $string = str_replace('', '', $string); 199 | $xml = simplexml_load_string($string); 200 | if (!$xml) { 201 | throw new Exception('Error:Invalid XML!'); 202 | } 203 | 204 | $result = array(); 205 | try { 206 | // We need to store the name of the function to be used 207 | // for string length. mb_strlen() is better (especially 208 | // for diacrictics) but not available on all systems so 209 | // sometimes we need to use the default strlen() 210 | $strlen_function = "strlen"; 211 | if (function_exists('mb_strlen')) { 212 | $strlen_function = "mb_strlen"; 213 | } 214 | 215 | foreach( $xml->page as $page) { 216 | 217 | foreach($page->attributes() as $a => $b) { 218 | if ($a == 'height') $page_height = (string)$b ; 219 | if ($a == 'width') $page_width = (string)$b ; 220 | if ($a == 'number') $page_number = (string)$b ; 221 | } 222 | $t = 1; 223 | foreach( $page->text as $row) { 224 | $boxes = array(); 225 | $zone_text = strip_tags($row->asXML()); 226 | foreach($queryWords as $q) { 227 | if($strlen_function($q) >= 3) { 228 | if(preg_match("/$q/Uui", $zone_text) > 0) { 229 | foreach($row->attributes() as $a => $b) { 230 | if ($a == 'top') $zone_top = (string)$b; 231 | if ($a == 'left') $zone_left = (string)$b; 232 | if ($a == 'height') $zone_height = (string)$b; 233 | if ($a == 'width') $zone_width = (string)$b; 234 | } 235 | $zone_right = ($page_width - $zone_left - $zone_width); 236 | $zone_bottom = ($page_height - $zone_top - $zone_height); 237 | 238 | $zone_width_char = strlen($zone_text); 239 | $word_start_char = stripos($zone_text, $q); 240 | $word_width_char = strlen($q); 241 | 242 | $word_left = $zone_left + ( (($word_start_char -1) * $zone_width) / $zone_width_char); 243 | $word_right = $word_left + ( ( ( $word_width_char + 2) * $zone_width) / $zone_width_char ); 244 | 245 | $word_left = round($word_left * $widths[$page_number - 1] / $page_width); 246 | $word_right = round( $word_right * $widths[$page_number - 1] / $page_width); 247 | 248 | $word_top = round($zone_top * $heights[$page_number - 1] / $page_height); 249 | $word_bottom = round($word_top + ( $zone_height * $heights[$page_number - 1] / $page_height )); 250 | 251 | $boxes[] = array( 252 | 'r' => $word_right, 253 | 'l' => $word_left, 254 | 'b' => $word_bottom, 255 | 't' => $word_top, 256 | 'page' => $page_number-1, 257 | ); 258 | 259 | $zone_text = str_ireplace($q, '{{{' . $q . '}}}', $zone_text); 260 | $result['text'] = $zone_text; 261 | $result['par'] = array(); 262 | $result['par'][] = array( 263 | 't' => $zone_top, 264 | 'r' => $zone_right, 265 | 'b' => $zone_bottom, 266 | 'l' => $zone_left, 267 | 'page' => $page_number-1, 268 | 'boxes' => $boxes, 269 | ); 270 | 271 | $results[] = $result; 272 | } 273 | $t += 1; 274 | } 275 | } 276 | } 277 | } 278 | } catch (Exception $e) { 279 | throw new Exception('Error:PDF to XML conversion failed!'); 280 | } 281 | } 282 | return $results; 283 | } 284 | 285 | /** 286 | * Prepares data to be highlighted via javascript. 287 | * 288 | * @see BookReader_IndexController::fulltextAction() 289 | * 290 | * @todo To be updated. 291 | * 292 | * @return array 293 | * Array of matches with coordinates. 294 | */ 295 | public function highlightFiles($textsToHighlight) 296 | { 297 | if (empty($this->_item)) { 298 | return; 299 | } 300 | 301 | return $textsToHighlight; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /libraries/BookReader/Creator/Simple.php: -------------------------------------------------------------------------------- 1 | _item)) { 36 | return; 37 | } 38 | 39 | $leaves = $this->getLeaves(); 40 | $numbers = array(); 41 | foreach ($leaves as $leaf) { 42 | if (empty($leaf)) { 43 | $number = ''; 44 | } 45 | else { 46 | $txt = $leaf->getElementTexts('Dublin Core', 'Title'); 47 | if (empty($txt)) { 48 | $number = null; 49 | } 50 | else { 51 | $firstSpace = strrpos($txt[0]->text, ' '); 52 | if (strtolower(substr($txt[0]->text, 0, $firstSpace)) == 'page') { 53 | $txt = trim(substr($txt[0]->text, $firstSpace + 1)); 54 | $number = ((int) $txt == $txt) 55 | ? (integer) $txt 56 | : (string) $txt; 57 | } 58 | else { 59 | $number = ''; 60 | } 61 | } 62 | } 63 | $numbers[] = $number; 64 | } 65 | return $numbers; 66 | } 67 | 68 | /** 69 | * Get the list of labels of pages of an item. 70 | * 71 | * This function is used to get quickly all page labels of an item. 72 | * 73 | * A label is used first for pages without pagination, like cover, summary, 74 | * title page, index, inserted page, planches, etc. If there is a page 75 | * number, this label is not needed, but it can be used to add a specific 76 | * information ("Page XIV : Illustration"). 77 | * 78 | * In this example, numbers are saved in Dublin Core:Title as 'Cover', etc. 79 | * in metadata of each file. 80 | * 81 | * @see getPageNumbers() 82 | * 83 | * @return array of strings 84 | */ 85 | public function getPageLabels() 86 | { 87 | if (empty($this->_item)) { 88 | return; 89 | } 90 | 91 | $leaves = $this->getLeaves(); 92 | $labels = array(); 93 | foreach ($leaves as $leaf) { 94 | if (empty($leaf)) { 95 | $label = ''; 96 | } 97 | else { 98 | $txt = $leaf->getElementTexts('Dublin Core', 'Title'); 99 | if (empty($txt)) { 100 | $label = ''; 101 | } 102 | else { 103 | // Don't add a label if the label is like a page number. 104 | $firstSpace = strrpos($txt[0]->text, ' '); 105 | $label = strtolower(substr($txt[0]->text, 0, $firstSpace)) == 'page' 106 | ? '' 107 | : $txt[0]->text; 108 | } 109 | } 110 | $labels[] = $label; 111 | } 112 | return $labels; 113 | } 114 | 115 | /** 116 | * Check if there are data for search. 117 | * 118 | * In this example, search is done inside "Item Type Metadata:Text". 119 | * 120 | * @return boolean 121 | * True if there are data for search, else false. 122 | */ 123 | public function hasDataForSearch() 124 | { 125 | if (empty($this->_item)) { 126 | return; 127 | } 128 | 129 | $this->_itemType = $this->_item->getItemType(); 130 | if (empty($this->_itemType) || $this->_itemType->name !== 'Text') { 131 | return false; 132 | } 133 | 134 | return $this->_item->hasElementText('Item Type Metadata', 'Text'); 135 | } 136 | 137 | /** 138 | * Returns answers to a full text query. 139 | * 140 | * This search is case insensitive and without punctuation. Diacritics are 141 | * not converted. 142 | * 143 | * @uses BookReader_Creator::_alnumString() 144 | * 145 | * @todo Use one query search or xml search or Zend_Search_Lucene. 146 | * 147 | * @return array 148 | * Result can be returned by leaf index or by file id. The custom 149 | * highlightFiles() function should use the same. 150 | * Associative array of leaf indexes or file ids as keys and an array 151 | * values for each result in the page (words and start position): 152 | * array( 153 | * leaf index = array( 154 | * array( 155 | * 'answer' => answer, findable in the original text, 156 | * 'position' => position of the answer in the original text, 157 | * ), 158 | * ), 159 | * ); 160 | */ 161 | public function searchFulltext($query) 162 | { 163 | if (empty($this->_item)) { 164 | return; 165 | } 166 | 167 | $minimumQueryLength = 4; 168 | $maxResult = 10; 169 | // Warning: PREG_OFFSET_CAPTURE is not Unicode safe. 170 | // So, if needed, uncomment the following line. 171 | // mb_internal_encoding("UTF-8"); 172 | 173 | // Simplify checks, because arrays are 0-based. 174 | $maxResult--; 175 | 176 | $results = array(); 177 | 178 | // Normalize query because the search occurs inside a normalized text. 179 | $cleanQuery = $this->_alnumString($query); 180 | if (strlen($cleanQuery) < $minimumQueryLength) { 181 | return $results; 182 | } 183 | 184 | // Prepare regex: replace all spaces to allow any characters, except 185 | // those accepted (letters, numbers and symbols). 186 | $pregQuery = '/' . str_replace(' ', '[\p{C}\p{M}\p{P}\p{Z}]*', preg_quote($cleanQuery)) . '/Uui'; 187 | 188 | // For this example, search is done at item level only. 189 | $text = $this->_item->getElementTexts('Item Type Metadata', 'Text'); 190 | if (!empty($text) && preg_match_all($pregQuery, $text[0]->text, $matches, PREG_OFFSET_CAPTURE)) { 191 | // For this example, the answer is found in the first image only. 192 | $files = $this->_item->Files; 193 | $file = $files[0]; 194 | 195 | $results[$file->id] = array(); 196 | foreach ($matches as $match) { 197 | $results[$file->id][] = array( 198 | 'answer' => $match[0][0], 199 | 'position' => $match[0][1], 200 | ); 201 | } 202 | } 203 | 204 | return $results; 205 | } 206 | 207 | /** 208 | * Prepares data to be highlighted via javascript. 209 | * 210 | * @see BookReader_IndexController::fulltextAction() 211 | * 212 | * @return array 213 | * Array of matches with coordinates. 214 | */ 215 | public function highlightFiles($textsToHighlight) 216 | { 217 | if (empty($this->_item)) { 218 | return; 219 | } 220 | 221 | $imageType = 'fullsize'; 222 | $beforeContext = 120; 223 | $afterContext = 120; 224 | // If needed, uncomment the following line. 225 | // mb_internal_encoding("UTF-8"); 226 | 227 | $results = array(); 228 | foreach ($textsToHighlight as $file_id => $data) { 229 | $file = get_record_by_id('file', $file_id); 230 | $pageIndex = $this->getPageIndex($file); 231 | 232 | $imageSize = $this->getImageSize($file, $imageType); 233 | $width = $imageSize['width']; 234 | $height = $imageSize['height']; 235 | 236 | // Get the ratio between original widths and heights and fullsize 237 | // ones, because highlight is done first on a fullsize image, but 238 | // data are set for original image. 239 | $imageSize = $this->getImageSize($file, 'original'); 240 | $originalWidth = $imageSize['width']; 241 | $originalHeight = $imageSize['height']; 242 | $ratio = $height / $originalHeight; 243 | 244 | // Text is needed only to get context. 245 | $this->_item = $file->getItem(); 246 | $text = $this->_item->getElementTexts('Item Type Metadata', 'Text'); 247 | $text = $text[0]->text; 248 | $lengthText = mb_strlen($text); 249 | 250 | // For this example, one third size rectangle is drawn on the 251 | // middle of the image. 252 | foreach ($data as $dataToHighlight) { 253 | $answer = $dataToHighlight['answer']; 254 | $position = $dataToHighlight['position']; 255 | $length = mb_strlen($answer); 256 | 257 | // Get the context of the answer. 258 | $context = '...' . $answer . '...'; 259 | 260 | // Create the parallel zone. 261 | // TODO Currently, the parallel zone is not really used, so the 262 | // first word coordinates is taken as zone coordinates. 263 | $zone_left = $originalWidth / 3; 264 | $zone_top = $originalHeight / 3; 265 | $zone_right = $originalWidth * 2 / 3; 266 | $zone_bottom = $originalHeight * 2 / 3; 267 | 268 | // Creates boxes for each word. 269 | $boxes = array(); 270 | $word_left = $originalWidth / 3; 271 | $word_top = $originalHeight / 3; 272 | $word_right = $originalWidth * 2 / 3; 273 | $word_bottom = $originalHeight * 2 / 3; 274 | 275 | $boxes[] = array( 276 | 't' => round($word_top * $ratio), 277 | 'l' => round($word_left * $ratio), 278 | 'b' => round($word_bottom * $ratio), 279 | 'r' => round($word_right * $ratio), 280 | 'index' => $pageIndex, 281 | ); 282 | 283 | // Aggregate zones to prepare current result. 284 | $result = array(); 285 | $result['text'] = $context; 286 | $result['par'] = array(); 287 | $result['par'][] = array( 288 | 't' => round($zone_top * $ratio), 289 | 'l' => round($zone_left * $ratio), 290 | 'b' => round($zone_bottom * $ratio), 291 | 'r' => round($zone_right * $ratio), 292 | 'index' => $pageIndex, 293 | 'boxes' => $boxes, 294 | ); 295 | 296 | $results[] = $result; 297 | } 298 | } 299 | 300 | return $results; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /models/BookReader.php: -------------------------------------------------------------------------------- 1 | _creator = new $creator($item); 18 | } 19 | 20 | /** 21 | * Return a property of the selected creator for all other properties. 22 | * 23 | * @param string $property 24 | * @return var 25 | */ 26 | public function __get($property) 27 | { 28 | if (property_exists($this->_creator, $property)) { 29 | return $this->_creator->$property; 30 | } 31 | throw new BadMethodCallException("Property named '$property' does not exist in BookReader class."); 32 | } 33 | 34 | /** 35 | * Check if a property exists in the creator for all other properties. 36 | * 37 | * @param string $property 38 | * @return bool 39 | */ 40 | public function __isset($property) 41 | { 42 | return property_exists($this->_creator, $property); 43 | } 44 | 45 | /** 46 | * Delegate to the selected creator for all other method calls. 47 | */ 48 | public function __call($method, $args) 49 | { 50 | if (method_exists($this->_creator, $method)) { 51 | return call_user_func_array(array($this->_creator, $method), $args); 52 | } 53 | throw new BadMethodCallException("Method named '$method' does not exist in BookReader class."); 54 | } 55 | 56 | /** 57 | * Prepare a string for html display. 58 | * 59 | * @return string 60 | */ 61 | public static function htmlCharacter($string) 62 | { 63 | $string = strip_tags($string); 64 | $string = html_entity_decode($string, ENT_QUOTES); 65 | $string = utf8_encode($string); 66 | $string = htmlspecialchars_decode($string); 67 | $string = addslashes($string); 68 | $string = utf8_decode($string); 69 | 70 | return $string; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /plugin.ini: -------------------------------------------------------------------------------- 1 | [info] 2 | name = "BookReader" 3 | author = "Julien Sicot (upgraded for Omeka 2.0 by Daniel Berthereau)" 4 | description = "Adds the Internet Archive BookReader (http://openlibrary.org/dev/docs/bookreader) into Omeka and allows to create online flip book from image files constituting an item." 5 | link = "https://github.com/jsicot/BookReader" 6 | support_link = "https://github.com/jsicot/BookReader" 7 | license = "GNU/GPL" 8 | version = "2.7" 9 | omeka_minimum_version = "2.2.2" 10 | omeka_target_version = "2.3" 11 | tags = "viewer, books, digital library, live book" 12 | optional_plugins = "Extract OCR, PDF TOC" 13 | -------------------------------------------------------------------------------- /routes.ini: -------------------------------------------------------------------------------- 1 | [routes] 2 | bookreader_viewer.route = "items/viewer/:id" 3 | bookreader_viewer.defaults.module = "book-reader" 4 | bookreader_viewer.defaults.controller = "viewer" 5 | bookreader_viewer.defaults.action = "show" 6 | bookreader_viewer.reqs.id = "\d+" 7 | 8 | bookreader_table.route = "items/view/:id" 9 | bookreader_table.defaults.module = "book-reader" 10 | bookreader_table.defaults.controller = "viewer" 11 | bookreader_table.defaults.action = "view" 12 | bookreader_table.reqs.id = "\d+" 13 | -------------------------------------------------------------------------------- /views/admin/forms/bookreader-batch-edit.php: -------------------------------------------------------------------------------- 1 |
    2 |

    3 |

    8 |
    9 |
    10 | formLabel('orderByFilename', 11 | __('Order files')); ?> 12 |
    13 |
    14 | formCheckbox('custom[bookreader][orderByFilename]', null, array( 15 | 'checked' => false, 'class' => 'order-by-filename-checkbox')); ?> 16 |

    17 | 18 |

    19 |
    20 |
    21 |
    22 |
    23 | formLabel('mixFileTypes', 24 | __('Mix file types')); ?> 25 |
    26 |
    27 | formCheckbox('custom[bookreader][mixFileTypes]', null, array( 28 | 'checked' => false, 'class' => 'mix-files-types-checkbox')); ?> 29 |

    30 | 31 |

    32 |
    33 |
    34 |
    35 |
    36 | formLabel('checkImageSize', 37 | __('Rebuild metadata when missing')); ?> 38 |
    39 |
    40 | formCheckbox('custom[bookreader][checkImageSize]', null, array( 41 | 'checked' => false, 'class' => 'check-image-size-checkbox')); ?> 42 |

    43 | 44 |

    45 |
    46 |
    47 |
    48 | -------------------------------------------------------------------------------- /views/admin/plugins/bookreader-config-form.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | formLabel('bookreader_creator', 5 | __('Creator')); ?> 6 |
    7 |
    8 |

    9 | 10 | 11 |

    12 | formText('bookreader_creator', get_option('bookreader_creator'), null); ?> 13 |
    14 |
    15 |
    16 |
    17 | formLabel('bookreader_sorting_mode', 18 | __('Sorting by original filename')); ?> 19 |
    20 |
    21 |

    22 | 26 |

    27 | formCheckbox('bookreader_sorting_mode', true, 28 | array('checked' => (boolean) get_option('bookreader_sorting_mode'))); ?> 29 |
    30 |
    31 |
    32 |
    33 |
    34 |
    35 | formLabel('bookreader_append_items_show', 36 | __('Append to "item show" page')); ?> 37 |
    38 |
    39 |

    40 | 41 |

    42 | formCheckbox('bookreader_append_items_show', true, 43 | array('checked' => (boolean) get_option('bookreader_append_items_show'))); ?> 44 |
    45 |
    46 |
    47 |
    48 | formLabel('bookreader_mode_page', 49 | __('Number of pages')); ?> 50 |
    51 |
    52 |

    53 | 54 |

    55 | formText('bookreader_mode_page', get_option('bookreader_mode_page'), null); ?> 56 |
    57 |
    58 |
    59 |
    60 | formLabel('bookreader_embed_functions', 61 | __('Functions in embed mode')); ?> 62 |
    63 |
    64 |

    65 | 66 |

    67 | formText('bookreader_embed_functions', get_option('bookreader_embed_functions'), null); ?> 68 |
    69 |
    70 |
    71 |
    72 | formLabel('bookreader_class', 73 | __('Class of inline frame')); ?> 74 |
    75 |
    76 |

    77 | 78 |

    79 | formText('bookreader_class', get_option('bookreader_class'), null); ?> 80 |
    81 |
    82 |
    83 |
    84 | formLabel('bookreader_width', 85 | __('Width of the inline frame')); ?> 86 |
    87 |
    88 |

    89 | 90 |

    91 | formText('bookreader_width', get_option('bookreader_width'), null); ?> 92 |
    93 |
    94 |
    95 |
    96 | formLabel('bookreader_height', 97 | __('Height of the inline frame')); ?> 98 |
    99 |
    100 |

    101 | 102 |

    103 | formText('bookreader_height', get_option('bookreader_height'), null); ?> 104 |
    105 |
    106 |
    107 | -------------------------------------------------------------------------------- /views/helpers/GetBookReader.php: -------------------------------------------------------------------------------- 1 | fileCount() > 1) { 45 | $mode_page = isset($args['mode_page']) 46 | ? $args['mode_page'] 47 | : get_option('bookreader_mode_page'); 48 | } 49 | // Unique page. 50 | else { 51 | $mode_page = 1; 52 | } 53 | 54 | // Build url of the page with iframe. 55 | $queryParams = array(); 56 | if ($part > 1) $queryParams['part'] = $part; 57 | if (empty($embed_functions)) $queryParams['ui'] = 'embed'; 58 | $url = absolute_url(array('id' => $item->id), 'bookreader_viewer', $queryParams); 59 | $url .= '#'; 60 | $url .= empty($page) ? '' : 'page/n' . $page . '/'; 61 | $url .= 'mode/' . $mode_page . 'up'; 62 | 63 | $class = get_option('bookreader_class'); 64 | if (!empty($class)) { 65 | $class = ' class="' . $class . '"'; 66 | } 67 | $width = get_option('bookreader_width'); 68 | if (!empty($width)) { 69 | $width = ' width="' . $width . '"'; 70 | } 71 | $height = get_option('bookreader_height'); 72 | if (!empty($height)) { 73 | $height = ' height="' . $height . '"'; 74 | } 75 | 76 | $html = '
    '; 77 | return $html; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /views/public/index/fulltext.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsicot/BookReader/4e98fc1e7a2d86fbff63fb4bbcc8ba4e0da931a5/views/public/index/fulltext.php -------------------------------------------------------------------------------- /views/public/index/image-proxy.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsicot/BookReader/4e98fc1e7a2d86fbff63fb4bbcc8ba4e0da931a5/views/public/index/image-proxy.php -------------------------------------------------------------------------------- /views/public/viewer/view.php: -------------------------------------------------------------------------------- 1 | '; 4 | echo __('This item has no viewable files.'); 5 | echo ''; 6 | return; 7 | } 8 | 9 | $title = metadata($item, array('Dublin Core', 'Title')); 10 | $creator = metadata($item, array('Dublin Core', 'Creator')); 11 | if ($creator) { 12 | $title .= ' - ' . $creator; 13 | } 14 | $title = BookReader::htmlCharacter($title); 15 | 16 | $coverFile = $bookreader->getCoverFile(); 17 | 18 | list($pageIndexes, $pageNumbers, $pageLabels, $imgWidths, $imgHeights) = $bookreader->imagesData(); 19 | 20 | $server = preg_replace('#^https?://#', '', WEB_ROOT); 21 | $serverFullText = $server . '/book-reader/index/fulltext'; 22 | $imgDir = WEB_PLUGIN . '/BookReader/views/shared/images/'; 23 | 24 | try { 25 | $favicon = src('favicon.ico'); 26 | } catch (Exception $e) { 27 | $favicon = ''; 28 | } 29 | ?> 30 | 31 | lang="fr"> 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | <?php echo $title; ?> 45 | 46 | 47 | 48 | 58 | 59 | 60 |
    61 |
    62 |
    63 | 68 |
    69 | 70 | 509 | 510 | $this, 514 | 'item' => $item, 515 | )); 516 | ?> 517 | 518 | 519 | -------------------------------------------------------------------------------- /views/shared/css/BookReaderEmbed.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom overrides for embedded bookreader. 3 | */ 4 | 5 | /* Hide some navigation buttons */ 6 | #BRtoolbar .label, 7 | .pageform, 8 | .play, 9 | .embed, 10 | .one_page_mode, 11 | .two_page_mode, 12 | .thumbnail_mode, 13 | .book_leftmost, 14 | .book_rightmost, 15 | .book_top, 16 | .book_bottom, 17 | .print { 18 | display: none; 19 | } 20 | 21 | #BRtoolbar .title { 22 | font-size: 0.9em; 23 | } 24 | 25 | #BRembedreturn { 26 | /* Center text */ 27 | font-size: 14px; 28 | line-height: 40px; 29 | height: 40px; 30 | font-family: "Lucida Grande","Arial",sans-serif; 31 | } 32 | 33 | #BRembedreturn a { 34 | font-size: 14px; 35 | color: #036daa; 36 | } 37 | -------------------------------------------------------------------------------- /views/shared/css/BookReaderLending.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright(c)2011 Internet Archive. Software license AGPL version 3. 3 | 4 | This file is part of BookReader 0) { 4 | barHeight = $('#BRtoolbar').height() + $('#BRnav').height(); 5 | $('#ToCmenu').css({ 6 | height : $('#BookReader').height()-barHeight, 7 | top : barHeight/2 8 | }); 9 | $(window).resize(); 10 | $('#ToCmenu ul').each(function() { 11 | if ($(this).parent().attr('id') != 'ToCmenu') { 12 | var ul = $(this); 13 | var button = $('-'); 14 | ul.hide(); 15 | ul.prev().before(button); 16 | button.css({ 17 | marginLeft : ($(this).parents('ul').size()-1)*20+4 18 | }); 19 | button.next().hover(function() { 20 | button.addClass('hover'); 21 | },function() { 22 | button.removeClass('hover'); 23 | }); 24 | button.click(function() { 25 | if (button.hasClass('right-arrow')) { 26 | button.removeClass('right-arrow').addClass('down-arrow'); 27 | } else { 28 | button.removeClass('down-arrow').addClass('right-arrow'); 29 | } 30 | ul.slideToggle(400,function() { 31 | $('#ToCbutton').css({ 32 | left : $('#ToCmenu').width(), 33 | top : $('#ToCmenu').position()['top'] 34 | }); 35 | }); 36 | $('#ToCbutton').css({ 37 | left : $('#ToCmenu').width(), 38 | top : $('#ToCmenu').position()['top'] 39 | }); 40 | return false; 41 | }); 42 | } 43 | }); 44 | $('#ToCmenu li').each(function() { 45 | $(this).css({ 46 | paddingLeft : $(this).parents('ul').size()*20, 47 | fontSize : (16-$(this).parents('ul').size()*2)+'px' 48 | }); 49 | }); 50 | $('#ToCbutton').css({ 51 | left : $('#ToCmenu').width(), 52 | top : $('#ToCmenu').position()['top'] 53 | }); 54 | $(window).resize(function() { 55 | if ($('#BRnavCntlBtm').hasClass('BRup')) { 56 | $('#ToCmenu').css({ 57 | height : $('#BookReader').height(), 58 | top : 0 59 | }); 60 | } else { 61 | $('#ToCmenu').css({ 62 | height : $('#BookReader').height()-barHeight, 63 | top : barHeight/2 64 | }); 65 | } 66 | $('#ToCbutton').css({ 67 | left : $('#ToCmenu').position()['left']+$('#ToCmenu').width(), 68 | top : $('#ToCmenu').position()['top'] 69 | }); 70 | }); 71 | $('#BRnavCntlBtm').click(function() { 72 | if ($(this).hasClass('BRup')) { 73 | $('#ToCmenu').animate({ 74 | height : $('#BookReader').height(), 75 | top : 0 76 | },{ 77 | step : function() { 78 | $('#ToCmenu').css('overflow-y','auto'); 79 | $('#ToCbutton').css({ 80 | top : $('#ToCmenu').position()['top'] 81 | }); 82 | }, 83 | complete : function() { 84 | $('#ToCmenu').css('overflow-y','auto'); 85 | } 86 | }); 87 | } else { 88 | $('#ToCmenu').animate({ 89 | height : $('#BookReader').height()-barHeight, 90 | top : barHeight/2 91 | },{ 92 | step : function() { 93 | $('#ToCmenu').css('overflow-y','auto'); 94 | $('#ToCbutton').css({ 95 | top : $('#ToCmenu').position()['top'] 96 | }); 97 | }, 98 | complete : function() { 99 | $('#ToCmenu').css('overflow-y','auto'); 100 | } 101 | }); 102 | } 103 | 104 | }); 105 | $('#ToCbutton').click(function() { 106 | if ($(this).hasClass('open')) { 107 | $(this).removeClass('open').addClass('close'); 108 | $(this).animate({ 109 | left : 0 110 | }); 111 | $('#ToCmenu').animate({ 112 | left : -$('#ToCmenu').width() 113 | },{ 114 | step : function() { 115 | $('#ToCmenu').css('overflow-y','auto'); 116 | }, 117 | complete : function() { 118 | $('#ToCmenu').css('overflow-y','auto'); 119 | }, 120 | queue : false 121 | }); 122 | } else { 123 | $(this).removeClass('close').addClass('open'); 124 | $(this).animate({ 125 | left : $('#ToCmenu').width() 126 | }) 127 | $('#ToCmenu').animate({ 128 | left : 0 129 | },{ 130 | step : function() { 131 | $('#ToCmenu').css('overflow-y','auto'); 132 | }, 133 | complete : function() { 134 | $('#ToCmenu').css('overflow-y','auto'); 135 | }, 136 | queue : false 137 | }); 138 | $('#ToCmenu').animate({opacity:0.75}) 139 | } 140 | }); 141 | $('#ToCbutton').mouseover(function(){ 142 | if ($(this).hasClass('close')) { 143 | $('#ToCbutton').animate({opacity:0.75},{ 144 | duration : 500, 145 | queue : false 146 | }); 147 | }; 148 | }); 149 | $('#ToCbutton').mouseleave(function(){ 150 | if ($(this).hasClass('close')) { 151 | $('#ToCbutton').animate({opacity:.25},{ 152 | duration : 500, 153 | queue : false 154 | }); 155 | }; 156 | }); 157 | } 158 | }); 159 | -------------------------------------------------------------------------------- /views/shared/javascripts/dragscrollable.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery dragscrollable Plugin 3 | * version: 1.0 (25-Jun-2009) 4 | * Copyright (c) 2009 Miquel Herrera 5 | * 6 | * Portions Copyright (c) 2010 Reg Braithwaite 7 | * Copyright (c) 2010 Internet Archive / Michael Ang 8 | * 9 | * Dual licensed under the MIT and GPL licenses: 10 | * http://www.opensource.org/licenses/mit-license.php 11 | * http://www.gnu.org/licenses/gpl.html 12 | * 13 | */ 14 | ;(function($){ // secure $ jQuery alias 15 | 16 | /** 17 | * Adds the ability to manage elements scroll by dragging 18 | * one or more of its descendant elements. Options parameter 19 | * allow to specifically select which inner elements will 20 | * respond to the drag events. 21 | * 22 | * options properties: 23 | * ------------------------------------------------------------------------ 24 | * dragSelector | jquery selector to apply to each wrapped element 25 | * | to find which will be the dragging elements. 26 | * | Defaults to '>:first' which is the first child of 27 | * | scrollable element 28 | * ------------------------------------------------------------------------ 29 | * acceptPropagatedEvent| Will the dragging element accept propagated 30 | * | events? default is yes, a propagated mouse event 31 | * | on a inner element will be accepted and processed. 32 | * | If set to false, only events originated on the 33 | * | draggable elements will be processed. 34 | * ------------------------------------------------------------------------ 35 | * preventDefault | Prevents the event to propagate further effectivey 36 | * | dissabling other default actions. Defaults to true 37 | * ------------------------------------------------------------------------ 38 | * scrollWindow | Scroll the window rather than the element 39 | * | Defaults to false 40 | * ------------------------------------------------------------------------ 41 | * 42 | * usage examples: 43 | * 44 | * To add the scroll by drag to the element id=viewport when dragging its 45 | * first child accepting any propagated events 46 | * $('#viewport').dragscrollable(); 47 | * 48 | * To add the scroll by drag ability to any element div of class viewport 49 | * when dragging its first descendant of class dragMe responding only to 50 | * evcents originated on the '.dragMe' elements. 51 | * $('div.viewport').dragscrollable({dragSelector:'.dragMe:first', 52 | * acceptPropagatedEvent: false}); 53 | * 54 | * Notice that some 'viewports' could be nested within others but events 55 | * would not interfere as acceptPropagatedEvent is set to false. 56 | * 57 | */ 58 | 59 | var append_namespace = function (string_of_events, ns) { 60 | 61 | /* IE doesn't have map 62 | return string_of_events 63 | .split(' ') 64 | .map(function (name) { return name + ns; }) 65 | .join(' '); 66 | */ 67 | var pieces = string_of_events.split(' '); 68 | var ret = new Array(); 69 | for (var i = 0; i < pieces.length; i++) { 70 | ret.push(pieces[i] + ns); 71 | } 72 | return ret.join(' '); 73 | }; 74 | 75 | var left_top = function(event) { 76 | 77 | var x; 78 | var y; 79 | if (typeof(event.clientX) != 'undefined') { 80 | x = event.clientX; 81 | y = event.clientY; 82 | } 83 | else if (typeof(event.screenX) != 'undefined') { 84 | x = event.screenX; 85 | y = event.screenY; 86 | } 87 | else if (typeof(event.targetTouches) != 'undefined') { 88 | x = event.targetTouches[0].pageX; 89 | y = event.targetTouches[0].pageY; 90 | } 91 | else if (typeof(event.originalEvent) == 'undefined') { 92 | var str = ''; 93 | for (i in event) { 94 | str += ', ' + i + ': ' + event[i]; 95 | } 96 | console.error("don't understand x and y for " + event.type + ' event: ' + str); 97 | } 98 | else if (typeof(event.originalEvent.clientX) != 'undefined') { 99 | x = event.originalEvent.clientX; 100 | y = event.originalEvent.clientY; 101 | } 102 | else if (typeof(event.originalEvent.screenX) != 'undefined') { 103 | x = event.originalEvent.screenX; 104 | y = event.originalEvent.screenY; 105 | } 106 | else if (typeof(event.originalEvent.targetTouches) != 'undefined') { 107 | x = event.originalEvent.targetTouches[0].pageX; 108 | y = event.originalEvent.targetTouches[0].pageY; 109 | } 110 | 111 | return {left: x, top:y}; 112 | }; 113 | 114 | $.fn.dragscrollable = function( options ) { 115 | 116 | var handling_element = $(this); 117 | 118 | var settings = $.extend( 119 | { 120 | dragSelector:'>:first', 121 | acceptPropagatedEvent: true, 122 | preventDefault: true, 123 | dragstart: 'mousedown touchstart', 124 | dragcontinue: 'mousemove touchmove', 125 | dragend: 'mouseup mouseleave touchend', 126 | dragMinDistance: 5, 127 | namespace: '.ds', 128 | scrollWindow: false 129 | },options || {}); 130 | 131 | settings.dragstart = append_namespace(settings.dragstart, settings.namespace); 132 | settings.dragcontinue = append_namespace(settings.dragcontinue, settings.namespace); 133 | settings.dragend = append_namespace(settings.dragend, settings.namespace); 134 | 135 | var dragscroll= { 136 | dragStartHandler : function(event) { 137 | // console.log('dragstart'); 138 | 139 | // mousedown, left click, check propagation 140 | if (event.which > 1 || 141 | (!event.data.acceptPropagatedEvent && event.target != this)){ 142 | return false; 143 | } 144 | 145 | event.data.firstCoord = left_top(event); 146 | // Initial coordinates will be the last when dragging 147 | event.data.lastCoord = event.data.firstCoord; 148 | 149 | handling_element 150 | .bind(settings.dragcontinue, event.data, dragscroll.dragContinueHandler) 151 | .bind(settings.dragend, event.data, dragscroll.dragEndHandler); 152 | 153 | if (event.data.preventDefault) { 154 | event.preventDefault(); 155 | return false; 156 | } 157 | }, 158 | dragContinueHandler : function(event) { // User is dragging 159 | // console.log('drag continue'); 160 | 161 | var lt = left_top(event); 162 | 163 | // How much did the mouse move? 164 | var delta = {left: (lt.left - event.data.lastCoord.left), 165 | top: (lt.top - event.data.lastCoord.top)}; 166 | 167 | /* 168 | console.log(event.data.scrollable); 169 | console.log('delta.left - ' + delta.left); 170 | console.log('delta.top - ' + delta.top); 171 | */ 172 | 173 | var scrollTarget = event.data.scrollable; 174 | if (event.data.scrollWindow) { 175 | scrollTarget = $(window); 176 | } 177 | // Set the scroll position relative to what ever the scroll is now 178 | scrollTarget.scrollLeft( scrollTarget.scrollLeft() - delta.left ); 179 | scrollTarget.scrollTop( scrollTarget.scrollTop() - delta.top ); 180 | 181 | // Save where the cursor is 182 | event.data.lastCoord = lt; 183 | 184 | if (event.data.preventDefault) { 185 | event.preventDefault(); 186 | return false; 187 | } 188 | 189 | }, 190 | dragEndHandler : function(event) { // Stop scrolling 191 | // console.log('drag END'); 192 | 193 | handling_element 194 | .unbind(settings.dragcontinue) 195 | .unbind(settings.dragend); 196 | 197 | // How much did the mouse move total? 198 | var delta = {left: Math.abs(event.data.lastCoord.left - event.data.firstCoord.left), 199 | top: Math.abs(event.data.lastCoord.top - event.data.firstCoord.top)}; 200 | var distance = Math.max(delta.left, delta.top); 201 | 202 | // Trigger 'tap' if did not meet drag distance 203 | // $$$ does not differentiate single vs multi-touch 204 | if (distance < settings.dragMinDistance) { 205 | //$(event.originalEvent.target).trigger('tap'); 206 | $(event.target).trigger('tap'); // $$$ always the right target? 207 | } 208 | 209 | // Allow event to propage if min distance was not achieved 210 | if (event.data.preventDefault && distance > settings.dragMinDistance) { 211 | event.preventDefault(); 212 | return false; 213 | } 214 | } 215 | } 216 | 217 | // set up the initial events 218 | return this.each(function() { 219 | // closure object data for each scrollable element 220 | var data = {scrollable : $(this), 221 | acceptPropagatedEvent : settings.acceptPropagatedEvent, 222 | preventDefault : settings.preventDefault, 223 | scrollWindow : settings.scrollWindow } 224 | // Set mouse initiating event on the desired descendant 225 | $(this).find(settings.dragSelector). 226 | bind(settings.dragstart, data, dragscroll.dragStartHandler); 227 | }); 228 | }; //end plugin dragscrollable 229 | 230 | $.fn.removedragscrollable = function (namespace) { 231 | if (typeof(namespace) == 'undefined') 232 | namespace = '.ds'; 233 | return this.each(function() { 234 | var x = $(document).find('*').andSelf().unbind(namespace); 235 | }); 236 | }; 237 | 238 | })( jQuery ); // confine scope 239 | -------------------------------------------------------------------------------- /views/shared/javascripts/excanvas.compiled.js: -------------------------------------------------------------------------------- 1 | // Copyright 2006 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | document.createElement("canvas").getContext||(function(){var s=Math,j=s.round,F=s.sin,G=s.cos,V=s.abs,W=s.sqrt,k=10,v=k/2;function X(){return this.context_||(this.context_=new H(this))}var L=Array.prototype.slice;function Y(b,a){var c=L.call(arguments,2);return function(){return b.apply(a,c.concat(L.call(arguments)))}}var M={init:function(b){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var a=b||document;a.createElement("canvas");a.attachEvent("onreadystatechange",Y(this.init_,this,a))}},init_:function(b){b.namespaces.g_vml_|| 15 | b.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML");b.namespaces.g_o_||b.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML");if(!b.styleSheets.ex_canvas_){var a=b.createStyleSheet();a.owningElement.id="ex_canvas_";a.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}g_vml_\\:*{behavior:url(#default#VML)}g_o_\\:*{behavior:url(#default#VML)}"}var c=b.getElementsByTagName("canvas"),d=0;for(;d','","");this.element_.insertAdjacentHTML("BeforeEnd",t.join(""))};i.stroke=function(b){var a=[],c=P(b?this.fillStyle:this.strokeStyle),d=c.color,f=c.alpha*this.globalAlpha;a.push("g.x)g.x=e.x;if(h.y==null||e.yg.y)g.y=e.y}}a.push(' ">');if(b)if(typeof this.fillStyle=="object"){var m=this.fillStyle,r=0,n={x:0,y:0},o=0,q=1;if(m.type_=="gradient"){var t=m.x1_/this.arcScaleX_,E=m.y1_/this.arcScaleY_,p=this.getCoords_(m.x0_/this.arcScaleX_,m.y0_/this.arcScaleY_), 30 | z=this.getCoords_(t,E);r=Math.atan2(z.x-p.x,z.y-p.y)*180/Math.PI;if(r<0)r+=360;if(r<1.0E-6)r=0}else{var p=this.getCoords_(m.x0_,m.y0_),w=g.x-h.x,x=g.y-h.y;n={x:(p.x-h.x)/w,y:(p.y-h.y)/x};w/=this.arcScaleX_*k;x/=this.arcScaleY_*k;var R=s.max(w,x);o=2*m.r0_/R;q=2*m.r1_/R-o}var u=m.colors_;u.sort(function(ba,ca){return ba.offset-ca.offset});var J=u.length,da=u[0].color,ea=u[J-1].color,fa=u[0].alpha*this.globalAlpha,ga=u[J-1].alpha*this.globalAlpha,S=[],l=0;for(;l')}else a.push('');else{var K=this.lineScale_*this.lineWidth;if(K<1)f*=K;a.push("')}a.push("");this.element_.insertAdjacentHTML("beforeEnd",a.join(""))};i.fill=function(){this.stroke(true)};i.closePath=function(){this.currentPath_.push({type:"close"})};i.getCoords_=function(b,a){var c=this.m_;return{x:k*(b*c[0][0]+a*c[1][0]+c[2][0])-v,y:k*(b*c[0][1]+a*c[1][1]+c[2][1])-v}};i.save=function(){var b={};O(this,b);this.aStack_.push(b);this.mStack_.push(this.m_);this.m_=y(I(),this.m_)};i.restore=function(){O(this.aStack_.pop(), 33 | this);this.m_=this.mStack_.pop()};function ha(b){var a=0;for(;a<3;a++){var c=0;for(;c<2;c++)if(!isFinite(b[a][c])||isNaN(b[a][c]))return false}return true}function A(b,a,c){if(!!ha(a)){b.m_=a;if(c)b.lineScale_=W(V(a[0][0]*a[1][1]-a[0][1]*a[1][0]))}}i.translate=function(b,a){A(this,y([[1,0,0],[0,1,0],[b,a,1]],this.m_),false)};i.rotate=function(b){var a=G(b),c=F(b);A(this,y([[a,c,0],[-c,a,0],[0,0,1]],this.m_),false)};i.scale=function(b,a){this.arcScaleX_*=b;this.arcScaleY_*=a;A(this,y([[b,0,0],[0,a, 34 | 0],[0,0,1]],this.m_),true)};i.transform=function(b,a,c,d,f,h){A(this,y([[b,a,0],[c,d,0],[f,h,1]],this.m_),true)};i.setTransform=function(b,a,c,d,f,h){A(this,[[b,a,0],[c,d,0],[f,h,1]],true)};i.clip=function(){};i.arcTo=function(){};i.createPattern=function(){return new U};function D(b){this.type_=b;this.r1_=this.y1_=this.x1_=this.r0_=this.y0_=this.x0_=0;this.colors_=[]}D.prototype.addColorStop=function(b,a){a=P(a);this.colors_.push({offset:b,color:a.color,alpha:a.alpha})};function U(){}G_vmlCanvasManager= 35 | M;CanvasRenderingContext2D=H;CanvasGradient=D;CanvasPattern=U})(); 36 | -------------------------------------------------------------------------------- /views/shared/javascripts/jquery-ui-1.8.1.custom.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery UI 1.8.1 3 | * 4 | * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about) 5 | * Dual licensed under the MIT (MIT-LICENSE.txt) 6 | * and GPL (GPL-LICENSE.txt) licenses. 7 | * 8 | * http://docs.jquery.com/UI 9 | */ 10 | jQuery.ui||function(c){c.ui={version:"1.8.1",plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&&a.element[0].parentNode)for(var e=0;e0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a=0)&&c(a).is(":focusable")}})}(jQuery); 16 | ;/* 17 | * jQuery UI Effects 1.8.1 18 | * 19 | * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about) 20 | * Dual licensed under the MIT (MIT-LICENSE.txt) 21 | * and GPL (GPL-LICENSE.txt) licenses. 22 | * 23 | * http://docs.jquery.com/UI/Effects/ 24 | */ 25 | jQuery.effects||function(f){function k(c){var a;if(c&&c.constructor==Array&&c.length==3)return c;if(a=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c))return[parseInt(a[1],10),parseInt(a[2],10),parseInt(a[3],10)];if(a=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c))return[parseFloat(a[1])*2.55,parseFloat(a[2])*2.55,parseFloat(a[3])*2.55];if(a=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c))return[parseInt(a[1], 26 | 16),parseInt(a[2],16),parseInt(a[3],16)];if(a=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c))return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16)];if(/rgba\(0, 0, 0, 0\)/.exec(c))return l.transparent;return l[f.trim(c).toLowerCase()]}function q(c,a){var b;do{b=f.curCSS(c,a);if(b!=""&&b!="transparent"||f.nodeName(c,"body"))break;a="backgroundColor"}while(c=c.parentNode);return k(b)}function m(){var c=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle, 27 | a={},b,d;if(c&&c.length&&c[0]&&c[c[0]])for(var e=c.length;e--;){b=c[e];if(typeof c[b]=="string"){d=b.replace(/\-(\w)/g,function(g,h){return h.toUpperCase()});a[d]=c[b]}}else for(b in c)if(typeof c[b]==="string")a[b]=c[b];return a}function n(c){var a,b;for(a in c){b=c[a];if(b==null||f.isFunction(b)||a in r||/scrollbar/.test(a)||!/color/i.test(a)&&isNaN(parseFloat(b)))delete c[a]}return c}function s(c,a){var b={_:0},d;for(d in a)if(c[d]!=a[d])b[d]=a[d];return b}function j(c,a,b,d){if(typeof c=="object"){d= 28 | a;b=null;a=c;c=a.effect}if(f.isFunction(a)){d=a;b=null;a={}}if(f.isFunction(b)){d=b;b=null}if(typeof a=="number"||f.fx.speeds[a]){d=b;b=a;a={}}a=a||{};b=b||a.duration;b=f.fx.off?0:typeof b=="number"?b:f.fx.speeds[b]||f.fx.speeds._default;d=d||a.complete;return[c,a,b,d]}f.effects={};f.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","color","outlineColor"],function(c,a){f.fx.step[a]=function(b){if(!b.colorInit){b.start=q(b.elem,a);b.end=k(b.end);b.colorInit= 29 | true}b.elem.style[a]="rgb("+Math.max(Math.min(parseInt(b.pos*(b.end[0]-b.start[0])+b.start[0],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[1]-b.start[1])+b.start[1],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[2]-b.start[2])+b.start[2],10),255),0)+")"}});var l={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189, 30 | 183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255, 31 | 165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},o=["add","remove","toggle"],r={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};f.effects.animateClass=function(c,a,b,d){if(f.isFunction(b)){d=b;b=null}return this.each(function(){var e=f(this),g=e.attr("style")||" ",h=n(m.call(this)),p,t=e.attr("className");f.each(o,function(u, 32 | i){c[i]&&e[i+"Class"](c[i])});p=n(m.call(this));e.attr("className",t);e.animate(s(h,p),a,b,function(){f.each(o,function(u,i){c[i]&&e[i+"Class"](c[i])});if(typeof e.attr("style")=="object"){e.attr("style").cssText="";e.attr("style").cssText=g}else e.attr("style",g);d&&d.apply(this,arguments)})})};f.fn.extend({_addClass:f.fn.addClass,addClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{add:c},a,b,d]):this._addClass(c)},_removeClass:f.fn.removeClass,removeClass:function(c,a,b,d){return a? 33 | f.effects.animateClass.apply(this,[{remove:c},a,b,d]):this._removeClass(c)},_toggleClass:f.fn.toggleClass,toggleClass:function(c,a,b,d,e){return typeof a=="boolean"||a===undefined?b?f.effects.animateClass.apply(this,[a?{add:c}:{remove:c},b,d,e]):this._toggleClass(c,a):f.effects.animateClass.apply(this,[{toggle:c},a,b,d])},switchClass:function(c,a,b,d,e){return f.effects.animateClass.apply(this,[{add:a,remove:c},b,d,e])}});f.extend(f.effects,{version:"1.8.1",save:function(c,a){for(var b=0;b").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0});c.wrap(b);b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(d,e){a[e]=c.css(e);if(isNaN(parseInt(a[e],10)))a[e]="auto"}); 36 | c.css({position:"relative",top:0,left:0})}return b.css(a).show()},removeWrapper:function(c){if(c.parent().is(".ui-effects-wrapper"))return c.parent().replaceWith(c);return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=j.apply(this,arguments);a={options:a[1],duration:a[2],callback:a[3]};var b=f.effects[c];return b&&!f.fx.off?b.call(this,a):this},_show:f.fn.show,show:function(c){if(!c|| 37 | typeof c=="number"||f.fx.speeds[c])return this._show.apply(this,arguments);else{var a=j.apply(this,arguments);a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(!c||typeof c=="number"||f.fx.speeds[c])return this._hide.apply(this,arguments);else{var a=j.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||typeof c=="boolean"||f.isFunction(c))return this.__toggle.apply(this, 38 | arguments);else{var a=j.apply(this,arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c,a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c, 39 | a,b,d,e){if((a/=e/2)<1)return d/2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+ 40 | b},easeInQuint:function(c,a,b,d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2, 41 | 10*(a/e-1))+b},easeOutExpo:function(c,a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)* 42 | a)+1)+b},easeInElastic:function(c,a,b,d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h1&&opts.trigger[0]!=opts.trigger[1]){$(this).bind(opts.trigger[0],function(){this.btOn();}).bind(opts.trigger[1],function(){this.btOff();});}else{$(this).bind(opts.trigger[0],function(){if($(this).hasClass("bt-active")){this.btOff();}else{this.btOn();}});}}}}}this.btOn=function(){if(typeof $(this).data("bt-box")=="object"){this.btOff();}opts.preBuild.apply(this);$(jQuery.bt.vars.closeWhenOpenStack).btOff();$(this).addClass("bt-active "+opts.activeClass);if(contentSelect&&opts.ajaxPath==null){if(opts.killTitle){$(this).attr("title",$(this).attr("bt-xTitle"));}content=$.isFunction(opts.contentSelector)?opts.contentSelector.apply(this):eval(opts.contentSelector);if(opts.killTitle){$(this).attr("title","");}}if(opts.ajaxPath!=null&&content==false){if(typeof opts.ajaxPath=="object"){var url=eval(opts.ajaxPath[0]);url+=opts.ajaxPath[1]?" "+opts.ajaxPath[1]:"";}else{var url=opts.ajaxPath;}var off=url.indexOf(" ");if(off>=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}var cacheData=opts.ajaxCache?$(document.body).data("btCache-"+url.replace(/\./g,"")):null;if(typeof cacheData=="string"){content=selector?$("
    ").append(cacheData.replace(//g,"")).find(selector):cacheData;}else{var target=this;var ajaxOpts=jQuery.extend(false,{type:opts.ajaxType,data:opts.ajaxData,cache:opts.ajaxCache,url:url,complete:function(XMLHttpRequest,textStatus){if(textStatus=="success"||textStatus=="notmodified"){if(opts.ajaxCache){$(document.body).data("btCache-"+url.replace(/\./g,""),XMLHttpRequest.responseText);}ajaxTimeout=false;content=selector?$("
    ").append(XMLHttpRequest.responseText.replace(//g,"")).find(selector):XMLHttpRequest.responseText;}else{if(textStatus=="timeout"){ajaxTimeout=true;}content=opts.ajaxError.replace(/%error/g,XMLHttpRequest.statusText);}if($(target).hasClass("bt-active")){target.btOn();}}},opts.ajaxOpts);jQuery.ajax(ajaxOpts);content=opts.ajaxLoading;}}var shadowMarginX=0;var shadowMarginY=0;var shadowShiftX=0;var shadowShiftY=0;if(opts.shadow&&!shadowSupport()){opts.shadow=false;jQuery.extend(opts,opts.noShadowOpts);}if(opts.shadow){if(opts.shadowBlur>Math.abs(opts.shadowOffsetX)){shadowMarginX=opts.shadowBlur*2;}else{shadowMarginX=opts.shadowBlur+Math.abs(opts.shadowOffsetX);}shadowShiftX=(opts.shadowBlur-opts.shadowOffsetX)>0?opts.shadowBlur-opts.shadowOffsetX:0;if(opts.shadowBlur>Math.abs(opts.shadowOffsetY)){shadowMarginY=opts.shadowBlur*2;}else{shadowMarginY=opts.shadowBlur+Math.abs(opts.shadowOffsetY);}shadowShiftY=(opts.shadowBlur-opts.shadowOffsetY)>0?opts.shadowBlur-opts.shadowOffsetY:0;}if(opts.offsetParent){var offsetParent=$(opts.offsetParent);var offsetParentPos=offsetParent.offset();var pos=$(this).offset();var top=numb(pos.top)-numb(offsetParentPos.top)+numb($(this).css("margin-top"))-shadowShiftY;var left=numb(pos.left)-numb(offsetParentPos.left)+numb($(this).css("margin-left"))-shadowShiftX;}else{var offsetParent=($(this).css("position")=="absolute")?$(this).parents().eq(0).offsetParent():$(this).offsetParent();var pos=$(this).btPosition();var top=numb(pos.top)+numb($(this).css("margin-top"))-shadowShiftY;var left=numb(pos.left)+numb($(this).css("margin-left"))-shadowShiftX;}var width=$(this).btOuterWidth();var height=$(this).outerHeight();if(typeof content=="object"){var original=content;var clone=$(original).clone(true).show();var origClones=$(original).data("bt-clones")||[];origClones.push(clone);$(original).data("bt-clones",origClones);$(clone).data("bt-orig",original);$(this).data("bt-content-orig",{original:original,clone:clone});content=clone;}if(typeof content=="null"||content==""){return;}var $text=$('
    ').append(content).css({padding:opts.padding,position:"absolute",width:(opts.shrinkToFit?"auto":opts.width),zIndex:opts.textzIndex,left:shadowShiftX,top:shadowShiftY}).css(opts.cssStyles);var $box=$('
    ').append($text).addClass(opts.cssClass).css({position:"absolute",width:opts.width,zIndex:opts.wrapperzIndex,visibility:"hidden"}).appendTo(offsetParent);if(jQuery.fn.bgiframe){$text.bgiframe();$box.bgiframe();}$(this).data("bt-box",$box);var scrollTop=numb($(document).scrollTop());var scrollLeft=numb($(document).scrollLeft());var docWidth=numb($(window).width());var docHeight=numb($(window).height());var winRight=scrollLeft+docWidth;var winBottom=scrollTop+docHeight;var space=new Object();var thisOffset=$(this).offset();space.top=thisOffset.top-scrollTop;space.bottom=docHeight-((thisOffset+height)-scrollTop);space.left=thisOffset.left-scrollLeft;space.right=docWidth-((thisOffset.left+width)-scrollLeft);var textOutHeight=numb($text.outerHeight());var textOutWidth=numb($text.btOuterWidth());if(opts.positions.constructor==String){opts.positions=opts.positions.replace(/ /,"").split(",");}if(opts.positions[0]=="most"){var position="top";for(var pig in space){position=space[pig]>space[position]?pig:position;}}else{for(var x in opts.positions){var position=opts.positions[x];if((position=="left"||position=="right")&&space[position]>textOutWidth+opts.spikeLength){break;}else{if((position=="top"||position=="bottom")&&space[position]>textOutHeight+opts.spikeLength){break;}}}}var horiz=left+((width-textOutWidth)*0.5);var vert=top+((height-textOutHeight)*0.5);var points=new Array();var textTop,textLeft,textRight,textBottom,textTopSpace,textBottomSpace,textLeftSpace,textRightSpace,crossPoint,textCenter,spikePoint;switch(position){case"top":$text.css("margin-bottom",opts.spikeLength+"px");$box.css({top:(top-$text.outerHeight(true))+opts.overlap,left:horiz});textRightSpace=(winRight-opts.windowMargin)-($text.offset().left+$text.btOuterWidth(true));var xShift=shadowShiftX;if(textRightSpace<0){$box.css("left",(numb($box.css("left"))+textRightSpace)+"px");xShift-=textRightSpace;}textLeftSpace=($text.offset().left+numb($text.css("margin-left")))-(scrollLeft+opts.windowMargin);if(textLeftSpace<0){$box.css("left",(numb($box.css("left"))-textLeftSpace)+"px");xShift+=textLeftSpace;}textTop=$text.btPosition().top+numb($text.css("margin-top"));textLeft=$text.btPosition().left+numb($text.css("margin-left"));textRight=textLeft+$text.btOuterWidth();textBottom=textTop+$text.outerHeight();textCenter={x:textLeft+($text.btOuterWidth()*opts.centerPointX),y:textTop+($text.outerHeight()*opts.centerPointY)};points[points.length]=spikePoint={y:textBottom+opts.spikeLength,x:((textRight-textLeft)*0.5)+xShift,type:"spike"};crossPoint=findIntersectX(spikePoint.x,spikePoint.y,textCenter.x,textCenter.y,textBottom);crossPoint.x=crossPoint.x(textRight-opts.spikeGirth/2)-opts.cornerRadius?(textRight-opts.spikeGirth/2)-opts.CornerRadius:crossPoint.x;points[points.length]={x:crossPoint.x-(opts.spikeGirth/2),y:textBottom,type:"join"};points[points.length]={x:textLeft,y:textBottom,type:"corner"};points[points.length]={x:textLeft,y:textTop,type:"corner"};points[points.length]={x:textRight,y:textTop,type:"corner"};points[points.length]={x:textRight,y:textBottom,type:"corner"};points[points.length]={x:crossPoint.x+(opts.spikeGirth/2),y:textBottom,type:"join"};points[points.length]=spikePoint;break;case"left":$text.css("margin-right",opts.spikeLength+"px");$box.css({top:vert+"px",left:((left-$text.btOuterWidth(true))+opts.overlap)+"px"});textBottomSpace=(winBottom-opts.windowMargin)-($text.offset().top+$text.outerHeight(true));var yShift=shadowShiftY;if(textBottomSpace<0){$box.css("top",(numb($box.css("top"))+textBottomSpace)+"px");yShift-=textBottomSpace;}textTopSpace=($text.offset().top+numb($text.css("margin-top")))-(scrollTop+opts.windowMargin);if(textTopSpace<0){$box.css("top",(numb($box.css("top"))-textTopSpace)+"px");yShift+=textTopSpace;}textTop=$text.btPosition().top+numb($text.css("margin-top"));textLeft=$text.btPosition().left+numb($text.css("margin-left"));textRight=textLeft+$text.btOuterWidth();textBottom=textTop+$text.outerHeight();textCenter={x:textLeft+($text.btOuterWidth()*opts.centerPointX),y:textTop+($text.outerHeight()*opts.centerPointY)};points[points.length]=spikePoint={x:textRight+opts.spikeLength,y:((textBottom-textTop)*0.5)+yShift,type:"spike"};crossPoint=findIntersectY(spikePoint.x,spikePoint.y,textCenter.x,textCenter.y,textRight);crossPoint.y=crossPoint.y(textBottom-opts.spikeGirth/2)-opts.cornerRadius?(textBottom-opts.spikeGirth/2)-opts.cornerRadius:crossPoint.y;points[points.length]={x:textRight,y:crossPoint.y+opts.spikeGirth/2,type:"join"};points[points.length]={x:textRight,y:textBottom,type:"corner"};points[points.length]={x:textLeft,y:textBottom,type:"corner"};points[points.length]={x:textLeft,y:textTop,type:"corner"};points[points.length]={x:textRight,y:textTop,type:"corner"};points[points.length]={x:textRight,y:crossPoint.y-opts.spikeGirth/2,type:"join"};points[points.length]=spikePoint;break;case"bottom":$text.css("margin-top",opts.spikeLength+"px");$box.css({top:(top+height)-opts.overlap,left:horiz});textRightSpace=(winRight-opts.windowMargin)-($text.offset().left+$text.btOuterWidth(true));var xShift=shadowShiftX;if(textRightSpace<0){$box.css("left",(numb($box.css("left"))+textRightSpace)+"px");xShift-=textRightSpace;}textLeftSpace=($text.offset().left+numb($text.css("margin-left")))-(scrollLeft+opts.windowMargin);if(textLeftSpace<0){$box.css("left",(numb($box.css("left"))-textLeftSpace)+"px");xShift+=textLeftSpace;}textTop=$text.btPosition().top+numb($text.css("margin-top"));textLeft=$text.btPosition().left+numb($text.css("margin-left"));textRight=textLeft+$text.btOuterWidth();textBottom=textTop+$text.outerHeight();textCenter={x:textLeft+($text.btOuterWidth()*opts.centerPointX),y:textTop+($text.outerHeight()*opts.centerPointY)};points[points.length]=spikePoint={x:((textRight-textLeft)*0.5)+xShift,y:shadowShiftY,type:"spike"};crossPoint=findIntersectX(spikePoint.x,spikePoint.y,textCenter.x,textCenter.y,textTop);crossPoint.x=crossPoint.x(textRight-opts.spikeGirth/2)-opts.cornerRadius?(textRight-opts.spikeGirth/2)-opts.cornerRadius:crossPoint.x;points[points.length]={x:crossPoint.x+opts.spikeGirth/2,y:textTop,type:"join"};points[points.length]={x:textRight,y:textTop,type:"corner"};points[points.length]={x:textRight,y:textBottom,type:"corner"};points[points.length]={x:textLeft,y:textBottom,type:"corner"};points[points.length]={x:textLeft,y:textTop,type:"corner"};points[points.length]={x:crossPoint.x-(opts.spikeGirth/2),y:textTop,type:"join"};points[points.length]=spikePoint;break;case"right":$text.css("margin-left",(opts.spikeLength+"px"));$box.css({top:vert+"px",left:((left+width)-opts.overlap)+"px"});textBottomSpace=(winBottom-opts.windowMargin)-($text.offset().top+$text.outerHeight(true));var yShift=shadowShiftY;if(textBottomSpace<0){$box.css("top",(numb($box.css("top"))+textBottomSpace)+"px");yShift-=textBottomSpace;}textTopSpace=($text.offset().top+numb($text.css("margin-top")))-(scrollTop+opts.windowMargin);if(textTopSpace<0){$box.css("top",(numb($box.css("top"))-textTopSpace)+"px");yShift+=textTopSpace;}textTop=$text.btPosition().top+numb($text.css("margin-top"));textLeft=$text.btPosition().left+numb($text.css("margin-left"));textRight=textLeft+$text.btOuterWidth();textBottom=textTop+$text.outerHeight();textCenter={x:textLeft+($text.btOuterWidth()*opts.centerPointX),y:textTop+($text.outerHeight()*opts.centerPointY)};points[points.length]=spikePoint={x:shadowShiftX,y:((textBottom-textTop)*0.5)+yShift,type:"spike"};crossPoint=findIntersectY(spikePoint.x,spikePoint.y,textCenter.x,textCenter.y,textLeft);crossPoint.y=crossPoint.y(textBottom-opts.spikeGirth/2)-opts.cornerRadius?(textBottom-opts.spikeGirth/2)-opts.cornerRadius:crossPoint.y;points[points.length]={x:textLeft,y:crossPoint.y-opts.spikeGirth/2,type:"join"};points[points.length]={x:textLeft,y:textTop,type:"corner"};points[points.length]={x:textRight,y:textTop,type:"corner"};points[points.length]={x:textRight,y:textBottom,type:"corner"};points[points.length]={x:textLeft,y:textBottom,type:"corner"};points[points.length]={x:textLeft,y:crossPoint.y+opts.spikeGirth/2,type:"join"};points[points.length]=spikePoint;break;}var canvas=document.createElement("canvas");$(canvas).attr("width",(numb($text.btOuterWidth(true))+opts.strokeWidth*2+shadowMarginX)).attr("height",(numb($text.outerHeight(true))+opts.strokeWidth*2+shadowMarginY)).appendTo($box).css({position:"absolute",zIndex:opts.boxzIndex});if(typeof G_vmlCanvasManager!="undefined"){canvas=G_vmlCanvasManager.initElement(canvas);}if(opts.cornerRadius>0){var newPoints=new Array();var newPoint;for(var i=0;i0){$box.css("top",(numb($box.css("top"))-(opts.shadowOffsetX+opts.shadowBlur-shadowOverlap)));}break;case"right":if(shadowShiftX-shadowOverlap>0){$box.css("left",(numb($box.css("left"))+shadowShiftX-shadowOverlap));}break;case"bottom":if(shadowShiftY-shadowOverlap>0){$box.css("top",(numb($box.css("top"))+shadowShiftY-shadowOverlap));}break;case"left":if(opts.shadowOffsetY+opts.shadowBlur-shadowOverlap>0){$box.css("left",(numb($box.css("left"))-(opts.shadowOffsetY+opts.shadowBlur-shadowOverlap)));}break;}}drawIt.apply(ctx,[points],opts.strokeWidth);ctx.fillStyle=opts.fill;if(opts.shadow){ctx.shadowOffsetX=opts.shadowOffsetX;ctx.shadowOffsetY=opts.shadowOffsetY;ctx.shadowBlur=opts.shadowBlur;ctx.shadowColor=opts.shadowColor;}ctx.closePath();ctx.fill();if(opts.strokeWidth>0){ctx.shadowColor="rgba(0, 0, 0, 0)";ctx.lineWidth=opts.strokeWidth;ctx.strokeStyle=opts.strokeStyle;ctx.beginPath();drawIt.apply(ctx,[points],opts.strokeWidth);ctx.closePath();ctx.stroke();}opts.preShow.apply(this,[$box[0]]);$box.css({display:"none",visibility:"visible"});opts.showTip.apply(this,[$box[0]]);if(opts.overlay){var overlay=$('
    ').css({position:"absolute",backgroundColor:"blue",top:top,left:left,width:width,height:height,opacity:".2"}).appendTo(offsetParent);$(this).data("overlay",overlay);}if((opts.ajaxPath!=null&&opts.ajaxCache==false)||ajaxTimeout){content=false;}if(opts.clickAnywhereToClose){jQuery.bt.vars.clickAnywhereStack.push(this);$(document).click(jQuery.bt.docClick);}if(opts.closeWhenOthersOpen){jQuery.bt.vars.closeWhenOpenStack.push(this);}opts.postShow.apply(this,[$box[0]]);};this.btOff=function(){var box=$(this).data("bt-box");opts.preHide.apply(this,[box]);var i=this;i.btCleanup=function(){var box=$(i).data("bt-box");var contentOrig=$(i).data("bt-content-orig");var overlay=$(i).data("bt-overlay");if(typeof box=="object"){$(box).remove();$(i).removeData("bt-box");}if(typeof contentOrig=="object"){var clones=$(contentOrig.original).data("bt-clones");$(contentOrig).data("bt-clones",arrayRemove(clones,contentOrig.clone));}if(typeof overlay=="object"){$(overlay).remove();$(i).removeData("bt-overlay");}jQuery.bt.vars.clickAnywhereStack=arrayRemove(jQuery.bt.vars.clickAnywhereStack,i);jQuery.bt.vars.closeWhenOpenStack=arrayRemove(jQuery.bt.vars.closeWhenOpenStack,i);$(i).removeClass("bt-active "+opts.activeClass);opts.postHide.apply(i);};opts.hideTip.apply(this,[box,i.btCleanup]);};var refresh=this.btRefresh=function(){this.btOff();this.btOn();};});function drawIt(points,strokeWidth){this.moveTo(points[0].x,points[0].y);for(i=1;i=3.1){return true;}}}catch(err){}return false;}function betweenPoint(point1,point2,dist){var y,x;if(point1.x==point2.x){y=point1.yarcEnd.y){startAngle=(Math.PI/180)*180;endAngle=(Math.PI/180)*90;}else{startAngle=(Math.PI/180)*90;endAngle=0;}}else{if(arcStart.y>arcEnd.y){startAngle=(Math.PI/180)*270;endAngle=(Math.PI/180)*180;}else{startAngle=0;endAngle=(Math.PI/180)*270;}}return{x:x,y:y,type:"center",startAngle:startAngle,endAngle:endAngle};}function findIntersect(r1x1,r1y1,r1x2,r1y2,r2x1,r2y1,r2x2,r2y2){if(r2x1==r2x2){return findIntersectY(r1x1,r1y1,r1x2,r1y2,r2x1);}if(r2y1==r2y2){return findIntersectX(r1x1,r1y1,r1x2,r1y2,r2y1);}var r1m=(r1y1-r1y2)/(r1x1-r1x2);var r1b=r1y1-(r1m*r1x1);var r2m=(r2y1-r2y2)/(r2x1-r2x2);var r2b=r2y1-(r2m*r2x1);var x=(r2b-r1b)/(r1m-r2m);var y=r1m*x+r1b;return{x:x,y:y};}function findIntersectY(r1x1,r1y1,r1x2,r1y2,x){if(r1y1==r1y2){return{x:x,y:r1y1};}var r1m=(r1y1-r1y2)/(r1x1-r1x2);var r1b=r1y1-(r1m*r1x1);var y=r1m*x+r1b;return{x:x,y:y};}function findIntersectX(r1x1,r1y1,r1x2,r1y2,y){if(r1x1==r1x2){return{x:r1x1,y:y};}var r1m=(r1y1-r1y2)/(r1x1-r1x2);var r1b=r1y1-(r1m*r1x1);var x=(y-r1b)/r1m;return{x:x,y:y};}};jQuery.fn.btPosition=function(){function num(elem,prop){return elem[0]&&parseInt(jQuery.curCSS(elem[0],prop,true),10)||0;}var left=0,top=0,results;if(this[0]){var offsetParent=this.offsetParent(),offset=this.offset(),parentOffset=/^body|html$/i.test(offsetParent[0].tagName)?{top:0,left:0}:offsetParent.offset();offset.top-=num(this,"marginTop");offset.left-=num(this,"marginLeft");parentOffset.top+=num(offsetParent,"borderTopWidth");parentOffset.left+=num(offsetParent,"borderLeftWidth");results={top:offset.top-parentOffset.top,left:offset.left-parentOffset.left};}return results;};jQuery.fn.btOuterWidth=function(margin){function num(elem,prop){return elem[0]&&parseInt(jQuery.curCSS(elem[0],prop,true),10)||0;}return this["innerWidth"]()+num(this,"borderLeftWidth")+num(this,"borderRightWidth")+(margin?num(this,"marginLeft")+num(this,"marginRight"):0);};jQuery.fn.btOn=function(){return this.each(function(index){if(jQuery.isFunction(this.btOn)){this.btOn();}});};jQuery.fn.btOff=function(){return this.each(function(index){if(jQuery.isFunction(this.btOff)){this.btOff();}});};jQuery.bt.vars={clickAnywhereStack:[],closeWhenOpenStack:[]};jQuery.bt.docClick=function(e){if(!e){var e=window.event;}if(!$(e.target).parents().andSelf().filter(".bt-wrapper, .bt-active").length&&jQuery.bt.vars.clickAnywhereStack.length){$(jQuery.bt.vars.clickAnywhereStack).btOff();$(document).unbind("click",jQuery.bt.docClick);}};jQuery.bt.defaults={trigger:"hover",clickAnywhereToClose:true,closeWhenOthersOpen:false,shrinkToFit:false,width:"200px",padding:"10px",spikeGirth:10,spikeLength:15,overlap:0,overlay:false,killTitle:true,textzIndex:9999,boxzIndex:9998,wrapperzIndex:9997,offsetParent:null,positions:["most"],fill:"rgb(255, 255, 102)",windowMargin:10,strokeWidth:1,strokeStyle:"#000",cornerRadius:5,centerPointX:0.5,centerPointY:0.5,shadow:false,shadowOffsetX:2,shadowOffsetY:2,shadowBlur:3,shadowColor:"#000",shadowOverlap:false,noShadowOpts:{strokeStyle:"#999"},cssClass:"",cssStyles:{},activeClass:"bt-active",contentSelector:"$(this).attr('title')",ajaxPath:null,ajaxError:"ERROR: %error",ajaxLoading:"Loading...",ajaxData:{},ajaxType:"GET",ajaxCache:true,ajaxOpts:{},preBuild:function(){},preShow:function(box){},showTip:function(box){$(box).show();},postShow:function(box){},preHide:function(box){},hideTip:function(box,callback){$(box).hide();callback();},postHide:function(){},hoverIntentOpts:{interval:300,timeout:500}};jQuery.bt.options={};})(jQuery); -------------------------------------------------------------------------------- /views/shared/javascripts/jquery.colorbox-min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | jQuery Colorbox v1.4.13 - 2013-04-11 3 | (c) 2013 Jack Moore - jacklmoore.com/colorbox 4 | license: http://www.opensource.org/licenses/mit-license.php 5 | */ 6 | (function(t,e,i){function o(i,o,n){var r=e.createElement(i);return o&&(r.id=te+o),n&&(r.style.cssText=n),t(r)}function n(){return i.innerHeight?i.innerHeight:t(i).height()}function r(t){var e=H.length,i=(j+t)%e;return 0>i?e+i:i}function h(t,e){return Math.round((/%/.test(t)?("x"===e?E.width():n())/100:1)*parseInt(t,10))}function l(t,e){return t.photo||t.photoRegex.test(e)}function s(t,e){return t.retinaUrl&&i.devicePixelRatio>1?e.replace(t.photoRegex,t.retinaSuffix):e}function a(t){"contains"in x[0]&&!x[0].contains(t.target)&&(t.stopPropagation(),x.focus())}function d(){var e,i=t.data(O,Z);null==i?(D=t.extend({},Y),console&&console.log&&console.log("Error: cboxElement missing settings object")):D=t.extend({},i);for(e in D)t.isFunction(D[e])&&"on"!==e.slice(0,2)&&(D[e]=D[e].call(O));D.rel=D.rel||O.rel||t(O).data("rel")||"nofollow",D.href=D.href||t(O).attr("href"),D.title=D.title||O.title,"string"==typeof D.href&&(D.href=t.trim(D.href))}function c(i,o){t(e).trigger(i),se.trigger(i),t.isFunction(o)&&o.call(O)}function u(){var t,e,i,o,n,r=te+"Slideshow_",h="click."+te;D.slideshow&&H[1]?(e=function(){clearTimeout(t)},i=function(){(D.loop||H[j+1])&&(t=setTimeout(J.next,D.slideshowSpeed))},o=function(){M.html(D.slideshowStop).unbind(h).one(h,n),se.bind(ne,i).bind(oe,e).bind(re,n),x.removeClass(r+"off").addClass(r+"on")},n=function(){e(),se.unbind(ne,i).unbind(oe,e).unbind(re,n),M.html(D.slideshowStart).unbind(h).one(h,function(){J.next(),o()}),x.removeClass(r+"on").addClass(r+"off")},D.slideshowAuto?o():n()):x.removeClass(r+"off "+r+"on")}function f(i){G||(O=i,d(),H=t(O),j=0,"nofollow"!==D.rel&&(H=t("."+ee).filter(function(){var e,i=t.data(this,Z);return i&&(e=t(this).data("rel")||i.rel||this.rel),e===D.rel}),j=H.index(O),-1===j&&(H=H.add(O),j=H.length-1)),g.css({opacity:parseFloat(D.opacity),cursor:D.overlayClose?"pointer":"auto",visibility:"visible"}).show(),V&&x.add(g).removeClass(V),D.className&&x.add(g).addClass(D.className),V=D.className,K.html(D.close).show(),$||($=q=!0,x.css({visibility:"hidden",display:"block"}),W=o(ae,"LoadedContent","width:0; height:0; overflow:hidden").appendTo(v),B=b.height()+k.height()+v.outerHeight(!0)-v.height(),N=C.width()+T.width()+v.outerWidth(!0)-v.width(),z=W.outerHeight(!0),A=W.outerWidth(!0),D.w=h(D.initialWidth,"x"),D.h=h(D.initialHeight,"y"),J.position(),u(),c(ie,D.onOpen),_.add(F).hide(),x.focus(),e.addEventListener&&(e.addEventListener("focus",a,!0),se.one(he,function(){e.removeEventListener("focus",a,!0)})),D.returnFocus&&se.one(he,function(){t(O).focus()})),w())}function p(){!x&&e.body&&(X=!1,E=t(i),x=o(ae).attr({id:Z,"class":t.support.opacity===!1?te+"IE":"",role:"dialog",tabindex:"-1"}).hide(),g=o(ae,"Overlay").hide(),S=o(ae,"LoadingOverlay").add(o(ae,"LoadingGraphic")),y=o(ae,"Wrapper"),v=o(ae,"Content").append(F=o(ae,"Title"),I=o(ae,"Current"),P=t('