Repository: RussCoder/djvujs Branch: master Commit: b41bd0557bd5 Files: 308 Total size: 724.3 KB Directory structure: gitextract_y7vg5pf7/ ├── .gitattributes ├── .gitignore ├── .js ├── GNU_GPL_v2 ├── LICENSE.md ├── README.md ├── THE_UNLICENSE ├── TRANSLATION.md ├── extension/ │ ├── background.js │ ├── content.js │ ├── initializer.js │ ├── manifest_v2.json │ ├── manifest_v3.json │ └── viewer.html ├── library/ │ ├── .gitignore │ ├── API.md │ ├── CHANGELOG.md │ ├── README.md │ ├── app/ │ │ ├── app.css │ │ ├── app.html │ │ └── app.js │ ├── assets/ │ │ ├── DjVu3Spec.djvu │ │ ├── DjVu3Spec_5-10.djvu │ │ ├── DjVu3Spec_bundled.djvu │ │ ├── DjVu3Spec_contents.json │ │ ├── DjVu3Spec_indirect/ │ │ │ ├── dict0020.iff │ │ │ ├── dict0040.iff │ │ │ ├── dict0060.iff │ │ │ ├── dict0071.iff │ │ │ ├── index.djvu │ │ │ ├── p0001_1.djvu │ │ │ ├── p0002.djvu │ │ │ ├── p0003.djvu │ │ │ ├── p0004.djvu │ │ │ ├── p0005.djvu │ │ │ ├── p0006.djvu │ │ │ ├── p0007.djvu │ │ │ ├── p0008.djvu │ │ │ ├── p0009.djvu │ │ │ ├── p0010.djvu │ │ │ ├── p0011.djvu │ │ │ ├── p0012.djvu │ │ │ ├── p0013.djvu │ │ │ ├── p0014.djvu │ │ │ ├── p0015.djvu │ │ │ ├── p0016.djvu │ │ │ ├── p0017.djvu │ │ │ ├── p0018.djvu │ │ │ ├── p0019.djvu │ │ │ ├── p0020.djvu │ │ │ ├── p0021.djvu │ │ │ ├── p0022.djvu │ │ │ ├── p0023.djvu │ │ │ ├── p0024.djvu │ │ │ ├── p0025.djvu │ │ │ ├── p0026.djvu │ │ │ ├── p0027.djvu │ │ │ ├── p0028.djvu │ │ │ ├── p0029.djvu │ │ │ ├── p0030.djvu │ │ │ ├── p0031.djvu │ │ │ ├── p0032.djvu │ │ │ ├── p0033.djvu │ │ │ ├── p0034.djvu │ │ │ ├── p0035.djvu │ │ │ ├── p0036.djvu │ │ │ ├── p0037.djvu │ │ │ ├── p0038.djvu │ │ │ ├── p0039.djvu │ │ │ ├── p0040.djvu │ │ │ ├── p0041.djvu │ │ │ ├── p0042.djvu │ │ │ ├── p0043.djvu │ │ │ ├── p0044.djvu │ │ │ ├── p0045.djvu │ │ │ ├── p0046.djvu │ │ │ ├── p0047.djvu │ │ │ ├── p0048.djvu │ │ │ ├── p0049.djvu │ │ │ ├── p0050.djvu │ │ │ ├── p0051.djvu │ │ │ ├── p0052.djvu │ │ │ ├── p0053.djvu │ │ │ ├── p0054.djvu │ │ │ ├── p0055.djvu │ │ │ ├── p0056.djvu │ │ │ ├── p0057.djvu │ │ │ ├── p0058.djvu │ │ │ ├── p0059.djvu │ │ │ ├── p0060.djvu │ │ │ ├── p0061.djvu │ │ │ ├── p0062.djvu │ │ │ ├── p0063.djvu │ │ │ ├── p0064.djvu │ │ │ ├── p0065.djvu │ │ │ ├── p0066.djvu │ │ │ ├── p0067.djvu │ │ │ ├── p0068.djvu │ │ │ ├── p0069.djvu │ │ │ ├── p0070.djvu │ │ │ ├── p0071.djvu │ │ │ ├── thum0001.thumb │ │ │ ├── thum0002.thumb │ │ │ ├── thum0003.thumb │ │ │ ├── thum0004.thumb │ │ │ ├── thum0005.thumb │ │ │ ├── thum0006.thumb │ │ │ ├── thum0007.thumb │ │ │ ├── thum0008.thumb │ │ │ ├── thum0009.thumb │ │ │ └── thum0010.thumb │ │ ├── big-scanned-page.djvu │ │ ├── boy.djvu │ │ ├── boy_and_chicken.djvu │ │ ├── boy_jb2.djvu │ │ ├── boy_jb2_rotate180.djvu │ │ ├── boy_jb2_rotate270.djvu │ │ ├── boy_jb2_rotate90.djvu │ │ ├── carte.djvu │ │ ├── ccitt_2.djvu │ │ ├── century_dict/ │ │ │ ├── index08.djvu │ │ │ ├── p6683.djvu │ │ │ └── p6698.djvu │ │ ├── chicken.djvu │ │ ├── colorbook.djvu │ │ ├── czech.djvu │ │ ├── czech_1-3.djvu │ │ ├── czech_indirect/ │ │ │ ├── anno0001.iff │ │ │ ├── black_1.djvu │ │ │ ├── dict0085.iff │ │ │ ├── index.djvu │ │ │ ├── p0001.djvu │ │ │ ├── p0002.djvu │ │ │ └── slovnik │ │ ├── deutsch.djvu │ │ ├── djvu3spec+.djvu │ │ ├── happy_birthday.djvu │ │ ├── history.djvu │ │ ├── history_2.djvu │ │ ├── irish.djvu │ │ ├── links.djvu │ │ ├── malliavin.djvu │ │ ├── navm_fgbz.djvu │ │ ├── polish_indirect/ │ │ │ ├── index.djvu │ │ │ ├── shared_anno.iff │ │ │ └── sw1-0002.djvu │ │ ├── problem_page.djvu │ │ ├── slow.djvu │ │ └── vega.djvu │ ├── debug/ │ │ ├── async.html │ │ ├── css/ │ │ │ └── style.css │ │ ├── examples.html │ │ ├── index.html │ │ ├── js/ │ │ │ ├── DjVuGlobals.js │ │ │ ├── DjVuViewer.js │ │ │ ├── async.js │ │ │ ├── debug.js │ │ │ ├── examples.js │ │ │ ├── handler.js │ │ │ ├── initScript.js │ │ │ └── reloader.js │ │ └── sync.html │ ├── package.json │ ├── rollup.config.js │ ├── server.js │ ├── src/ │ │ ├── ByteStream.js │ │ ├── ByteStreamWriter.js │ │ ├── DjVu.js │ │ ├── DjVuDocument.js │ │ ├── DjVuErrors.js │ │ ├── DjVuPage.js │ │ ├── DjVuWorker.js │ │ ├── DjVuWorkerScript.js │ │ ├── DjVuWriter.js │ │ ├── ZPCodec.js │ │ ├── bzz/ │ │ │ ├── BZZDecoder.js │ │ │ └── BZZEncoder.js │ │ ├── chunks/ │ │ │ ├── DirmChunk.js │ │ │ ├── DjViChunk.js │ │ │ ├── DjVuAnno.js │ │ │ ├── DjVuPalette.js │ │ │ ├── DjVuText.js │ │ │ ├── IFFChunks.js │ │ │ ├── NavmChunk.js │ │ │ └── ThumChunk.js │ │ ├── index.js │ │ ├── iw44/ │ │ │ ├── IWCodecBaseClass.js │ │ │ ├── IWDecoder.js │ │ │ ├── IWEncoder.js │ │ │ ├── IWImage.js │ │ │ ├── IWImageWriter.js │ │ │ └── IWStructures.js │ │ ├── jb2/ │ │ │ ├── JB2Codec.js │ │ │ ├── JB2Dict.js │ │ │ ├── JB2Image.js │ │ │ └── JB2Structures.js │ │ └── methods/ │ │ ├── bundle.js │ │ └── load.js │ └── tests/ │ ├── embed.html │ ├── tests.css │ ├── tests.html │ └── tests.js ├── package.json └── viewer/ ├── .gitignore ├── CHANGELOG.md ├── cypress/ │ ├── e2e/ │ │ ├── fullscreen_mode.cy.js │ │ ├── initial_screen.cy.js │ │ ├── menu.cy.js │ │ ├── mobile_version.cy.js │ │ ├── modal_windows.cy.js │ │ └── toolbar.cy.js │ ├── shared.js │ └── utils.js ├── cypress.config.js ├── index.html ├── jsconfig.json ├── package.json ├── public/ │ └── manifest.json ├── src/ │ ├── App.test.js │ ├── DjVu.js │ ├── DjVuViewer.jsx │ ├── actions/ │ │ └── actions.js │ ├── components/ │ │ ├── App.jsx │ │ ├── AppContext.jsx │ │ ├── ErrorPage.jsx │ │ ├── FileBlock.jsx │ │ ├── FileLoadingScreen.jsx │ │ ├── ImageBlock/ │ │ │ ├── CanvasImage.jsx │ │ │ ├── ComplexImage.jsx │ │ │ ├── ImageBlock.jsx │ │ │ ├── TextLayer.jsx │ │ │ └── VirtualList.jsx │ │ ├── InitialScreen/ │ │ │ ├── FileZone.jsx │ │ │ ├── InitialScreen.jsx │ │ │ ├── LinkBlock.jsx │ │ │ └── ThemeSwitcher.jsx │ │ ├── Language/ │ │ │ ├── AddLanguageButton.jsx │ │ │ ├── IncompleteTranslationWindow.jsx │ │ │ ├── LanguagePanel.jsx │ │ │ ├── LanguageSelector.jsx │ │ │ └── LanguageWarningSign.jsx │ │ ├── LeftPanel/ │ │ │ ├── ContentsPanel.jsx │ │ │ ├── LeftPanel.jsx │ │ │ └── TreeItem.jsx │ │ ├── LoadingLayer.jsx │ │ ├── Main.jsx │ │ ├── Menu.jsx │ │ ├── ModalWindows/ │ │ │ ├── ErrorWindow.jsx │ │ │ ├── HelpWindow.jsx │ │ │ ├── ModalWindow.jsx │ │ │ ├── OptionsWindow.jsx │ │ │ ├── PrintDialog.jsx │ │ │ └── SaveDialog.jsx │ │ ├── StyledPrimitives.jsx │ │ ├── TextBlock.jsx │ │ ├── Toolbar/ │ │ │ ├── ContentsButton.jsx │ │ │ ├── CursorModeButtonGroup.jsx │ │ │ ├── HideButton.jsx │ │ │ ├── MenuButton.jsx │ │ │ ├── PageNumber.jsx │ │ │ ├── PageNumberBlock.jsx │ │ │ ├── PinButton.jsx │ │ │ ├── RotationControl.jsx │ │ │ ├── ScaleGizmo.jsx │ │ │ ├── Toolbar.jsx │ │ │ └── ViewModeButtons.jsx │ │ ├── Translation.jsx │ │ ├── cssMixins.js │ │ ├── helpers.js │ │ └── misc/ │ │ ├── CloseButton.jsx │ │ ├── FullPageViewButton.jsx │ │ ├── FullscreenButton.jsx │ │ ├── HelpButton.jsx │ │ ├── LoadingPhrase.jsx │ │ ├── OptionsButton.jsx │ │ ├── ProgressBar.jsx │ │ ├── SaveButton.jsx │ │ └── SaveNotification.jsx │ ├── constants/ │ │ ├── Constants.js │ │ ├── actionTypes.js │ │ └── index.js │ ├── hotkeys.js │ ├── index.js │ ├── locales/ │ │ ├── ChineseSimplified.js │ │ ├── English.js │ │ ├── French.js │ │ ├── Italian.js │ │ ├── Portuguese.js │ │ ├── Russian.js │ │ ├── Spanish.js │ │ ├── Swedish.js │ │ ├── Ukrainian.js │ │ └── index.js │ ├── reducers/ │ │ ├── commonReducer.js │ │ ├── fileLoadingReducer.js │ │ ├── fileProcessingReducer.js │ │ ├── index.js │ │ ├── pageReducer.js │ │ └── printReducer.js │ ├── sagas/ │ │ ├── ContinuousScrollManager.js │ │ ├── PageStorage.js │ │ ├── PagesCache.js │ │ ├── PrintManager.js │ │ └── rootSaga.js │ ├── store.js │ └── utils.js ├── syncLocales.js └── vite.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # .bin files containing text are used as test assets *.bin binary ================================================ FILE: .gitignore ================================================ .idea .vscode/ node_modules _src build dist extension/web-ext-artifacts extension/manifest.json ================================================ FILE: .js ================================================ /** * Used in npm scripts to copy files */ 'use strict'; const fs = require('fs'); async function copy() { const buildFolder = 'build/'; const extensionFolder = 'extension/dist/' if (!fs.existsSync(buildFolder)) fs.mkdirSync(buildFolder); if (!fs.existsSync(extensionFolder)) fs.mkdirSync(extensionFolder); const copyFile = (path) => { const fileName = path.split('/').at(-1); fs.copyFileSync(path, buildFolder + fileName); fs.copyFileSync(path, extensionFolder + fileName); } copyFile('viewer/dist/djvu_viewer.js'); copyFile('library/dist/djvu.js'); console.info('Dist files are copied to the ./build/ and ./extension/ directories'); } async function prepareManifest(v = 2) { fs.copyFileSync(`./extension/manifest_v${v}.json`, `./extension/manifest.json`); console.info(`Copied manifest_v${v} to manifest.json`); } async function main() { const command = process.argv[2]; switch (command) { case 'copy': return await copy(); case 'v2': return prepareManifest(2); case 'v3': return prepareManifest(3); default: throw new Error('Unsupported command: ' + command); } } void main(); ================================================ FILE: GNU_GPL_v2 ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: LICENSE.md ================================================ The DjVu.js Library (everything that is in `library/src` directory) is subject to, and may be distributed under, the [GNU General Public License, Version 2](GNU_GPL_v2). Everything else in this repository (including the DjVu.js Viewer and the browser extension) is distributed under the terms of [The Unlicense](THE_UNLICENSE). The choice of GNU GPL v2 for the library is conditioned by the fact DjVu.js contains some code fragments copied from DjVuLibre or Java DjVu libraries, which are distributed under this very license. The DjVu.js Library isn't a port of DjVuLibre or Java DjVu, but the published specification of the DjVu format refers to DjVuLibre as the de facto standard of the DjVu format and advises to study its code. Also, some parts of DjVu codecs aren't described in details and can be gotten only from the source code of DjVuLibre. Here I put all the copyright notices, which I found in the source files of both DjVuLibre and Java DjVu. ``` DjVuLibre-3.5 Copyright (c) 2002 Leon Bottou and Yann Le Cun. Copyright (c) 2001 AT&T DjVu (r) Reference Library (v. 3.5) Copyright (c) 1999-2001 LizardTech, Inc. All Rights Reserved. Java DjVu (r) (v. 0.8) Copyright (c) 2004-2005 LizardTech, Inc. All Rights Reserved. ``` ================================================ FILE: README.md ================================================ # DjVu.js ## About / О проекте **DjVu.js** is a program library for working with `.djvu` online. It's written in JavaScript and can be run in a web browser without any connection with a server. DjVu.js can be used for splitting (and concatenation) of `.djvu` files, rendering pages of a `.djvu` document, converting (and compressing) images into `.djvu` documents and for analyzing of metadata of `.djvu` documents. **DjVu.js Viewer** is an app which uses DjVu.js to render DjVu documents. The app may be easily included into any html page. You can look at it and try it out on the official website (the link is below). **DjVu.js Viewer browser extension**. By and large, it's a copy of the viewer, but also it allows opening links to `.djvu` files right in the browser without downloading them explicitly. The links to the extension are below.
**DjVu.js** - это программная библиотека написанная на JavaScript и предназначенная для работы с файлами формата `.djvu` онлайн. DjVu.js ориентирована на исполнение в браузере пользователя без связи с сервером. Библиотека может быть использована для разделения (объединения) файлов `.djvu`, преобразования картинок в документы `.djvu`, отрисовки страниц документов `.djvu`, а также для анализа мета данных и структуры `.djvu` документов. **DjVu.js Viewer** - приложение, которое можно легко встроить в любую html-страницу. Данное приложение служит для просмотра документов DjVu непосредственно в браузере. Вы можете ознакомиться с ним по ссылке ниже. **Расширение для браузера DjVu.js Viewer**. По большей части это копия приложения DjVu.js Viewer, однако также расширение позволяет открывать ссылки на `.djvu` файлы прямо в браузере, не скачивая их явно. Ссылки на расширение доступны ниже. ## Translation (localization) If you want to add a new translation to the viewer [read here](TRANSLATION.md) how to do it. ## Tools and supported browsers You need to have Node.js 18+ (although older versions should work too) and npm 9+ installed to work with the project. The viewer and the library are supposed to run in a browser. Technically, it should not be difficult to update the library so that it could be used in Node.js projects - the main code is pure JS and doesn't rely on browser specific APIs. Currently, the following browsers are supported: ``` Chrome >= 88 Firefox >= 78 Safari >= 14 Edge >= 88 ``` The list above is conditioned by the [default Vite settings](https://vitejs.dev/guide/build.html#browser-compatibility) and the support of the [`:where` CSS pseudo class](https://caniuse.com/mdn-css_selectors_where). ## How to build it Clone the repo and run: ```sh npm run make ```` in the root folder of the repository. The command will install all dependencies and create bundles of the library and viewer (the `build` folder should appear). There is another variant: ```sh npm run remake ``` It does the same as `make`, but first it removes all git-ignored files (including dependencies). ## How to run it locally If you want to work with the library you should read [the library's README](./library/README.md). As for the viewer, you have to build the library once and start the dev server. It can be achieved with the following commands: ```sh npm run make # run it only once cd viewer npm start ``` A page with the viewer will open automatically. ### Tests Once the dev server has been started, you can run E2E tests via `test*` npm scripts that you can find in the `viewer/package.json` file. ## How to pack the extension After the project has been built (`npm run make`), the extension folder contains all the necessary files. However, there are two manifests: v2 and v3. You should copy and rename one of them to `manifest.json`. After it's done, the folder is an unpacked extension - it can be installed in the "developer mode". If you want to pack the extension, you can either zip it yourself or run: ```sh npm run ext # it uses "npx web-ext", so you will be asked to install the package ``` It will pack the extension with both v2 and v3 manifests. ## Links - The **official website** with the DjVu.js Viewer demo is https://djvu.js.org - You may **download the library** and the viewer on https://djvu.js.org/downloads - The **browser extension** for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/djvu-js-viewer/) - The **browser extension** for [Google Chrome](https://chrome.google.com/webstore/detail/djvujs-viewer/bpnedgjmphmmdgecmklcopblfcbhpefm) - The **technical documentation** of the library is available [in the wiki](https://github.com/RussCoder/djvujs/wiki/DjVu.js-Documentation) - [CHANGELOG of the library](library/CHANGELOG.md) - [CHANGELOG of the viewer](viewer/CHANGELOG.md) ## License / Лицензия The DjVu.js Library is distributed under the terms of [GNU GPL v2](GNU_GPL_v2). Everything else in this repository (including the DjVu.js Viewer and the browser extension) is under [The Unlicense](THE_UNLICENSE). Read more in the [LICENSE file](LICENSE.md).
Библиотека DjVu.js распространяется под лицензией [GNU GPL v2](GNU_GPL_v2). Все остальное в этом репозитории (включая DjVu.js Viewer и расширение для браузера) является общественным достоянием ([The Unlicense](THE_UNLICENSE)). Читайте подробнее в [файле лицензии](LICENSE.md). ================================================ FILE: THE_UNLICENSE ================================================ This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ================================================ FILE: TRANSLATION.md ================================================ # How to add a new translation to the viewer or improve an existing one If you want to add one more translation to the viewer, you need to fulfill the following steps: 1. Copy [the Russian dictionary file](viewer/src/locales/Russian.js) and rename it according to the name of your language in English. Put your file into the same directory, where the Russian file is. 2. Then change all the Russian translations of English phrases with your own. You can look at the [the English dictionary file](viewer/src/locales/English.js), which essentially does not translate anything. But it may serve you as an additional example. 3. Pay attention to **the topmost comments** in the Russian dictionary file. Especially, read about placeholders which start with #, e.g. #helpButton. Other comments throughout the file will help you to find where a phrase is used in the app. Some phrases you will not see if you start the viewer locally (not as in the extension). While others you can see only if you remove some phrases from a dictionary (namely notifications that the translation isn't complete), and start the viewer locally (it's written in [README](README.md) how to do it). But you do not need to find all phrases, you can translate some of them blindly. 4. If you want to **improve an existing translation**, the notification window should have told you what phrases are missing. So find where those phrases are placed in the Russian dictionary and add them with translations to the dictionary you want to improve. However, most probably untranslated phrases have been already added to the file, but with `null` values as placeholders. In this case, replace all `null` values with corresponding translations. Also, if there are missing phrases, but they are not present in the file, you can add them via the command `npm run syncLocales`. It should be run inside `viewer` directory. You do not need to connect the dictionary to the code, I will do it myself. However, if you want you can find where it's connected in the code and add it there. **But in general you need only to create a dictionary and nothing more.** It's better to create **a pull request on GitHub**, but if you do not know how to do it, and do not want to learn how to do it, you can just send the dictionary at djvujs@yandex.ru and I will add it to the project myself. ================================================ FILE: extension/background.js ================================================ /** * The execution starts in the main() function */ 'use strict'; function isManifestV3() { return chrome.runtime.getManifest().manifest_version === 3; } const extensionUrl = chrome.runtime.getURL('viewer.html'); const httpRedirectRuleId = 1; const fileRedirectRuleId = 2; function updateContextMenu() { chrome.contextMenus.removeAll(); chrome.contextMenus.create({ id: 'open_with', title: 'Open with DjVu.js Viewer', contexts: ['link'], targetUrlPatterns: [ '*://*/*.djvu', '*://*/*.djv', '*://*/*.djvu?*', '*://*/*.djv?*', '*://*/*.DJVU', '*://*/*.DJV', '*://*/*.DJVU?*', '*://*/*.DJV?*', ] }); chrome.contextMenus.onClicked.addListener(info => { if (info.menuItemId === 'open_with') { openViewerTab(info.linkUrl); } }); } function promisify(func) { return function (...args) { return new Promise(resolve => { func(...args, resolve); }); }; } const getViewerUrl = (djvuUrl = null, djvuName = null) => { const params = new URLSearchParams(); djvuUrl && params.set('url', djvuUrl); djvuName && params.set('name', djvuName); const queryString = params.toString(); return extensionUrl + (queryString ? '?' + queryString : ''); }; const executeScript = (src, sender) => { if (isManifestV3()) { return chrome.scripting.executeScript({ files: [src], target: { tabId: sender.tab.id, frameIds: [sender.frameId], } }); } return promisify(chrome.tabs.executeScript)(sender.tab.id, { frameId: sender.frameId, file: src, runAt: 'document_end' }); } function listenForMessages() { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (sender.tab && message === 'include_scripts') { Promise.all([ executeScript('dist/djvu.js', sender), executeScript('dist/djvu_viewer.js', sender), ]).then(() => { sendResponse(); }) return true; // do not send response immediately } if (message.command === 'open_viewer_tab') { openViewerTab(message.url); } sendResponse(); }); } function openViewerTab(djvuUrl = null) { chrome.tabs.create({ url: getViewerUrl(djvuUrl) }); } function enableFileOpeningInterception() { if (isManifestV3()) { chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: [fileRedirectRuleId], addRules: [{ id: fileRedirectRuleId, action: { type: 'redirect', redirect: { regexSubstitution: `${extensionUrl}?url=\\0` }, }, condition: { isUrlFilterCaseSensitive: false, regexFilter: '^file:///.+\\.djvu?$', resourceTypes: ['main_frame'], }, }], }); } else { chrome.webRequest.onBeforeRequest.addListener(details => { return { redirectUrl: getViewerUrl(details.url) }; }, { urls: [ 'file:///*/*.djvu', 'file:///*/*.djvu?*', 'file:///*/*.djv', 'file:///*/*.djv?*', 'file:///*/*.DJVU', 'file:///*/*.DJVU?*', 'file:///*/*.DJV', 'file:///*/*.DJV?*', ], types: ['main_frame'] }, ['blocking'] ); } } // it shouldn't be the same function as the file opening interceptor, // since this event listener can be removed independently of the file opening interceptor const requestInterceptor = details => { // http://*/*.djvu also corresponds to "http://localhost/page.php?file=doc.djvu" // so we have to add this additional check, because it's not a link to a file. if (/\.djvu?$/i.test(new URL(details.url).pathname)) { return { redirectUrl: getViewerUrl(details.url) }; } } // it's "undefined" for manifest v3, because the "webRequest" permission isn't requested const onBeforeRequest = chrome.webRequest?.onBeforeRequest; const onHeadersReceived = chrome.webRequest?.onHeadersReceived; // Detect djvu only by URL const enableHttpIntercepting = () => { if (isManifestV3()) { chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: [httpRedirectRuleId], addRules: [{ id: httpRedirectRuleId, action: { type: 'redirect', redirect: { regexSubstitution: `${extensionUrl}?url=\\0` }, }, condition: { isUrlFilterCaseSensitive: false, regexFilter: '^https?://[^?]+\\.djvu?(\\?.*)?', resourceTypes: ['main_frame', 'sub_frame'], }, }], }); } else { !onBeforeRequest.hasListener(requestInterceptor) && onBeforeRequest.addListener(requestInterceptor, { urls: [ 'http://*/*.djvu', 'http://*/*.djvu?*', 'https://*/*.djvu', 'https://*/*.djvu?*', 'http://*/*.djv', 'http://*/*.djv?*', 'https://*/*.djv', 'https://*/*.djv?*', 'http://*/*.DJVU', 'http://*/*.DJVU?*', 'https://*/*.DJVU', 'https://*/*.DJVU?*', 'http://*/*.DJV', 'http://*/*.DJV?*', 'https://*/*.DJV', 'https://*/*.DJV?*', ], types: ['main_frame', 'sub_frame'], }, ['blocking'] ); } }; const disableHttpIntercepting = () => { if (isManifestV3()) { chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: [httpRedirectRuleId] }); } else { onBeforeRequest.hasListener(requestInterceptor) && onBeforeRequest.removeListener(requestInterceptor); } }; const headersAnalyzer = details => { const getFileName = () => { const contentDisposition = details.responseHeaders.find(item => item.name.toLowerCase() === 'content-disposition'); if (contentDisposition) { // In fact, there may be also filename*= in the header, so perhaps, it will be needed for someone in the future const matches = /(?:attachment|inline);\s+filename="(.+\.djvu?)"/.exec(contentDisposition.value); return matches && matches[1]; } }; const contentType = details.responseHeaders.find(item => item.name.toLowerCase() === 'content-type'); if (contentType) { if (contentType.value === 'image/vnd.djvu' || contentType.value === 'image/x.djvu') { // analyse Content-Disposition only if there is no filename in the URL return { redirectUrl: getViewerUrl(details.url, /\.djvu?(?:\?.*)?$/.test(details.url) ? null : getFileName()) }; } else if (contentType.value === 'application/octet-stream') { const fileName = getFileName(); if (fileName) { return { redirectUrl: getViewerUrl(details.url, fileName) }; } } } }; const enableHeadersAnalysis = () => { !onHeadersReceived.hasListener(headersAnalyzer) && onHeadersReceived.addListener(headersAnalyzer, { urls: [ 'http://*/*', 'https://*/*', ], types: ['main_frame', 'sub_frame'], }, ['blocking', 'responseHeaders']); }; const disableHeadersAnalysis = () => { onHeadersReceived.hasListener(headersAnalyzer) && onHeadersReceived.removeListener(headersAnalyzer) }; const defaultOptions = Object.freeze({ // here we duplicated only the options, which are used by the extension code interceptHttpRequests: true, analyzeHeaders: false, }); const onOptionsChanged = json => { let parsedOptions = {}; try { parsedOptions = json ? JSON.parse(json) : {}; } catch (e) { console.error('DjVu.js Extension: cannot parse options json from the storage. The json: \n', json); console.error(e); } try { const options = { ...defaultOptions, ...parsedOptions }; if (options.interceptHttpRequests) { enableHttpIntercepting(); } else { disableHttpIntercepting(); } if (isManifestV3()) return; if (options.interceptHttpRequests && options.analyzeHeaders) { enableHeadersAnalysis(); } else { disableHeadersAnalysis(); } } catch (e) { console.error('DjVu.js Extension: some options might not have been applied due to an error.'); console.error(e); } }; function applySavedOptions() { chrome.storage.local.get('djvu_js_options', options => onOptionsChanged(options['djvu_js_options'])); } function listenForOptionChanges() { chrome.storage.onChanged.addListener((changes, area) => { if (area === 'local' && changes['djvu_js_options']) { if (changes['djvu_js_options'].newValue) { onOptionsChanged(changes['djvu_js_options'].newValue); } } }); } function main() { // For manifest v3 onInstalled and onStartup events could be used to update the context menu // and to register the file opening interception rules, but it seems to work well // this way - it's updated every time the service worker is started. updateContextMenu(); enableFileOpeningInterception(); chrome[isManifestV3() ? 'action' : 'browserAction'].onClicked.addListener(() => openViewerTab()); listenForMessages(); listenForOptionChanges(); applySavedOptions(); } main(); ================================================ FILE: extension/content.js ================================================ (function () { 'use strict'; var includeScriptsPromise = null; function includeScripts() { return includeScriptsPromise || (includeScriptsPromise = new Promise(resolve => { chrome.runtime.sendMessage("include_scripts", resolve); })); } function processTag(tag, src) { function isJustNumber(value) { return Number(value).toString() === String(value).trim(); } var div = document.createElement('div'); div.style.minWidth = '600px'; // to fit the toolbar div.style.minHeight = '200px'; if (tag.height) { // deliberately use attribute, not styles div.style.height = isJustNumber(tag.height) ? Number(tag.height) + "px" : tag.height; } else { div.style.height = '90vh'; div.style.maxHeight = '90%'; } if (tag.width) { div.style.width = isJustNumber(tag.width) ? Number(tag.width) + "px" : tag.width; } div.style.overflow = "hidden"; div.className = "djvu_js_viewer_container"; tag.parentNode.replaceChild(div, tag); var viewer = new DjVu.Viewer(); viewer.loadDocumentByUrl(src); viewer.render(div); } const objects = document.querySelectorAll( 'object[classid="clsid:0e8d0700-75df-11d3-8b4a-0008c7450c4a"]' + ', object[type="image/x.djvu"]' ); if (objects.length) { includeScripts().then(() => { objects.forEach(object => { var srcParam = object.querySelector('param[name="src"]'); if (srcParam && srcParam.value) { processTag(object, srcParam.value); } }); processEmbeds(); }) } else { processEmbeds(); } function processEmbeds() { // should be processed after objects, since embeds may be nested in objects as a fallback const embeds = document.querySelectorAll('embed[type="image/x-djvu"], embed[type="image/vnd.djvu"]'); if (embeds.length) { includeScripts().then(() => { embeds.forEach(embed => { processTag(embed, embed.src); }); }); } } })(); ================================================ FILE: extension/initializer.js ================================================ 'use strict'; window.onload = () => { const viewer = new DjVu.Viewer({ uiOptions: { hideFullPageSwitch: true, } }); viewer.render(document.getElementById('root')); viewer.on(DjVu.Viewer.Events.DOCUMENT_CHANGED, () => { document.title = viewer.getDocumentName(); }); viewer.on(DjVu.Viewer.Events.DOCUMENT_CLOSED, () => { document.title = 'DjVu.js Viewer'; }); const params = new URLSearchParams(location.search.slice(1)); if (params.get('url')) { viewer.loadDocumentByUrl(params.get('url'), params.get('name') ? { name: params.get('name') } : undefined); } }; ================================================ FILE: extension/manifest_v2.json ================================================ { "manifest_version": 2, "name": "DjVu.js Viewer", "short_name": "DV", "version": "0.10.1.0", "author": "RussCoder", "homepage_url": "https://github.com/RussCoder/djvujs", "description": "Opens links to .djvu files. Allows opening files from a local disk. Processes & tags.", "background": { "scripts": [ "background.js" ] }, "content_security_policy": "script-src 'self'; object-src 'self';", "content_scripts": [ { "matches": [ "*://*/*" ], "js": [ "content.js" ], "all_frames": true, "run_at": "document_end" } ], "permissions": [ "storage", "webRequest", "webRequestBlocking", "", "contextMenus" ], "web_accessible_resources": [ "viewer.html" ], "icons": { "16": "djvu16.png", "32": "djvu32.png", "48": "djvu48.png", "64": "djvu64.png", "96": "djvu96.png" }, "browser_action": { "default_icon": { "16": "djvu16.png", "32": "djvu32.png", "48": "djvu48.png", "64": "djvu64.png", "96": "djvu96.png" } } } ================================================ FILE: extension/manifest_v3.json ================================================ { "manifest_version": 3, "name": "DjVu.js Viewer", "short_name": "DV", "version": "0.10.1.0", "author": "RussCoder", "homepage_url": "https://github.com/RussCoder/djvujs", "description": "Opens links to .djvu files. Allows opening files from a local disk. Processes & tags.", "background": { "service_worker": "background.js" }, "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self';" }, "content_scripts": [ { "matches": [ "*://*/*" ], "js": [ "content.js" ], "all_frames": true, "run_at": "document_end" } ], "permissions": [ "storage", "declarativeNetRequest", "scripting", "contextMenus" ], "host_permissions": [ "" ], "web_accessible_resources": [ { "resources": ["viewer.html"], "matches": [""] } ], "icons": { "16": "djvu16.png", "32": "djvu32.png", "48": "djvu48.png", "64": "djvu64.png", "96": "djvu96.png" }, "action": { "default_icon": { "16": "djvu16.png", "32": "djvu32.png", "48": "djvu48.png", "64": "djvu64.png", "96": "djvu96.png" } } } ================================================ FILE: extension/viewer.html ================================================ DjVu.js Viewer
================================================ FILE: library/.gitignore ================================================ .idea/ node_modules/ /samples/ .vscode/ access.log error.log favicon.ico jsconfig.json ================================================ FILE: library/API.md ================================================ # DjVu.js Library API > The library is supposed to work in the browser. Theoretically, it should work > in Node.js too (with some limitations), but I have never did it, and the > current bundle is just an IIFE. The whole API is available in two forms - synchronous, when all operations are run in the main thread, and the asynchronous, when all operations are run in the Web Worker. The last one is preferred in case of browsers, because it may take up to several seconds to render a page, and no one wants to freeze the UI for such a long time. However, the async API is mostly a wrapper around the sync one, so all methods are described in their sync version. The library adds one object to the global scope - `DjVu`. ## Synchronous API The sync API is represented via the `DjVu.Document` constructor: ```js DjVu.Document(arrayBuffer, { baseUrl = null, memoryLimit = MEMORY_LIMIT } = {}) ``` Arguments: - `arrayBuffer` is an `ArrayBuffer` object representing the file. - `baseUrl` is the URL to the folder where the indirect djvu is stored. It's required only in case of indirect djvu documents (the documents where each page is a separate file) to construct an absolute URL to the pages (cause inside the document all references are relative). - `memoryLimit` - shouldn't be provided at all in most cases. The default value is 50 MB. This value is the upper border of the memory used to store pages of an indirect djvu. If the total size of downloaded pages exceeds this limit, the library removes some of them before downloading new pages. And example for a bundled (one file) djvu document: ```js const bundledDjVuArrayBuffer = await fetch('/bundled.djvu').then(r => r.arrayBuffer()); const doc = new DjVu.Document(bundledDjVuArrayBuffer); ``` And example for an indirect (multi-file) djvu document: ```js const indexFileBuffer = await fetch('/some_indirect_djvu/index.djvu').then(r => r.arrayBuffer()); const doc = new DjVu.Document(indexFileBuffer, { baseUrl: '/some_indirect_djvu' }); ``` The constructor creates a `DjVuDocument` instance which has the following methods: - `getPagesSizes(): Array<{width: number, height: number, dpi: number}>` - returns an array of pages sizes. Needed for the continuous scroll view mode in the viewer to determine the total height of the view area and of each page. - `isBundled(): boolean` - returns `true` if the document is bundled (one-file). `false` if it's indirect (multi-file). - `getPagesQuantity(): number` - returns the total number of pages in the document. - `getContents(): Array`, where `Bookmark` is `{description: string, url: string, children?: Array}` - returns the table of contents, if it exists in the document. - `getMemoryUsage(): number` - returns the amount of the memory used to store parts of an indirect djvu document. - `getMemoryLimit(): number` - returns the current memory limit for an indirect djvu. - `setMemoryLimit(limit = MEMORY_LIMIT): void` - sets the memory limit. - `getPageNumberByUrl(url: string): ?number` - returns the page number corresponding to the `url` from a `Bookmark`, that is, from the table of contents. If the page cannot be found, `null` is returned. - `async getPage(number: number): Promise` - this method is async, cause it works both in case of a single-file djvu and an indirect one, and in the latter case the page and its dependencies have to be downloaded first. It accepts the page number starting from 1 (not from 0). What's more, this method automatically reset the previously requested page (read about it in the methods of `DjVuPage`), which allows you not to care about memory leaks. - `getPageUnsafe(number: number): DjVuPage` - in case of a bundled djvu, you can get a page synchronously. But you will have to `page.reset()` manually after you finished working with the page. Otherwise, you risk overusing memory. Prefer `getPage()` to this method. - `createObjectURL(): string` - creates a url to download the file (it should be revoked afterwards). - `slice(from = 1, to = this.getPagesQuantity()): DjVuDocument` - creates a document from a subset of pages, including the first and the last page. Pages are counted from 1. This method isn't production-ready. It may work incorrectly in some cases, and it doesn't split the table of contents, but copies it completely to the new document. - `async bundle(progressCallback: (progress: number) => void): Promise` \- downloads and bundles an indirect djvu into one-file document. Accepts a callback which is invoked with a number parameter which takes values from 0 to 1 and provides an ability to track the progress. - `toString(): string` - returns metadata describing the structure of the document. Useful if you are familiar with the DjVu Specification. The most important method is `async getPage(number)` which returns `DjVuPage` with the following methods: - `getWidth(): number` - width in pixels. - `getHeight(): number` - height in pixels. - `getDpi(): number` - returns the dpi value. This value is required to determine the "100%" scale factor. E.g. a usual monitor has 96 dots per inch (let's say 100). If a document has 300 dpi (more precisely, it was scanned with the resolution of 300 dpi), it means that its "real size" is 300 / 100 = 3 times smaller than its full size in pixels. - `getRotation(): 0 | 90 | 180 | 270` - the rotation of the page. It's needed only to show it properly to the user. - `getImageData(rotate = true): ImageData` - returns `ImageData` object representing the page. By default, it has been already rotated (if it's required), and you do not need `getRotation()` at all. - `async createPngObjectUrl(): Promise` - creates a PNG image of the page, and forms a URL via `URL.createObjectURL()`. It means that you have to `URL.revokeObjectURL(url)` (or `worker.revokeObjectURL(url)` in case of the async API) once you need it no longer, otherwise there will be memory leaks. The `PngObjectData` has the following structure: ```ts { url: string, // do not forget to revoke it byteLength: number, // the size of the PNG image retained by the URL width: number, height: number, dpi: number, } ``` This method uses `OffscreenCanvas`, but if it's not available (as in Firefox) it uses `png.js` library as a fallback. `png.js` is the only dependency of the library, and it takes more than 50% of the eventual bundle. The method itself is very useful, because a djvu page can easily take 30 MB of memory (and more) as a raw `ImageData` object (4 bytes per a pixel), while the same image in the PNG format takes less than 0.5 MB. Also, images are much better scaled via CSS than canvases. The continuous scroll mode would be impossible without this method, because it would take too much memory to render many pages on canvases. - `getText(): string` - returns the page's text as one string, if it exists on the page. - `getNormalizedTextZones(): ?Array` - returns the array of text zones to form a text layer above the page's image. The `TextZone` object is the following: ```ts { x: number, y: number, width: number, height: number, text: string, } ``` Its coordinates are relative to the page's top left corner, that is, all zones should be absolutely positioned. - `toString(): string` - returns metadata describing the structure of the page. Useful if you are familiar with the DjVu Specification. - `reset(): void` - resets the page's inner structures. During the decoding phase, which is called lazily when different parts of the page's data are requested, a lot of temporary structures are allocated. To release the memory, you have to reset the page. Otherwise, it will retain a lot of memory for that structures. Page objects are created in the constructor of the document, so they are not garbage collected, until the document is removed. If you get pages via `await doc.getPage(number)` method, you can do nothing since it takes care to reset a page when the next one is requested. ## Asynchronous API The asynchronous API is represented via the `DjVu.Worker` constructor: ```js new DjVu.Worker(urlToTheLibrary = DEFAULT_VALUE); ``` It may accept a URL to the DjVu.js Library, but in a normal case you do not have to provide it explicitly at all, since the library creates an ObjectURL from its code automatically. This param can be required only if you run the code in some environment which prohibits to execute code from ObjectURL or data URIs, e.g. in case of a browser extension. But in case of a usual web page it's not needed. So the example is: ```js const worker = new DjVu.Worker(); ``` The `DjVuWorker` instance has the following methods and props: - `async createDocument(buffer: ArrayBuffer, options: Object): Promise` - invokes the `DjVu.Document` constructor in the Web Worker. Accepts the same parameters. Note that `buffer` is transferred to the Web Worker, so it will be unavailable after you call his method. - `async run(): Promise` - a special methods to execute a `DjVuWorkerTask` object (or several). Read about the `doc` property to understand how to use it. - `get doc: DjVuWorkerTask` - a read only property which is the heart of the async API. It mimics the `DjVuDocument` object, but in fact it's a `DjVuWorkerTask` (which is a `Proxy`), and you can call any method on it, and it always returns another `DjVuWorkerTask` (until you call the `run()` method). It's better to look at the examples first: ```js const [text, textZones] = await worker.run( worker.doc.getPage(pageNumber).getText(), worker.doc.getPage(pageNumber).getNormalizedTextZones(), ); const pagesSizes = await worker.doc.getPagesSizes().run(); ``` In the first example two tasks are run in one bunch, and the array of results is returned (inside a `Promise` of course). In the second example only one task is executed via a special method `run()` which is the same as to do: ```js const pagesSizes = await worker.run(worker.doc.getPagesSizes()); ``` Using this API you can call any chain of methods on the `DjVuDocument` inside the Web Worker. However, you should remember, that you **cannot get complex objects like `DjVuPage`** (but you still **can pass callbacks to the worker**, e.g. in case of the `bundle()` method). You can only get the eventual results like `ArrayBuffer`'s, `ImageData`'s, strings, plain objects and numbers. Also, despite the fact `DjVuDocument.getPage()` method is async, you can use in as a sync one in the methods chain. The same takes place in case of any other async methods. The fact we cannot access the `DjVuPage` directly via the async API conditions the current architecture, due to which we have to `reset()` pages manually in case of the sync API - otherwise two tasks in one bunch would require to decode the page twice, while now it's decoded lazily and only once. In essence, when you call methods on a `DjVuWorkerTask` object it just pushes the method's name and its arguments into an array, which is passed to the Web Worker when you call the `run()` method. All those methods are applied to the `DjVuDocument` instance one by one, and the eventual result is passed back. - `cancelTask(promise: Promise): void` - cancels the task. Accept the promise returned by the `run()` method. - `emptyTaskQueue(): void` - cancels all tasks except the current one. - `dropCurrentTask(): void` - forgets about the current task (it cannot be really stopped once it began to execute). - `cancelAllTasks(): void` - invokes two previous methods. It's worth saying that if you initiate a lot of tasks via the `run()` method, they are not passed to the Web Worker at once, so they can be just deleted from the queue. But when a task has been sent, there is no way to stop it, except for the Web Worker termination and recreation, but in this case you will lose the `DjVuDocument` created inside. Since the library doesn't work too fast, these "cancel task" methods are useful in some cases, e.g. the DjVu.js Viewer renders the current page, the previous and the next in case of the single page mode, and 15 pages back and forward in case of the continuous scroll mode. If a user looks at the current page, the viewer at the same time may be rendering other pages to show them quickly when the user turns a page over. But if he just jumps in 100 pages, there is no use in those pages which were to be rendered, so those tasks have to be cancelled and new tasks are to be initiated. The same need to cancel a task occurs when the user quickly clicks on the next page button. - `isTaskInQueue(promise: Promise): boolean` - checks whether the task is in the queue. - `isTaskInProcess(promise: Promise): boolean` - checks whether the task has been already started. - `revokeObjectURL(url: string): void` - formerly, if an ObjectURL had been created inside a worker it could be revoked only inside this very worker. I checked it by myself, but now it seems that this behavior has been fixed and usual `URL.revokeObjectURL()` works too. But this method is still available if you wish to revoke the URL inside the worker in which it was created. - `reset(): void` - recreates the worker. ## Additional Notes Besides `DjVu.Worker` and `DjVu.Document`, there are also: - `DjVu.VERSION` - a string with the current version. - `DjVu.ErrorCodes` - an object with all possible error codes created by the library. Perhaps, it's worth describing them more profoundly. But for now, just print it to the console to see the codes. These error codes I use mostly in tests, they are not something too important. If you want more practical examples of the library usage, you can take a look at `viewer/src/sagas` files. There are real examples of the async API usage. If you want to know more about the inner DjVu structure, it's worth reading the [DjVu Specification](./assets/DjVu3Spec.djvu?raw=true). If something isn't intelligible enough, feel free to create an issue. ================================================ FILE: library/CHANGELOG.md ================================================ # DjVu.js Library's Changelog ## v.0.5.4 (01.02.2023) - Fix: error messages are sent from the worker again. ## v.0.5.3 (18.02.2021) - Error handlers for unhandled promise rejections and errors in the worker. - Object URL to the whole library code is created only once to avoid memory leaks. ## v.0.5.2 (18.02.2021) - Use DIRM registry to get a page number by its id. This approach works the same for bundled and indirect documents. ## v.0.5.1 (09.01.2021) - More robust memory management for document creation (usage of `WebAssembly.Memory` with its `grow()` method instead of manual `ArrayBuffer` expansion). - `'use strict';` in the Web Worker (typo correction). - Returned to the old behaviour: wait for the completion of a forgotten task, before sending the next to the Web Worker. ## v.0.5.0 (06.12.2020) - Feature: bundle indirect djvu documents. - Removed old redundant DjVuWorker's methods duplicating "doc" proxy API. - Now callbacks can be passed to the DjVuWorker. - Minor improvements. ## v.0.4.5 (18.11.2020) - Use standard TextDecoder API to handle ill-formed utf-8 arrays. ## v.0.4.4 (28.10.2020) - Significant reduction of memory consumption in IWDecoder (LazyBlock). - Automatic reset of temporary IW structures after the decoding phase, if the image is big. ## v.0.4.3 (30.07.2020) - Fixed a bug due to which an empty DJVI chunk caused an error. ## v.0.4.2 (30.06.2020) - Fixed an error, which took place when there is no location.origin (when a web page is opened directly in a browser). ## v.0.4.1 (22.04.2020) - Wrapped some loop's bodies into functions to avoid code deoptimizations in Chrome in some cases. ## v.0.4.0 (18.05.2019) - png.js was integrated into djvu.js to create png files (and Object URLs to them) of the pages inside a worker. It's required for the continuous scroll mode, since a png file is much less than a raw ImageData object. ## v.0.3.5 (03.04.2019) - Fixed a bug having taken place when there were more than 1 block in bzz encoded data. ## v.0.3.4 (30.03.2019) - Now XHR is used instead of fetch(), since the latter can't load local files (i.e. file:/// urls). ## v.0.3.3 (02.03.2019) - Fixed a bug. Now empty edges are removed for all symbols added to the dict. ## v.0.3.2 (11.02.2019) - Fixed a bug when baseline (y coord) was computed incorrectly. ## v.0.3.1 (15.11.2018) - New method for getting quantity of pages. - Correct processing of page urls with leading zeros (like "#002"). ## v.0.3.0 (12.10.2018) - The support of indirect djvu files. - Bug fixes. ## v.0.2.2 (14.09.2018) - Rotation flags are processed now. A image of page is rotated by default if required. ## v.0.2.1 (20.08.2018) - Empty pages are processed correctly. ## v.0.2.0 (16.06.2018) - DjVuWorker is created from the Data URL which is generated automatically, so there is no need in explicit script URL. - Additional method run() for the DjVuWorkerTask (the proxy object which is return by the "doc" property of the worker). - Utils.loadFile() is deprecated now. - The whole script is available through the DjVu.DjVuScript() method, which is added as a wrapper in the build process. ## v.0.1.9 (25.05.2018) - TXT* chunks are decoded completely - text zones are decoded. - Normalized text zones for the text layer of page. ## v.0.1.8 (15.05.2018) - New universal Proxy-based DjVuWorker API, allowing to automatically use most of methods of DjVuDocument. ## v.0.1.7 (19.04.2018) - UTF-8 ids of pages and dictionaries are supported. ## v.0.1.6 (15.04.2018) - JB2 codec performance optimizations (more efficient memory access) ## v.0.1.5 (05.04.2018) - Old files with INFO chunks less than 10 bytes are supported. - A specific error for corrupted files. ## v.0.1.4 (27.03.2018) - A table of contents can be gotten. - A page number may be gotten by a url. ## v.0.1.3 (25.03.2018) - All worker tasks-promises can be cancelled now. - A task is posted to the worker only after the previous one is fulfilled. ## v.0.1.2 (24.03.2018) - Unified style of DjVuErrors, which are errors that are thrown manually, when a file is corrupted, there is no requested page and so on. - DjVuErrors are rather simple objects that may be copied between workers. ## v.0.1.1 (19.03.2018) - UTF-8 strings are decoded correctly now. ## v.0.1.0 (14.03.2018) - IW44, BZZ and ZP codecs are fully implemented with some constraints in case of BZZ codec. - JB2 codec is implemented only for decoding. - BGjp, FGjp, Smmr are not supported at all. - ANTa, ANTz, NAVM, FORM:THUM and TH44 are not supported, but there are dummies for them, so they are processed somehow. - Support of TXTz and TXTa is implemented partly, only pure text is decoded. - The library can split a djvu file, render pages, generate metadata of djvu files, and create a document from a set of images (using IW44 codec). ================================================ FILE: library/README.md ================================================ # DjVu.js Library This file contains some information about the inner structure of the project and about how to use the library. It may be useful for you, if you want to play with code or contribute to the project. It's implied that you have run `npm install` and all dependencies are installed correctly. ## Documentation If you are interested only in the library's API [read it here](./API.md). ## How to use it Besides the API docs, there is a good example script with many comments, which can give you a rather good understanding how to use the library. It's located at `library/debug/js/examples.js`. To run it, you should clone the repository and in the root directory do the following: ``` cd library npm install npm start ``` After the debug server is run (usually on 9000 port) access `http://localhost:9000/examples.html` to see the results, and then read the code and the comments to understand how it works. You can edit the code (and the page will reload automatically). Also, you can read source code of `DjVuDocument.js`, `DjVuPage.js` and `DjVuWorker.js` in the `src` directory to know more about the API. If you have more questions, feel free to create an issue. ## The structure There are the following directories: - `app` - contains an old application, which is poorly maintained now. It can split a djvu file, convert images to a document, and show metadata of a document. - `assets` - contains test .djvu files and images. They are used in the automatic tests. - `debug` - contains css and js files for debugging, which are not the part of the source code of the library. - `dist` - a directory where the final bundle file is saved to (the eventual `djvu.js` file). - `src` - a main directory, containing the source code of the library. Its inner structure is self-descriptive, at least I think so. - `tests` - a directory containing tests, which are run in a browser. There are the following npm commands that may be run: - `start` - starts a local static server and runs a rollup watch command, which build the library and rebuild it on each change. Also, on each change of the bundle or a file from `js` folder, the server sends a message to a client script to reload the page. - `watch` - just runs a rollup watch method, which builds the library and rebuilds it on each change. - `build` - just builds the library once. So if you don't know what to start with, run `npm start` and head to `http://localhost:9000/` - you will see the old app. `http://localhost:9000/sync.html` - is a debug page, which I use most often. If you decide to create your own debug page I suggest you to add a `/debug/js/reloader.js` script to your page, as it's done in case of `/tests/tests.html` and other pages, and your page will be reloaded on each change of the library source code. ## Tests There are some automatic tests. In order to run them you should run `npm start` and then open `http://localhost:9000/tests`. The tests are run automatically when the page loads. If everything is ok, you will see that all messages are green. If you see a green message with an orange message, it means that a test has passed, but your browser differs from mine. The thing is that different browsers differently render `.png` files, which are used for tests. So I use Opera, and all tests pass well in my case. In case of other browsers there may be some problems. So, if you use Mozilla, you should write `about:config` in the address line and then find the parameter `gfx.color_management.mode` and set it to `0`. After it, all tests should be green, except for one, which has an orange message as well. When you change the source code of the library, you may open the tests page (which reloads automatically) and check whether your changes break the current functionality or not. ## Build process The library is built with Rollup. I chose it rather than Webpack, since Rollup creates a very simple and light bundle ( eventually it just copies all classes in one file in right order and wrap them with an anonymous function). All files are es6 modules with corresponding import and export statements. So when you create a new file, it's automatically added to the bundle (if you import something from it to the other files). ================================================ FILE: library/app/app.css ================================================ .func_menu_block { display: flex; justify-content: space-between; flex-flow: row wrap; color: gray; max-width: 50em; margin: 1em auto; box-shadow: 0 0 1px gray; padding: 1em; overflow: hidden; } .wrapper { color: gray; max-width: 50em; margin: 1em auto; box-shadow: 0 0 1px gray; padding: 1em; overflow: hidden; } .djvu_version { position: relative; color: gray; text-shadow: 0 0 1px lightgray; top: 0; left: 0; } .additionalBlock { height: 5em; text-align: center; } .funcelem, .disabledfunc { font-size: 1.1em; font-weight: bold; flex: 0 0 auto; box-sizing: content-box; width: 10em; height: 2em; box-shadow: 0 0 1px gray; text-align: center; color: gray; margin: 1em; padding: 1em; } .disabledfunc { text-shadow: 0 0 1px gray; background: #eee; } .funcelem:hover { cursor: pointer; background: #eee; } .inputext { display: inline-block; margin: 5px; width: 150px; } #finput { margin: 10px; } .filehref, #filehref { text-decoration: none; border: 1px solid orangered; padding: 3px; margin: 3px; border-radius: 5px; color: orangered; display: none; } .filehref:hover, #filehref:hover { text-shadow: 0 0 1px orangered; } #sliceblock, .funcblock { display: none; } #warnmess { color: orangered; font-weight: bold; } #procmess { color: blue; } #metaDataBlock #metadata { color: black; border: dotted 1px black; padding: 5px; } .activebut { cursor: pointer; background: white; text-decoration: none; text-align: center; border: 1px solid #0f0f0f; padding: 0.3em; margin: 0.1em; border-radius: 5px; color: #0f0f0f; outline: none; } .activebut:hover { box-shadow: 0 0 1px gray; } .activebut:active { box-shadow: 0 0 1px gray inset; } .activebut:disabled { cursor: not-allowed; opacity: 0.5; box-shadow: none; } #backbutton { display: none; } .djvu_viewer { overflow: hidden; position: relative; box-shadow: 0 0 4px gray; margin: 1em auto; padding: 1em; } .djvu_viewer .controls { display: block; position: absolute; bottom: 0px; margin: 1em; width: 100%; height: 5%; text-align: center; } .djvu_viewer .controls .scale_label { display: inline-block; min-width: 3em; } .djvu_viewer .scale { display: inline-block; width: 10em; } .image_wrapper { overflow: auto; height: 95%; text-align: center; } .djvu_viewer .image { margin: 0.5em; box-shadow: 0 0 1px lightgray; } .djvu_viewer .page_number { width: 5em; } ================================================ FILE: library/app/app.html ================================================ DjVu.js | Работа с DjVu файлами онлайн
Разделить DjVu
Картинки в DjVu
Метаданные DjVu

Выберите djvu документ. Введите номер первой и последней страницы, которые Вы хотите поместить в новый документ.

Номер первой страницы
Номер последней страницы

Выберите одну или несколько картинок для создания djvu документа. Можно настраивать качество изображение (влияет на размер файла).

Выбетите качество кодирования. При хорошем качестве изображение в djvu весит примерно вдвое меньше, чем в формате jpeg. Другие варианты, еще более экономичны.

Хорошее Среднее Плохое


Серое изображение (отбросить цвета при кодировании)

Выберите djvu документ. Метаданные представляют структуру djvu файла. Каждая единица (порция) данных или Data Chunk расположены в том порядке, в котором они встречаются в файле. Некоторые порции описаны подробно в соответствии с их назначение и устройством, другие же характеризуются лишь заголовком и длиной, так как библиотека способна читать не все порции данных, или же не представляется возможным вывести информацию в текстовом виде кратко. Перед каждой страницей или словарем выводится id машинного оглавления.


Сохранить файл
================================================ FILE: library/app/app.js ================================================ 'use strict'; var djvuWorker = new DjVu.Worker(); function initDjVuApplication() { $('#backbutton').click(reset); $('.funcelem').on('click', () => { $('#backbutton').show(400); }); $('#slicefunc').click(sliceFuncPrepare); $('#picturefunc').click(pictureFuncPrepare); $('#metadatafunc').click(metaDataFuncPrepare); if (DjVu.VERSION) { $('#djvu_app').prepend('
djvu.js version: ' + DjVu.VERSION + '
'); } } function reset(event) { event.preventDefault(); event.stopPropagation(); $('.funcblock').hide(400); $('#backbutton').hide(400); $('#funcmenublock').show(400); $('#finput').wrap('
').closest('form').get(0).reset(); $('#finput').unwrap().removeAttr('multiple').off('change'); $('.info').text(''); $('#procmess').text(''); $('#filehref').hide(); djvuWorker && djvuWorker.reset(); } function metaDataFuncPrepare() { $('#funcmenublock').hide(400); $('#funcblock').show(400); var mtblock = $('#metaDataBlock').show(400); $("#finput").change(metaDataFunc); $("#procmess").text(""); $("#metaDataBlock #metadata").html(''); } function metaDataFunc() { $("#procmess").text(""); $("#metaDataBlock #metadata").html(''); if (this.files.length) { if (this.files[0].name.substr(-5) !== '.djvu') { $('#warnmess').text("Расширение файла не .djvu !!!"); return; } $('#warnmess').text(""); var fr = new FileReader(); fr.readAsArrayBuffer($("#finput")[0].files[0]); $("#procmess").text('Загрузка документа ...'); fr.onload = () => { var buf = fr.result; djvuWorker.createDocument(buf) .then(() => { $("#procmess").text('Задание выполняется ...'); return djvuWorker.doc.toString(true).run(); }) .then(str => { $("#procmess").text("Задание выполнено !"); $("#metaDataBlock #metadata").html(str); }) .catch(() => { $("#procmess").text("Ошибка при обработке файла !!!"); }); } } } function pictureFuncPrepare() { $('#funcmenublock').hide(400); $('#funcblock').show(400); var picuture = $('#pictureblock').show(400); $('#finput').prop('multiple', true).off('change').change(function () { $('#filehref').hide(); $('#picturebut').prop('disabled', false); }); $('#picturebut').click(readImagesAndCreateDocument); } function readImagesAndCreateDocument() { var delayInit = 0; var slices = +$('input[name=imagequality]:checked').val(); var grayscale = $('#grayscale').prop('checked') ? 1 : 0; var files = $('#finput')[0].files; djvuWorker.startMultyPageDocument(slices, delayInit, grayscale); $('#filehref').hide(); var i = 0; var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); $("#procmess").text("Задание выполняется ..."); var func = () => { createImageBitmap(files[i]) .then((image) => { canvas.width = image.width; canvas.height = image.height; ctx.drawImage(image, 0, 0); var imageData = ctx.getImageData(0, 0, image.width, image.height); return djvuWorker.addPageToDocument(imageData); }, (e) => { $("#procmess").text("Ошибка при загрузке файлов! " + e.message); }) .then(() => { if (++i < files.length) { $("#procmess").text("Задание выполняется ... " + Math.round(i / files.length * 100) + ' %'); func(); } else { $("#procmess").text("Сборка файла ... "); djvuWorker.endMultyPageDocument() .then((buffer) => { $("#procmess").text("Задание выполненено !!!"); $('#filehref').prop('href', URL.createObjectURL(new Blob([buffer]))).show(400); }); } }); } func(); } function createPicDocument(imageArray) { var delayInit = 0; var slices = +$('input[name=imagequality]:checked').val(); var grayscale = $('#grayscale').prop('checked') ? 1 : 0; djvuWorker.createDocumentFromPictures(imageArray, slices, delayInit, grayscale) .then((buffer) => { $("#procmess").text("Задание выполненено !!!"); $('#filehref').prop('href', URL.createObjectURL(new Blob([buffer]))).show(400); }, () => { $("#procmess").text("Ошибка при обработке файла !!!"); }); djvuWorker.onprocess = (percent) => { $("#procmess").text("Задание выполняется ... " + (percent * 100 >> 0) + '%'); } } function sliceFuncPrepare() { $('#funcmenublock').hide(400); $('#funcblock').show(400); var sliceblock = $('#sliceblock').show(400); $("#finput").off('change').change(function () { $('#filehref').hide(); if (this.files.length) { if (this.files[0].name.substr(-5) !== '.djvu') { $('#warnmess').text("Расширение файла не .djvu !!!"); return; } $('#warnmess').text(""); sliceblock.find('.info').text(''); var fr = new FileReader(); fr.readAsArrayBuffer($("#finput")[0].files[0]); fr.onload = () => { var buf = fr.result; djvuWorker.createDocument(buf) .then(() => djvuWorker.doc.getPagesQuantity().run()) .then(pageCount => { $("#procmess").text(''); sliceblock.find('.info').text('Документ содержит ' + pageCount + ' страниц. Вы можете ввести значение от 1 до ' + pageCount); $('#slicebut').off('off').click(sliceFunc).prop('disabled', false); }, () => { $("#procmess").text("Ошибка при обработке файла !!!"); }); } } else { $('#slicebut').prop('disabled', true); } }); } function sliceFunc() { $("#procmess").text("Задание выполняется ..."); $('#filehref').hide(); var from = +$("#firstnum").val(); var to = +$("#secondnum").val(); djvuWorker.slice(from, to) .then((buffer) => { $("#procmess").text("Задание выполненено !!!"); $('#filehref').prop('href', URL.createObjectURL(new Blob([buffer]))).show(400); }, (e) => { // reject console.error(e); $("#procmess").text("Ошибка при обработке файла !!!"); }); } initDjVuApplication(); ================================================ FILE: library/assets/DjVu3Spec_contents.json ================================================ [{"description":"DjVu3SpecFinal.djvu","url":"","children":[{"description":"Introduction","url":"#1","children":[{"description":"p0001.djvu","url":"#1"}]},{"description":"Document organization","url":"#2","children":[{"description":"p0002.djvu","url":"#p0002.djvu"}]},{"description":"Overview","url":"#2","children":[{"description":"p0003.djvu","url":"#p0003.djvu"}]},{"description":"What's new","url":"#3","children":[{"description":"p0004.djvu","url":"#p0004.djvu"}]},{"description":"Acknowledgements","url":"#4","children":[{"description":"p0005.djvu","url":"#p0005.djvu"}]},{"description":"References","url":"#4"},{"description":"Component Pieces (IFF chunks)","url":"#5","children":[{"description":"p0006.djvu","url":"#p0006.djvu"},{"description":"p0007.djvu","url":"#p0007.djvu"}]},{"description":"Low-level chunk structure and definition","url":"#8","children":[{"description":"Header","url":"#8","children":[{"description":"p0008.djvu","url":"#p0008.djvu"}]},{"description":"DjVu file structure","url":"#8","children":[{"description":"IFF wrapper","url":"#8"},{"description":"Chunk summary","url":"#9","children":[{"description":"p0009.djvu","url":"#p0009.djvu"}]}]},{"description":"IFF chunk types","url":"#10","children":[{"description":"Container chunk: FORM","url":"#10","children":[{"description":"p0010.djvu","url":"#p0010.djvu"},{"description":"p0011.djvu","url":"#p0011.djvu"}]}]},{"description":"Directory chunk: DIRM","url":"#12","children":[{"description":"p0012.djvu","url":"#p0012.djvu"}]},{"description":"Document Outline chunk: NAVM","url":"#13","children":[{"description":"p0013.djvu","url":"#p0013.djvu"},{"description":"p0014.djvu","url":"#p0014.djvu"}]},{"description":"Annotation chunk: ANTa, ANTz","url":"#15","children":[{"description":"p0015.djvu","url":"#p0015.djvu"},{"description":"p0016.djvu","url":"#p0016.djvu"},{"description":"p0017.djvu","url":"#p0017.djvu"},{"description":"p0018.djvu","url":"#p0018.djvu"}]},{"description":"Text chunk: TXTa, TXTz","url":"#19","children":[{"description":"p0019.djvu","url":"#p0019.djvu"},{"description":"p0020.djvu","url":"#p0020.djvu"},{"description":"p0021.djvu","url":"#p0021.djvu"},{"description":"p0022.djvu","url":"#p0022.djvu"}]},{"description":"Bitonal Mask chunk: Sjbz","url":"#23","children":[{"description":"p0023.djvu","url":"#p0023.djvu"}]},{"description":"Wavelet chunks: FG44, BG44, TH44","url":"#23"},{"description":"Foreground Color JB2 chunk: FGbz","url":"#23"},{"description":"Document Info chunk: INFO","url":"#24","children":[{"description":"p0024.djvu","url":"#p0024.djvu"}]},{"description":"Included chunk: INCL","url":"#25","children":[{"description":"p0025.djvu","url":"#p0025.djvu"}]},{"description":"Alternative encoding chunks: BGjp, BFjp, Smmr","url":"#25"}]},{"description":"DjVu in the raw","url":"#27","children":[{"description":"p0027.djvu","url":"#p0027.djvu"},{"description":"p0028.djvu","url":"#p0028.djvu"},{"description":"p0029.djvu","url":"#p0029.djvu"}]},{"description":"Appendix 1: IW44 coding","url":"","children":[{"description":"p0030.djvu","url":"#p0030.djvu"},{"description":"p0031.djvu","url":"#p0031.djvu"},{"description":"p0032.djvu","url":"#p0032.djvu"},{"description":"p0033.djvu","url":"#p0033.djvu"},{"description":"p0034.djvu","url":"#p0034.djvu"},{"description":"p0035.djvu","url":"#p0035.djvu"},{"description":"p0036.djvu","url":"#p0036.djvu"},{"description":"p0037.djvu","url":"#p0037.djvu"},{"description":"p0038.djvu","url":"#p0038.djvu"},{"description":"p0039.djvu","url":"#p0039.djvu"},{"description":"p0040.djvu","url":"#p0040.djvu"},{"description":"p0041.djvu","url":"#p0041.djvu"},{"description":"p0042.djvu","url":"#p0042.djvu"},{"description":"p0043.djvu","url":"#p0043.djvu"}]},{"description":"Appendix 2: JB2 coding","url":"#44","children":[{"description":"p0044.djvu","url":"#p0044.djvu"},{"description":"p0045.djvu","url":"#p0045.djvu"},{"description":"p0046.djvu","url":"#p0046.djvu"},{"description":"p0047.djvu","url":"#p0047.djvu"},{"description":"p0048.djvu","url":"#p0048.djvu"},{"description":"p0049.djvu","url":"#p0049.djvu"},{"description":"p0050.djvu","url":"#p0050.djvu"},{"description":"p0051.djvu","url":"#p0051.djvu"},{"description":"p0052.djvu","url":"#p0052.djvu"},{"description":"p0053.djvu","url":"#p0053.djvu"},{"description":"p0054.djvu","url":"#p0054.djvu"},{"description":"p0055.djvu","url":"#p0055.djvu"},{"description":"p0056.djvu","url":"#p0056.djvu"}]},{"description":"Appendix 3: Z´coding ","url":"#57","children":[{"description":"p0057.djvu","url":"#p0057.djvu"},{"description":"p0058.djvu","url":"#p0058.djvu"},{"description":"p0059.djvu","url":"#p0059.djvu"},{"description":"p0060.djvu","url":"#p0060.djvu"},{"description":"p0061.djvu","url":"#p0061.djvu"},{"description":"p0062.djvu","url":"#p0062.djvu"},{"description":"p0063.djvu","url":"#p0063.djvu"}]},{"description":"Appendix 4: BZZ coding","url":"#64","children":[{"description":"p0064.djvu","url":"#p0064.djvu"},{"description":"p0065.djvu","url":"#p0065.djvu"},{"description":"p0066.djvu","url":"#p0066.djvu"},{"description":"p0067.djvu","url":"#p0067.djvu"},{"description":"p0068.djvu","url":"#p0068.djvu"},{"description":"p0069.djvu","url":"#p0069.djvu"},{"description":"p0070.djvu","url":"#p0070.djvu"},{"description":"p0071.djvu","url":"#p0071.djvu"}]}]}] ================================================ FILE: library/debug/async.html ================================================ DjVu.js

Скачать
================================================ FILE: library/debug/css/style.css ================================================ .control_block{ box-shadow: 0 0 1px gray; padding: 0.5em; margin: 0.5em; } #time_output { color: blue; } #canvas, #canvas2 { box-shadow: 0 0 1px gray; } #img { box-shadow: 0 0 1px gold; } #dochref { text-decoration: none; border: 1px solid blue; padding: 3px; margin: 3px; border-radius: 5px; color: blue; display: inline-block; } #dochref:hover { color: white; background: blue; } .djvu_viewer { overflow: hidden; position: relative; box-shadow: 0 0 4px gray; margin: 1em auto; padding: 1em; } .djvu_viewer .controls { display: block; position: absolute; bottom: 0px; margin: 1em; width: 100%; height: 5%; text-align: center; } .djvu_viewer .controls .scale_label { display: inline-block; min-width: 3em; } .image_wrapper { overflow: auto; height: 95%; text-align: center; } .djvu_viewer .image { margin: 0.5em; box-shadow: 0 0 1px lightgray; } .djvu_viewer .page_number { width: 5em; } ================================================ FILE: library/debug/examples.html ================================================ DjVu.js usage examples

DjVu.js library usage examples

(open the console and read the source code to know how it works)

Canvas gotten with sync interface

Canvas gotten with async interface

Image gotten with async interface (as an image URL)

================================================ FILE: library/debug/index.html ================================================ DjVu.js | Работа с DjVu файлами онлайн
================================================ FILE: library/debug/js/DjVuGlobals.js ================================================ 'use strict'; /** * Just a set of debug functions. */ function writeln(str) { str = str || ""; output.innerHTML += str + "
"; } function write(str) { output.innerHTML += str; } function clear() { output.innerHTML = ""; } // вспомогательный класс для быстрого доступа к разделяемым ресурсам /** * @type {DjVuGlobals} */ var Globals = { init() { var canvas = document.getElementById('canvas'); var c = canvas.getContext('2d'); this.defaultDPI = 100; // число точек на дюйм для монитора, в реальности 96. this.Timer = new DebugTimer(); this.canvas = canvas; this.canvasCtx = c; this.dict = []; this.img = document.getElementById('img'); this.counter = 0; }, clearCanvas() { this.canvasCtx.fillStyle = 'white'; this.canvasCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); }, /** * @returns {Promise} */ loadFile(url) { return new Promise(resolve => { var xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.responseType = "arraybuffer"; xhr.onload = (e) => { DjVu.IS_DEBUG && console.log("File loaded: ", e.loaded); resolve(xhr.response); }; xhr.send(); }); }, drawImage(image, dpi) { var tmp; var scale = dpi ? dpi / Globals.defaultDPI : 1; var time = performance.now(); Globals.canvas.width = image.width / scale; this.canvas.height = image.height / scale; var oc = document.createElement('canvas'); var octx = oc.getContext('2d'); oc.width = image.width; oc.height = image.height; octx.putImageData(image, 0, 0); var tmpH, tmpW, tmpH2, tmpW2; tmpH = tmpH2 = oc.height; tmpW = tmpW2 = oc.width; if (scale > 4) { tmpH = oc.height / scale * 4; tmpW = oc.width / scale * 4; //первое сжатие octx.drawImage(oc, 0, 0, tmpW, tmpH); } if (scale > 2) { tmpH2 = oc.height / scale * 2; tmpW2 = oc.width / scale * 2; //второе сжатие octx.drawImage(oc, 0, 0, tmpW, tmpH, 0, 0, tmpW2, tmpH2); } //итоговое сжатие //this.canvasCtx.translate(- this.canvas.width / 2, - this.canvas.height / 2); // this.canvasCtx.translate(this.canvas.width / 2, this.canvas.height / 2); // this.canvasCtx.rotate(180* Math.PI / 180); // this.canvasCtx.translate(-this.canvas.width / 2, -this.canvas.height / 2); this.canvasCtx.drawImage(oc, 0, 0, tmpW2, tmpH2, 0, 0, canvas.width, canvas.height); DjVu.IS_DEBUG && console.log("Canvas resizing time = ", performance.now() - time); //this.canvasCtx.setTransform(1, 0, 0, 1, 0, 0); }, drawImageNS(image, dpi) { Globals.Timer.start('drawImageNS'); var tmp; var scale = dpi ? Globals.defaultDPI / dpi : 1; var time = performance.now(); Globals.canvas.width = image.width / scale; this.canvas.height = image.height / scale; var oc = document.createElement('canvas'); var octx = oc.getContext('2d'); oc.width = image.width; oc.height = image.height; octx.putImageData(image, 0, 0); var resImg = downScaleCanvas(oc, scale); this.canvas.width = resImg.width; this.canvas.height = resImg.height; this.canvasCtx.putImageData(resImg, 0, 0); Globals.Timer.end('drawImageNS'); }, drawImageSmooth(image, dpi) { var time = performance.now(); var tmp; var scale = dpi ? dpi / Globals.defaultDPI : 1; Globals.canvas.width = image.width; this.canvas.height = image.height; Globals.canvasCtx.putImageData(image, 0, 0); this.img.src = this.canvas.toDataURL(); DjVu.IS_DEBUG && console.log("DataURL creating time = ", performance.now() - time); this.img.width = image.width / scale; (tmp = this.canvas.parentNode) ? tmp.removeChild(this.canvas) : 0; DjVu.IS_DEBUG && console.log("DataURL creating time = ", performance.now() - time); }, /** рисует символ на хосте с учетом его координат (можно посимвольно рисовать картинку) - отладочная функция*/ drawBitmapOnImageCanvas(bm, x, y, jb2Image) { if (this._drawTime && (Date.now() - this._drawTime) < 100) { // если не под отладчиком, то не рисовать return; } else { this._drawTime = Date.now(); } if (!this.testImageData) { console.warn("Debug draw function is enabled!"); this.testImageData = document.createElement('canvas') .getContext('2d') .createImageData(jb2Image.width, jb2Image.height); this.testImageData.data.fill(255); // все белым непрозрачным } var pixelArray = this.testImageData.data; for (var i = y, k = 0; k < bm.height; k++ , i++) { for (var j = x, t = 0; t < bm.width; t++ , j++) { if (bm.get(k, t)) { var pixelIndex = ((jb2Image.height - i - 1) * jb2Image.width + j) * 4; pixelArray[pixelIndex] = 0; pixelArray[pixelIndex + 1] = 0; pixelArray[pixelIndex + 2] = 0; } } } Globals.drawImage(this.testImageData, 300); } }; function downScaleCanvas(cv, scale) { if (!(scale < 1) || !(scale > 0)) throw ('scale must be a positive number <1 '); var sqScale = scale * scale; // square scale = area of source pixel within target var sw = cv.width; // source image width var sh = cv.height; // source image height var tw = Math.floor(sw * scale); // target image width var th = Math.floor(sh * scale); // target image height var sx = 0 , sy = 0 , sIndex = 0; // source x,y, index within source array var tx = 0 , ty = 0 , yIndex = 0 , tIndex = 0; // target x,y, x,y index within target array var tX = 0 , tY = 0; // rounded tx, ty var w = 0 , nw = 0 , wx = 0 , nwx = 0 , wy = 0 , nwy = 0; // weight / next weight x / y // weight is weight of current source point within target. // next weight is weight of current source point within next target's point. var crossX = false; // does scaled px cross its current px right border ? var crossY = false; // does scaled px cross its current px bottom border ? var sBuffer = cv.getContext('2d'). getImageData(0, 0, sw, sh).data; // source buffer 8 bit rgba var tBuffer = new Float32Array(3 * tw * th); // target buffer Float32 rgb var sR = 0 , sG = 0 , sB = 0; // source's current point r,g,b /* untested ! var sA = 0; //source alpha */ for (sy = 0; sy < sh; sy++) { ty = sy * scale; // y src position within target tY = 0 | ty; // rounded : target pixel's y yIndex = 3 * tY * tw; // line index within target array crossY = (tY != (0 | ty + scale)); if (crossY) { // if pixel is crossing botton target pixel wy = (tY + 1 - ty); // weight of point within target pixel nwy = (ty + scale - tY - 1); // ... within y+1 target pixel } for (sx = 0; sx < sw; sx++ , sIndex += 4) { tx = sx * scale; // x src position within target tX = 0 | tx; // rounded : target pixel's x tIndex = yIndex + tX * 3; // target pixel index within target array crossX = (tX != (0 | tx + scale)); if (crossX) { // if pixel is crossing target pixel's right wx = (tX + 1 - tx); // weight of point within target pixel nwx = (tx + scale - tX - 1); // ... within x+1 target pixel } sR = sBuffer[sIndex]; // retrieving r,g,b for curr src px. sG = sBuffer[sIndex + 1]; sB = sBuffer[sIndex + 2]; /* !! untested : handling alpha !! sA = sBuffer[sIndex + 3]; if (!sA) continue; if (sA != 0xFF) { sR = (sR * sA) >> 8; // or use /256 instead ?? sG = (sG * sA) >> 8; sB = (sB * sA) >> 8; } */ if (!crossX && !crossY) { // pixel does not cross // just add components weighted by squared scale. tBuffer[tIndex] += sR * sqScale; tBuffer[tIndex + 1] += sG * sqScale; tBuffer[tIndex + 2] += sB * sqScale; } else if (crossX && !crossY) { // cross on X only w = wx * scale; // add weighted component for current px tBuffer[tIndex] += sR * w; tBuffer[tIndex + 1] += sG * w; tBuffer[tIndex + 2] += sB * w; // add weighted component for next (tX+1) px nw = nwx * scale tBuffer[tIndex + 3] += sR * nw; tBuffer[tIndex + 4] += sG * nw; tBuffer[tIndex + 5] += sB * nw; } else if (crossY && !crossX) { // cross on Y only w = wy * scale; // add weighted component for current px tBuffer[tIndex] += sR * w; tBuffer[tIndex + 1] += sG * w; tBuffer[tIndex + 2] += sB * w; // add weighted component for next (tY+1) px nw = nwy * scale tBuffer[tIndex + 3 * tw] += sR * nw; tBuffer[tIndex + 3 * tw + 1] += sG * nw; tBuffer[tIndex + 3 * tw + 2] += sB * nw; } else { // crosses both x and y : four target points involved // add weighted component for current px w = wx * wy; tBuffer[tIndex] += sR * w; tBuffer[tIndex + 1] += sG * w; tBuffer[tIndex + 2] += sB * w; // for tX + 1; tY px nw = nwx * wy; tBuffer[tIndex + 3] += sR * nw; tBuffer[tIndex + 4] += sG * nw; tBuffer[tIndex + 5] += sB * nw; // for tX ; tY + 1 px nw = wx * nwy; tBuffer[tIndex + 3 * tw] += sR * nw; tBuffer[tIndex + 3 * tw + 1] += sG * nw; tBuffer[tIndex + 3 * tw + 2] += sB * nw; // for tX + 1 ; tY +1 px nw = nwx * nwy; tBuffer[tIndex + 3 * tw + 3] += sR * nw; tBuffer[tIndex + 3 * tw + 4] += sG * nw; tBuffer[tIndex + 3 * tw + 5] += sB * nw; } } // end for sx } // end for sy // create result canvas var resCV = document.createElement('canvas'); resCV.width = tw; resCV.height = th; var resCtx = resCV.getContext('2d'); var imgRes = resCtx.getImageData(0, 0, tw, th); var tByteBuffer = imgRes.data; // convert float32 array into a UInt8Clamped Array var pxIndex = 0; // for (sIndex = 0, tIndex = 0; pxIndex < tw * th; sIndex += 3, tIndex += 4, pxIndex++) { tByteBuffer[tIndex] = Math.ceil(tBuffer[sIndex]); tByteBuffer[tIndex + 1] = Math.ceil(tBuffer[sIndex + 1]); tByteBuffer[tIndex + 2] = Math.ceil(tBuffer[sIndex + 2]); tByteBuffer[tIndex + 3] = 255; } // writing result to canvas. resCtx.putImageData(imgRes, 0, 0); return imgRes; } ================================================ FILE: library/debug/js/DjVuViewer.js ================================================ 'use strict'; /** * Old Viewer with manual DOM manipulation. Saved just for history. * Isn't used anymore. */ class DjVuViewer { constructor(selector, worker) { this.defaultDPI = 100; this.selector = selector; this.element = document.querySelector(selector); this.fileReader = new FileReader(); this.tmpCanvas = document.createElement('canvas'); this.tmpCanvasCtx = this.tmpCanvas.getContext('2d'); this.prevBut = this.element.querySelector('.controls .navbut.prev'); this.nextBut = this.element.querySelector('.controls .navbut.next'); this.pageNumberBox = this.element.querySelector('.controls .page_number'); this.scaleSlider = this.element.querySelector('.controls .scale'); this.scaleLabel = this.element.querySelector('.controls .scale_label'); this.img = this.element.querySelector('.image_wrapper img'); this.img.style.display = 'none'; this.canvas = this.element.querySelector('.image_wrapper canvas'); this.canvasCtx = this.canvas.getContext('2d'); this.imgWrapper = this.element.querySelector('.image_wrapper'); this._curPage = 0; this.pageNumber = null; this.stdWidth /** @type {DjVuWorker} */ this.worker = worker || new DjVuWorker(); this.isCanvasMode = true; this.element.style.width = window.innerWidth * 0.9 + 'px'; this.element.style.height = window.innerHeight * 0.9 + 'px'; this.nextBut.onclick = () => this.showNextPage(); this.prevBut.onclick = () => this.showPrevPage(); this.pageNumberBox.onblur = (e) => this.renderEnteredPage(e); this.pageNumberBox.onkeypress = (e) => this.renderEnteredPageByEnter(e); this.scaleSlider.oninput = () => this.changeScale(); } reset() { clearTimeout(this.improveImageTimeout); if (this.nextBut) { this.nextBut.onclick = null; this.prevBut.onclick = null; this.pageNumberBox.onblur = null; this.pageNumberBox.onkeypress = null; this.scaleSlider.oninput = null; this.worker = null; this.img.src = ''; } } changeScale() { this.scaleLabel.innerText = this.scaleSlider.value; this.isCanvasMode ? this.drawImageOnCanvas() : this.rescaleImageOnImg(); } renderEnteredPageByEnter(e) { if (e.keyCode === 13) { this.pageNumberBox.blur(); // it will call showEnteredPage() as the event handler } } renderEnteredPage(e) { var page = +this.pageNumberBox.value; this.curPage = page; } get curPage() { return this._curPage + 1; } set curPage(value) { this.setPage(value); } showNextPage() { this.curPage += 1; } showPrevPage() { this.curPage -= 1; } lockNavButtons() { this.nextBut.disabled = true; this.prevBut.disabled = true; } unlockNavButtons() { this.nextBut.disabled = false; this.prevBut.disabled = false; } getScaledImageWidth() { return this.imageData.width / this.standardScale * (+this.scaleSlider.value / 100); } renderCurPage() { clearTimeout(this.improveImageTimeout); return this.worker.getPageImageDataWithDPI(this._curPage).then(obj => { this.imageData = obj.imageData; this.imageDPI = obj.dpi; this.standardScale = this.imageDPI ? this.imageDPI / this.defaultDPI : 1; this.drawImageOnCanvas(); this.improveImageTimeout = setTimeout(() => { this.drawImageViaImg(); }, 1000); }); } /** * @returns {Promise} */ loadFile(url) { return new Promise(resolve => { var xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.responseType = "arraybuffer"; xhr.onload = (e) => { DjVu.IS_DEBUG && console.log("File loaded: ", e.loaded); resolve(xhr.response); }; xhr.send(); }); } loadDjVu(url) { // debug functions return this.loadFile(url) .then(buffer => this.worker.createDocument(buffer)) .then(() => this.worker.getPageNumber()) .then(number => { this.pageNumber = number; this.curPage = 1; }); } loadDjVuFromBuffer(buffer) { return this.worker.createDocument(buffer) .then(() => this.worker.getPageNumber()) .then(number => { this.pageNumber = number; this.curPage = 1; }); } relockNavButtons() { this.unlockNavButtons(); if (this._curPage === 0) { this.prevBut.disabled = true; } if (this._curPage === this.pageNumber - 1) { this.nextBut.disabled = true; } } setPage(page) { page--; if (page < 0) { page = 0; } else if (page > this.pageNumber - 1) { page = this.pageNumber - 1; } this._curPage = page; this.relockNavButtons(); this.pageNumberBox.value = this.curPage; this.lockNavButtons(); this.renderCurPage().then(() => { this.relockNavButtons(); }); } _switchToImageMode() { this.img.style.display = 'block'; this.canvas.style.display = 'none'; this.isCanvasMode = false; } _switchToCanvasMode() { this.img.style.display = 'none'; this.canvas.style.display = 'block'; this.isCanvasMode = true; } getImageDataURL() { this.tmpCanvas.width = this.imageData.width; this.tmpCanvas.height = this.imageData.height; this.tmpCanvasCtx.putImageData(this.imageData, 0, 0); return this.tmpCanvas.toDataURL(); } getImageDataURLAsync() { return new Promise(resolve => { this.tmpCanvas.width = this.imageData.width; this.tmpCanvas.height = this.imageData.height; this.tmpCanvasCtx.putImageData(this.imageData, 0, 0); this.tmpCanvas.toBlob(imageBlob => { this.fileReader.onload = event => { resolve(event.target.result); }; this.fileReader.readAsDataURL(imageBlob); }); }); } drawImageViaImg() { this.img.src = this.getImageDataURL(); this.rescaleImageOnImg(); this._switchToImageMode(); } rescaleImageOnImg() { this.img.width = this.getScaledImageWidth(); } drawImageOnCanvas() { //var time = performance.now(); var image = this.imageData; var scale = this.imageDPI ? this.imageDPI / this.defaultDPI : 1; scale /= (+this.scaleSlider.value / 100) this.stdWidth = image.width / scale * (+this.scaleSlider.value / 100); this.stdHeight = image.height / scale * (+this.scaleSlider.value / 100); this.tmpCanvas.width = image.width; this.tmpCanvas.height = image.height; this.tmpCanvasCtx.putImageData(image, 0, 0); var tmpH, tmpW, tmpH2, tmpW2; tmpH = tmpH2 = this.tmpCanvas.height; tmpW = tmpW2 = this.tmpCanvas.width; if (scale > 4) { tmpH = this.tmpCanvas.height / scale * 4; tmpW = this.tmpCanvas.width / scale * 4; //первое сжатие this.tmpCanvasCtx.drawImage(this.tmpCanvas, 0, 0, tmpW, tmpH); } if (scale > 2) { tmpH2 = this.tmpCanvas.height / scale * 2; tmpW2 = this.tmpCanvas.width / scale * 2; //второе сжатие this.tmpCanvasCtx.drawImage(this.tmpCanvas, 0, 0, tmpW, tmpH, 0, 0, tmpW2, tmpH2); } //итоговое сжатие this.canvas.width = image.width / scale; this.canvas.height = image.height / scale; this.canvasCtx.drawImage(this.tmpCanvas, 0, 0, tmpW2, tmpH2, 0, 0, this.canvas.width, this.canvas.height); this._switchToCanvasMode(); //console.log('Render time', performance.now() - time, scale); } } ================================================ FILE: library/debug/js/async.js ================================================ "use strict"; /** * Скрипт для тестирования библиотеки через Web Worker */ var fileSize = 0; var output; var worker; var timeOutput = document.querySelector('#time_output'); var renderTimeOutput = document.querySelector('#render_time_output'); var rerunButton = document.querySelector('#rerun'); rerunButton.onclick = rerun; document.querySelector('#redraw').onclick = redrawPage; var pageNumber = 1; var djvuUrl = '/assets/DjVu3Spec_indirect/index.djvu'; var baseUrl = '/assets/DjVu3Spec_indirect/'; document.querySelector('#next').onclick = () => { pageNumber++; redrawPage(); }; document.querySelector('#prev').onclick = () => { pageNumber--; redrawPage(); }; window.onload = function () { output = document.getElementById("output"); var canvas = document.getElementById('canvas'); var c = canvas.getContext('2d'); Globals.defaultDPI = 100; Globals.Timer = new DebugTimer(); Globals.canvas = canvas; Globals.canvasCtx = c; Globals.dict = []; Globals.img = document.getElementById('img'); // testFunc(); //loadPicture(); renderDjVu(); //initViewer(); //Globals.loadFile('samples/csl.djvu').then(buf => showMetaData(buf)); } function initViewer() { /** @type {DjVuViewer} */ var viewer = new DjVuViewer('.djvu_viewer'); viewer.loadDjVu('samples/csl.djvu'); } async function renderDjVu() { /** @type {DjVuWorker} */ worker = new DjVu.Worker(); const buffer = await fetch(djvuUrl).then(r => r.arrayBuffer()); await worker.createDocument(buffer, { baseUrl }); // const bundle = await worker.doc.bundle(progress => { // console.log(progress); // }).run(); await redrawPage(); } async function redrawPage() { console.log('**** Render Page ****'); var time = performance.now(); var [imageData, dpi] = await worker.run( worker.doc.getPage(pageNumber).getImageData(), worker.doc.getPage(pageNumber).getDpi() ); Globals.drawImage(imageData, dpi * 1.5); time = performance.now() - time; console.log("Redraw time", time); console.log('**** ***** **** ****'); renderTimeOutput.innerText = Math.round(time); } function loadPicture() { var xhr = new XMLHttpRequest(); xhr.open("GET", "samples/bear.jpg"); xhr.responseType = "arraybuffer"; xhr.onload = function (e) { console.log(e.loaded); fileSize = e.loaded; var buf = xhr.response; readPicture(buf); } xhr.send(); } function readPicture(buffer) { createImageBitmap(new Blob([buffer])).then(function (image) { var pictureTotalTime = performance.now(); var canvas = document.getElementById('canvas2'); var c = canvas.getContext('2d'); canvas.width = image.width; canvas.height = image.height; c.drawImage(image, 0, 0); var imageData = c.getImageData(0, 0, image.width, image.height); var iwiw = new IWImageWriter(90, 0, 0); var doc = iwiw.createMultyPageDocument([imageData, imageData, imageData]); // var doc = iwiw.createOnePageDocument(imageData); console.log('docCreateTime = ', performance.now() - pictureTotalTime); var link = document.querySelector('#dochref'); link.href = doc.createObjectURL(); c.putImageData(doc.pages[0].getImage(), 0, 0); console.log('Counter', Globals.counter); //console.log('PZP', Globals.pzp.log.length, ' ', Globals.pzp.offset ); writeln(doc.toString()); console.log('pictureTotalTime = ', performance.now() - pictureTotalTime); }); } function showMetaData(buffer) { var worker = new DjVuWorker(); worker.createDocument(buffer) .then(() => worker.getDocumentMetaData(true)) .then(text => writeln(text)); } function readDjvu(buf) { console.log("DJ1"); var link = document.querySelector('#dochref'); var time = performance.now(); console.log("Buffer length = " + buf.byteLength); //var doc = new DjVuDocument(buf); Globals.counter = 0; var worker = new DjVuWorker(); setTimeout(() => { Globals.Timer.start('TotalTime'); worker.createDocument(buf) .then(() => { Globals.Timer.end('TotalTime', true); return worker.getDocumentMetaData(true); }) .then((str) => { //link.href = URL.createObjectURL(new Blob([buffer])) writeln(str); Globals.Timer.end('TotalTime', true); }); }, 1000); console.log(Globals.Timer.toString()); console.log("Total execution time = ", performance.now() - time); } /** * Функция для работы с файлами загруженными вручную. */ function main(files) { clear(); console.log(files.length); //readFile(file); var fileReader = new FileReader(); var doc1, doc2; fileReader.onload = function () { if (!doc1) { doc1 = new DjVuDocument(this.result); fileReader.readAsArrayBuffer(files[1]); return; } doc2 = new DjVuDocument(this.result); testFunc(doc1, doc2); }; if (files.length > 0) { fileReader.readAsArrayBuffer(files[0]); } } function testFunc(doc1, doc2) { var doc = DjVuDocument.concat(doc1, doc2); Globals.drawImageSmooth(doc.pages[0].getImage(), 600); writeln(doc.toString()); var link = document.querySelector('#dochref'); link.href = doc.createObjectURL(); } ================================================ FILE: library/debug/js/debug.js ================================================ 'use strict'; /** * One more set of debug funcitions that was used in development of the library. * Saved mostly for history. */ //класс для измерения времени с точностью до микросекунд class DebugTimer { constructor() { this.timers = {}; } start(id) { var timer; if (this.timers[id]) { timer = this.timers[id]; } else { timer = { totalTime: 0, timeArray: [], startTime: 0 }; this.timers[id] = timer; } timer.startTime = performance.now(); } end(id, print) { if (!this.timers[id]) { console.log("Несуществующий таймер: ", id); } var timer = this.timers[id]; var time = performance.now() - timer.startTime timer.totalTime += time; timer.timeArray.push(time); if (print) { console.log("Timer '", id, "'", time); } } toString() { var str = '**DebugTimer**\n'; for (var p in this.timers) { str += ">>" + p + " " + this.timers[p].totalTime + "\n" + /*JSON.stringify(this.timers[p].timeArray) + '\n'*/ + '<<\n'; } str += "**DebugTimer**\n"; return str; } } /* * Псевдо ZPСoder для того чтобы видеть битовый поток. */ class PseudoZP { constructor() { this.log = []; this.offset = 0; } encode(bit, ctx, n) { bit = +bit; if (ctx) { var tmp = { bit: bit, ctx: ctx[n], off: n, len: this.log.length }; } else { var tmp = { bit: bit, ctx: -1, off: -1, len: this.log.length }; } this.log.push(tmp); } decode(ctx, n) { var tmp = this.log[this.offset++]; if(!tmp) { Globals.counter++; return 1;} if (ctx) { var cv = ctx[n]; if (!(tmp.ctx === cv && n === tmp.off && tmp.len === (this.offset - 1))) { 4; throw new Error("Context dismatch"); } } else { if (!(tmp.ctx === -1 && tmp.off === -1 && tmp.len === (this.offset - 1))) { throw new Error("Context dismatch"); } } return tmp.bit; } eflush() { console.log("PseudoZP eflushed"); } } function tmpFunc(doc) { var writer = new DjVuWriter(1000000); writer.writeStr("AT&T"); writer.writeStr("FORM"); //todo переделать writer.writeInt32(0); writer.writeStr("DJVU"); var page = doc.pages[3]; writer.writeChunk(page.info); for (var i = 0; i < page.bg44arr.length; i++) { writer.writeChunk(page.bg44arr[i]); } var bs = writer.getByteStream(); console.log(bs.readStr4()); var link = document.querySelector('#dochref'); var nb = writer.getBuffer(); var blob = new Blob([nb]); var url = URL.createObjectURL(blob); link.href = url; var dd = new DjVuDocument(nb); Globals.drawImage(dd.pages[0].getImage()) } function ZPtest() { var bsw = new ByteStreamWriter(100000); var zp = new ZPEncoder(bsw); var n = 64; var ctx = [0]; var arr = []; for (var i = 0; i < n; arr.push(Math.random() * 2 >> 0), i++) {} for (i = 0; i < n; i++) { var byte = arr[i]; var mask = 128; for (var j = 7; j >= 0; j--) { var bit = (byte & mask) >> j; mask >>= 1; zp.encode(bit, ctx, 0); } } zp.eflush(); console.log(arr); ctx = [0]; var bs = new ByteStream(bsw.getBuffer()); var zp = new ZPDecoder(bs); for (i = 0; i < n; i++) { var byte = 0; for (var j = 7; j >= 0; j--) { var bit = zp.decode(ctx, 0); byte = (byte << 1) | bit; } arr[i] = byte; } console.log(arr); console.log("Full length = ", n, " Coded length = ", bs.length); } function BZZtest() { var bs = new ByteStreamWriter(); var zp = new ZPEncoder(bs); var pzp = new PseudoZP(); var bzz = new BZZEncoder(zp); var data = Uint8Array.of(11, 3, 2, 10, 2, 10, 2, 0); bzz.encode(data.buffer); var bsbs = new ByteStream(bs.getBuffer()); var zp2 = new ZPDecoder(bsbs); zp2.pzp = zp.pzp; var bzz = new BZZCodec(zp2); bzz.decode(); var bsz = bzz.getByteStream(); data = new Uint8Array(bsz.buffer); console.log(data); } /* function tmpFunc() { var zigzagRow = []; var zigzagCol = []; for (var i = 0; i < 1024; i++) { var bits = []; for (let j = 0; j < 10; j++) { bits.push((i & Math.pow(2, j)) >> j); } let row = 16 * bits[1] + 8 * bits[3] + 4 * bits[5] + 2 * bits[7] + bits[9]; let col = 16 * bits[0] + 8 * bits[2] + 4 * bits[4] + 2 * bits[6] + bits[8]; zigzagRow.push(row); zigzagCol.push(col); } //console.log(JSON.stringify(zigzagRow)); //console.log(JSON.stringify(zigzagCol)); let r = "["; let c = "["; let k = 0; for (let i = 0; i < 1024; i++) { r += zigzagRow[i] + ','; c += zigzagCol[i] + ','; k++; if (k === 16) { k = 0; r += '\n'; c += '\n'; } } r += ']'; c += ']'; console.log(r); console.log(c); }*/ ================================================ FILE: library/debug/js/examples.js ================================================ /** * This code serves as an example of the API provided by the DjVu.js library. * This very API is used by the DjVu.js Viewer. * * Start to read the code form the main() function in the same direction as it is executed. */ 'use strict'; async function syncInterfaceExamples(djvuDocument) { // The method is async just because it can be used for indirect djvu files, and then // different parts of a document are loaded lazily. // In case of bundled djvu (one file djvu) there are no async operations actually. const djvuPage = await djvuDocument.getPage(1); // note that pages start with 1 NOT WITH 0 // we can get page size even before it's decoded console.log("Page width", djvuPage.getWidth()); console.log("Page height", djvuPage.getHeight()); // get the standard ImageData object representing the page const imageData = djvuPage.getImageData(); // it's the longest operation, here the page is decoded and image is created. // Let's draw the ImageData on a canvas const canvas = document.querySelector('#sync-canvas'); canvas.width = imageData.width; canvas.height = imageData.height; const ctx = canvas.getContext('2d'); ctx.putImageData(imageData, 0, 0); // But it will be much // In fact, our image is very large, so we need to scale it somehow. Let's use its DPI. // DPI is needed to render an image in so called 100% scale. // A usual monitor has 96 dpi (let's say 100). Thus, if an image save with 300 dpi, then // on a usual monitor you should decrease it 300 / 100 = 3 times. // The DjVu.js Viewer uses css scaling only for the initial render. Then it rewrites imageData // on the canvas several times, decreasing it 2 times on each render (or less on the last render). // Such manual scaling gives much better quality, than css scaling. // In case of continuous scroll mode, it uses 's rather than canvases, and imgs are scaled via css much better. // But here we use only css scaling on a canvas. const imageDpi = djvuPage.getDpi(); canvas.style.width = imageData.width / (imageDpi / 100) + 'px'; // when a document has a contents table you can get it const contents = djvuDocument.getContents(); console.log('DjVu Document contents \n\n', contents); // if you want a raw text of the page. This text is used in the DjVu.js Viewer's text mode. const text = djvuPage.getText(); console.log('DjVu Page text \n\n', text); // if you want a structured text zones to create a text layer over an image. const topTextZone = djvuPage.getPageTextZone(); console.log('DjVu Page top text zone \n\n', topTextZone); // or another variant (more convenient for absolute positioning, look at the structure of output to understand the difference) const textZones = djvuPage.getNormalizedTextZones(); console.log('DjVu Page text zones \n\n', textZones); // get the number of page in a document const pageCount = djvuDocument.getPagesQuantity(); console.log("There are ", pageCount, " pages in the document"); // we can get sizes of all pages too. E.g. to render empty pages of an appropriate size while they are being loaded. const pageSizes = djvuDocument.getPagesSizes(); console.log("Pages sizes ", pageSizes); } async function asyncInterfaceExamples(djvuWorker) { // In case of the worker, you cannot get a DjVuPage object explicitly. // You can get only eventual results. // The async interface is similar to the sync one. // djvuWorker.doc is a proxy object, called DjVuTask, which remembers what methods you have called. // Each method of the task returns another task. When it's run, the consequence of methods is executed on // a DjVuDocument object which is created inside the Web Worker, and the result is transferred to the main thread. // You can execute task in batches (it's faster than one by one) // You will get an array of results. const [width, height] = await djvuWorker.run( djvuWorker.doc.getPage(60).getWidth(), djvuWorker.doc.getPage(60).getHeight(), // here you can add more tasks ); console.log('Page size is', width, height) // But there is a convenience method .run() to execute one task const imageData = await djvuWorker.doc.getPage(60).getImageData().run(); // Let's draw the ImageData on a canvas const canvas = document.querySelector('#async-canvas'); canvas.width = imageData.width; canvas.height = imageData.height; const ctx = canvas.getContext('2d'); ctx.putImageData(imageData, 0, 0); // But it will be much // You can execute only one task via djvuWorker.run() too. // In this case you will get only the result, not an array of results. const imageDpi = await djvuWorker.run(djvuWorker.doc.getPage(60).getDpi()); // actually could be executed in one batch with getImageData() canvas.style.width = imageData.width / (imageDpi / 100) + 'px'; // let's execute more operations const [contents, count, sizes, test, textZones] = await djvuWorker.run( djvuWorker.doc.getContents(), djvuWorker.doc.getPagesQuantity(), djvuWorker.doc.getPagesSizes(), djvuWorker.doc.getPage(60).getText(), djvuWorker.doc.getPage(60).getNormalizedTextZones(), ); console.log('More data from async interface: ', contents, count, sizes, test, textZones); // What's more is that the async interface allow you to create image URLs in a Web Worker // asynchronously. If you get an ImageData and then create a URL from it via a canvas element, // you still execute a rather costly operation on the main thread. So it's better to do it on the background thread. // Namely for this feature, DjVu.js is bundled with Png.js. In Chrome, an image URL can be created via OffscreenCanvas // in a web worker, but since Firefox hasn't supported OffscreenCanvas yet, Png.js is used as a polyfill. // The Viewer's continuous scroll mode is based on this very feature. // This method returns not a simple URL, but an object with some additional data // to simplify rendering and memory management. const pageData = await djvuWorker.doc.getPage(21).createPngObjectUrl().run(); console.log('Page image URL with some data', pageData); const img = document.querySelector('#async-image'); img.src = pageData.url; img.style.width = pageData.width / (pageData.dpi / 100) + 'px'; // scale it as well as canvases // Internally URL.createObjectURL is used. It doesn't create a dataURI, but a short url to a blob inside the worker's memory. // It's much faster than to create a dataURI string, but such a URL should be revoked manually after it is used, // otherwise you will get a memory leak. Also, as practice has shown, you can't revoke a URL, created in a web worker, // on the main thread, so you should revoke it in a web worker via djvuWorker.revokeObjectURL() method. // The byteLength property allow you to count how much memory image blobs occupy in the memory now. Since png format is used, // blobs are rather small compared to raw ImageData objects, but still if you don't revoke URLs at all you can get a // noticeable memory leak. It should be mentioned, that you will not see this memory leak in a JS profiler - it can be seen // only in an OS task manager. img.onload = () => { djvuWorker.revokeObjectURL(pageData.url); console.log(pageData.byteLength, 'bytes were released in the worker memory after the image URL was revoked'); } // In fact this feature with Object URL of pages can be used in the sync interface too. // However there is not much sense in it, because it uses OffscreenCanvas or Png.js, while on the main thread you can // just use a normal canvas element and control the process by yourself. } async function main() { // A DjVuDocument is built on top of an ArrayBuffer representing a file. // In case of one-file bundled djvu an ArrayBuffer is all you need to construct a document. const arrayBuffer = await fetch('/assets/DjVu3Spec.djvu').then(res => res.arrayBuffer()); // The sync interface is represented by DjVu.Document. // Usually, you should use DjVu.Document only for debug purposes or maybe on server side (don't know will it work there). // Synchronous operations on the main thread block UI and it spoils user experience badly. // The DjVu.js Viewer uses only async interface (DjVu.Worker). // But since the async API is based on the sync one, you should look at the sync interface first. console.log('%c\n\n\n Sync Interface results \n\n\n\n', "font-size: 3em; color: blue"); const djvuDocument = new DjVu.Document(arrayBuffer); await syncInterfaceExamples(djvuDocument); // The async interface is represented by DjVu.Worker. // DjVu.Worker implicitly creates a Web Worker and all operations are executed on a background thread, // not blocking the UI. For this reason, in case of the browser, // it's by all means recommended to always use DjVu.Worker instead of DjVu.Document. // We copy the array buffer, since the buffer is transferred to the worker and will not be available on the main thread anymore. // Read about Transferable object here https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage console.log('%c\n\n\n Async Interface results \n\n\n\n', "font-size: 3em; color: green"); const arrayBufferCopy = arrayBuffer.slice(0); const djvuWorker = new DjVu.Worker(); // do not pass anything to the constructor! await djvuWorker.createDocument(arrayBufferCopy); await asyncInterfaceExamples(djvuWorker); // After all operation are done or you want to load another document to the worker, // you may reset it to free inner structure and recreate a Web Worker. // Right now there is no method to destroy it, but you can do it via djvuWorker.worker.terminate(). djvuWorker.reset(); // When you work with both async or sync interfaces you should remember about one thing. // When a page is decoded, a lot of memory is allocated for inner structures during a decoding process. // So a decoded page object takes about 30 MB of memory // (by the way, an ImageData of 2539 * 3295 pixels takes 2539 * 3295 * 4 / 1024 / 1024 = 31.9 MB too). // So if you decode 10 pages you get an overhead of 300 MB. It is pretty much. // For this reason, .getPage() method saves the last requested page and reset it (clearing all inner buffers), // when another page is requested. Thus, if you access pages only via .getPage() you will not get 300 MB overhead. // But such behaviour means, that you should avoid accessing pages arbitrary. In other words, try to get all required data // from one page before getting the second, otherwise a page will be decoded several times, every time you access it, and // it's a rather long process, it can take from 100 to 1100 ms depending on a document. // Actually, if you want to get something like width, height or dpi of a page, it's not decoded fully, and this metadata // is extracted rather quickly, but you should remember: once you requested another page your current one is reset, and needs a full // decoding to create an ImageData one more time. } void main(); ================================================ FILE: library/debug/js/handler.js ================================================ 'use strict'; (function() { var canvas = document.getElementById("canvas"); var output = document.getElementById("output2"); function write(str) { output.innerText = str; } canvas.onclick = function (e) { var rect = this.getBoundingClientRect(); write((e.clientX - rect.left) + " " + (e.clientY - rect.top)); } })(); ================================================ FILE: library/debug/js/initScript.js ================================================ "use strict"; /** * Скрипт для тестирования библиотеки непосредственно в синхронном режиме */ DjVu.setDebugMode(true); var fileSize = 0; var output; var djvuArrayBuffer; var djvuDocument; var timeOutput = document.querySelector('#time_output'); var renderTimeOutput = document.querySelector('#render_time_output'); var rerunButton = document.querySelector('#rerun'); rerunButton.onclick = rerun; document.querySelector('#redraw').onclick = redrawPage; var pageNumber = 1; var djvuUrl = 'assets/DjVu3Spec_5-10.djvu'; // var djvuUrl = 'assets/carte.djvu'; var baseUrl = 'assets/DjVu3Spec_indirect/'; document.querySelector('#next').onclick = () => { pageNumber++; redrawPage(); }; document.querySelector('#prev').onclick = () => { pageNumber--; redrawPage(); } function saveStringAsFile(string) { var link = document.createElement('a'); link.download = 'string.txt'; var blob = new Blob([string], { type: 'text/plain' }); link.href = window.URL.createObjectURL(blob); link.click(); } function saveImage(imageData) { var canvas = document.createElement('canvas'); canvas.width = imageData.width; canvas.height = imageData.height; var ctx = canvas.getContext('2d'); ctx.putImageData(imageData, 0, 0); var link = document.createElement('a'); link.download = 'image.png'; link.href = canvas.toDataURL(); link.click(); } function saveStringAsBinFile(string) { var link = document.createElement('a'); link.download = 'string.bin'; var array = new Uint16Array(string.length); for (var i = 0; i < string.length; i++) { array[i] = string.charCodeAt(i); } var blob = new Blob([array], { type: 'application/octet-binary' }); link.href = window.URL.createObjectURL(blob); link.click(); } function rerun() { Globals.init(); Globals.clearCanvas(); setTimeout(async () => { var start = performance.now(); await readDjvu(djvuArrayBuffer); var time = performance.now() - start; timeOutput.innerText = Math.round(time); }, 0); } window.onload = function () { output = document.getElementById("output"); Globals.init(); // testFunc(); loadDjVu(); //loadPicture(); } function loadDjVu() { var xhr = new XMLHttpRequest(); xhr.open("GET", djvuUrl); xhr.responseType = "arraybuffer"; xhr.onload = function (e) { console.log(e.loaded); fileSize = e.loaded; djvuArrayBuffer = xhr.response; rerun(); //splitDjvu(buf); } xhr.send(); } function loadPicture() { Globals.loadFile('samples/csl.djvu').then(buffer => { readDjvu(buffer); }); } function readPicture(buffer) { createImageBitmap(new Blob([buffer])).then(function (image) { var pictureTotalTime = performance.now(); var canvas = document.getElementById('canvas2'); var c = canvas.getContext('2d'); canvas.width = image.width; canvas.height = image.height; c.drawImage(image, 0, 0); var imageData = c.getImageData(0, 0, image.width, image.height); var iwiw = new IWImageWriter(90, 0, 1); // var doc = iwiw.createMultyPageDocument([imageData, imageData, imageData]); iwiw.startMultyPageDocument(); iwiw.addPageToDocument(imageData); //for (var i = 0; i < 5; i++) var buffer = iwiw.endMultyPageDocument(); //var doc = new DjVuDocument(buffer); // var doc = iwiw.createOnePageDocument(imageData); console.log('docCreateTime = ', performance.now() - pictureTotalTime); var link = document.querySelector('#dochref'); link.href = URL.createObjectURL(new Blob([buffer])); // c.putImageData(doc.pages[0].getImage(), 0, 0); console.log('Counter', Globals.counter); //console.log('PZP', Globals.pzp.log.length, ' ', Globals.pzp.offset ); // writeln(doc.toString()); console.log('pictureTotalTime = ', performance.now() - pictureTotalTime); }); } const sleep = (timeout = 0) => new Promise(resolve => setTimeout(resolve, timeout)); async function readDjvu(buf) { console.log('redraw'); var link = document.querySelector('#dochref'); var time = performance.now(); console.log("Buffer length = " + buf.byteLength); djvuDocument = new DjVu.Document(buf, { baseUrl: baseUrl }); //console.log(djvuDocument.toString()); Globals.counter = 0; // console.time('Document bundle'); // const bundle = await djvuDocument.bundle(p => { // console.log(p * 100); // }); // console.timeEnd('Document bundle'); // link.href = bundle.createObjectURL(); //writeln(djvuDocument.toString(true)); //saveStringAsFile(djvuDocument.toString()) //return; //saveStringAsFile(JSON.stringify(djvuDocument.getContents())); // const text = (await djvuDocument.getPage(pageNumber)).getText(); // console.log(text.length, text); // writeln(text); await redrawPage(); //saveStringAsBinFile(djvuDocument.toString()); // doc.countFiles(); //console.log(Globals.Timer.toString()); console.log("Total execution time = ", performance.now() - time); } async function redrawPage() { console.log('**** Render Page ****'); var time = performance.now(); var page = await djvuDocument.getPage(pageNumber); var imageData = page.getImageData(); const dpi = page.getDpi(); //page.reset(); //saveImage(imageData); Globals.drawImage( imageData, dpi * 4 ); //console.log(doc.pages[pageNumber].getText()); time = performance.now() - time; console.log("Redraw time", time); console.log('**** ***** **** ****'); renderTimeOutput.innerText = Math.round(time); // setTimeout(() => { // console.log('**** Refine Page ****'); // var time = performance.now(); // Globals.drawImage( // page.getImageData(), // page.getDpi() * 1.5 // ); // time = performance.now() - time; // console.log("Refine time", time); // console.log('**** ***** **** ****'); // }, 50); } function prepareIframe() { const iframe = document.createElement('iframe'); iframe.style.cssText = ` width: 0; height: 0; position: absolute; left: 0; top: 0; opacity: 0; `; document.body.appendChild(iframe); return iframe; } document.querySelector(('#print_button')).onclick = async () => { const iframe = prepareIframe(); // await sleep(1); console.log(iframe.contentWindow); const promises = []; for (let i = 1; i <= 2; i++) { const page = await djvuDocument.getPage(i); const image = await page.createPngObjectUrl(); const img = iframe.contentWindow.document.createElement('img'); promises.push(new Promise(resolve => img.onload = resolve)); img.style.display = 'block'; img.style.breakAfter = 'page'; img.style.breakInside = 'avoid'; img.style.margin = '0 auto'; img.src = image.url; img.width = image.width; img.height = image.height; img.style.width = (image.width / image.dpi) + 'in'; img.style.height = (image.height / image.dpi) + 'in'; iframe.contentWindow.document.body.appendChild(img); } window.w = iframe.contentWindow; if (/Firefox/.test(navigator.userAgent)) { iframe.contentWindow.print(); } else { await Promise.all(promises); iframe.contentWindow.print(); } } function splitDjvu(buf) { var link = document.querySelector('#dochref'); console.log("Buffer length = " + buf.byteLength); var doc = new DjVuDocument(buf); var slice = doc.slice(0, 11); link.href = slice.createObjectURL(); } /** * Функция для работы с файлами загруженными вручную. */ function main(files) { clear(); console.log(files.length); //readFile(file); var fileReader = new FileReader(); var doc1, doc2; fileReader.onload = function () { if (!doc1) { doc1 = new DjVuDocument(this.result); fileReader.readAsArrayBuffer(files[1]); return; } doc2 = new DjVuDocument(this.result); testFunc(doc1, doc2); }; if (files.length > 0) { fileReader.readAsArrayBuffer(files[0]); } } function testFunc(doc1, doc2) { var doc = DjVuDocument.concat(doc1, doc2); Globals.drawImageSmooth(doc.pages[0].getImage(), 600); writeln(doc.toString()); var link = document.querySelector('#dochref'); link.href = doc.createObjectURL(); } ================================================ FILE: library/debug/js/reloader.js ================================================ /** * A simple websocket client reloading a page when a bundle is updated. */ (function () { 'use strict'; function setConnection() { var address = 'ws://' + location.host; console.log(`%cTrying to open a connention with ${address} ...`, "color: blue"); var ws = new WebSocket(address); ws.onopen = () => console.info(`%cConnection is opened with ${address}. The page will be reloaded on each update.`, "color: green"); ws.onmessage = message => { if (message.data === 'reload') { window.location.reload(); } }; ws.onclose = (e) => { console.info(`%cConnection is closed!`, 'color: red'); setConnection(); } } setConnection(); })(); ================================================ FILE: library/debug/sync.html ================================================ DjVu.js

    
    
Скачать Печать
================================================ FILE: library/package.json ================================================ { "name": "DjVu.js_Library", "scripts": { "start": "node server.js", "watch": "rollup --config --watch", "build": "rollup --config" }, "devDependencies": { "colors": "^1.4.0", "express": "^4.18.2", "rollup": "^3.28.0", "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^5.2.0", "ws": "^8.13.0" }, "dependencies": { "pngjs": "5.0.0" } } ================================================ FILE: library/rollup.config.js ================================================ 'use strict'; const cleanup = require('rollup-plugin-cleanup'); const resolve = require('rollup-plugin-node-resolve'); const commonjs = require('rollup-plugin-commonjs'); const outputTemplate = { format: 'iife', name: 'DjVu', intro: "function DjVuScript() {\n'use strict';", outro: "}\nreturn Object.assign(DjVuScript(), {DjVuScript});" }; module.exports = { input: './src/index.js', output: [ Object.assign({ file: 'dist/djvu.js' }, outputTemplate), Object.assign({ file: '../viewer/public/tmp/djvu.js' }, outputTemplate), Object.assign({ file: '../extension/dist/djvu.js' }, outputTemplate) ], plugins: [ resolve(), commonjs(), cleanup() ] }; ================================================ FILE: library/server.js ================================================ /** * A server used for debugging. */ 'use strict'; const http = require('http'); const fs = require('fs'); const WebSocket = require('ws'); require('colors'); const path = require('path'); const rollup = require('rollup'); const express = require('express'); const rollupConfig = require('./rollup.config.js'); // you can pass the parameter in the command line. e.g. node server.js 3000 const port = process.argv[2] || 9000; const app = express(); app.use(express.static(__dirname)); app.use(express.static(__dirname + '/debug')); app.use('/tests', express.static('./tests', { index: 'tests.html' })); // used to debug the request interception in the extension according to the headers app.get('/file_without_extension', (req, res) => { res.sendFile(path.resolve('./assets/DjVu3Spec.djvu'), { headers: { 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'inline; filename="TestFileName.djvu"', } }); }); app.get('/get_embed_djvu_html', (req, res) => { res.send(` `); }); app.use((req, res) => { res.status(404).end('No such a page! 404 error!'); }); const server = http.createServer(app); const wss = new WebSocket.Server({ server: server }); wss.broadcast = function (data) { this.clients.forEach(client => { client.send(data); }); } fs.watch('./debug/', { recursive: true }, () => wss.broadcast('reload')); // watch debug .js files fs.watch('./tests/', () => wss.broadcast('reload')); // watch tests files const watcher = rollup.watch(rollupConfig); watcher.on('event', event => { switch (event.code) { case 'BUNDLE_START': console.log('Start building ...'.blue.bold); break; case 'BUNDLE_END': console.log(`Bundle was created in ${event.duration} ms! \n`.green.bold); wss.broadcast('reload'); break; case 'ERROR': console.log('\n Error occured! \n'.red.bold); console.log(event); console.log('\n'); break; case 'FATAL': console.log('\n Fatal Error occured! \n'.red.bold); console.log(event); process.exit(); break; } }); server.listen(parseInt(port)); console.log(`Http and WebSocket servers are listening on port ${port}`); ================================================ FILE: library/src/ByteStream.js ================================================ import { createStringFromUtf8Array } from './DjVu' /** @typedef {ByteStream} ByteStream */ /** * Объект байтового потока. Предоставляет API для чтения сырого ArrayBuffer как потока байт. * После вызова каждого метода чтения, внутренний указатель смещается автоматически. * Можно читать числа, строки, массив байт разной длины. */ export default class ByteStream { constructor(buffer, offsetx, length) { this._buffer = buffer; this.offsetx = offsetx || 0; this.offset = 0; this._length = length || buffer.byteLength; if (this._length + offsetx > buffer.byteLength) { this._length = buffer.byteLength - offsetx; console.error("Incorrect length in ByteStream!"); } this.viewer = new DataView(this._buffer, this.offsetx, this._length); } /** @returns {number} */ get length() { return this._length; } /** @returns {ArrayBuffer} */ get buffer() { return this._buffer; } // "читает" следующие length байт в массив, возвращает массив основанный на том же ArrayBuffer getUint8Array(length = this.remainingLength()) { var off = this.offset; this.offset += length; return new Uint8Array(this._buffer, this.offsetx + off, length); } // возвращает массив полностью представляющий весь поток toUint8Array() { return new Uint8Array(this._buffer, this.offsetx, this._length); } remainingLength() { return this._length - this.offset; } reset() { this.offset = 0; } byte() { // the function is used inside other codecs (look at ZPCodec) if (this.offset >= this._length) { this.offset++; return 0xff; } return this.viewer.getUint8(this.offset++); } getInt8() { return this.viewer.getInt8(this.offset++); } getInt16() { var tmp = this.viewer.getInt16(this.offset); this.offset += 2; return tmp; } getUint16() { var tmp = this.viewer.getUint16(this.offset); this.offset += 2; return tmp; } getInt32() { var tmp = this.viewer.getInt32(this.offset); this.offset += 4; return tmp; } getUint8() { return this.viewer.getUint8(this.offset++); } getInt24() { var uint = this.getUint24(); return (uint & 0x800000) ? (0xffffff - val + 1) * -1 : uint } getUint24() { return (this.byte() << 16) | (this.byte() << 8) | this.byte(); } jump(length) { this.offset += length; return this; } setOffset(offset) { this.offset = offset; } readStr4() { // used to read chunk names, just ASCII characters return String.fromCharCode(...this.getUint8Array(4)); } readStrNT() { var array = []; var byte = this.getUint8(); while (byte) { array.push(byte); byte = this.getUint8(); } return createStringFromUtf8Array(new Uint8Array(array)); } readStrUTF(byteLength) { return createStringFromUtf8Array(this.getUint8Array(byteLength)); } fork(length = this.remainingLength()) { return new ByteStream(this._buffer, this.offsetx + this.offset, length); } clone() { return new ByteStream(this._buffer, this.offsetx, this._length); } isEmpty() { return this.offset >= this._length; } } ================================================ FILE: library/src/ByteStreamWriter.js ================================================ import { stringToCodePoints, codePointsToUtf8 } from './DjVu'; const pageSize = 64 * 1024; const growthLimit = 20 * 1024 * 1024 / pageSize; export default class ByteStreamWriter { constructor(length = 0) { // As the practice has shown, usage of WebAssembly.Memory and its grow() method // is more robust than the manual expansion of ArrayBuffer // via `new Uint8Array(newBuffer).set(new Uint8Array(oldBuffer))`. // In particular, with WebAssembly.Memory it's possible to download and bundle // a document that is about 1.7 GB in size, while with raw ArrayBuffers // a browser tab crashes (in Chrome) when the buffer reaches about 1.5 GB // (or there is an error that a buffer cannot be allocated). this.memory = new WebAssembly.Memory({ initial: Math.ceil(length / pageSize), maximum: 65536 }); this.assignBufferFromMemory(); this.offset = 0; this.offsetMarks = {}; } assignBufferFromMemory() { this.buffer = this.memory.buffer; this.viewer = new DataView(this.buffer); } /** * Переводит смещение на начало и зачищает сохраненные смещения */ reset() { this.offset = 0; this.offsetMarks = {}; } saveOffsetMark(mark) { this.offsetMarks[mark] = this.offset; return this; } writeByte(byte) { this.checkOffset(1); this.viewer.setUint8(this.offset++, byte); return this; } writeStr(str) { this.writeArray(codePointsToUtf8(stringToCodePoints(str))); return this; } writeInt32(val) { this.checkOffset(4); this.viewer.setInt32(this.offset, val); this.offset += 4; return this; } /** * Перезапись числа. Принимает смещение или метку смещения и число */ rewriteInt32(off, val) { var xoff = off; if (typeof (xoff) === 'string') { xoff = this.offsetMarks[off]; this.offsetMarks[off] += 4; } this.viewer.setInt32(xoff, val); } /** * Перезапись размера в 4 байта по сохраненной метке */ rewriteSize(offmark) { if (!this.offsetMarks[offmark]) throw new Error('Unexisting offset mark'); var xoff = this.offsetMarks[offmark]; this.viewer.setInt32(xoff, this.offset - xoff - 4); } getBuffer() { if (this.offset === this.buffer.byteLength) { return this.buffer; } return this.buffer.slice(0, this.offset); } checkOffset(requiredBytesNumber = 0) { const bool = this.offset + requiredBytesNumber > this.buffer.byteLength; if (bool) { this._expand(requiredBytesNumber); } return bool; } _expand(requiredBytesNumber) { this.memory.grow(Math.max( Math.ceil(requiredBytesNumber / pageSize), Math.min(this.memory.buffer.byteLength / pageSize, growthLimit) )); this.assignBufferFromMemory(); } //смещение на length байт jump(length) { length = +length; if (length > 0) { this.checkOffset(length); } this.offset += length; return this; } writeByteStream(bs) { this.writeArray(bs.toUint8Array()); } writeArray(arr) { while (this.checkOffset(arr.length)) { } new Uint8Array(this.buffer).set(arr, this.offset); this.offset += arr.length; } writeBuffer(buffer) { this.writeArray(new Uint8Array(buffer)); } writeStrNT(str) { this.writeStr(str); this.writeByte(0); } writeInt16(val) { this.checkOffset(2); this.viewer.setInt16(this.offset, val); this.offset += 2; return this; } writeUint16(val) { this.checkOffset(2); this.viewer.setUint16(this.offset, val); this.offset += 2; return this; } writeInt24(val) { this.writeByte((val >> 16) & 0xff) .writeByte((val >> 8) & 0xff) .writeByte(val & 0xff); return this; } } ================================================ FILE: library/src/DjVu.js ================================================ var DjVu = { VERSION: '0.5.4', IS_DEBUG: false, setDebugMode: (flag) => DjVu.IS_DEBUG = flag }; export function pLimit(limit = 4) { const queue = []; let running = 0; const runNext = async () => { if (!queue.length || running >= limit) return; const func = queue.shift(); try { running++; await func(); } finally { running--; runNext(); } }; return func => new Promise((resolve, reject) => { queue.push(() => func().then(resolve, reject)); runNext(); }); } /** * @returns {Promise} */ export function loadFileViaXHR(url, responseType = 'arraybuffer') { return new Promise((resolve, reject) => { var xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.responseType = responseType; xhr.onload = (e) => resolve(xhr); xhr.onerror = (e) => reject(xhr); xhr.send(); }); } const utf8Decoder = self.TextDecoder ? new self.TextDecoder() : { decode(utf8array) { const codePoints = utf8ToCodePoints(utf8array); return String.fromCodePoint(...codePoints); } }; export function createStringFromUtf8Array(utf8array) { return utf8Decoder.decode(utf8array); } /** * Creates an array of Unicode code points from an array, representing a utf8 encoded string * The code assumes that the utf-8 input is well formed. Otherwise, can produce illegal code * points. As the practice has shown, there are ill-formed utf-8 arrays in some djvu files. * * This function should be removed in the future. The standard TextDecoder/TextEncoder should * be used instead. Its was initially written only for the old Edge browser * which didn't support TextDecoder. */ export function utf8ToCodePoints(utf8array) { var i, c; var codePoints = []; i = 0; while (i < utf8array.length) { c = utf8array[i++]; switch (c >> 4) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: // 0xxx xxxx codePoints.push(c); break; case 12: case 13: // 110x xxxx 10xx xxxx codePoints.push(((c & 0x1F) << 6) | (utf8array[i++] & 0x3F)); break; case 14: // 1110 xxxx 10xx xxxx 10xx xxxx codePoints.push( ((c & 0x0F) << 12) | ((utf8array[i++] & 0x3F) << 6) | (utf8array[i++] & 0x3F) ); break; case 15: // 1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx codePoints.push( ((c & 0x07) << 18) | ((utf8array[i++] & 0x3F) << 12) | ((utf8array[i++] & 0x3F) << 6) | (utf8array[i++] & 0x3F) ); break; } } return codePoints.map(codePoint => { return codePoint > 0x10FFFF ? 120 : codePoint; // replace all bad code points with "x" }); } export function codePointsToUtf8(codePoints) { var utf8array = []; codePoints.forEach(codePoint => { if (codePoint < 0x80) { utf8array.push(codePoint); } else if (codePoint < 0x800) { utf8array.push(0xC0 | (codePoint >> 6)); utf8array.push(0x80 | (codePoint & 0x3F)); } else if (codePoint < 0x10000) { utf8array.push(0xE0 | (codePoint >> 12)); utf8array.push(0x80 | ((codePoint >> 6) & 0x3F)); utf8array.push(0x80 | (codePoint & 0x3F)); } else { utf8array.push(0xF0 | (codePoint >> 18)); utf8array.push(0x80 | ((codePoint >> 12) & 0x3F)); utf8array.push(0x80 | ((codePoint >> 6) & 0x3F)); utf8array.push(0x80 | (codePoint & 0x3F)); } }); return new Uint8Array(utf8array); } export function stringToCodePoints(str) { var codePoints = []; for (var i = 0; i < str.length; i++) { var code = str.codePointAt(i); codePoints.push(code); if (code > 65535) { i++; // skip the second part of 4 byte symbol } } return codePoints; } // unicode test: symbols of 1, 2, 4 and 3 bytes (in utf8 encoding) are encoded and decoded // var str = 'szвф' + String.fromCodePoint(0x1F702) + String.fromCodePoint(0x1F704) + String.fromCodePoint(0x2C00) + String.fromCodePoint(0x2C08); // var str2 = String.fromCodePoint(...utf8ToCodePoints(codePointsToUtf8(stringToCodePoints(str)))); // console.log(str, str2, str === str2); export default DjVu; ================================================ FILE: library/src/DjVuDocument.js ================================================ import DjViChunk from './chunks/DjViChunk'; import DjVuPage from './DjVuPage'; import DIRMChunk from './chunks/DirmChunk'; import NAVMChunk from './chunks/NavmChunk'; import DjVuWriter from './DjVuWriter'; import DjVu from './DjVu'; import ThumChunk from './chunks/ThumChunk'; import ByteStream from './ByteStream'; import { loadPageDependency, loadPage } from './methods/load'; import { IncorrectFileFormatDjVuError, NoSuchPageDjVuError, CorruptedFileDjVuError, NoBaseUrlDjVuError, } from './DjVuErrors'; /** @typedef {DjVuDocument} DjVuDocument */ const MEMORY_LIMIT = 50 * 1024 * 1024; // 50 MB export default class DjVuDocument { constructor(arraybuffer, { baseUrl = null, memoryLimit = MEMORY_LIMIT } = {}) { this.buffer = arraybuffer; this.baseUrl = baseUrl && baseUrl.trim(); if (typeof this.baseUrl === 'string') { if (this.baseUrl[this.baseUrl.length - 1] !== '/') { this.baseUrl += '/'; } if (!/^[A-Za-z]+:\/\//.test(this.baseUrl)) { // a relative URL // all URL in a worker should be absolute // in case of a local web page opened as file:/// there is no location.origin. this.baseUrl = location.origin && (new URL(this.baseUrl, location.origin).href); } } this.memoryLimit = memoryLimit; // required to limit the size of cache in case of indirect djvu this.djvi = {}; //разделяемые ресурсы. Могут потребоваться и в случае одностраничного документа this.getINCLChunkCallback = id => this.djvi[id].innerChunk; this.bs = new ByteStream(arraybuffer); this.formatID = this.bs.readStr4(); if (this.formatID !== 'AT&T') { throw new IncorrectFileFormatDjVuError(); } this.id = this.bs.readStr4(); this.length = this.bs.getInt32(); this.id += this.bs.readStr4(); if (this.id === 'FORMDJVM') { this._initMultiPageDocument(); } else if (this.id === 'FORMDJVU') { this.bs.jump(-12); this.pages = [new DjVuPage(this.bs.fork(this.length + 8), this.getINCLChunkCallback)]; } else { throw new CorruptedFileDjVuError( `The id of the first chunk of the document should be either FORMDJVM or FORMDJVU, but there is ${this.id}` ); } } _initMultiPageDocument() { // for FORMDJVM this._readMetaDataChunk(); this._readContentsChunkIfExists(); /** * @type {Array} */ this.pages = []; //страницы FORMDJVU this.thumbs = []; if (this.dirm.isBundled) { this._parseComponents(); } else { this.pages = new Array(this.dirm.getPagesQuantity()); // fixed length array in order to know what pages are loaded and what are not. this.memoryUsage = this.bs.buffer.byteLength; this.loadedPageNumbers = []; } } _readMetaDataChunk() { // DIRM chunk var id = this.bs.readStr4(); if (id !== 'DIRM') { throw new CorruptedFileDjVuError("The DIRM chunk must be the first but there is " + id + " instead!"); } var length = this.bs.getInt32(); this.bs.jump(-8); this.dirm = new DIRMChunk(this.bs.fork(length + 8)); // document directory, metadata for multi-page documents this.bs.jump(8 + length + (length & 1 ? 1 : 0)); } _readContentsChunkIfExists() { // NAVM chunk this.navm = null; // человеческое оглавление if (this.bs.remainingLength() > 8) { var id = this.bs.readStr4(); var length = this.bs.getInt32(); this.bs.jump(-8); if (id === 'NAVM') { this.navm = new NAVMChunk(this.bs.fork(length + 8)) } } } _parseComponents() { // all chunks of the file in the order which they are listed in the DIRM chunk this.dirmOrderedChunks = new Array(this.dirm.getFilesQuantity()); for (var i = 0; i < this.dirm.offsets.length; i++) { this.bs.setOffset(this.dirm.offsets[i]); var id = this.bs.readStr4(); var length = this.bs.getInt32(); id += this.bs.readStr4(); this.bs.jump(-12); switch (id) { case "FORMDJVU": this.pages.push(this.dirmOrderedChunks[i] = new DjVuPage( this.bs.fork(length + 8), this.getINCLChunkCallback )); break; case "FORMDJVI": //через строчку id chunk INCL ссылается на нужный ресурс this.dirmOrderedChunks[i] = this.djvi[this.dirm.ids[i]] = new DjViChunk(this.bs.fork(length + 8)); break; case "FORMTHUM": this.thumbs.push(this.dirmOrderedChunks[i] = new ThumChunk(this.bs.fork(length + 8))); break; default: console.error("Incorrect chunk ID: ", id); } } } /** * @returns {Array<{ width: number, height: number, dpi: number }>} */ getPagesSizes() { var sizes = this.pages.map(page => { return { width: page.getWidth(), height: page.getHeight(), dpi: page.getDpi(), }; }); this.pages.forEach(page => page.reset()); return sizes; } isBundled() { return this.dirm ? this.dirm.isBundled : true; } getPagesQuantity() { return this.dirm ? this.dirm.getPagesQuantity() : 1; } /** @returns {import('./chunks/NavmChunk').Contents} */ getContents() { return this.navm ? this.navm.getContents() : null; } getMemoryUsage() { return this.memoryUsage; } getMemoryLimit() { return this.memoryLimit; } setMemoryLimit(limit = MEMORY_LIMIT) { this.memoryLimit = limit; } getPageNumberByUrl(url) { if (url[0] !== '#') { return null; } const ref = url.slice(1); let pageNumber = this.dirm.getPageNumberByItsId(ref); if (!pageNumber) { const num = Math.round(Number(ref)); if (num >= 1 && num <= this.pages.length) { // there can be refs like "#057"; pageNumber = num; } } return pageNumber || null; } releaseMemoryIfRequired(preservedDependencies = null) { if (this.memoryUsage <= this.memoryLimit) { //console.log(`%c Memory wasnt released ${this.memoryUsage}, ${this.memoryLimit}, ${this.loadedPageNumbers.length}, ${Object.keys(this.djvi).length}`, "color: green"); return; } //var was = this.memoryUsage; while (this.memoryUsage > this.memoryLimit && this.loadedPageNumbers.length) { var number = this.loadedPageNumbers.shift(); this.memoryUsage -= this.pages[number].bs.buffer.byteLength; this.pages[number] = null; } if (this.memoryUsage > this.memoryLimit && !this.loadedPageNumbers.length) { // remove all dictionaries, if there is no pages this.resetLastRequestedPage(); var newDjVi = {}; if (preservedDependencies) { preservedDependencies.forEach(id => { newDjVi[id] = this.djvi[id]; this.memoryUsage += newDjVi[id].bs.buffer.byteLength; // will be subtracted back further }); } Object.keys(this.djvi).forEach(key => { this.memoryUsage -= this.djvi[key].bs.buffer.byteLength; }); this.djvi = newDjVi; } //console.log(`%c Memory was released ${was}, ${this.memoryUsage}, ${this.loadedPageNumbers.length}, ${Object.keys(this.djvi).length}`, "color: red"); } _getUrlByPageNumber(number) { return this.baseUrl + this.dirm.getPageNameByItsNumber(number); } async getPage(number) { var page = this.pages[number - 1]; if (this.lastRequestedPage && this.lastRequestedPage !== page) { this.lastRequestedPage.reset(); } this.lastRequestedPage = page; if (!page) { if (number < 1 || number > this.pages.length || this.isBundled()) { throw new NoSuchPageDjVuError(number); } else { if (this.baseUrl === null) { throw new NoBaseUrlDjVuError(); } const bs = await loadPage( number, this._getUrlByPageNumber(number) ); const page = new DjVuPage(bs, this.getINCLChunkCallback); this.memoryUsage += bs.buffer.byteLength; await this._loadDependencies(page.getDependencies(), number); this.releaseMemoryIfRequired(page.getDependencies()); // should be called before the page are added to the pages array this.pages[number - 1] = page; this.loadedPageNumbers.push(number - 1); this.lastRequestedPage = page; } } else if (!this.isOnePageDependenciesLoaded && this.id === "FORMDJVU") { // single page document var dependencies = page.getDependencies(); if (dependencies.length) { await this._loadDependencies(dependencies, 1); } this.isOnePageDependenciesLoaded = true; } return this.lastRequestedPage; } async _loadDependencies(dependencies, pageNumber = null) { var unloadedDependencies = dependencies.filter(id => !this.djvi[id]); if (!unloadedDependencies.length) { return; } await Promise.all(unloadedDependencies.map(async id => { const bs = await loadPageDependency( id, this.dirm ? this.dirm.getComponentNameByItsId(id) : id, this.baseUrl, pageNumber ); this.djvi[id] = new DjViChunk(bs); this.memoryUsage += bs.buffer.byteLength; })); } getPageUnsafe(number) { return this.pages[number - 1]; } resetLastRequestedPage() { this.lastRequestedPage && this.lastRequestedPage.reset(); this.lastRequestedPage = null; } /** A debug function, isn't actually used */ countFiles() { var count = 0; var bs = this.bs.clone(); bs.jump(16); while (!bs.isEmpty()) { var id = bs.readStr4(); var length = bs.getInt32(); // перепрыгнули к следующей порции bs.jump(length + (length & 1 ? 1 : 0)); if (id === 'FORM') { count++; } } return count; } /** * Возвращает метаданные документа. * @param {Boolean} html - заменять ли \n на
* @returns {string} строка метаданных */ toString(html) { var str = this.formatID + '\n'; if (this.dirm) { // multi page document str += this.id + " " + this.length + '\n\n'; str += this.dirm.toString(); str += this.navm ? this.navm.toString() : ''; if (this.isBundled()) { this.dirmOrderedChunks.forEach((chunk, i) => { str += this.dirm.getMetadataStringByIndex(i) + chunk.toString(); }); } else { for (let i = 0; i < this.dirm.getFilesQuantity(); i++) { str += this.dirm.getMetadataStringByIndex(i); } } } else { // single page document str += this.pages[0].toString(); } return html ? str.replace(/\n/g, '
').replace(/\s/g, ' ') : str; } /** * Создает ссылку для скачивания документа */ createObjectURL() { var blob = new Blob([this.bs.buffer]); var url = URL.createObjectURL(blob); return url; } /** * Creates a new DjVuDocument with pages from "from" to "to", including first and last pages. */ slice(from = 1, to = this.pages.length) { const djvuWriter = new DjVuWriter(); djvuWriter.startDJVM(); const dirm = { dflags: this.dirm.dflags, flags: [], names: [], titles: [], sizes: [], ids: [], }; const chunkByteStreams = []; const totalPageCount = to - from + 1; // все зависимости страниц в новом документе // нужно чтобы не копировать лишние словари const dependencies = {}; const filesQuantity = this.dirm.getFilesQuantity(); // находим все зависимости в первом проходе for ( let i = 0, pageIndex = 0, addedPageCount = 0; i < filesQuantity && addedPageCount < totalPageCount; i++ ) { const isPage = (this.dirm.flags[i] & 63) === 1; if (!isPage) continue; pageIndex++; if (pageIndex < from) continue; addedPageCount++; const pageByteStream = new ByteStream(this.buffer, this.dirm.offsets[i], this.dirm.sizes[i]); const deps = new DjVuPage(pageByteStream).getDependencies(); for (const dependencyId of deps) { dependencies[dependencyId] = 1; } } // теперь все словари и страницы, которые нужны for ( let i = 0, pageIndex = 0, addedPageCount = 0; // ?? maybe dicts can go after pages and we should check all chunks (remove addedPageCount < totalPageCount) i < filesQuantity && addedPageCount < totalPageCount; i++ ) { const isPage = (this.dirm.flags[i] & 63) === 1; if (isPage) { pageIndex++; //если она не входит в заданный диапазон if (pageIndex < from) continue; addedPageCount++; } //копируем страницы и словари. Эскизы пропускаем - пока что это не реализовано if ((this.dirm.ids[i] in dependencies) || isPage) { dirm.flags.push(this.dirm.flags[i]); dirm.sizes.push(this.dirm.sizes[i]); dirm.ids.push(this.dirm.ids[i]); dirm.names.push(this.dirm.names[i]); dirm.titles.push(this.dirm.titles[i]); chunkByteStreams.push( new ByteStream(this.buffer, this.dirm.offsets[i], this.dirm.sizes[i]) ); } } djvuWriter.writeDirmChunk(dirm); if (this.navm) { djvuWriter.writeChunk(this.navm); } for (const chunkByteStream of chunkByteStreams) { djvuWriter.writeFormChunkBS(chunkByteStream); } const newBuffer = djvuWriter.getBuffer(); DjVu.IS_DEBUG && console.log("New Buffer size = ", newBuffer.byteLength); return new DjVuDocument(newBuffer); } /** * Функция склейки двух документов */ static concat(doc1, doc2) { var dirm = {}; var length = doc1.pages.length + doc2.pages.length; dirm.dflags = 129; dirm.flags = []; dirm.sizes = []; dirm.ids = []; var pages = []; var idset = new Set(); // чтобы убрать повторяющиеся id if (!doc1.dirm) { // тогда записываем свой id dirm.flags.push(1); dirm.sizes.push(doc1.pages[0].bs.length); dirm.ids.push('single'); idset.add('single'); pages.push(doc1.pages[0]); } else { for (var i = 0; i < doc1.pages.length; i++) { dirm.flags.push(doc1.dirm.flags[i]); dirm.sizes.push(doc1.dirm.sizes[i]); dirm.ids.push(doc1.dirm.ids[i]); idset.add(doc1.dirm.ids[i]); pages.push(doc1.pages[i]); } } if (!doc2.dirm) { // тогда записываем свой id dirm.flags.push(1); dirm.sizes.push(doc2.pages[0].bs.length); var newid = 'single2'; var tmp = 0; while (idset.has(newid)) { // генерируем уникальный id newid = 'single2' + tmp.toString(); tmp++; } dirm.ids.push(newid); pages.push(doc2.pages[0]); } else { for (var i = 0; i < doc2.pages.length; i++) { dirm.flags.push(doc2.dirm.flags[i]); dirm.sizes.push(doc2.dirm.sizes[i]); var newid = doc2.dirm.ids[i]; var tmp = 0; while (idset.has(newid)) { // генерируем уникальный id newid = doc2.dirm.ids[i] + tmp.toString(); tmp++; } dirm.ids.push(newid); idset.add(newid); pages.push(doc2.pages[i]); } } var dw = new DjVuWriter(); dw.startDJVM(); dw.writeDirmChunk(dirm); for (var i = 0; i < length; i++) { dw.writeFormChunkBS(pages[i].bs); } return new DjVuDocument(dw.getBuffer()); } } import bundle from './methods/bundle'; Object.assign(DjVuDocument.prototype, { bundle, }); ================================================ FILE: library/src/DjVuErrors.js ================================================ /** * Простейший класс ошибки, не содержит рекурсивных данных, чтобы иметь возможность копироваться * между потоками в сообщениях */ export class DjVuError { constructor(code, message, additionalData = null) { this.code = code; this.message = message; if (additionalData) this.additionalData = additionalData; } } export class IncorrectFileFormatDjVuError extends DjVuError { constructor() { super(DjVuErrorCodes.INCORRECT_FILE_FORMAT, "The provided file is not a .djvu file!"); } } export class NoSuchPageDjVuError extends DjVuError { constructor(pageNumber) { super(DjVuErrorCodes.NO_SUCH_PAGE, "There is no page with the number " + pageNumber + " !"); this.pageNumber = pageNumber; } } export class CorruptedFileDjVuError extends DjVuError { constructor(message = "", data = null) { super(DjVuErrorCodes.FILE_IS_CORRUPTED, "The file is corrupted! " + message, data); } } export class UnableToTransferDataDjVuError extends DjVuError { constructor(tasks) { super(DjVuErrorCodes.DATA_CANNOT_BE_TRANSFERRED, "The data cannot be transferred from the worker to the main page! " + "Perhaps, you requested a complex object like DjVuPage, but only simple objects can be transferred between workers." ); this.tasks = tasks; } } export class IncorrectTaskDjVuError extends DjVuError { constructor(task) { super(DjVuErrorCodes.INCORRECT_TASK, "The task contains an incorrect sequence of functions!"); this.task = task; } } export class NoBaseUrlDjVuError extends DjVuError { constructor() { super(DjVuErrorCodes.NO_BASE_URL, "The base URL is required for the indirect djvu to load components," + " but no base URL was provided to the document constructor!" ); } } function getErrorMessageByData(data) { var message = ''; if (data.pageNumber) { if (data.dependencyId) { message = `A dependency ${data.dependencyId} for the page number ${data.pageNumber} can't be loaded!\n`; } else { message = `The page number ${data.pageNumber} can't be loaded!`; } } else if (data.dependencyId) { message = `A dependency ${data.dependencyId} can't be loaded!\n`; } return message; } export class UnsuccessfulRequestDjVuError extends DjVuError { constructor(xhr, data = { pageNumber: null, dependencyId: null }) { var message = getErrorMessageByData(data); super(DjVuErrorCodes.UNSUCCESSFUL_REQUEST, message + '\n' + `The request to ${xhr.responseURL} wasn't successful.\n` + `The response status is ${xhr.status}.\n` + `The response status text is: "${xhr.statusText}".` ); this.status = xhr.status; this.statusText = xhr.statusText; this.url = xhr.responseURL; if (data.pageNumber) { this.pageNumber = data.pageNumber; } if (data.dependencyId) { this.dependencyId = data.dependencyId; } } } export class NetworkDjVuError extends DjVuError { constructor(data = { pageNumber: null, dependencyId: null, url: null }) { super(DjVuErrorCodes.NETWORK_ERROR, getErrorMessageByData(data) + '\n' + "A network error occurred! Check your network connection!" ); if (data.pageNumber) { this.pageNumber = data.pageNumber; } if (data.dependencyId) { this.dependencyId = data.dependencyId; } if (data.url) { this.url = data.url; } } } export const DjVuErrorCodes = Object.freeze({ FILE_IS_CORRUPTED: 'FILE_IS_CORRUPTED', INCORRECT_FILE_FORMAT: 'INCORRECT_FILE_FORMAT', NO_SUCH_PAGE: 'NO_SUCH_PAGE', UNEXPECTED_ERROR: 'UNEXPECTED_ERROR', DATA_CANNOT_BE_TRANSFERRED: 'DATA_CANNOT_BE_TRANSFERRED', INCORRECT_TASK: 'INCORRECT_TASK', NO_BASE_URL: 'NO_BASE_URL', NETWORK_ERROR: 'NETWORK_ERROR', UNSUCCESSFUL_REQUEST: 'UNSUCCESSFUL_REQUEST', }); ================================================ FILE: library/src/DjVuPage.js ================================================ import { INCLChunk, ColorChunk, CIDaChunk, IFFChunk, INFOChunk, CompositeChunk, ErrorChunk } from './chunks/IFFChunks'; import JB2Dict from './jb2/JB2Dict'; import JB2Image from './jb2/JB2Image'; import DjVuPalette from './chunks/DjVuPalette'; import IWImage from './iw44/IWImage'; import DjVuText from './chunks/DjVuText'; import { ZPDecoder } from './ZPCodec'; import DjVu from './DjVu'; import { CorruptedFileDjVuError } from './DjVuErrors'; import png from 'pngjs/browser'; const offscreenCanvas = self.OffscreenCanvas ? new OffscreenCanvas(0, 0) : null; const ctx = offscreenCanvas ? offscreenCanvas.getContext('2d') : null; async function createBlobFromImageData(imageData) { if (!offscreenCanvas) { return null; } offscreenCanvas.width = imageData.width; offscreenCanvas.height = imageData.height; ctx.putImageData(imageData, 0, 0); const blob = await offscreenCanvas.convertToBlob(); offscreenCanvas.width = 0; offscreenCanvas.height = 0; return blob; } /** * Страница документа */ export default class DjVuPage extends CompositeChunk { /** * @param {import('./ByteStream').ByteStream} bs * @param {Function} getINCLChunkCallback */ constructor(bs, getINCLChunkCallback) { super(bs); this.getINCLChunkCallback = getINCLChunkCallback; // метод для получения глобальной порции данных (словарь обычно) от документа по id this.reset(); } reset() { this.bs.setOffset(12); // skip id, length and secondary id this.djbz = null; this.bg44arr = new Array(); this.fg44 = null; /** * @type {?IWImage} */ this.bgimage = null; /** * @type {?IWImage} */ this.fgimage = null; /** * @type {?JB2Image} */ this.sjbz = null; /** * @type {?DjVuPalette} */ this.fgbz = null; /** @type {?DjVuText} */ this.text = null; this.decoded = false; this.isBackgroundCompletelyDecoded = false; this.isFirstBgChunkDecoded = false; this.info = null; // список всех порций данных - для toString this.iffchunks = []; // id разделяемых данных (в частности словарей) this.dependencies = null; //this.init(); } /** * Свойство необходимое для корректного отображения страницы - влияет на 100% масштаб. */ getDpi() { if (this.info) { return this.info.dpi; } else { return this.init().info.dpi; } } getHeight() { return this.info ? this.info.height : this.init().info.height; } getWidth() { return this.info ? this.info.width : this.init().info.width; } async createPngObjectUrl() { var time = performance.now(); var imageData = this.getImageData(); var imageBlob = await createBlobFromImageData(imageData); if (!imageBlob) { const pngImage = png.PNG.sync.write(this.getImageData()) imageBlob = new Blob([pngImage.buffer]); } DjVu.IS_DEBUG && console.log("Png creation time = ", performance.now() - time); var url = URL.createObjectURL(imageBlob); return { //url: URL.createObjectURL(new Blob([new ArrayBuffer(10 * 1024 * 1024)])), url: url, byteLength: imageBlob.size, width: this.getWidth(), height: this.getHeight(), dpi: this.getDpi(), }; } // метод поиска зависимостей, то есть INCLChunk // возвращает массив id /** @returns {Array} */ getDependencies() { //чтобы не вызывалось более 1 раза if (this.info || this.dependencies) { return this.dependencies; } this.dependencies = []; var bs = this.bs.fork(); while (!bs.isEmpty()) { var chunk; var id = bs.readStr4(); var length = bs.getInt32(); bs.jump(-8); // вернулись назад var chunkBs = bs.fork(length + 8); bs.jump(8 + length + (length & 1 ? 1 : 0)); // перепрыгнули к следующей порции if (id === "INCL") { chunk = new INCLChunk(chunkBs); this.dependencies.push(chunk.ref); } } return this.dependencies; } /** * Метод предварительного разбора страницы. * Вызывается вручную или автоматически * @returns {DjVuPage} */ init() { //чтобы не вызывалось более 1 раза if (this.info) { return this; } this.dependencies = []; var id = this.bs.readStr4(); if (id !== 'INFO') { throw new CorruptedFileDjVuError("The very first chunk must be INFO chunk, but we got " + id + '!') } var length = this.bs.getInt32(); this.bs.jump(-8); this.info = new INFOChunk(this.bs.fork(length + 8)); this.bs.jump(8 + length + (this.info.length & 1)); this.iffchunks.push(this.info); while (!this.bs.isEmpty()) { var chunk; var id = this.bs.readStr4(); var length = this.bs.getInt32(); this.bs.jump(-8); // вернулись назад var chunkBs = this.bs.fork(length + 8); // создали поток включающий только 1 порцию this.bs.jump(8 + length + (length & 1)); // перепрыгнули к следующей порции if (!length) { // empty chunk chunk = new IFFChunk(chunkBs); // save it for metadata } else if (id == "FG44") { chunk = this.fg44 = new ColorChunk(chunkBs); } else if (id == "BG44") { this.bg44arr.push(chunk = new ColorChunk(chunkBs)); } else if (id == 'Sjbz') { chunk = this.sjbz = new JB2Image(chunkBs); } else if (id === "INCL") { chunk = this.incl = new INCLChunk(chunkBs); var inclChunk = this.getINCLChunkCallback(this.incl.ref); if (inclChunk) { // it takes place in case of polish_indirect, where shared_anno.iff is empty inclChunk.id === "Djbz" ? this.djbz = inclChunk : this.iffchunks.push(inclChunk); } this.dependencies.push(chunk.ref); } else if (id === "CIDa") { try { chunk = new CIDaChunk(chunkBs); } catch (e) { chunk = new ErrorChunk('CIDa', e); } } else if (id === 'Djbz') { chunk = this.djbz = new JB2Dict(chunkBs); } else if (id === 'FGbz') { chunk = this.fgbz = new DjVuPalette(chunkBs); } else if (id === 'TXTa' || id === 'TXTz') { chunk = this.text = new DjVuText(chunkBs); } else { chunk = new IFFChunk(chunkBs); } //тут все порции в том порядке, в каком встретились при разборе this.iffchunks.push(chunk); } return this; } getRotation() { switch (this.info.flags) { case 5: return 90; case 2: return 180; case 6: return 270; default: return 0; } } rotateIfRequired(imageData) { if (this.info.flags === 5 || this.info.flags === 6) { var newImageData = new ImageData(this.info.height, this.info.width); var newPixelArray = new Uint32Array(newImageData.data.buffer); var oldPixelArray = new Uint32Array(imageData.data.buffer); var height = this.info.height; var width = this.info.width; if (this.info.flags === 6) { // 270 for (var i = 0; i < width; i++) { var rowOffset = (width - i - 1) * height; var to = height + rowOffset; for (var newIndex = rowOffset, oldIndex = i; newIndex < to; newIndex++, oldIndex += width) { newPixelArray[newIndex] = oldPixelArray[oldIndex]; } } } else { // 90 for (var i = 0; i < width; i++) { var rowOffset = i * height; var from = height + rowOffset - 1; for (var newIndex = from, oldIndex = i; newIndex >= rowOffset; newIndex--, oldIndex += width) { newPixelArray[newIndex] = oldPixelArray[oldIndex]; } } } return newImageData; } if (this.info.flags === 2) { // 180 new Uint32Array(imageData.data.buffer).reverse(); return imageData; } return imageData; } getImageData(rotate = true) { const image = this._getImageData(); const rotatedImage = rotate ? this.rotateIfRequired(image) : image; // In the decoding phase, each pixel is stored in 6 bytes in YCbCr // and converted to 4 bytes RGBA ImageData, that means that in this moment about // (4 + 6) * width * height bytes of RAM are used. // 10 000 000 pixels corresponds to about 95 MB of RAM. If more we should // reset the page to free all the memory retained by YCbCr pixels. // (however this very measure doesn't help reduce the peak memory usage) if (image.width * image.height > 10000000) { this.reset(); } return rotatedImage; } /** * Метод генерации изображения для общего случая (все 3 слоя) без разворота * @returns {ImageData} */ _getImageData() { this.decode(); var time = performance.now(); //достаем маску if (!this.sjbz) { //если только фоновый слой if (this.bgimage) { return this.bgimage.getImage(); }//это вряд ли может быть но на всякий случай else if (this.fgimage) { return this.fgimage.getImage(); } else { var emptyImage = new ImageData(this.info.width, this.info.height); emptyImage.data.fill(255); return emptyImage; } } if (!this.bgimage && !this.fgimage) { return this.sjbz.getImage(this.fgbz); } var fgscale, bgscale, fgpixelmap, bgpixelmap; function fakePixelMap(r, g, b) { // ??? нужно ли это вообще ??? Пока что не встречал таких примеров return { writePixel(index, pixelArray, pixelIndex) { pixelArray[pixelIndex] = r; pixelArray[pixelIndex | 1] = g; pixelArray[pixelIndex | 2] = b; } } } if (this.bgimage) { //масштабы на случай если закодированы в более меньшем разрешении bgscale = Math.round(this.info.width / this.bgimage.info.width); bgpixelmap = this.bgimage.pixelmap; } else { bgscale = 1; bgpixelmap = fakePixelMap(255, 255, 255); } if (this.fgimage) { //масштабы на случай если закодированы в более меньшем разрешении fgscale = Math.round(this.info.width / this.fgimage.info.width); fgpixelmap = this.fgimage.pixelmap; } else { fgscale = 1; fgpixelmap = fakePixelMap(0, 0, 0); } var image; if (!this.fgbz) { // если нет палитры image = this.createImageFromMaskImageAndPixelMaps( this.sjbz.getMaskImage(), fgpixelmap, bgpixelmap, fgscale, bgscale ); } else { // тут уже предполагается, что переднего плана нет, а только палитра (in DjVu_Tech_Primer it is so) image = this.createImageFromMaskImageAndBackgroundPixelMap( this.sjbz.getImage(this.fgbz, true), bgpixelmap, bgscale ); } DjVu.IS_DEBUG && console.log("DataImage creating time = ", performance.now() - time); return image; } createImageFromMaskImageAndPixelMaps(maskImage, fgpixelmap, bgpixelmap, fgscale, bgscale) { var image = maskImage; var pixelArray = image.data; //набираем изображение по пикселям var rowIndexOffset = ((this.info.height - 1) * this.info.width) << 2; var width4 = this.info.width << 2; for (var i = 0; i < this.info.height; i++) { var bis = i / bgscale >> 0; var fis = i / fgscale >> 0; var bgIndexOffset = bgpixelmap.width * bis; var fgIndexOffset = fgpixelmap.width * fis; var index = rowIndexOffset; for (var j = 0; j < this.info.width; j++) { if (pixelArray[index]) { bgpixelmap.writePixel(bgIndexOffset + (j / bgscale >> 0), pixelArray, index); } else { fgpixelmap.writePixel(fgIndexOffset + (j / fgscale >> 0), pixelArray, index); } index += 4; } rowIndexOffset -= width4; } return image; } createImageFromMaskImageAndBackgroundPixelMap(coloredMaskImage, bgpixelmap, bgscale) { var pixelArray = coloredMaskImage.data; //набираем изображение по пикселям var rowOffset = (this.info.height - 1) * this.info.width << 2; var width4 = this.info.width << 2; for (var i = 0; i < this.info.height; i++) { var bgRowOffset = (i / bgscale >> 0) * bgpixelmap.width; var index = rowOffset; for (var j = 0; j < this.info.width; j++) { if (pixelArray[index | 3]) { bgpixelmap.writePixel(bgRowOffset + (j / bgscale >> 0), pixelArray, index); } else { pixelArray[index | 3] = 255; } index += 4; } rowOffset -= width4; } return coloredMaskImage; } decodeForeground() { if (this.fg44) { this.fgimage = new IWImage(); var zp = new ZPDecoder(this.fg44.bs); this.fgimage.decodeChunk(zp, this.fg44.header); var pixelMapTime = performance.now(); this.fgimage.createPixelmap(); DjVu.IS_DEBUG && console.log("Foreground pixelmap creating time = ", performance.now() - pixelMapTime); } } /** * Decoding of the only first chunk was an experimental feature. * Now it's not used at all. */ decodeBackground(isOnlyFirstChunk = false) { if (this.isBackgroundCompletelyDecoded || this.isFirstBgChunkDecoded && isOnlyFirstChunk) { return; } if (this.bg44arr.length) { this.bgimage = this.bgimage || new IWImage(); var to = isOnlyFirstChunk ? 1 : this.bg44arr.length; var from = this.isFirstBgChunkDecoded ? 1 : 0; for (var i = from; i < to; i++) { var chunk = this.bg44arr[i]; var zp = new ZPDecoder(chunk.bs); var time = performance.now(); this.bgimage.decodeChunk(zp, chunk.header); DjVu.IS_DEBUG && console.log("Background chunk decoding time = ", performance.now() - time); } var pixelMapTime = performance.now(); this.bgimage.createPixelmap(); DjVu.IS_DEBUG && console.log("Background pixelmap creating time = ", performance.now() - pixelMapTime); if (isOnlyFirstChunk) { this.isFirstBgChunkDecoded = true; } else { this.isBackgroundCompletelyDecoded = true; } } } /** * Раскодирование всех 3 слоев изображения страницы, вызыват init() * @returns {DjVuPage} */ decode() { if (this.decoded) { this.decodeBackground(); return this; } this.init(); var time = performance.now(); this.sjbz ? this.sjbz.decode(this.djbz) : 0; DjVu.IS_DEBUG && console.log("Mask decoding time = ", performance.now() - time); time = performance.now(); this.decodeForeground(); DjVu.IS_DEBUG && console.log("Foreground decoding time = ", performance.now() - time); time = performance.now(); this.decodeBackground(); DjVu.IS_DEBUG && console.log("Background decoding time = ", performance.now() - time); this.decoded = true; return this; } /** * Фоновой слой * @returns {ImageData} */ getBackgroundImageData() { this.decode(); if (this.bg44arr.length) { this.bg44arr.forEach((chunk) => { var zp = new ZPDecoder(chunk.bs); this.bgimage.decodeChunk(zp, chunk.header); } ); return this.bgimage.getImage(); } else { return null; } } /** * @returns {ImageData} */ getForegroundImageData() { this.decode(); if (this.fg44) { this.fgimage = new IWImage(); var zp = new ZPDecoder(this.fg44.bs); this.fgimage.decodeChunk(zp, this.fg44.header); return this.fgimage.getImage(); } else { return null; } } /** @return {ImageData} */ getMaskImageData() { this.decode(); return this.sjbz && this.sjbz.getImage(this.fgbz); } getText() { this.init(); return this.text ? this.text.getText() : ""; } /** @returns {?import('./chunks/DjVuText').RawTextZone} */ getPageTextZone() { // returns the top text zone of the whole page (which contains nested zones) this.init(); return this.text ? this.text.getPageZone() : null; } /** @returns {?Array str + chunk.toString(), ''); return super.toString(str); } } ================================================ FILE: library/src/DjVuWorker.js ================================================ /** * @typedef {{ * command: string, * data?: {funcs: string[], args: any[][]}[] * } & Partial>} CommandObject */ /** * DjVuScript is a function containing the whole library. * It's a wrapper added in the build process. Look at the build config file. */ function getLinkToTheWholeLibrary() { if (!getLinkToTheWholeLibrary.url) { getLinkToTheWholeLibrary.url = URL.createObjectURL(new Blob( ["(" + DjVuScript.toString() + ")();"], { type: 'application/javascript' } )); } return getLinkToTheWholeLibrary.url; } /** * Класс создающий фоновый поток. Предоставляет интерфейс и инкапсулирует логику связи с * объектом DjVuDocument в фоновом потоке выполнения. */ export default class DjVuWorker { constructor(path = getLinkToTheWholeLibrary()) { this.path = path; this.reset(); } reset() { this.terminate(); this.worker = new Worker(this.path); this.worker.onmessage = (e) => this.messageHandler(e); this.worker.onerror = (e) => this.errorHandler(e); this.promiseCallbacks = null; this.currentPromise = null; this.promiseMap = new Map(); this.isWorking = false; this.commandCounter = 0; this.currentCommandId = null; // Hyper callback is a callback working even from inside the Worker :) this.hyperCallbacks = {}; this.hyperCallbackCounter = 0; } registerHyperCallback(func) { const id = this.hyperCallbackCounter++; this.hyperCallbacks[id] = func; return { hyperCallback: true, id: id }; } unregisterHyperCallback(id) { delete this.hyperCallbacks[id]; } terminate() { this.worker && this.worker.terminate(); } get doc() { return DjVuWorkerTask.instance(this); } errorHandler(event) { console.error("DjVu.js Worker error!", event); } cancelTask(promise) { if (!this.promiseMap.delete(promise)) { if (this.currentPromise === promise) { this.dropCurrentTask(); } } } dropCurrentTask() { this.currentPromise = null; this.promiseCallbacks = null; this.currentCommandId = null; } emptyTaskQueue() { this.promiseMap.clear(); } cancelAllTasks() { this.emptyTaskQueue(); this.dropCurrentTask(); } /** * @param {CommandObject} commandObj * @param {Array} transferList */ createNewPromise(commandObj, transferList = undefined) { var callbacks; var promise = new Promise((resolve, reject) => { callbacks = { resolve, reject }; }); this.promiseMap.set(promise, { callbacks, commandObj, transferList }); this.runNextTask(); return promise; } /** * Replaces functions with special "hyper callback" objects - it allows invoking callbacks from the web worker * (asynchronously of course, but it's mostly used to track progress) * @param {CommandObject} commandObj * @returns {CommandObject} */ prepareCommandObject(commandObj) { if (!(commandObj.data instanceof Array)) return commandObj; const hyperCallbackIds = []; for (const { args: argsList } of commandObj.data) { for (const args of argsList) { for (let i = 0; i < args.length; i++) { if (typeof args[i] === 'function') { const hyperCallback = this.registerHyperCallback(args[i]); args[i] = hyperCallback; hyperCallbackIds.push(hyperCallback.id); } } } } if (hyperCallbackIds.length) { commandObj.sendBackData = { ...commandObj.sendBackData, hyperCallbackIds }; } return commandObj; } runNextTask() { if (this.isWorking) { return; } var next = this.promiseMap.entries().next().value; if (next) { const [promise, { callbacks, commandObj, transferList }] = next; this.promiseCallbacks = callbacks; this.currentPromise = promise; this.currentCommandId = this.commandCounter++; commandObj.sendBackData = { commandId: this.currentCommandId, }; this.worker.postMessage(this.prepareCommandObject(commandObj), transferList); this.isWorking = true; this.promiseMap.delete(promise); } else { this.dropCurrentTask(); } } isTaskInProcess(promise) { return this.currentPromise === promise; } isTaskInQueue(promise) { return this.promiseMap.has(promise) || this.isTaskInProcess(promise); } processAction(obj) { // usually progress messages, not the commands' finish switch (obj.action) { case 'Process': this.onprocess ? this.onprocess(obj.percent) : 0; break; case 'hyperCallback': if (this.hyperCallbacks[obj.id]) this.hyperCallbacks[obj.id](...obj.args); break; } } messageHandler({ data: obj }) { if (obj.action) return this.processAction(obj); this.isWorking = false; const callbacks = this.promiseCallbacks; const commandId = obj.sendBackData && obj.sendBackData.commandId; // either a result or a forgotten command returned if (commandId === this.currentCommandId || this.currentCommandId === null) { // in fact, this invocation is essential, since this.isWorking // isn't reset when all tasks are cancelled. // So we still wait for a cancelled task to finish - it's important, because otherwise // cancelAllTasks() would have no sense - the real worker's queue would be overwhelmed with "current" tasks, // which cannot be cancelled once sent, while now it's possible to really cancel all tasks several times // while some other task is being processed in the worker. // Real example: a user is quickly turning over pages in the single page mode in the viewer. // commandIds only prevent us from forgetting current task // in case when something comes from the worker and it's not an action // (it shouldn't happen, just an additional measure) this.runNextTask(); } else { // it shouldn't happen, it means that one more task has been already sent // without waiting for a forgotten one. Or an action is sent incorrectly. // Or there is an unhandled promise rejection or an error. if (obj === "unhandledrejection" || obj === "error") { console.warn("DjVu.js: " + obj + " occurred in the worker!"); this.runNextTask(); } else { console.warn('DjVu.js: Something strange came from the worker.', obj); } return; } if (!callbacks) return; // in case of all tasks cancellation const { resolve, reject } = callbacks; switch (obj.command) { case 'Error': reject(obj.error); break; case 'createDocument': case 'startMultiPageDocument': case 'addPageToDocument': resolve(); break; case 'createDocumentFromPictures': case 'endMultiPageDocument': resolve(obj.buffer); break; case 'run': var restoredResult = !obj.result ? obj.result : obj.result.length && obj.result.map ? obj.result.map(result => this.restoreValueAfterTransfer(result)) : this.restoreValueAfterTransfer(obj.result); resolve(restoredResult); break; default: console.error("Unexpected message from DjVuWorker: ", obj); } if (obj.sendBackData && obj.sendBackData.hyperCallbackIds) { obj.sendBackData.hyperCallbackIds.forEach(id => this.unregisterHyperCallback(id)); } } restoreValueAfterTransfer(value) { if (value) { if (value.width && value.height && value.buffer) { return new ImageData(new Uint8ClampedArray(value.buffer), value.width, value.height); } } return value; } /** @param {DjVuWorkerTask} tasks */ run(...tasks) { const data = tasks.map(task => task._); return this.createNewPromise({ command: 'run', data: data, //time: Date.now(), }); } revokeObjectURL(url) { // if an ObjectURL was created inside a worker it can be revoked only inside this very worker this.worker.postMessage({ action: this.revokeObjectURL.name, url: url, }); } startMultiPageDocument(slicenumber, delayInit, grayscale) { return this.createNewPromise({ command: 'startMultiPageDocument', slicenumber: slicenumber, delayInit: delayInit, grayscale: grayscale }); } addPageToDocument(imageData) { var simpleImage = { buffer: imageData.data.buffer, width: imageData.width, height: imageData.height }; return this.createNewPromise({ command: 'addPageToDocument', simpleImage: simpleImage }, [simpleImage.buffer]); } endMultiPageDocument() { return this.createNewPromise({ command: 'endMultiPageDocument' }); } createDocument(buffer, options) { return this.createNewPromise({ command: 'createDocument', buffer: buffer, options: options }, [buffer]); } createDocumentFromPictures(imageArray, slicenumber, delayInit, grayscale) { var simpleImages = new Array(imageArray.length); var buffers = new Array(imageArray.length); for (var i = 0; i < imageArray.length; i++) { // разлагаем картинки для передачи в фоновый поток по частям simpleImages[i] = { buffer: imageArray[i].data.buffer, width: imageArray[i].width, height: imageArray[i].height }; buffers[i] = imageArray[i].data.buffer; } return this.createNewPromise({ command: 'createDocumentFromPictures', images: simpleImages, slicenumber: slicenumber, delayInit: delayInit, grayscale: grayscale }, buffers); } } class DjVuWorkerTask { /** * @property {{funcs: string[], args: any[][]}} _ */ /** * @param {DjVuWorker} worker * @param {string[]} funcs * @param {any[][]} args */ static instance(worker, funcs = [], args = []) { var proxy = new Proxy(DjVuWorkerTask.emptyFunc, { get: (target, key) => { switch (key) { case '_': return { funcs, args }; case 'run': return () => worker.run(proxy); default: return DjVuWorkerTask.instance(worker, [...funcs, key], args); } }, apply: (target, that, _args) => { // when method is called, just add args to the array return DjVuWorkerTask.instance(worker, funcs, [...args, _args]); } }); return proxy; } static emptyFunc() { } } ================================================ FILE: library/src/DjVuWorkerScript.js ================================================ import DjVuDocument from './DjVuDocument'; import IWImageWriter from './iw44/IWImageWriter'; import { DjVuError, DjVuErrorCodes, IncorrectTaskDjVuError, UnableToTransferDataDjVuError } from './DjVuErrors'; /** * Это скрипт для выполнения в фоновом потоке. */ export default function initWorker() { /** @type {DjVuDocument} */ var djvuDocument; // главный объект документа /** @type {IWImageWriter} */ var iwiw; // объект записи документов addEventListener("error", e => { console.error(e); postMessage("error"); }); addEventListener("unhandledrejection", e => { console.error(e); postMessage("unhandledrejection"); }); // обработчик приема событий onmessage = async function ({ data: obj }) { if (obj.action) return handlers[obj.action](obj); // action that doesn't require response try { // отлавливаем все исключения const { data, transferList } = await handlers[obj.command](obj) || {}; try { postMessage({ command: obj.command, ...data, ...(obj.sendBackData ? { sendBackData: obj.sendBackData } : null), }, transferList && transferList.length ? transferList : undefined); } catch (e) { throw new UnableToTransferDataDjVuError(obj.data); } } catch (error) { console.error(error); // we can't pass the native Error object between workers, so only several properties are copied var errorObj = error instanceof DjVuError ? error : { code: DjVuErrorCodes.UNEXPECTED_ERROR, name: error.name, message: error.message }; errorObj.commandObject = obj; postMessage({ command: 'Error', error: errorObj, ...(obj.sendBackData ? { sendBackData: obj.sendBackData } : null), }); } }; function processValueBeforeTransfer(value, transferList) { if (value instanceof ArrayBuffer) { transferList.push(value); return value; } if (value instanceof ImageData) { transferList.push(value.data.buffer); return { width: value.width, height: value.height, buffer: value.data.buffer }; } if (value instanceof DjVuDocument) { transferList.push(value.buffer); return value.buffer; } return value; } function restoreHyperCallbacks(args) { // we should not change the initial array, // cause it is sent back in case of error, and a function cannot be sent return args.map(arg => { if (arg && (typeof arg === 'object') && arg.hyperCallback) { return (...params) => postMessage({ action: 'hyperCallback', id: arg.id, args: params }); } return arg; }); } var handlers = { /* A universal command which handles all tasks created via doc proxy property of the DjVuWorker class */ async run(obj) { //console.log("Got task request", Date.now() - obj.time); const results = await Promise.all(obj.data.map(async task => { var res = djvuDocument; for (var i = 0; i < task.funcs.length; i++) { if (typeof res[task.funcs[i]] !== 'function') { throw new IncorrectTaskDjVuError(task); } res = await res[task.funcs[i]](...restoreHyperCallbacks(task.args[i])); } return res; })); //var time = Date.now(); var transferList = []; var processedResults = results.map(result => processValueBeforeTransfer(result, transferList)); return { data: { result: processedResults.length === 1 ? processedResults[0] : processedResults }, transferList }; }, revokeObjectURL(obj) { URL.revokeObjectURL(obj.url); }, startMultiPageDocument(obj) { iwiw = new IWImageWriter(obj.slicenumber, obj.delayInit, obj.grayscale); iwiw.startMultiPageDocument(); }, addPageToDocument(obj) { var imageData = new ImageData(new Uint8ClampedArray(obj.simpleImage.buffer), obj.simpleImage.width, obj.simpleImage.height); iwiw.addPageToDocument(imageData); }, endMultiPageDocument(obj) { var buffer = iwiw.endMultiPageDocument(); return { data: { buffer: buffer }, transferList: [buffer] }; }, createDocumentFromPictures(obj) { var sims = obj.images; var imageArray = new Array(sims.length); // собираем объекты ImageData for (var i = 0; i < sims.length; i++) { imageArray[i] = new ImageData(new Uint8ClampedArray(sims[i].buffer), sims[i].width, sims[i].height); } var iw = new IWImageWriter(obj.slicenumber, obj.delayInit, obj.grayscale); iw.onprocess = (percent) => { postMessage({ action: 'Process', percent: percent }); }; var ndoc = iw.createMultyPageDocument(imageArray); return { data: { buffer: ndoc.buffer }, transferList: [ndoc.buffer] }; }, createDocument(obj) { djvuDocument = new DjVuDocument(obj.buffer, obj.options); }, }; } ================================================ FILE: library/src/DjVuWriter.js ================================================ import ByteStreamWriter from './ByteStreamWriter'; import { ZPEncoder } from './ZPCodec'; import BZZEncoder from './bzz/BZZEncoder'; /** * Класс предназначенный для создания итогового файла. * Определяет более высокоуровневые функции, нежели ByteStreamWriter */ export default class DjVuWriter { constructor(length) { this.bsw = new ByteStreamWriter(length || 1024 * 1024); } startDJVM() { // пропускаем 4 байта для длины файла this.bsw.writeStr('AT&T').writeStr('FORM').saveOffsetMark('fileSize') .jump(4).writeStr('DJVM'); } writeDirmChunk(dirm) { this.dirm = dirm; this.bsw.writeStr('DIRM').saveOffsetMark('DIRMsize').jump(4); this.dirm.offsets = []; this.bsw.writeByte(dirm.dflags) .writeInt16(dirm.flags.length) .saveOffsetMark('DIRMoffsets') // will be written in getBuffer() method .jump(4 * dirm.flags.length); //начинаем фазу кодирования bzz; var tmpBS = new ByteStreamWriter(); for (var i = 0; i < dirm.sizes.length; i++) { tmpBS.writeInt24(dirm.sizes[i]); } for (var i = 0; i < dirm.flags.length; i++) { tmpBS.writeByte(dirm.flags[i]); } for (var i = 0; i < dirm.ids.length; i++) { tmpBS.writeStrNT(dirm.ids[i]); if (dirm.flags[i] & 128) { // has name flag tmpBS.writeStrNT(dirm.names[i]); } if (dirm.flags[i] & 64) { // has title flag tmpBS.writeStrNT(dirm.titles[i]); } } //todo для BWT конечный символ EOB - временный код tmpBS.writeByte(0); var tmpBuffer = tmpBS.getBuffer(); var bzzBS = new ByteStreamWriter(); var zp = new ZPEncoder(bzzBS); var bzz = new BZZEncoder(zp); bzz.encode(tmpBuffer); var encodedBuffer = bzzBS.getBuffer(); //записываем полученный буфер в основной поток this.bsw.writeBuffer(encodedBuffer); //записали длину this.bsw.rewriteSize('DIRMsize'); } get offset() { return this.bsw.offset; } writeByte(byte) { this.bsw.writeByte(byte); return this; } writeStr(str) { this.bsw.writeStr(str); return this; } writeInt32(val) { this.bsw.writeInt32(val); return this; } writeFormChunkBS(bs) { //проверка на четную границу if (this.bsw.offset & 1) { this.bsw.writeByte(0); } var off = this.bsw.offset; this.dirm.offsets.push(off); this.bsw.writeByteStream(bs); } writeFormChunkBuffer(buffer) { //проверка на четную границу if (this.bsw.offset & 1) { this.bsw.writeByte(0); } var off = this.bsw.offset; this.dirm.offsets.push(off); this.bsw.writeBuffer(buffer); } writeChunk(chunk) { //проверка на четную границу if (this.bsw.offset & 1) { this.bsw.writeByte(0); } this.bsw.writeByteStream(chunk.bs); } /*getByteStream() { var bs = new ByteStream(this.buffer); return bs; }*/ getBuffer() { //пишем длину файла this.bsw.rewriteSize('fileSize'); if (this.dirm.offsets.length !== (this.dirm.flags.length)) { throw new Error("Записаны не все страницы и словари !!!"); } for (var i = 0; i < this.dirm.offsets.length; i++) { this.bsw.rewriteInt32('DIRMoffsets', this.dirm.offsets[i]); } return this.bsw.getBuffer(); } } ================================================ FILE: library/src/ZPCodec.js ================================================ import ByteStreamWriter from './ByteStreamWriter'; export class ZPEncoder { constructor(bsw) { //byteStreamWriter this.bsw = bsw || new ByteStreamWriter(); this.a = 0; this.scount = 0; this.byte = 0; this.delay = 25; this.subend = 0; this.buffer = 0xffffff; this.nrun = 0; //this.pzp = new PseudoZP(); } outbit(bit) { if (this.delay > 0) { if (this.delay < 0xff) // delay=0xff suspends emission forever this.delay -= 1; } else { /* Insert a bit */ this.byte = (this.byte << 1) | bit; /* Output a byte */ if (++this.scount == 8) { this.bsw.writeByte(this.byte); this.scount = 0; this.byte = 0; } } } zemit(b) { /* Shift new bit into 3bytes buffer */ this.buffer = (this.buffer << 1) + b; /* Examine bit going out of the 3bytes buffer */ b = (this.buffer >> 24); this.buffer = (this.buffer & 0xffffff); switch (b) { /* Similar to WN&C upper renormalization */ case 1: this.outbit(1); while (this.nrun-- > 0) this.outbit(0); this.nrun = 0; break; /* Similar to WN&C lower renormalization */ case 0xff: this.outbit(0); while (this.nrun-- > 0) this.outbit(1); this.nrun = 0; break; /* Similar to WN&C central renormalization */ case 0: this.nrun += 1; break; default: //assert(0); throw new Exception('ZPEncoder::zemit() error!'); } } //откопировано из djvulibre encode(bit, ctx, n) { //this.pzp.encode(bit, ctx, n); bit = +bit; if (!ctx) { //return this.IWencode(bit); // можно было бы использовать IWencode всегда, но так сделано в djvulibre видимо для оптимизации return this._ptencode(bit, 0x8000 + (this.a >> 1)); } var z = this.a + this.p[ctx[n]]; if (bit != (ctx[n] & 1)) { //encode_lps(ctx, z); var d = 0x6000 + ((z + this.a) >> 2); if (z > d) { z = d; } /* Adaptation */ ctx[n] = this.dn[ctx[n]]; /* Code LPS */ z = 0x10000 - z; this.subend += z; this.a += z; } else if (z >= 0x8000) { //encode_mps var d = 0x6000 + ((z + this.a) >> 2); if (z > d) { z = d; } /* Adaptation */ if (this.a >= this.m[ctx[n]]) ctx[n] = this.up[ctx[n]]; /* Code MPS */ this.a = z; } else { this.a = z; // чтобы выйти тут return; } /* Export bits */// выполнится только для первых 2 случаев while (this.a >= 0x8000) { this.zemit(1 - (this.subend >> 15)); // 0xffff & ... вместо (unsigned short) в С++ this.subend = 0xffff & (this.subend << 1); this.a = 0xffff & (this.a << 1); } } //используется для кодирования изображений, может всегда использоваться как показала практика IWencode(bit) { //this.pzp.encode(bit); this._ptencode(bit, 0x8000 + ((this.a + this.a + this.a) >> 3)); } // тут скопировано с IWEncoder() может нужен просто Encoder() _ptencode(bit, z) { // IWEncoder() //var z = 0x8000 + ((this.a + this.a + this.a) >> 3); // просто Encoder() //var z = 0x8000 + (this.a >> 1); if (bit) { //encode_lps_simple(z); /* Code LPS */ z = 0x10000 - z; this.subend += z; this.a += z; } else { //encode_mps_simple(z); /* Code MPS */ this.a = z; } /* Export bits */// выполнится только для первыйх 2 случаев while (this.a >= 0x8000) { this.zemit(1 - (this.subend >> 15)); // 0xffff & ... вместо (unsigned short) в С++ this.subend = 0xffff & (this.subend << 1); this.a = 0xffff & (this.a << 1); } } // функция выполняемая в деструкторе в С++. Надо вызывать вручную в js чтобы записать последние байты eflush() { /* adjust subend */ if (this.subend > 0x8000) this.subend = 0x10000; else if (this.subend > 0) this.subend = 0x8000; /* zemit many mps bits */ while (this.buffer != 0xffffff || this.subend) { this.zemit(1 - (this.subend >> 15)); this.subend = 0xffff & (this.subend << 1); } /* zemit pending run */ this.outbit(1); while (this.nrun-- > 0) this.outbit(0); this.nrun = 0; /* zemit 1 until full byte */ while (this.scount > 0) this.outbit(1); /* prevent further emission */ this.delay = 0xff; } } export class ZPDecoder { constructor(bs) { this.bs = bs; this.a = 0x0000; this.c = this.bs.byte(); //code this.c <<= 8; var tmp = this.bs.byte(); this.c |= tmp; this.z = 0; this.d = 0; //fence this.f = Math.min(this.c, 0x7fff); this.ffzt = new Int8Array(256); // Create machine independent ffz table for (var i = 0; i < 256; i++) { this.ffzt[i] = 0; for (var j = i; j & 0x80; j <<= 1) this.ffzt[i] += 1; } /* Preload buffer */ this.delay = 25; this.scount = 0; this.buffer = 0; // буфер на 4 байта this.preload(); } preload() { // загрузка байтов из потока в буфер while (this.scount <= 24) { var byte = this.bs.byte(); this.buffer = (this.buffer << 8) | byte; this.scount += 8; } } ffz(x) { return (x >= 0xff00) ? (this.ffzt[x & 0xff] + 8) : (this.ffzt[(x >> 8) & 0xff]); } /* Функции реализованы не как в документации, а скопированы из djvulibre */ decode(ctx, n) { if (!ctx) { //упрощенный декодер, но можно было использовать IWdecode return this._ptdecode(0x8000 + (this.a >> 1)); } this.b = ctx[n] & 1; this.z = this.a + this.p[ctx[n]]; if (this.z <= this.f) { this.a = this.z; //console.log("123"); /* if (this.pzp) { var tmp = this.pzp.decode(ctx, n); if (tmp != this.b) { throw new Exception('Bit dismatch'); } }*/ return this.b; } this.d = 0x6000 + ((this.a + this.z) >> 2); if (this.z > this.d) { this.z = this.d; } if (this.z > this.c) { this.b = 1 - this.b; /*if (this.pzp) { var tmp = this.pzp.decode(ctx, n); if (tmp != this.b) { throw new Exception('Bit dismatch'); } }*/ this.z = 0x10000 - this.z; this.a += this.z; this.c += this.z; ctx[n] = this.dn[ctx[n]]; var shift = this.ffz(this.a); this.scount -= shift; this.a = 0xffff & (this.a << shift); this.c = 0xffff & ( (this.c << shift) | (this.buffer >> this.scount) & ((1 << shift) - 1) ); } else { /*if (this.pzp) { var tmp = this.pzp.decode(ctx, n); if (tmp != this.b) { throw new Exception('Bit dismatch'); } }*/ if (this.a >= this.m[ctx[n]]) { ctx[n] = this.up[ctx[n]]; } this.scount--; this.a = 0xffff & (this.z << 1); this.c = 0xffff & ( (this.c << 1) | ((this.buffer >> this.scount) & 1) ); } if (this.scount < 16) this.preload(); this.f = Math.min(this.c, 0x7fff); return this.b; } // для раскодирования картинок, но вообще можно всегда использовать IWdecode() { return this._ptdecode(0x8000 + ((this.a + this.a + this.a) >> 3)); } _ptdecode(z) { //z = 0x8000 + ((this.a + this.a + this.a) >> 3); this.b = 0; if (z > this.c) { this.b = 1; z = 0x10000 - z; this.a += z; this.c += z; var shift = this.ffz(this.a); this.scount -= shift; this.a = 0xffff & (this.a << shift); this.c = 0xffff & ( (this.c << shift) | (this.buffer >> this.scount) & ((1 << shift) - 1) ); } else { this.b = 0; this.scount--; this.a = 0xffff & (z << 1); this.c = 0xffff & ( (this.c << 1) | ((this.buffer >> this.scount) & 1) ); } if (this.scount < 16) this.preload(); this.f = Math.min(this.c, 0x7fff); /* if (this.pzp) { var tmp = this.pzp.decode(); if (tmp != this.b) { throw new Exception('Bit dismatch'); } }*/ return this.b; } /*decodex(ctx, n) { if (!ctx) { return this.ptdecode(); } this.b = ctx[n] & 1; this.z = this.a + this.p[ctx[n]]; if (this.z <= this.f) { this.a = this.z; //console.log("123"); return this.b; } this.d = 0x6000 + ((this.a + this.z) >> 2); if (this.z > this.d) { this.z = this.d; } if (this.c > this.z) { if (this.a > this.m[ctx[n]]) { ctx[n] = this.up[ctx[n]]; } this.a = this.z; } else { this.b = 1 - this.b; this.z = 0x10000 - this.z; this.a += this.z; this.c += this.z; ctx[n] = this.dn[ctx[n]]; } var flag = 0; while (this.a > 0x8000) { flag = 1; this.a += this.a - 0x10000; this.c += this.c - 0x10000 + this.bs.bit(); //console.log("+"); } if (flag) { this.f = Math.min(this.c, 0x7fff); } else { // console.log("()"); } return this.b; }*/ /*ptdecodex() { this.z = 0x8000 + ((this.a + this.a + this.a) >> 3); if (this.c > this.z) { this.b = 0; this.a = this.z; } else { this.b = 1; this.z = 0x10000 - this.z; this.a += this.z; this.c += this.z; } while (this.a > 0x8000) { this.a += this.a - 0x10000; this.c += this.c - 0x10000 + this.bs.bit(); } return this.b; }*/ } ZPEncoder.prototype.p = ZPDecoder.prototype.p = Uint16Array.of( 0x8000, 0x8000, 0x8000, 0x6bbd, 0x6bbd, 0x5d45, 0x5d45, 0x51b9, 0x51b9, 0x4813, 0x4813, 0x3fd5, 0x3fd5, 0x38b1, 0x38b1, 0x3275, 0x3275, 0x2cfd, 0x2cfd, 0x2825, 0x2825, 0x23ab, 0x23ab, 0x1f87, 0x1f87, 0x1bbb, 0x1bbb, 0x1845, 0x1845, 0x1523, 0x1523, 0x1253, 0x1253, 0xfcf, 0xfcf, 0xd95, 0xd95, 0xb9d, 0xb9d, 0x9e3, 0x9e3, 0x861, 0x861, 0x711, 0x711, 0x5f1, 0x5f1, 0x4f9, 0x4f9, 0x425, 0x425, 0x371, 0x371, 0x2d9, 0x2d9, 0x259, 0x259, 0x1ed, 0x1ed, 0x193, 0x193, 0x149, 0x149, 0x10b, 0x10b, 0xd5, 0xd5, 0xa5, 0xa5, 0x7b, 0x7b, 0x57, 0x57, 0x3b, 0x3b, 0x23, 0x23, 0x13, 0x13, 0x7, 0x7, 0x1, 0x1, 0x5695, 0x24ee, 0x8000, 0xd30, 0x481a, 0x481, 0x3579, 0x17a, 0x24ef, 0x7b, 0x1978, 0x28, 0x10ca, 0xd, 0xb5d, 0x34, 0x78a, 0xa0, 0x50f, 0x117, 0x358, 0x1ea, 0x234, 0x144, 0x173, 0x234, 0xf5, 0x353, 0xa1, 0x5c5, 0x11a, 0x3cf, 0x1aa, 0x285, 0x286, 0x1ab, 0x3d3, 0x11a, 0x5c5, 0xba, 0x8ad, 0x7a, 0xccc, 0x1eb, 0x1302, 0x2e6, 0x1b81, 0x45e, 0x24ef, 0x690, 0x2865, 0x9de, 0x3987, 0xdc8, 0x2c99, 0x10ca, 0x3b5f, 0xb5d, 0x5695, 0x78a, 0x8000, 0x50f, 0x24ee, 0x358, 0xd30, 0x234, 0x481, 0x173, 0x17a, 0xf5, 0x7b, 0xa1, 0x28, 0x11a, 0xd, 0x1aa, 0x34, 0x286, 0xa0, 0x3d3, 0x117, 0x5c5, 0x1ea, 0x8ad, 0x144, 0xccc, 0x234, 0x1302, 0x353, 0x1b81, 0x5c5, 0x24ef, 0x3cf, 0x2b74, 0x285, 0x201d, 0x1ab, 0x1715, 0x11a, 0xfb7, 0xba, 0xa67, 0x1eb, 0x6e7, 0x2e6, 0x496, 0x45e, 0x30d, 0x690, 0x206, 0x9de, 0x155, 0xdc8, 0xe1, 0x2b74, 0x94, 0x201d, 0x188, 0x1715, 0x252, 0xfb7, 0x383, 0xa67, 0x547, 0x6e7, 0x7e2, 0x496, 0xbc0, 0x30d, 0x1178, 0x206, 0x19da, 0x155, 0x24ef, 0xe1, 0x320e, 0x94, 0x432a, 0x188, 0x447d, 0x252, 0x5ece, 0x383, 0x8000, 0x547, 0x481a, 0x7e2, 0x3579, 0xbc0, 0x24ef, 0x1178, 0x1978, 0x19da, 0x2865, 0x24ef, 0x3987, 0x320e, 0x2c99, 0x432a, 0x3b5f, 0x447d, 0x5695, 0x5ece, 0x8000, 0x8000, 0x5695, 0x481a, 0x481a ); ZPEncoder.prototype.m = ZPDecoder.prototype.m = Uint16Array.of( 0x0, 0x0, 0x0, 0x10a5, 0x10a5, 0x1f28, 0x1f28, 0x2bd3, 0x2bd3, 0x36e3, 0x36e3, 0x408c, 0x408c, 0x48fd, 0x48fd, 0x505d, 0x505d, 0x56d0, 0x56d0, 0x5c71, 0x5c71, 0x615b, 0x615b, 0x65a5, 0x65a5, 0x6962, 0x6962, 0x6ca2, 0x6ca2, 0x6f74, 0x6f74, 0x71e6, 0x71e6, 0x7404, 0x7404, 0x75d6, 0x75d6, 0x7768, 0x7768, 0x78c2, 0x78c2, 0x79ea, 0x79ea, 0x7ae7, 0x7ae7, 0x7bbe, 0x7bbe, 0x7c75, 0x7c75, 0x7d0f, 0x7d0f, 0x7d91, 0x7d91, 0x7dfe, 0x7dfe, 0x7e5a, 0x7e5a, 0x7ea6, 0x7ea6, 0x7ee6, 0x7ee6, 0x7f1a, 0x7f1a, 0x7f45, 0x7f45, 0x7f6b, 0x7f6b, 0x7f8d, 0x7f8d, 0x7faa, 0x7faa, 0x7fc3, 0x7fc3, 0x7fd7, 0x7fd7, 0x7fe7, 0x7fe7, 0x7ff2, 0x7ff2, 0x7ffa, 0x7ffa, 0x7fff, 0x7fff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 ); ZPEncoder.prototype.up = ZPDecoder.prototype.up = Uint8Array.of( 84, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 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, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 81, 82, 9, 86, 5, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 82, 99, 76, 101, 70, 103, 66, 105, 106, 107, 66, 109, 60, 111, 56, 69, 114, 65, 116, 61, 118, 57, 120, 53, 122, 49, 124, 43, 72, 39, 60, 33, 56, 29, 52, 23, 48, 23, 42, 137, 38, 21, 140, 15, 142, 9, 144, 141, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 70, 157, 66, 81, 62, 75, 58, 69, 54, 65, 50, 167, 44, 65, 40, 59, 34, 55, 30, 175, 24, 177, 178, 179, 180, 181, 182, 183, 184, 69, 186, 59, 188, 55, 190, 51, 192, 47, 194, 41, 196, 37, 198, 199, 72, 201, 62, 203, 58, 205, 54, 207, 50, 209, 46, 211, 40, 213, 36, 215, 30, 217, 26, 219, 20, 71, 14, 61, 14, 57, 8, 53, 228, 49, 230, 45, 232, 39, 234, 35, 138, 29, 24, 25, 240, 19, 22, 13, 16, 13, 10, 7, 244, 249, 10, 89, 230 ); ZPEncoder.prototype.dn = ZPDecoder.prototype.dn = Uint8Array.of( 145, 4, 3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 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, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 85, 226, 6, 176, 143, 138, 141, 112, 135, 104, 133, 100, 129, 98, 127, 72, 125, 102, 123, 60, 121, 110, 119, 108, 117, 54, 115, 48, 113, 134, 59, 132, 55, 130, 51, 128, 47, 126, 41, 62, 37, 66, 31, 54, 25, 50, 131, 46, 17, 40, 15, 136, 7, 32, 139, 172, 9, 170, 85, 168, 248, 166, 247, 164, 197, 162, 95, 160, 173, 158, 165, 156, 161, 60, 159, 56, 71, 52, 163, 48, 59, 42, 171, 38, 169, 32, 53, 26, 47, 174, 193, 18, 191, 222, 189, 218, 187, 216, 185, 214, 61, 212, 53, 210, 49, 208, 45, 206, 39, 204, 195, 202, 31, 200, 243, 64, 239, 56, 237, 52, 235, 48, 233, 44, 231, 38, 229, 34, 227, 28, 225, 22, 223, 16, 221, 220, 63, 8, 55, 224, 51, 2, 47, 87, 43, 246, 37, 244, 33, 238, 27, 236, 21, 16, 15, 8, 241, 242, 7, 10, 245, 2, 1, 83, 250, 2, 143, 246 ); ================================================ FILE: library/src/bzz/BZZDecoder.js ================================================ import { ZPDecoder } from '../ZPCodec'; import ByteStreamWriter from '../ByteStreamWriter'; import ByteStream from '../ByteStream'; export default class BZZDecoder { constructor(zp) { this.zp = zp; // this.minblock = 10; // нигде не используется, оставлено для документации this.maxblock = 4096; this.FREQMAX = 4; this.CTXIDS = 3; this.ctx = new Uint8Array(300); this.size = 0; this.blocksize = 0; this.data = null; } decode_raw(bits) { var n = 1; var m = (1 << bits); while (n < m) { var b = this.zp.decode(); n = (n << 1) | b; } return n - m; } decode_binary(ctxoff, bits) { var n = 1; var m = (1 << bits); ctxoff--; while (n < m) { var b = this.zp.decode(this.ctx, ctxoff + n); n = (n << 1) | b; } return n - m; } _decode() { this.size = this.decode_raw(24); if (!this.size) { //сработать должно если читать несколько блоков return 0; } if (this.size > this.maxblock * 1024) { throw new Error("Too big block. Error"); } // Allocate if (this.blocksize < this.size) { this.blocksize = this.size; this.data = new Uint8Array(this.blocksize); } else if (this.data == null) { this.data = new Uint8Array(this.blocksize); } // Decode Estimation Speed var fshift = 0; if (this.zp.decode()) { fshift++; if (this.zp.decode()) { fshift++; } } // Prepare Quasi MTF var mtf = new Uint8Array(256); for (var i = 0; i < 256; i++) { mtf[i] = i; } var freq = new Array(this.FREQMAX); for (var i = 0; i < this.FREQMAX; freq[i++] = 0); var fadd = 4; // Decode var mtfno = 3; var markerpos = -1; for (var i = 0; i < this.size; i++) { var ctxid = this.CTXIDS - 1; if (ctxid > mtfno) { ctxid = mtfno; } var ctxoff = 0; switch (0) // чтобы можно было использовать break { default: if (this.zp.decode(this.ctx, ctxoff + ctxid) != 0) { mtfno = 0; this.data[i] = mtf[mtfno]; break; } ctxoff += this.CTXIDS; if (this.zp.decode(this.ctx, ctxoff + ctxid) != 0) { mtfno = 1; this.data[i] = mtf[mtfno]; break; } ctxoff += this.CTXIDS; if (this.zp.decode(this.ctx, ctxoff + 0) != 0) { mtfno = 2 + this.decode_binary(ctxoff + 1, 1); this.data[i] = mtf[mtfno]; break; } ctxoff += (1 + 1); if (this.zp.decode(this.ctx, ctxoff + 0) != 0) { mtfno = 4 + this.decode_binary(ctxoff + 1, 2); this.data[i] = mtf[mtfno]; break; } ctxoff += (1 + 3); if (this.zp.decode(this.ctx, ctxoff + 0) != 0) { mtfno = 8 + this.decode_binary(ctxoff + 1, 3); this.data[i] = mtf[mtfno]; break; } ctxoff += (1 + 7); if (this.zp.decode(this.ctx, ctxoff + 0) != 0) { mtfno = 16 + this.decode_binary(ctxoff + 1, 4); this.data[i] = mtf[mtfno]; break; } ctxoff += (1 + 15); if (this.zp.decode(this.ctx, ctxoff + 0) != 0) { mtfno = 32 + this.decode_binary(ctxoff + 1, 5); this.data[i] = mtf[mtfno]; break; } ctxoff += (1 + 31); if (this.zp.decode(this.ctx, ctxoff + 0) != 0) { mtfno = 64 + this.decode_binary(ctxoff + 1, 6); this.data[i] = mtf[mtfno]; break; } ctxoff += (1 + 63); if (this.zp.decode(this.ctx, ctxoff + 0) != 0) { mtfno = 128 + this.decode_binary(ctxoff + 1, 7); this.data[i] = mtf[mtfno]; break; } mtfno = 256; this.data[i] = 0; markerpos = i; continue; } // Rotate mtf according to empirical frequencies (new!) // Adjust frequencies for overflow var k; fadd = fadd + (fadd >> fshift); if (fadd > 0x10000000) { fadd >>= 24; freq[0] >>= 24; freq[1] >>= 24; freq[2] >>= 24; freq[3] >>= 24; for (k = 4; k < this.FREQMAX; k++) { freq[k] >>= 24; } } // Relocate new char according to new freq var fc = fadd; if (mtfno < this.FREQMAX) { fc += freq[mtfno]; } for (k = mtfno; k >= this.FREQMAX; k--) { mtf[k] = mtf[k - 1]; } for (; (k > 0) && ((0xffffffff & fc) >= (0xffffffff & freq[k - 1])); k--) { mtf[k] = mtf[k - 1]; freq[k] = freq[k - 1]; } mtf[k] = this.data[i]; freq[k] = fc; } ///////////////////////////////// ////////// Reconstruct the string if ((markerpos < 1) || (markerpos >= this.size)) { throw new Error("BZZ byte stream is corrupted"); } // Allocate poleters var pos = new Uint32Array(this.size); for (var j = 0; j < this.size; pos[j++] = 0); // Prepare count buffer var count = new Array(256); for (var i = 0; i < 256; count[i++] = 0); // Fill count buffer for (var i = 0; i < markerpos; i++) { var c = this.data[i]; pos[i] = (c << 24) | (count[0xff & c] & 0xffffff); count[0xff & c]++; } for (var i = markerpos + 1; i < this.size; i++) { var c = this.data[i]; pos[i] = (c << 24) | (count[0xff & c] & 0xffffff); count[0xff & c]++; } // Compute sorted char positions var last = 1; for (var i = 0; i < 256; i++) { var tmp = count[i]; count[i] = last; last += tmp; } // Undo the sort transform var j = 0; last = this.size - 1; while (last > 0) { var n = pos[j]; var c = pos[j] >> 24; this.data[--last] = 0xff & c; j = count[0xff & c] + (n & 0xffffff); } // Free and check if (j != markerpos) { throw new Error("BZZ byte stream is corrupted"); } return this.size; } /** @return {ByteStream} */ getByteStream() { var bsw, size; while (size = this._decode()) { if (!bsw) { bsw = new ByteStreamWriter(size - 1); } // From specification: "The array DATA[0...BLOCKSIZE-2] then contains the decoded bytes of the block." So size - 1; var arr = new Uint8Array(this.data.buffer, 0, size - 1); bsw.writeArray(arr); } // для высвобождения памяти. this.data = null; return new ByteStream(bsw.getBuffer()); } /** * @param {ByteStream} bs * @return {ByteStream} */ static decodeByteStream(bs) { return new BZZDecoder(new ZPDecoder(bs)).getByteStream(); } } ================================================ FILE: library/src/bzz/BZZEncoder.js ================================================ import { ZPEncoder } from '../ZPCodec'; /* * Предполагается, что все данные будут закодированы одним блоком. * Причем блок уже будет оканчиваться дополнительным 0 в качестве конечного символа */ export default class BZZEncoder { constructor(zp) { this.zp = zp || new ZPEncoder(); // this.minblock = 10; // оставлено для документации, сейчас не используется // this.maxblock = 4096; this.FREQMAX = 4; this.CTXIDS = 3; this.ctx = new Uint8Array(300); this.size = 0; this.blocksize = 0; this.FREQS0 = 100000; this.FREQS1 = 1000000; } // сортировка на основе встроенной функции, может быть не очень оптимальна blocksort(arr) { var length = arr.length; //массив смещений var offs = new Array(arr.length); //var markerpos = this.markerpos; for (var i = 0; i < length; offs[i] = i++) { } // сортируем массив смещений offs.sort((a, b) => { for (var i = 0; i < length; i++) { // конечный символ предполагается самым маленьким, // то есть идет первым при сортировке по возрастанию if (a === this.markerpos) { return -1; } else if (b === this.markerpos) { return 1; } var res = arr[a % length] - arr[b % length]; if (res) { return res; } a++; b++; } return 0; }); var narr = new Uint8Array(length); for (var i = 0; i < length; i++) { var pos = offs[i] - 1; if (pos >= 0) { narr[i] = arr[pos]; } else { narr[i] = 0; this.markerpos = i; } } return narr; } encode_raw(bits, x) { var n = 1; var m = (1 << bits); while (n < m) { x = (x & (m - 1)) << 1; var b = (x >> bits); this.zp.encode(b); n = (n << 1) | b; } } encode_binary(cxtoff, bits, x) { // Require 2^bits-1 contexts var n = 1; var m = (1 << bits); cxtoff--; while (n < m) { x = (x & (m - 1)) << 1; var b = (x >> bits); this.zp.encode(b, this.ctx, cxtoff + n); n = (n << 1) | b; } } encode(buffer) { ///////////////////////////////// //////////// Block Sort Tranform var data = new Uint8Array(buffer); var size = data.length; var markerpos = size - 1; this.markerpos = markerpos; data = this.blocksort(data); markerpos = this.markerpos; ///////////////////////////////// //////////// Encode Output Stream // Header this.encode_raw(24, size); // Determine and Encode Estimation Speed var fshift = 0; if (size < this.FREQS0) { fshift = 0; this.zp.encode(0); } else if (size < this.FREQS1) { fshift = 1; this.zp.encode(1); this.zp.encode(0); } else { fshift = 2; this.zp.encode(1); this.zp.encode(1); } // MTF var mtf = new Uint8Array(256); var rmtf = new Uint8Array(256); var freq = new Uint32Array(this.FREQMAX); var m = 0; for (m = 0; m < 256; m++) mtf[m] = m; for (m = 0; m < 256; m++) rmtf[mtf[m]] = m; var fadd = 4; for (m = 0; m < this.FREQMAX; m++) freq[m] = 0; // Encode var i; var mtfno = 3; for (i = 0; i < size; i++) { // Get MTF data var c = data[i]; var ctxid = this.CTXIDS - 1; if (ctxid > mtfno) ctxid = mtfno; mtfno = rmtf[c]; if (i == markerpos) mtfno = 256; // Encode using ZPEncoder var b; //вместо BitContext *cx = ctx; на С++ var ctxoff = 0; switch (0) // чтобы можно было использовать break { default: b = (mtfno == 0); this.zp.encode(b, this.ctx, ctxoff + ctxid); if (b) // вместо goto rotate; break; ctxoff += this.CTXIDS; b = (mtfno == 1); this.zp.encode(b, this.ctx, ctxoff + ctxid); if (b) break; ctxoff += this.CTXIDS; b = (mtfno < 4); this.zp.encode(b, this.ctx, ctxoff); if (b) { this.encode_binary(ctxoff + 1, 1, mtfno - 2); break; } ctxoff += 1 + 1; b = (mtfno < 8); this.zp.encode(b, this.ctx, ctxoff); if (b) { this.encode_binary(ctxoff + 1, 2, mtfno - 4); break; } ctxoff += 1 + 3; b = (mtfno < 16); this.zp.encode(b, this.ctx, ctxoff); if (b) { this.encode_binary(ctxoff + 1, 3, mtfno - 8); break; } ctxoff += 1 + 7; b = (mtfno < 32); this.zp.encode(b, this.ctx, ctxoff); if (b) { this.encode_binary(ctxoff + 1, 4, mtfno - 16); break; } ctxoff += 1 + 15; b = (mtfno < 64); this.zp.encode(b, this.ctx, ctxoff); if (b) { this.encode_binary(ctxoff + 1, 5, mtfno - 32); break; } ctxoff += 1 + 31; b = (mtfno < 128); this.zp.encode(b, this.ctx, ctxoff); if (b) { this.encode_binary(ctxoff + 1, 6, mtfno - 64); break; } ctxoff += 1 + 63; b = (mtfno < 256); this.zp.encode(b, this.ctx, ctxoff); if (b) { this.encode_binary(ctxoff + 1, 7, mtfno - 128); break; } continue; // Rotate MTF according to empirical frequencies (new!) } // Adjust frequencies for overflow fadd = fadd + (fadd >> fshift); if (fadd > 0x10000000) { fadd = fadd >> 24; freq[0] >>= 24; freq[1] >>= 24; freq[2] >>= 24; freq[3] >>= 24; for (var k = 4; k < this.FREQMAX; k++) freq[k] = freq[k] >> 24; } // Relocate new char according to new freq var fc = fadd; if (mtfno < this.FREQMAX) fc += freq[mtfno]; var k; for (k = mtfno; k >= this.FREQMAX; k--) { mtf[k] = mtf[k - 1]; rmtf[mtf[k]] = k; } for (; k > 0 && fc >= freq[k - 1]; k--) { mtf[k] = mtf[k - 1]; freq[k] = freq[k - 1]; rmtf[mtf[k]] = k; } mtf[k] = c; freq[k] = fc; rmtf[mtf[k]] = k; } // Encode EOF marker this.encode_raw(24, 0); this.zp.eflush(); // Terminate return 0; } } ================================================ FILE: library/src/chunks/DirmChunk.js ================================================ import { IFFChunk } from './IFFChunks'; import BZZDecoder from '../bzz/BZZDecoder'; /** * Порция данных машинного оглавления документа. * Содержит сведения о структуре многостраничного документа */ export default class DIRMChunk extends IFFChunk { constructor(bs) { super(bs); this.dflags = bs.byte(); // saved just to copy to a new file in the DjVuDocument::slice() method (look at DjVuWriter) this.isBundled = this.dflags >> 7; this.nfiles = bs.getInt16(); if (this.isBundled) { this.offsets = new Int32Array(this.nfiles); for (var i = 0; i < this.nfiles; i++) { this.offsets[i] = bs.getInt32(); } } this.sizes = new Uint32Array(this.nfiles); this.flags = new Uint8Array(this.nfiles); this.ids = new Array(this.nfiles); this.names = new Array(this.nfiles); this.titles = new Array(this.nfiles); var bsz = BZZDecoder.decodeByteStream(bs.fork()); for (var i = 0; i < this.nfiles; i++) { this.sizes[i] = bsz.getUint24(); } for (var i = 0; i < this.nfiles; i++) { this.flags[i] = bsz.byte(); } this.pagesIds = []; this.idToNameRegistry = {}; for (var i = 0; i < this.nfiles && !bsz.isEmpty(); i++) { this.ids[i] = bsz.readStrNT(); this.names[i] = this.flags[i] & 128 ? bsz.readStrNT() : this.ids[i]; // check hasname flag this.titles[i] = this.flags[i] & 64 ? bsz.readStrNT() : this.ids[i]; // check hastitle flag if (this.isPageIndex(i)) { this.pagesIds.push(this.ids[i]); } this.idToNameRegistry[this.ids[i]] = this.names[i]; } } isPageIndex(i) { return (this.flags[i] & 63) === 1; } isThumbnailIndex(i) { return (this.flags[i] & 63) === 2; } /*isDependencyIndex(i) { // function just for documentation return (this.flags[i] & 63) === 0; }*/ getPageNameByItsNumber(number) { return this.getComponentNameByItsId(this.pagesIds[number - 1]); } getPageNumberByItsId(id) { const index = this.pagesIds.indexOf(id); return index === -1 ? null : (index + 1); } getComponentNameByItsId(id) { return this.idToNameRegistry[id]; } getPagesQuantity() { return this.pagesIds.length; } getFilesQuantity() { return this.nfiles; } getMetadataStringByIndex(i) { return ( `[id: "${this.ids[i]}", flag: ${this.flags[i]}, ` + `offset: ${this.offsets ? this.offsets[i] : 'indirect'}, size: ${this.sizes[i]}]\n` ); } toString() { var str = super.toString(); str += (this.isBundled ? 'Bundled' : 'Indirect') + '\n'; str += "FilesCount: " + this.nfiles + '\n'; return str + '\n'; } } ================================================ FILE: library/src/chunks/DjViChunk.js ================================================ import JB2Dict from '../jb2/JB2Dict'; import { IFFChunk, CompositeChunk } from './IFFChunks'; import DjVuAnno from './DjVuAnno'; export default class DjViChunk extends CompositeChunk { constructor(bs) { super(bs); this.innerChunk = null; this.init(); } init() { // In some cases there maybe only DJVI headers and nothing else (assets/polish_indirect/shared_anno.iff), // so the innerChunk can remain null while (!this.bs.isEmpty()) { var id = this.bs.readStr4(); var length = this.bs.getInt32(); this.bs.jump(-8); // вернулись назад var chunkBs = this.bs.fork(length + 8); // перепрыгнули к следующей порции this.bs.jump(8 + length + (length & 1 ? 1 : 0)); switch (id) { case 'Djbz': this.innerChunk = new JB2Dict(chunkBs); break; case 'ANTa': case 'ANTz': this.innerChunk = new DjVuAnno(chunkBs); break; default: this.innerChunk = new IFFChunk(chunkBs); console.error("Unsupported chunk inside the DJVI chunk: ", id); break; } } } toString() { return super.toString(this.innerChunk.toString()); } } ================================================ FILE: library/src/chunks/DjVuAnno.js ================================================ import { IFFChunk } from './IFFChunks'; export default class DjVuAnno extends IFFChunk { } ================================================ FILE: library/src/chunks/DjVuPalette.js ================================================ import { IFFChunk } from './IFFChunks'; import BZZDecoder from '../bzz/BZZDecoder'; import DjVu from '../DjVu'; export default class DjVuPalette extends IFFChunk { /** @param {ByteStream} bs */ constructor(bs) { var time = performance.now(); super(bs); this.pixel = { r: 0, g: 0, b: 0 }; this.version = bs.getUint8(); if (this.version & 0x7f) { throw "Bad Djvu Pallete version!"; } this.palleteSize = bs.getInt16(); if (this.palleteSize < 0 || this.palleteSize > 65535) { throw "Bad Djvu Pallete size!"; } this.colorArray = bs.getUint8Array(this.palleteSize * 3); if (this.version & 0x80) { this.dataSize = bs.getInt24(); if (this.dataSize < 0) { throw "Bad Djvu Pallete data size!"; } var bsz = BZZDecoder.decodeByteStream(bs.fork()); this.colorIndices = new Int16Array(this.dataSize); for (var i = 0; i < this.dataSize; i++) { var index = bsz.getInt16(); if (index < 0 || index >= this.palleteSize) { throw "Bad Djvu Pallete index! " + index; } this.colorIndices[i] = index; } } DjVu.IS_DEBUG && console.log('DjvuPalette time ', performance.now() - time); } getDataSize() { return this.dataSize; } getPixelByBlitIndex(index) { var colorIndex = this.colorIndices[index] * 3; this.pixel.r = this.colorArray[colorIndex + 2]; this.pixel.g = this.colorArray[colorIndex + 1]; this.pixel.b = this.colorArray[colorIndex]; return this.pixel; } toString() { var str = super.toString(); str += "Pallete size: " + this.palleteSize + "\n"; str += "Data size: " + this.dataSize + "\n"; return str; } } ================================================ FILE: library/src/chunks/DjVuText.js ================================================ import { IFFChunk } from './IFFChunks'; import BZZDecoder from '../bzz/BZZDecoder'; import { createStringFromUtf8Array } from '../DjVu'; /** * @typedef {Object} TextZone * @property {number} x - top left corner x coordinate relative to the page * @property {number} y - top left corner y coordinate relative to the page * @property {number} width * @property {number} height * @property {string} text */ /** * @typedef {Object} RawTextZone * @property {number} type * @property {number} x - top left corner x coordinate relative to the page * @property {number} y - top left corner y coordinate relative to the page * @property {number} width * @property {number} height * @property {number} textStart - offset of text in bytes in the raw UTF8 array. * @property {number} textLength - length of text in bytes in the raw UTF8 array. * @property {Array} [children] - nested raw text zones. */ /** * Класс для порций TXTa и TXTz. */ export default class DjVuText extends IFFChunk { constructor(bs) { super(bs); this.isDecoded = false; /** @type {import('../ByteStream').ByteStream} */ this.dbs = this.id === 'TXTz' ? null : this.bs; // decoded byte stream } decode() { if (this.isDecoded) { return; } if (!this.dbs) { this.dbs = BZZDecoder.decodeByteStream(this.bs); } this.textLength = this.dbs.getInt24(); this.utf8array = this.dbs.getUint8Array(this.textLength); this.version = this.dbs.getUint8(); if (this.version !== 1) { console.warn("The version in " + this.id + " isn't equal to 1!"); } this.pageZone = this.dbs.isEmpty() ? null : this.decodeZone(); this.isDecoded = true; } /** @returns {RawTextZone} */ decodeZone(parent = null, prev = null) { var type = this.dbs.getUint8(); var x = this.dbs.getUint16() - 0x8000; var y = this.dbs.getUint16() - 0x8000; var width = this.dbs.getUint16() - 0x8000; var height = this.dbs.getUint16() - 0x8000; var textStart = this.dbs.getUint16() - 0x8000; // must be always 0 var textLength = this.dbs.getInt24(); if (prev) { if (type === 1 /*PAGE*/ || type === 4 /*PARAGRAPH*/ || type === 5 /*LINE*/) { x = x + prev.x; y = prev.y - (y + height); } else // Either COLUMN or WORD or CHARACTER { x = x + prev.x + prev.width; y = y + prev.y; } textStart += prev.textStart + prev.textLength; } else if (parent) { x = x + parent.x; y = parent.y + parent.height - (y + height); textStart += parent.textStart; } var zone = { type, x, y, width, height, textStart, textLength }; var childrenCount = this.dbs.getInt24(); if (childrenCount) { var children = new Array(childrenCount); var childZone = null; for (var i = 0; i < childrenCount; i++) { childZone = this.decodeZone(zone, childZone); children[i] = childZone; } zone.children = children; } return zone; } /** @returns {string} */ getText() { this.decode(); this.text = this.text || createStringFromUtf8Array(this.utf8array); return this.text; } /** @returns {?RawTextZone} */ getPageZone() { this.decode(); return this.pageZone; } /** @returns {?Array { if (zone.children) { zone.children.forEach(zone => process(zone)); } else { var key = zone.x.toString() + zone.y + zone.width + zone.height; var zoneText = createStringFromUtf8Array(this.utf8array.slice(zone.textStart, zone.textStart + zone.textLength)); if (registry[key]) { // unite text of the same zone registry[key].text += zoneText } else { registry[key] = { x: zone.x, y: zone.y, width: zone.width, height: zone.height, text: zoneText }; this.normalizedZones.push(registry[key]); } } } process(this.pageZone); return this.normalizedZones; } toString() { this.decode(); var st = "Text length = " + this.textLength + "\n"; return super.toString() + st; } } ================================================ FILE: library/src/chunks/IFFChunks.js ================================================ import { CorruptedFileDjVuError } from '../DjVuErrors'; /** @typedef {import('../ByteStream').ByteStream} ByteStream */ // простейший шаблон порции данных export class IFFChunk { /** @param {ByteStream} bs */ constructor(bs) { this.id = bs.readStr4(); this.length = bs.getInt32(); this.bs = bs; } toString() { return this.id + " " + this.length + '\n'; } } export class CompositeChunk extends IFFChunk { /** @param {ByteStream} bs */ constructor(bs) { super(bs); this.id += ':' + bs.readStr4(); // read secondary id } toString(innerString = '') { return super.toString() + ' ' + innerString.replace(/\n/g, '\n ') + '\n'; } } export class ColorChunk extends IFFChunk { /** @param {ByteStream} bs */ constructor(bs) { super(bs); this.header = new ColorChunkDataHeader(bs); } toString() { return this.id + " " + this.length + this.header.toString(); } } /** * Порция данных содержащая в себе параметры изображения (всей страницы) */ export class INFOChunk extends IFFChunk { /** @param {ByteStream} bs */ constructor(bs) { super(bs); if (this.length < 5) { // the cases when there are less than 10 bytes are not mentioned in the specification, but they are handled in DjVuLibre throw new CorruptedFileDjVuError("The INFO chunk is shorter than 5 bytes!") } this.width = bs.getInt16(); this.height = bs.getInt16(); this.minver = bs.getInt8(); this.majver = this.length > 5 ? bs.getInt8() : 0; if (this.length > 7) { this.dpi = bs.getUint8(); this.dpi |= bs.getUint8() << 8; } else { this.dpi = 300; } this.gamma = this.length > 8 ? bs.getInt8() : 22; this.flags = this.length > 9 ? bs.getInt8() : 0; // Fixup - copied from DjVuLibre if (this.dpi < 25 || this.dpi > 6000) { this.dpi = 300; } if (this.gamma < 3) { this.gamma = 3; } if (this.gamma > 50) { this.gamma = 50; } } toString() { var str = super.toString(); str += "{" + 'width:' + this.width + ', ' + 'height:' + this.height + ', ' + 'minver:' + this.minver + ', ' + 'majver:' + this.majver + ', ' + 'dpi:' + this.dpi + ', ' + 'gamma:' + this.gamma + ', ' + 'flags:' + this.flags + '}\n'; return str; } } /** * Заголовок порции цветовых порций данных. Содержит сведения о закодированном изображении. * Предоставляет основную информацию о порции данных. */ class ColorChunkDataHeader { /** @param {ByteStream} bs */ constructor(bs) { this.serial = bs.getUint8(); // номер порции this.slices = bs.getUint8(); // количество кусочков данных if (!this.serial) { // если это первая порция данных изображения this.majver = bs.getUint8(); // номер версии кодироващика (первая цифра) вообще 1 this.grayscale = this.majver >> 7; // серое ли изображение this.minver = bs.getUint8(); // номер версии кодировщика (вторая цифра) вообще 2 // ширина (высота) изображения. // должна быть равна ширине(высоте) в INFOChunk или быть от 2 до 12 раз меньше this.width = bs.getUint16(); this.height = bs.getUint16(); var byte = bs.getUint8(); // задержка декодирования цветовой информации (старший бит должен быть 1, но вообще игнорируется) this.delayInit = byte & 127; if (!byte & 128) { console.warn('Old image reconstruction should be applied!'); } } } toString() { return '\n' + JSON.stringify(this) + "\n"; } } export class INCLChunk extends IFFChunk { /** @param {ByteStream} bs */ constructor(bs) { super(bs); this.ref = this.bs.readStrUTF(); } toString() { var str = super.toString(); str += "{Reference: " + this.ref + '}\n'; return str; } } /** * Нестандартная порция данных. * Обычно содержит в себе информацию о программе-кодировщике */ export class CIDaChunk extends INCLChunk { } export class ErrorChunk { constructor(id, e) { this.id = id; this.e = e; } toString() { return `Error creating ${this.id}: ${this.e.toString()}\n`; } } ================================================ FILE: library/src/chunks/NavmChunk.js ================================================ import { IFFChunk } from './IFFChunks'; import BZZDecoder from '../bzz/BZZDecoder'; /** * @typedef {Object} Bookmark * @property {string} description * @property {string} url * @property {Array} [children] */ /** @typedef {Array} Contents */ /** * Оглавление человеко-читаемое */ export default class NAVMChunk extends IFFChunk { constructor(bs) { super(bs); this.isDecoded = false; this.contents = []; this.decodedBookmarkCounter = 0; } /** * @returns {Contents} */ getContents() { this.decode(); return this.contents; } decode() { if (this.isDecoded) { return; } var dbs = BZZDecoder.decodeByteStream(this.bs); var bookmarksCount = dbs.getUint16(); while (this.decodedBookmarkCounter < bookmarksCount) { this.contents.push(this.decodeBookmark(dbs)); } this.isDecoded = true; } /** * @param {import('../ByteStream').ByteStream} bs * @returns {Bookmark} */ decodeBookmark(bs) { var childrenCount = bs.getUint8(); var descriptionLength = bs.getInt24(); var description = descriptionLength ? bs.readStrUTF(descriptionLength) : ''; var urlLength = bs.getInt24(); var url = urlLength ? bs.readStrUTF(urlLength) : ''; this.decodedBookmarkCounter++; var bookmark = { description, url }; if (childrenCount) { var children = new Array(childrenCount); for (var i = 0; i < childrenCount; i++) { children[i] = this.decodeBookmark(bs); } bookmark.children = children; } return bookmark; } toString() { this.decode(); var indent = ' '; function stringifyBookmark(bookmark, indentSize = 0) { var str = indent.repeat(indentSize) + `${bookmark.description} (${bookmark.url})\n`; if (bookmark.children) { str = bookmark.children.reduce((str, bookmark) => str + stringifyBookmark(bookmark, indentSize + 1), str); } return str; } var str = this.contents.reduce((str, bookmark) => str + stringifyBookmark(bookmark), super.toString()); return str + '\n'; } } ================================================ FILE: library/src/chunks/ThumChunk.js ================================================ import { CompositeChunk } from './IFFChunks'; export default class ThumChunk extends CompositeChunk { } ================================================ FILE: library/src/index.js ================================================ /** * Throughout the code mostly vars are used, not consts or lets. * It's because of that in 2015-2016, when the library was created, * Chrome couldn't optimize ES6 code properly, which resulted in a big performance decrease, * about 3 times or even more. Now, in 2020, it seems than ES6 variable declarations * don't have that devastating impact anymore, so they can be used. */ import DjVu from "./DjVu"; import DjVuDocument from "./DjVuDocument"; import DjVuWorker from "./DjVuWorker"; import initWorker from './DjVuWorkerScript'; import { DjVuErrorCodes } from './DjVuErrors'; if (!self.document) { // if inside a Worker initWorker(); } export default Object.assign({}, DjVu, { Worker: DjVuWorker, Document: DjVuDocument, ErrorCodes: DjVuErrorCodes }); ================================================ FILE: library/src/iw44/IWCodecBaseClass.js ================================================ //класс общих данных для кодирования и декодирования картинки /** * There are 4 magic values: * 1 for ZERO // this coeff never hits this bit * 2 for ACTIVE // this coeff is already active активный * 4 for NEW // this coeff is becoming active при закодировании используется, когда собираемся закодировать * 8 for UNK // потенциальный флаг * these 4 are flags. It turned out that it works much faster with raw constats * rather than with const variables or properties from the prototype. */ export default class IWCodecBaseClass { constructor() { this.quant_lo = Uint32Array.of( 0x004000, 0x008000, 0x008000, 0x010000, 0x010000, 0x010000, 0x010000, 0x010000, 0x010000, 0x010000, 0x010000, 0x010000, 0x020000, 0x020000, 0x020000, 0x020000 ); this.quant_hi = Uint32Array.of( 0, 0x020000, 0x020000, 0x040000, 0x040000, 0x040000, 0x080000, 0x040000, 0x040000, 0x080000 ); this.bucketstate = new Uint8Array(16); this.coeffstate = new Array(16); var buffer = new ArrayBuffer(256); for (var i = 0; i < 16; i++) { this.coeffstate[i] = new Uint8Array(buffer, i << 4, 16) } //for (var i = 0; i < 16; this.coeffstate[i++] = new Uint8Array(16)) { } this.bbstate = 0; this.decodeBucketCtx = new Uint8Array(1); this.decodeCoefCtx = new Uint8Array(80); this.activateCoefCtx = new Uint8Array(16); this.inreaseCoefCtx = new Uint8Array(1); this.curband = 0; } getBandBuckets(band) { return this.bandBuckets[band]; } //проверяем надо ли вообще что либо делать или просто уменьшить шаг is_null_slice() { if (this.curband == 0) // для нулевой группы шаги разные, поэтому надо проверить все { var is_null = 1; for (var i = 0; i < 16; i++) { var threshold = this.quant_lo[i]; //чтобы не проверять потом этот коэффициент this.coeffstate[0][i] = 1/*ZERO*/; if (threshold > 0 && threshold < 0x8000) { this.coeffstate[0][i] = 8/*UNK*/; is_null = 0; } } return is_null; } else // иначе просто смотрим шаг группы { var threshold = this.quant_hi[this.curband]; return (!(threshold > 0 && threshold < 0x8000)); } } //уменьшение шага после обработки одной порции данных // todo использовать curbit finish_code_slice() { this.quant_hi[this.curband] = this.quant_hi[this.curband] >> 1; if (this.curband === 0) { for (var i = 0; i < 16; i++) this.quant_lo[i] = this.quant_lo[i] >> 1; } this.curband++; if (this.curband === 10) { this.curband = 0; } } } // this coeff never hits this bit IWCodecBaseClass.prototype.ZERO = 1; // this coeff is already active активный IWCodecBaseClass.prototype.ACTIVE = 2; // this coeff is becoming active при закодировании используется, когда собираемся закодировать IWCodecBaseClass.prototype.NEW = 4; // потенциальный флаг IWCodecBaseClass.prototype.UNK = 8; IWCodecBaseClass.prototype.zigzagRow = Uint8Array.of(0, 0, 16, 16, 0, 0, 16, 16, 8, 8, 24, 24, 8, 8, 24, 24, 0, 0, 16, 16, 0, 0, 16, 16, 8, 8, 24, 24, 8, 8, 24, 24, 4, 4, 20, 20, 4, 4, 20, 20, 12, 12, 28, 28, 12, 12, 28, 28, 4, 4, 20, 20, 4, 4, 20, 20, 12, 12, 28, 28, 12, 12, 28, 28, 0, 0, 16, 16, 0, 0, 16, 16, 8, 8, 24, 24, 8, 8, 24, 24, 0, 0, 16, 16, 0, 0, 16, 16, 8, 8, 24, 24, 8, 8, 24, 24, 4, 4, 20, 20, 4, 4, 20, 20, 12, 12, 28, 28, 12, 12, 28, 28, 4, 4, 20, 20, 4, 4, 20, 20, 12, 12, 28, 28, 12, 12, 28, 28, 2, 2, 18, 18, 2, 2, 18, 18, 10, 10, 26, 26, 10, 10, 26, 26, 2, 2, 18, 18, 2, 2, 18, 18, 10, 10, 26, 26, 10, 10, 26, 26, 6, 6, 22, 22, 6, 6, 22, 22, 14, 14, 30, 30, 14, 14, 30, 30, 6, 6, 22, 22, 6, 6, 22, 22, 14, 14, 30, 30, 14, 14, 30, 30, 2, 2, 18, 18, 2, 2, 18, 18, 10, 10, 26, 26, 10, 10, 26, 26, 2, 2, 18, 18, 2, 2, 18, 18, 10, 10, 26, 26, 10, 10, 26, 26, 6, 6, 22, 22, 6, 6, 22, 22, 14, 14, 30, 30, 14, 14, 30, 30, 6, 6, 22, 22, 6, 6, 22, 22, 14, 14, 30, 30, 14, 14, 30, 30, 0, 0, 16, 16, 0, 0, 16, 16, 8, 8, 24, 24, 8, 8, 24, 24, 0, 0, 16, 16, 0, 0, 16, 16, 8, 8, 24, 24, 8, 8, 24, 24, 4, 4, 20, 20, 4, 4, 20, 20, 12, 12, 28, 28, 12, 12, 28, 28, 4, 4, 20, 20, 4, 4, 20, 20, 12, 12, 28, 28, 12, 12, 28, 28, 0, 0, 16, 16, 0, 0, 16, 16, 8, 8, 24, 24, 8, 8, 24, 24, 0, 0, 16, 16, 0, 0, 16, 16, 8, 8, 24, 24, 8, 8, 24, 24, 4, 4, 20, 20, 4, 4, 20, 20, 12, 12, 28, 28, 12, 12, 28, 28, 4, 4, 20, 20, 4, 4, 20, 20, 12, 12, 28, 28, 12, 12, 28, 28, 2, 2, 18, 18, 2, 2, 18, 18, 10, 10, 26, 26, 10, 10, 26, 26, 2, 2, 18, 18, 2, 2, 18, 18, 10, 10, 26, 26, 10, 10, 26, 26, 6, 6, 22, 22, 6, 6, 22, 22, 14, 14, 30, 30, 14, 14, 30, 30, 6, 6, 22, 22, 6, 6, 22, 22, 14, 14, 30, 30, 14, 14, 30, 30, 2, 2, 18, 18, 2, 2, 18, 18, 10, 10, 26, 26, 10, 10, 26, 26, 2, 2, 18, 18, 2, 2, 18, 18, 10, 10, 26, 26, 10, 10, 26, 26, 6, 6, 22, 22, 6, 6, 22, 22, 14, 14, 30, 30, 14, 14, 30, 30, 6, 6, 22, 22, 6, 6, 22, 22, 14, 14, 30, 30, 14, 14, 30, 30, 1, 1, 17, 17, 1, 1, 17, 17, 9, 9, 25, 25, 9, 9, 25, 25, 1, 1, 17, 17, 1, 1, 17, 17, 9, 9, 25, 25, 9, 9, 25, 25, 5, 5, 21, 21, 5, 5, 21, 21, 13, 13, 29, 29, 13, 13, 29, 29, 5, 5, 21, 21, 5, 5, 21, 21, 13, 13, 29, 29, 13, 13, 29, 29, 1, 1, 17, 17, 1, 1, 17, 17, 9, 9, 25, 25, 9, 9, 25, 25, 1, 1, 17, 17, 1, 1, 17, 17, 9, 9, 25, 25, 9, 9, 25, 25, 5, 5, 21, 21, 5, 5, 21, 21, 13, 13, 29, 29, 13, 13, 29, 29, 5, 5, 21, 21, 5, 5, 21, 21, 13, 13, 29, 29, 13, 13, 29, 29, 3, 3, 19, 19, 3, 3, 19, 19, 11, 11, 27, 27, 11, 11, 27, 27, 3, 3, 19, 19, 3, 3, 19, 19, 11, 11, 27, 27, 11, 11, 27, 27, 7, 7, 23, 23, 7, 7, 23, 23, 15, 15, 31, 31, 15, 15, 31, 31, 7, 7, 23, 23, 7, 7, 23, 23, 15, 15, 31, 31, 15, 15, 31, 31, 3, 3, 19, 19, 3, 3, 19, 19, 11, 11, 27, 27, 11, 11, 27, 27, 3, 3, 19, 19, 3, 3, 19, 19, 11, 11, 27, 27, 11, 11, 27, 27, 7, 7, 23, 23, 7, 7, 23, 23, 15, 15, 31, 31, 15, 15, 31, 31, 7, 7, 23, 23, 7, 7, 23, 23, 15, 15, 31, 31, 15, 15, 31, 31, 1, 1, 17, 17, 1, 1, 17, 17, 9, 9, 25, 25, 9, 9, 25, 25, 1, 1, 17, 17, 1, 1, 17, 17, 9, 9, 25, 25, 9, 9, 25, 25, 5, 5, 21, 21, 5, 5, 21, 21, 13, 13, 29, 29, 13, 13, 29, 29, 5, 5, 21, 21, 5, 5, 21, 21, 13, 13, 29, 29, 13, 13, 29, 29, 1, 1, 17, 17, 1, 1, 17, 17, 9, 9, 25, 25, 9, 9, 25, 25, 1, 1, 17, 17, 1, 1, 17, 17, 9, 9, 25, 25, 9, 9, 25, 25, 5, 5, 21, 21, 5, 5, 21, 21, 13, 13, 29, 29, 13, 13, 29, 29, 5, 5, 21, 21, 5, 5, 21, 21, 13, 13, 29, 29, 13, 13, 29, 29, 3, 3, 19, 19, 3, 3, 19, 19, 11, 11, 27, 27, 11, 11, 27, 27, 3, 3, 19, 19, 3, 3, 19, 19, 11, 11, 27, 27, 11, 11, 27, 27, 7, 7, 23, 23, 7, 7, 23, 23, 15, 15, 31, 31, 15, 15, 31, 31, 7, 7, 23, 23, 7, 7, 23, 23, 15, 15, 31, 31, 15, 15, 31, 31, 3, 3, 19, 19, 3, 3, 19, 19, 11, 11, 27, 27, 11, 11, 27, 27, 3, 3, 19, 19, 3, 3, 19, 19, 11, 11, 27, 27, 11, 11, 27, 27, 7, 7, 23, 23, 7, 7, 23, 23, 15, 15, 31, 31, 15, 15, 31, 31, 7, 7, 23, 23, 7, 7, 23, 23, 15, 15, 31, 31, 15, 15, 31, 31); IWCodecBaseClass.prototype.zigzagCol = Uint8Array.of(0, 16, 0, 16, 8, 24, 8, 24, 0, 16, 0, 16, 8, 24, 8, 24, 4, 20, 4, 20, 12, 28, 12, 28, 4, 20, 4, 20, 12, 28, 12, 28, 0, 16, 0, 16, 8, 24, 8, 24, 0, 16, 0, 16, 8, 24, 8, 24, 4, 20, 4, 20, 12, 28, 12, 28, 4, 20, 4, 20, 12, 28, 12, 28, 2, 18, 2, 18, 10, 26, 10, 26, 2, 18, 2, 18, 10, 26, 10, 26, 6, 22, 6, 22, 14, 30, 14, 30, 6, 22, 6, 22, 14, 30, 14, 30, 2, 18, 2, 18, 10, 26, 10, 26, 2, 18, 2, 18, 10, 26, 10, 26, 6, 22, 6, 22, 14, 30, 14, 30, 6, 22, 6, 22, 14, 30, 14, 30, 0, 16, 0, 16, 8, 24, 8, 24, 0, 16, 0, 16, 8, 24, 8, 24, 4, 20, 4, 20, 12, 28, 12, 28, 4, 20, 4, 20, 12, 28, 12, 28, 0, 16, 0, 16, 8, 24, 8, 24, 0, 16, 0, 16, 8, 24, 8, 24, 4, 20, 4, 20, 12, 28, 12, 28, 4, 20, 4, 20, 12, 28, 12, 28, 2, 18, 2, 18, 10, 26, 10, 26, 2, 18, 2, 18, 10, 26, 10, 26, 6, 22, 6, 22, 14, 30, 14, 30, 6, 22, 6, 22, 14, 30, 14, 30, 2, 18, 2, 18, 10, 26, 10, 26, 2, 18, 2, 18, 10, 26, 10, 26, 6, 22, 6, 22, 14, 30, 14, 30, 6, 22, 6, 22, 14, 30, 14, 30, 1, 17, 1, 17, 9, 25, 9, 25, 1, 17, 1, 17, 9, 25, 9, 25, 5, 21, 5, 21, 13, 29, 13, 29, 5, 21, 5, 21, 13, 29, 13, 29, 1, 17, 1, 17, 9, 25, 9, 25, 1, 17, 1, 17, 9, 25, 9, 25, 5, 21, 5, 21, 13, 29, 13, 29, 5, 21, 5, 21, 13, 29, 13, 29, 3, 19, 3, 19, 11, 27, 11, 27, 3, 19, 3, 19, 11, 27, 11, 27, 7, 23, 7, 23, 15, 31, 15, 31, 7, 23, 7, 23, 15, 31, 15, 31, 3, 19, 3, 19, 11, 27, 11, 27, 3, 19, 3, 19, 11, 27, 11, 27, 7, 23, 7, 23, 15, 31, 15, 31, 7, 23, 7, 23, 15, 31, 15, 31, 1, 17, 1, 17, 9, 25, 9, 25, 1, 17, 1, 17, 9, 25, 9, 25, 5, 21, 5, 21, 13, 29, 13, 29, 5, 21, 5, 21, 13, 29, 13, 29, 1, 17, 1, 17, 9, 25, 9, 25, 1, 17, 1, 17, 9, 25, 9, 25, 5, 21, 5, 21, 13, 29, 13, 29, 5, 21, 5, 21, 13, 29, 13, 29, 3, 19, 3, 19, 11, 27, 11, 27, 3, 19, 3, 19, 11, 27, 11, 27, 7, 23, 7, 23, 15, 31, 15, 31, 7, 23, 7, 23, 15, 31, 15, 31, 3, 19, 3, 19, 11, 27, 11, 27, 3, 19, 3, 19, 11, 27, 11, 27, 7, 23, 7, 23, 15, 31, 15, 31, 7, 23, 7, 23, 15, 31, 15, 31, 0, 16, 0, 16, 8, 24, 8, 24, 0, 16, 0, 16, 8, 24, 8, 24, 4, 20, 4, 20, 12, 28, 12, 28, 4, 20, 4, 20, 12, 28, 12, 28, 0, 16, 0, 16, 8, 24, 8, 24, 0, 16, 0, 16, 8, 24, 8, 24, 4, 20, 4, 20, 12, 28, 12, 28, 4, 20, 4, 20, 12, 28, 12, 28, 2, 18, 2, 18, 10, 26, 10, 26, 2, 18, 2, 18, 10, 26, 10, 26, 6, 22, 6, 22, 14, 30, 14, 30, 6, 22, 6, 22, 14, 30, 14, 30, 2, 18, 2, 18, 10, 26, 10, 26, 2, 18, 2, 18, 10, 26, 10, 26, 6, 22, 6, 22, 14, 30, 14, 30, 6, 22, 6, 22, 14, 30, 14, 30, 0, 16, 0, 16, 8, 24, 8, 24, 0, 16, 0, 16, 8, 24, 8, 24, 4, 20, 4, 20, 12, 28, 12, 28, 4, 20, 4, 20, 12, 28, 12, 28, 0, 16, 0, 16, 8, 24, 8, 24, 0, 16, 0, 16, 8, 24, 8, 24, 4, 20, 4, 20, 12, 28, 12, 28, 4, 20, 4, 20, 12, 28, 12, 28, 2, 18, 2, 18, 10, 26, 10, 26, 2, 18, 2, 18, 10, 26, 10, 26, 6, 22, 6, 22, 14, 30, 14, 30, 6, 22, 6, 22, 14, 30, 14, 30, 2, 18, 2, 18, 10, 26, 10, 26, 2, 18, 2, 18, 10, 26, 10, 26, 6, 22, 6, 22, 14, 30, 14, 30, 6, 22, 6, 22, 14, 30, 14, 30, 1, 17, 1, 17, 9, 25, 9, 25, 1, 17, 1, 17, 9, 25, 9, 25, 5, 21, 5, 21, 13, 29, 13, 29, 5, 21, 5, 21, 13, 29, 13, 29, 1, 17, 1, 17, 9, 25, 9, 25, 1, 17, 1, 17, 9, 25, 9, 25, 5, 21, 5, 21, 13, 29, 13, 29, 5, 21, 5, 21, 13, 29, 13, 29, 3, 19, 3, 19, 11, 27, 11, 27, 3, 19, 3, 19, 11, 27, 11, 27, 7, 23, 7, 23, 15, 31, 15, 31, 7, 23, 7, 23, 15, 31, 15, 31, 3, 19, 3, 19, 11, 27, 11, 27, 3, 19, 3, 19, 11, 27, 11, 27, 7, 23, 7, 23, 15, 31, 15, 31, 7, 23, 7, 23, 15, 31, 15, 31, 1, 17, 1, 17, 9, 25, 9, 25, 1, 17, 1, 17, 9, 25, 9, 25, 5, 21, 5, 21, 13, 29, 13, 29, 5, 21, 5, 21, 13, 29, 13, 29, 1, 17, 1, 17, 9, 25, 9, 25, 1, 17, 1, 17, 9, 25, 9, 25, 5, 21, 5, 21, 13, 29, 13, 29, 5, 21, 5, 21, 13, 29, 13, 29, 3, 19, 3, 19, 11, 27, 11, 27, 3, 19, 3, 19, 11, 27, 11, 27, 7, 23, 7, 23, 15, 31, 15, 31, 7, 23, 7, 23, 15, 31, 15, 31, 3, 19, 3, 19, 11, 27, 11, 27, 3, 19, 3, 19, 11, 27, 11, 27, 7, 23, 7, 23, 15, 31, 15, 31, 7, 23, 7, 23, 15, 31, 15, 31); // массив соответсвия номера группы и номеров сегментов (bucket'ов) IWCodecBaseClass.prototype.bandBuckets = [ { from: 0, to: 0 }, { from: 1, to: 1 }, { from: 2, to: 2 }, { from: 3, to: 3 }, { from: 4, to: 7 }, { from: 8, to: 11 }, { from: 12, to: 15 }, { from: 16, to: 31 }, { from: 32, to: 47 }, { from: 48, to: 63 } ]; ================================================ FILE: library/src/iw44/IWDecoder.js ================================================ import IWCodecBaseClass from './IWCodecBaseClass'; import { LinearBytemap, Block, LazyBlock } from './IWStructures'; import DjVu from '../DjVu'; export default class IWDecoder extends IWCodecBaseClass { constructor() { super(); } init(imageinfo) { // инициализируем на первой порции данных this.info = imageinfo; var blockCount = Math.ceil(this.info.width / 32) * Math.ceil(this.info.height / 32); this.blocks = LazyBlock.createBlockArray(blockCount); } decodeSlice(zp, imageinfo) { if (!this.info) { this.init(imageinfo); } this.zp = zp; if (!this.is_null_slice()) { // по блокам идем this.blocks.forEach(block => { this.preliminaryFlagComputation(block); // четыре подхода декодирования if (this.blockBandDecodingPass()) { this.bucketDecodingPass(block, this.curband); this.newlyActiveCoefficientDecodingPass(block, this.curband); } this.previouslyActiveCoefficientDecodingPass(block); }); } // уменьшаем шаги this.finish_code_slice(); } previouslyActiveCoefficientDecodingPass(block) { var boff = 0; var step = this.quant_hi[this.curband]; var indices = this.getBandBuckets(this.curband); for (var i = indices.from; i <= indices.to; i++, boff++) { for (var j = 0; j < 16; j++) { if (this.coeffstate[boff][j] & 2 /*ACTIVE*/) { if (!this.curband) { step = this.quant_lo[j]; } var des = 0; var coef = block.getBucketCoef(i, j); var absCoef = Math.abs(coef); if (absCoef <= 3 * step) { des = this.zp.decode(this.inreaseCoefCtx, 0); absCoef += step >> 2; } else { des = this.zp.IWdecode(); } if (des) { absCoef += step >> 1; } else { absCoef += -step + (step >> 1); } block.setBucketCoef(i, j, coef < 0 ? -absCoef : absCoef); } } } } newlyActiveCoefficientDecodingPass(block, band) { //bucket offset var boff = 0; var indices = this.getBandBuckets(band); //проверка на 0 группу позже var step = this.quant_hi[this.curband]; for (var i = indices.from; i <= indices.to; i++, boff++) { if (this.bucketstate[boff] & 4/*NEW*/) { var shift = 0; if (this.bucketstate[boff] & 2/*ACTIVE*/) { shift = 8; } var np = 0; for (var j = 0; j < 16; j++) { if (this.coeffstate[boff][j] & 8/*UNK*/) { np++; } } for (var j = 0; j < 16; j++) { if (this.coeffstate[boff][j] & 8/*UNK*/) { var ip = Math.min(7, np); var des = this.zp.decode(this.activateCoefCtx, shift + ip); if (des) { var sign = this.zp.IWdecode() ? -1 : 1; np = 0; if (!this.curband) { step = this.quant_lo[j]; } //todo сравнить нужно ли 2 слагаемое block.setBucketCoef(i, j, sign * (step + (step >> 1) - (step >> 3))); } if (np) { np--; } } } } } } bucketDecodingPass(block, band) { var indices = this.getBandBuckets(band); // смещение сегмента var boff = 0; for (var i = indices.from; i <= indices.to; i++, boff++) { // проверка потенциального флага сегмента if (!(this.bucketstate[boff] & 8/*UNK*/)) { continue; } //вычисляем номер контекста var n = 0; if (band) { var t = 4 * i; for (var j = t; j < t + 4; j++) { if (block.getCoef(j)) { n++; } } if (n === 4) { n--; } } if (this.bbstate & 2/*ACTIVE*/) { //как и + 4 n |= 4; } if (this.zp.decode(this.decodeCoefCtx, n + band * 8)) { this.bucketstate[boff] |= 4/*NEW*/; } } } blockBandDecodingPass() { var indices = this.getBandBuckets(this.curband); var bcount = indices.to - indices.from + 1; if (bcount < 16 || (this.bbstate & 2/*ACTIVE*/)) { this.bbstate |= 4 /*NEW*/; } else if (this.bbstate & 8/*UNK*/) { if (this.zp.decode(this.decodeBucketCtx, 0)) { this.bbstate |= 4/*NEW*/; } } return this.bbstate & 4/*NEW*/; } preliminaryFlagComputation(block) { this.bbstate = 0; var bstatetmp = 0; var indices = this.getBandBuckets(this.curband); if (this.curband) { //смещение сегмента в массиве флагов var boff = 0; for (var j = indices.from; j <= indices.to; j++, boff++) { bstatetmp = 0; for (var k = 0; k < 16; k++) { if (block.getBucketCoef(j, k) === 0) { this.coeffstate[boff][k] = 8/*UNK*/; } else { this.coeffstate[boff][k] = 2/*ACTIVE*/; } bstatetmp |= this.coeffstate[boff][k]; } this.bucketstate[boff] = bstatetmp; this.bbstate |= bstatetmp; } } else { //если нулевая группа for (var k = 0; k < 16; k++) { //если шаг в допустимых пределах if (this.coeffstate[0][k] !== 1/*ZERO*/) { if (block.getBucketCoef(0, k) === 0) { this.coeffstate[0][k] = 8/*UNK*/; } else { this.coeffstate[0][k] = 2/*ACTIVE*/; } } bstatetmp |= this.coeffstate[0][k]; } this.bucketstate[0] = bstatetmp; this.bbstate |= bstatetmp; } } getBytemap() { var time = performance.now(); var fullWidth = Math.ceil(this.info.width / 32) * 32; var fullHeight = Math.ceil(this.info.height / 32) * 32; var blockRows = Math.ceil(this.info.height / 32); var blockCols = Math.ceil(this.info.width / 32); // (this.blocks[0] instanceof LazyBlock) && console.log('Memory usage ', // this.blocks[0].mm.retainedMemory / 1024 / 1024, // this.blocks[0].mm.usedMemory / 1024 / 1024); var bm = new LinearBytemap(fullWidth, fullHeight); for (var r = 0; r < blockRows; r++) { for (var c = 0; c < blockCols; c++) { var block = this.blocks[r * blockCols + c]; for (var i = 0; i < 1024; i++) { /*var bits = []; for (var j = 0; j < 10; j++) { bits.push((i & Math.pow(2, j)) >> j); } var row = 16 * bits[1] + 8 * bits[3] + 4 * bits[5] + 2 * bits[7] + bits[9]; var col = 16 * bits[0] + 8 * bits[2] + 4 * bits[4] + 2 * bits[6] + bits[8];*/ // bitmap[this.zigzagRow[i] + 32 * r][this.zigzagCol[i] + 32 * c] = block.getCoef(i); bm.set(this.zigzagRow[i] + (r << 5), this.zigzagCol[i] + (c << 5), block.getCoef(i)); } } } DjVu.IS_DEBUG && console.time("inverseTime"); this.inverseWaveletTransform(bm); DjVu.IS_DEBUG && console.timeEnd("inverseTime"); DjVu.IS_DEBUG && console.log("getBytemap time = ", performance.now() - time); return bm; } /** * Алгоритм для строк и для столбцов по сути один и тот же. Разница в том, * что индексы переставлены местами. * * The variant when the algorithm is extracted as a separate function * and used both for columns with the bytemap object and for rows via a shim object * (which swaps i and j in all methods' of the Bytemap, even * an optimized version working with the inner array directly) * works slower than the current variant. */ inverseWaveletTransform(bitmap) { var height = this.info.height; var width = this.info.width; var a, c, kmax, k, i, border; var prev3, prev1, next1, next3; for (var s = 16, sDegree = 4; s !== 0; s >>= 1, sDegree--) { // 2^4 === 16 //для столбцов kmax = (height - 1) >> sDegree; border = kmax - 3; for (i = 0; i < width; i += s) { //Lifting k = 0; prev1 = 0; next1 = 0; next3 = 1 > kmax ? 0 : bitmap.get(1 << sDegree, i); for (k = 0; k <= kmax; k += 2) { prev3 = prev1; prev1 = next1; next1 = next3; next3 = (k + 3) > kmax ? 0 : bitmap.get((k + 3) << sDegree, i); a = prev1 + next1; c = prev3 + next3; bitmap.sub(k << sDegree, i, ((a << 3) + a - c + 16) >> 5); } //Prediction k = 1; prev1 = bitmap.get((k - 1) << sDegree, i); if (k + 1 <= kmax) { next1 = bitmap.get((k + 1) << sDegree, i); bitmap.add(k << sDegree, i, (prev1 + next1 + 1) >> 1); } else { bitmap.add(k << sDegree, i, prev1); } if (border >= 3) { next3 = bitmap.get((k + 3) << sDegree, i); } for (k = 3; k <= border; k += 2) { prev3 = prev1; prev1 = next1; next1 = next3; next3 = bitmap.get((k + 3) << sDegree, i); a = prev1 + next1; bitmap.add(k << sDegree, i, ((a << 3) + a - (prev3 + next3) + 8) >> 4 ); } for (; k <= kmax; k += 2) { prev1 = next1; next1 = next3; next3 = 0; if (k + 1 <= kmax) { bitmap.add(k << sDegree, i, (prev1 + next1 + 1) >> 1); } else { bitmap.add(k << sDegree, i, prev1); } } } //для строк kmax = (width - 1) >> sDegree; border = kmax - 3; for (i = 0; i < height; i += s) { //Lifting k = 0; prev1 = 0; next1 = 0; next3 = 1 > kmax ? 0 : bitmap.get(i, 1 << sDegree); for (k = 0; k <= kmax; k += 2) { prev3 = prev1; prev1 = next1; next1 = next3; next3 = k + 3 > kmax ? 0 : bitmap.get(i, (k + 3) << sDegree); a = prev1 + next1; c = prev3 + next3; bitmap.sub(i, k << sDegree, ((a << 3) + a - c + 16) >> 5); } // Выполняется медленнее с этой оптимизацией. // for (; k <= kmax; k += 2) { // prev3 = prev1; prev1 = next1; next1 = next3; next3 = 0; // a = prev1 + next1; // c = prev3 + next3; // bitmap.sub(i, k << sDegree, ((a << 3) + a - c + 16) >> 5); // } //Prediction k = 1 prev1 = bitmap.get(i, (k - 1) << sDegree); if (k + 1 <= kmax) { next1 = bitmap.get(i, (k + 1) << sDegree); bitmap.add(i, k << sDegree, (prev1 + next1 + 1) >> 1); } else { bitmap.add(i, k << sDegree, prev1); } if (border >= 3) { next3 = bitmap.get(i, (k + 3) << sDegree); } for (k = 3; k <= border; k += 2) { prev3 = prev1; prev1 = next1; next1 = next3; next3 = bitmap.get(i, (k + 3) << sDegree); a = prev1 + next1; bitmap.add(i, k << sDegree, ((a << 3) + a - (prev3 + next3) + 8) >> 4 ); } for (; k <= kmax; k += 2) { prev1 = next1; next1 = next3; next3 = 0; if (k + 1 <= kmax) { bitmap.add(i, k << sDegree, (prev1 + next1 + 1) >> 1); } else { bitmap.add(i, k << sDegree, prev1); } } } } } } ================================================ FILE: library/src/iw44/IWEncoder.js ================================================ import IWCodecBaseClass from './IWCodecBaseClass'; import { Block } from './IWStructures'; export default class IWEncoder extends IWCodecBaseClass { constructor(bytemap) { super(); this.width = bytemap.width; this.height = bytemap.height; this.inverseWaveletTransform(bytemap); this.createBlocks(bytemap); } /** * Выполняет волновое преобразование */ inverseWaveletTransform(bytemap) { // LOOP ON SCALES for (var scale = 1; scale < 32; scale <<= 1) { //сначала строки this.filter_fh(scale, bytemap); //потом столбцы this.filter_fv(scale, bytemap); } return bytemap; } // по сути то же преобразование что и при раскодировании, только в обратном порядке filter_fv(s, bitmap) { //для столбцов var kmax = Math.floor((bitmap.height - 1) / s); for (var i = 0; i < bitmap.width; i += s) { //Prediction for (var k = 1; k <= kmax; k += 2) { if ((k - 3 >= 0) && (k + 3 <= kmax)) { bitmap[k * s][i] -= (9 * (bitmap[(k - 1) * s][i] + bitmap[(k + 1) * s][i]) - (bitmap[(k - 3) * s][i] + bitmap[(k + 3) * s][i]) + 8) >> 4; } else if (k + 1 <= kmax) { bitmap[k * s][i] -= (bitmap[(k - 1) * s][i] + bitmap[(k + 1) * s][i] + 1) >> 1; } else { bitmap[k * s][i] -= bitmap[(k - 1) * s][i]; } } //Lifting for (var k = 0; k <= kmax; k += 2) { var a, b, c, d; //------------- if (k - 1 < 0) { a = 0; } else { a = bitmap[(k - 1) * s][i]; } //------------- if (k - 3 < 0) { c = 0; } else { c = bitmap[(k - 3) * s][i]; } //------------- if (k + 1 > kmax) { b = 0; } else { b = bitmap[(k + 1) * s][i]; } //------------- if (k + 3 > kmax) { d = 0; } else { d = bitmap[(k + 3) * s][i]; } //------------- bitmap[k * s][i] += (9 * (a + b) - (c + d) + 16) >> 5; } } } filter_fh(s, bitmap) { //для строк var kmax = Math.floor((bitmap.width - 1) / s); for (var i = 0; i < bitmap.height; i += s) { //Prediction for (var k = 1; k <= kmax; k += 2) { if ((k - 3 >= 0) && (k + 3 <= kmax)) { bitmap[i][k * s] -= (9 * (bitmap[i][(k - 1) * s] + bitmap[i][(k + 1) * s]) - (bitmap[i][(k - 3) * s] + bitmap[i][(k + 3) * s]) + 8) >> 4; } else if (k + 1 <= kmax) { bitmap[i][k * s] -= (bitmap[i][(k - 1) * s] + bitmap[i][(k + 1) * s] + 1) >> 1; } else { bitmap[i][k * s] -= bitmap[i][(k - 1) * s]; } } //Lifting for (var k = 0; k <= kmax; k += 2) { var a, b, c, d; if (k - 1 < 0) { a = 0; } else { a = bitmap[i][(k - 1) * s]; } if (k - 3 < 0) { c = 0; } else { c = bitmap[i][(k - 3) * s]; } if (k + 1 > kmax) { b = 0; } else { b = bitmap[i][(k + 1) * s]; } if (k + 3 > kmax) { d = 0; } else { d = bitmap[i][(k + 3) * s]; } bitmap[i][k * s] += (9 * (a + b) - (c + d) + 16) >> 5; } } } // переводим матрицу в блоки createBlocks(bitmap) { var blockRows = Math.ceil(this.height / 32); var blockCols = Math.ceil(this.width / 32); var length = blockRows * blockCols; var buffer = new ArrayBuffer(length << 11); // выделяем память под все блоки // блоки исходного изображения this.blocks = []; // TODO: переделать через Block.createBlockArray() for (var r = 0; r < blockRows; r++) { for (var c = 0; c < blockCols; c++) { var block = new Block(buffer, (r * blockCols + c) << 11, true); for (var i = 0; i < 1024; i++) { /*var bits = []; for (var j = 0; j < 10; j++) { bits.push((i & Math.pow(2, j)) >> j); } var row = 16 * bits[1] + 8 * bits[3] + 4 * bits[5] + 2 * bits[7] + bits[9]; var col = 16 * bits[0] + 8 * bits[2] + 4 * bits[4] + 2 * bits[6] + bits[8];*/ var val = 0; //проверк если нацело на 32 не делится ширина или высота if (bitmap[this.zigzagRow[i] + 32 * r]) { val = bitmap[this.zigzagRow[i] + 32 * r][this.zigzagCol[i] + 32 * c]; // чтобы не было undefined val = val || 0; } block.setCoef(i, val); } this.blocks.push(block); } } buffer = new ArrayBuffer(length << 11); // выделяем память под все блоки // блоки в которые будем класть закодированные биты this.eblocks = new Array(length); for (var i = 0; i < length; i++) { this.eblocks[i] = new Block(buffer, i << 11, true); } } encodeSlice(zp) { this.zp = zp; if (!this.is_null_slice()) { // по блокам идем for (var i = 0; i < this.blocks.length; i++) { var block = this.blocks[i]; var eblock = this.eblocks[i]; this.preliminaryFlagComputation(block, eblock); // четыре подхода декодирования if (this.blockBandEncodingPass()) { this.bucketEncodingPass(eblock); this.newlyActiveCoefficientEncodingPass(block, eblock); } this.previouslyActiveCoefficientEncodingPass(block, eblock); } } // уменьшаем шаги return this.finish_code_slice(); } previouslyActiveCoefficientEncodingPass(block, eblock) { var boff = 0; var step = this.quant_hi[this.curband]; var indices = this.getBandBuckets(this.curband); for (var i = indices.from; i <= indices.to; i++ , boff++) { for (var j = 0; j < 16; j++) { if (this.coeffstate[boff][j] & this.ACTIVE) { if (!this.curband) { step = this.quant_lo[j]; } var des = 0; var coef = Math.abs(block.buckets[i][j]); // и так всегда > 0 в процессе кодирования var ecoef = eblock.buckets[i][j]; var pix = coef >= ecoef ? 1 : 0; if (ecoef <= 3 * step) { this.zp.encode(pix, this.inreaseCoefCtx, 0); //djvulibre не делает этого при кодировании //coef += step >> 2; } else { this.zp.IWencode(pix); } eblock.buckets[i][j] = ecoef - (pix ? 0 : step) + (step >> 1); } } } } newlyActiveCoefficientEncodingPass(block, eblock) { //bucket offset var boff = 0; var indices = this.getBandBuckets(this.curband); //проверка на 0 группу позже var step = this.quant_hi[this.curband]; for (var i = indices.from; i <= indices.to; i++ , boff++) { if (this.bucketstate[boff] & this.NEW) { var shift = 0; if (this.bucketstate[boff] & this.ACTIVE) { shift = 8; } var bucket = block.buckets[i]; var ebucket = eblock.buckets[i]; var np = 0; for (var j = 0; j < 16; j++) { if (this.coeffstate[boff][j] & this.UNK) { np++; } } for (var j = 0; j < 16; j++) { if (this.coeffstate[boff][j] & this.UNK) { var ip = Math.min(7, np); this.zp.encode((this.coeffstate[boff][j] & this.NEW) ? 1 : 0, this.activateCoefCtx, shift + ip); if (this.coeffstate[boff][j] & this.NEW) { //кодируем знак this.zp.IWencode((bucket[j] < 0) ? 1 : 0); np = 0; if (!this.curband) { step = this.quant_lo[j]; } //todo сравнить нужно ли 2 слагаемое ebucket[j] = (step + (step >> 1) - (step >> 3)); ebucket[j] = (step + (step >> 1)); } if (np) { np--; } } } } } } bucketEncodingPass(eblock) { var indices = this.getBandBuckets(this.curband); // смещение сегмента var boff = 0; for (var i = indices.from; i <= indices.to; i++ , boff++) { // проверка потенциального флага сегмента if (!(this.bucketstate[boff] & this.UNK)) { continue; } //вычисляем номер контекста var n = 0; if (this.curband) { var t = 4 * i; for (var j = t; j < t + 4; j++) { if (eblock.getCoef(j)) { n++; } } if (n === 4) { n--; } } if (this.bbstate & this.ACTIVE) { //как и + 4 n |= 4; } this.zp.encode((this.bucketstate[boff] & this.NEW) ? 1 : 0, this.decodeCoefCtx, n + this.curband * 8); } } blockBandEncodingPass() { var indices = this.getBandBuckets(this.curband); var bcount = indices.to - indices.from + 1; if (bcount < 16 || (this.bbstate & this.ACTIVE)) { this.bbstate |= this.NEW; } else if (this.bbstate & this.UNK) { //this.bbstate может быть NEW на этапе preliminaryFlagComputation this.zp.encode(this.bbstate & this.NEW ? 1 : 0, this.decodeBucketCtx, 0); } return this.bbstate & this.NEW; } // принимает исходный блок и кодируемый блок. Взято из djvulibre preliminaryFlagComputation(block, eblock) { this.bbstate = 0; var bstatetmp = 0; var indices = this.getBandBuckets(this.curband); var step = this.quant_hi[this.curband]; if (this.curband) { //смещение сегмента (bucket'а - bucket offset) в массиве флагов var boff = 0; for (var j = indices.from; j <= indices.to; j++ , boff++) { bstatetmp = 0; var bucket = block.buckets[j]; var ebucket = eblock.buckets[j]; for (var k = 0; k < bucket.length; k++) { //var index = k + 16 * boff; if (ebucket[k]) { this.coeffstate[boff][k] = this.ACTIVE; } else if (bucket[k] >= step || bucket[k] <= -step) { this.coeffstate[boff][k] = this.UNK | this.NEW; } else { this.coeffstate[boff][k] = this.UNK; } bstatetmp |= this.coeffstate[boff][k]; } this.bucketstate[boff] = bstatetmp; this.bbstate |= bstatetmp; } } else { //если нулевая группа var bucket = block.buckets[0]; var ebucket = eblock.buckets[0]; for (var k = 0; k < bucket.length; k++) { step = this.quant_lo[k]; //если шаг в допустимых пределах if (this.coeffstate[0][k] !== this.ZERO) { if (ebucket[k]) { this.coeffstate[0][k] = this.ACTIVE; } else if (bucket[k] >= step || bucket[k] <= -step) { this.coeffstate[0][k] = this.UNK | this.NEW; } else { this.coeffstate[0][k] = this.UNK; } } bstatetmp |= this.coeffstate[0][k]; } this.bucketstate[0] = bstatetmp; this.bbstate |= bstatetmp; } } } ================================================ FILE: library/src/iw44/IWImage.js ================================================ import IWDecoder from './IWDecoder'; import { LazyPixelmap, Pixelmap } from './IWStructures'; import DjVu from '../DjVu'; export default class IWImage { constructor() { this.info = null; this.pixelmap = null; this.resetCodecs(); } resetCodecs() { this.ycodec = new IWDecoder(); this.crcodec = this.crcodec ? new IWDecoder() : null; this.cbcodec = this.cbcodec ? new IWDecoder() : null; this.cslice = 0; // current slice } decodeChunk(zp, header) { if (!this.info) { this.info = header; if (!header.grayscale) { this.crcodec = new IWDecoder(); this.cbcodec = new IWDecoder(); } } else { this.info.slices = header.slices; } for (var i = 0; i < this.info.slices; i++) { this.cslice++; this.ycodec.decodeSlice(zp, header); if (this.crcodec && this.cbcodec && this.cslice > this.info.delayInit) { this.cbcodec.decodeSlice(zp, header); this.crcodec.decodeSlice(zp, header); } } } createPixelmap() { var time = performance.now(); var ybitmap = this.ycodec.getBytemap(); var cbbitmap = this.cbcodec ? this.cbcodec.getBytemap() : null; var crbitmap = this.crcodec ? this.crcodec.getBytemap() : null; var pixelMapTime = performance.now(); this.pixelmap = new LazyPixelmap(ybitmap, cbbitmap, crbitmap); DjVu.IS_DEBUG && console.log('Pixelmap constructor time = ', performance.now() - pixelMapTime); DjVu.IS_DEBUG && console.log('IWImage.createPixelmap time = ', performance.now() - time); // do it just to release RAM retained by IWBlocks // In practice, this function is never called twice without full reset of the page. // So technically we could just remove codecs (without create new objects) this.resetCodecs(); } /** * @returns {ImageData} */ getImage() { const time = performance.now(); if (!this.pixelmap) this.createPixelmap(); const width = this.info.width; const height = this.info.height; const image = new ImageData(width, height); const processRow = (i) => { const rowOffset = i * this.pixelmap.width; let pixelIndex = ((height - i - 1) * width) << 2; for (let j = 0; j < width; j++) { this.pixelmap.writePixel(rowOffset + j, image.data, pixelIndex); image.data[pixelIndex | 3] = 255; pixelIndex += 4; } } //const imageConstructTime = performance.now(); for (let i = 0; i < height; i++) { // Optimization for Chrome // When the loop body is a function, it can be easily optimized // In case of the last page of assets/slow.djvu, the whole loop takes // 69ms now, but when the loop body wasn't wrapped into a function it took // 21063ms in case of async mode, and usually in the viewer. Such a big difference // didn't reproduce always, but most frequently. It was a strange case of deoptimization of code by Chrome. // Without that arbitrary deoptimization, the time was the same - about 70ms, but it was unstable. // It seems that now it works more stable. processRow(i); } //DjVu.IS_DEBUG && console.log('***imageConstructTime = ', performance.now() - imageConstructTime); DjVu.IS_DEBUG && console.log('IWImage.getImage time = ', performance.now() - time); return image; } } ================================================ FILE: library/src/iw44/IWImageWriter.js ================================================ import DjVuWriter from '../DjVuWriter'; import DjVuDocument from '../DjVuDocument'; import ByteStreamWriter from '../ByteStreamWriter'; import IWEncoder from './IWEncoder'; import { ZPEncoder } from '../ZPCodec'; import { Bytemap } from './IWStructures'; export default class IWImageWriter { constructor(slicenumber, delayInit, grayscale) { // число кусочков кодируемых this.slicenumber = slicenumber || 100; // серые ли изображения this.grayscale = grayscale || 0; // задержка кодирования цветовой информации this.delayInit = (delayInit & 127) || 0; this.onprocess = undefined; // обработчик события записи страницы } get width() { return this.imageData.width; } get height() { return this.imageData.height; } startMultiPageDocument() { this.dw = new DjVuWriter(); this.dw.startDJVM(); this.pageBuffers = []; var dirm = {}; this.dirm = dirm; dirm.offsets = []; dirm.dflags = 129; // 1000 0001 dirm.flags = []; dirm.ids = []; dirm.sizes = []; // titles and names are not used by this encoder } addPageToDocument(imageData) { var tbsw = new ByteStreamWriter(); // временный буфер для записи this.writeImagePage(tbsw, imageData); var buffer = tbsw.getBuffer(); this.pageBuffers.push(buffer); this.dirm.flags.push(1); // страница без имени и заголовка this.dirm.ids.push('p' + this.dirm.ids.length); // просто уникальный id this.dirm.sizes.push(buffer.byteLength); // размеры } endMultiPageDocument() { this.dw.writeDirmChunk(this.dirm); var len = this.pageBuffers.length; for (var i = 0; i < len; i++) { this.dw.writeFormChunkBuffer(this.pageBuffers.shift()); } var buffer = this.dw.getBuffer(); delete this.dw; delete this.pageBuffers; delete this.dirm; return buffer; } createMultiPageDocument(imageArray) { var dw = new DjVuWriter(); dw.startDJVM(); var length = imageArray.length; var pageBuffers = new Array(imageArray.length); var dirm = {}; this.dirm = dirm; dirm.offsets = []; dirm.dflags = 129; // 1000 0001 dirm.flags = new Array(imageArray.length); dirm.ids = new Array(imageArray.length); dirm.sizes = new Array(imageArray.length); var tbsw = new ByteStreamWriter(); // временный буфер для записи // генерируем все необходимые данные for (var i = 0; i < imageArray.length; i++) { this.writeImagePage(tbsw, imageArray[i]); var buffer = tbsw.getBuffer(); pageBuffers[i] = buffer; tbsw.reset(); dirm.flags[i] = 1; // страница без имени и заголовка dirm.ids[i] = 'p' + i; // просто уникальный id dirm.sizes[i] = buffer.byteLength; // размеры this.onprocess ? this.onprocess((i + 1) / length) : 0; // событие обработки очередной страницы } dw.writeDirmChunk(dirm); for (var i = 0; i < imageArray.length; i++) { dw.writeFormChunkBuffer(pageBuffers[i]); } return new DjVuDocument(dw.getBuffer()); } /** * Кодирует и записывает в поток 1 картинку */ writeImagePage(bsw, imageData) { // пропускаем 4 байта для длины файла bsw.writeStr('FORM').saveOffsetMark('formSize').jump(4).writeStr('DJVU'); // записываем порцию информации bsw.writeStr('INFO') .writeInt32(10) .writeInt16(imageData.width) .writeInt16(imageData.height) .writeByte(24).writeByte(0) .writeByte(100 & 0xff) .writeByte(100 >> 8) .writeByte(22).writeByte(1); //начинаем запись порции цветной bsw.writeStr('BG44').saveOffsetMark('BG44Size').jump(4); //пишем заголовок bsw.writeByte(0) .writeByte(this.slicenumber) //majver .writeByte((this.grayscale << 7) | 1) // это 129 или 1 в зависимости от this.grayscale //minver .writeByte(2) .writeUint16(imageData.width) .writeUint16(imageData.height) .writeByte(this.delayInit); var ycodec = new IWEncoder(this.RGBtoY(imageData)); var crcodec, cbcodec; if (!this.grayscale) { cbcodec = new IWEncoder(this.RGBtoCb(imageData)); crcodec = new IWEncoder(this.RGBtoCr(imageData)); } var zp = new ZPEncoder(bsw); for (var i = 0; i < this.slicenumber; i++) { ycodec.encodeSlice(zp); if (cbcodec && crcodec && i >= this.delayInit) { cbcodec.encodeSlice(zp); crcodec.encodeSlice(zp); } } zp.eflush(); bsw.rewriteSize('formSize'); bsw.rewriteSize('BG44Size'); } createOnePageDocument(imageData) { var bsw = new ByteStreamWriter(10 * 1024); bsw.writeStr('AT&T'); this.writeImagePage(bsw, imageData); // возвращаем новый одностраничный документ return new DjVuDocument(bsw.getBuffer()); } /** * Перевод RGB в Y откопировано из djvulibre * @param {ImageData} imageData * @returns {Bytemap} двумерный байтовый массив */ RGBtoY(imageData) { var rmul = new Int32Array(256); var gmul = new Int32Array(256); var bmul = new Int32Array(256); var data = imageData.data; var width = imageData.width; var height = imageData.height; var bytemap = new Bytemap(width, height); //преобразование необходимое при кодировании серых изображений, чтобы усилить цвет. if (this.grayscale) { for (var i = 0; i < data.length; i++) { data[i] = 255 - data[i]; } } for (var k = 0; k < 256; k++) { rmul[k] = (k * 0x10000 * this.rgb_to_ycc[0][0]); gmul[k] = (k * 0x10000 * this.rgb_to_ycc[0][1]); bmul[k] = (k * 0x10000 * this.rgb_to_ycc[0][2]); } for (var i = 0; i < height; i++) { for (var j = 0; j < width; j++) { //сразу разворачиваем в прямые координаты var index = ((height - i - 1) * width + j) << 2; var y = rmul[data[index]] + gmul[data[index + 1]] + bmul[data[index + 2]] + 32768; bytemap[i][j] = ((y >> 16) - 128) << this.iw_shift; } } return bytemap; } /** * Перевод RGB в Cb откопировано из djvulibre * @param {ImageData} imageData * @returns {Bytemap} двумерный байтовый массив */ RGBtoCb(imageData) { var rmul = new Int32Array(256); var gmul = new Int32Array(256); var bmul = new Int32Array(256); var data = imageData.data; var width = imageData.width; var height = imageData.height; var bytemap = new Bytemap(width, height); for (var k = 0; k < 256; k++) { rmul[k] = (k * 0x10000 * this.rgb_to_ycc[2][0]); gmul[k] = (k * 0x10000 * this.rgb_to_ycc[2][1]); bmul[k] = (k * 0x10000 * this.rgb_to_ycc[2][2]); } for (var i = 0; i < height; i++) { for (var j = 0; j < width; j++) { //сразу разворачиваем в прямые координаты var index = ((height - i - 1) * width + j) << 2; var y = rmul[data[index]] + gmul[data[index + 1]] + bmul[data[index + 2]] + 32768; bytemap[i][j] = Math.max(-128, Math.min(127, y >> 16)) << this.iw_shift; } } return bytemap; } /** * Перевод RGB в Cr откопировано из djvulibre * @param {ImageData} imageData * @returns {Bytemap} двумерный байтовый массив */ RGBtoCr(imageData) { var rmul = new Int32Array(256); var gmul = new Int32Array(256); var bmul = new Int32Array(256); var data = imageData.data; var width = imageData.width; var height = imageData.height; var bytemap = new Bytemap(width, height); for (var k = 0; k < 256; k++) { rmul[k] = (k * 0x10000 * this.rgb_to_ycc[1][0]); gmul[k] = (k * 0x10000 * this.rgb_to_ycc[1][1]); bmul[k] = (k * 0x10000 * this.rgb_to_ycc[1][2]); } for (var i = 0; i < height; i++) { for (var j = 0; j < width; j++) { //сразу разворачиваем в прямые координаты var index = ((height - i - 1) * width + j) << 2; var y = rmul[data[index]] + gmul[data[index + 1]] + bmul[data[index + 2]] + 32768; bytemap[i][j] = Math.max(-128, Math.min(127, y >> 16)) << this.iw_shift; } } return bytemap; } } //сдвиг для кодирования изображений IWImageWriter.prototype.iw_shift = 6; IWImageWriter.prototype.rgb_to_ycc = [ [0.304348, 0.608696, 0.086956], [0.463768, -0.405797, -0.057971], [-0.173913, -0.347826, 0.521739]]; ================================================ FILE: library/src/iw44/IWStructures.js ================================================ function _normalize(val) { val = (val + 32) >> 6; // убираем 6 дробных бит в этом псевдо дробном числе if (val < -128) { return -128; } else if (val >= 128) { return 127; } return val; } export class LazyPixelmap { constructor(ybytemap, cbbytemap, crbytemap) { this.width = ybytemap.width; // required as outer property this.yArray = ybytemap.array; this.cbArray = cbbytemap ? cbbytemap.array : null; this.crArray = crbytemap ? crbytemap.array : null; this.writePixel = cbbytemap ? this.writeColoredPixel : this.writeGrayScalePixel; } writeGrayScalePixel(index, pixelArray, pixelIndex) { const value = 127 - _normalize(this.yArray[index]); pixelArray[pixelIndex] = value; pixelArray[pixelIndex | 1] = value; pixelArray[pixelIndex | 2] = value; } writeColoredPixel(index, pixelArray, pixelIndex) { const y = _normalize(this.yArray[index]); const b = _normalize(this.cbArray[index]); const r = _normalize(this.crArray[index]); const t2 = r + (r >> 1); const t3 = y + 128 - (b >> 2); pixelArray[pixelIndex] = y + 128 + t2; pixelArray[pixelIndex | 1] = t3 - (t2 >> 1); pixelArray[pixelIndex | 2] = t3 + (b << 1); } } export class Pixelmap { constructor(ybytemap, cbbytemap, crbytemap) { this.width = ybytemap.width; // required as outer property var length = ybytemap.array.length; this.r = new Uint8ClampedArray(length); this.g = new Uint8ClampedArray(length); this.b = new Uint8ClampedArray(length); if (cbbytemap) { this._constructColorfulPixelMap(ybytemap.array, cbbytemap.array, crbytemap.array); } else { this._constructGrayScalePixelMap(ybytemap.array); } } _constructGrayScalePixelMap(yArray) { yArray.forEach((v, i) => { this.r[i] = this.g[i] = this.b[i] = 127 - _normalize(v); }); } _constructColorfulPixelMap(yArray, cbArray, crArray) { // using forEach instead of for loop to make the loop body a function - // it helps Chrome not to deoptimize code. It was added for slow.djvu (the last page). yArray.forEach((val, i) => { const y = _normalize(val); const b = _normalize(cbArray[i]); const r = _normalize(crArray[i]); const t2 = r + (r >> 1); const t3 = y + 128 - (b >> 2); this.r[i] = y + 128 + t2; this.g[i] = t3 - (t2 >> 1); this.b[i] = t3 + (b << 1); }); } writePixel(index, pixelArray, pixelIndex) { //var index = this.width * i + j; pixelArray[pixelIndex] = this.r[index]; pixelArray[pixelIndex | 1] = this.g[index]; pixelArray[pixelIndex | 2] = this.b[index]; } // writeLayer(maskPixelArray, scaleFactor, imageWidth, imageHeight, checker) { // var maskRowOffset = (imageHeight - 1) * imageWidth << 2; // var width4 = imageWidth << 2; // var widthStep = width4 * scaleFactor; // var layerRowOffset = 0; // for (var i = 0, li = 0; i < imageHeight; i += scaleFactor, li++) { // for (var j = 0, lj = 0; j < imageWidth; j += scaleFactor, lj++) { // var layerIndex = layerRowOffset + lj; // var intermediateMaskRowOffset = maskRowOffset; // for (var k = 0; k < scaleFactor; k++) { // for (var m = 0; m < scaleFactor; m++) { // var index = intermediateMaskRowOffset + ((m + j) << 2); // if (maskPixelArray[index] === checker) { // maskPixelArray[index] = this.r[layerIndex]; // maskPixelArray[index | 1] = this.g[layerIndex]; // maskPixelArray[index | 2] = this.b[layerIndex]; // } // } // intermediateMaskRowOffset -= width4; // } // } // layerRowOffset += this.width; // maskRowOffset -= widthStep; // } // } } /** * A square variant (extends Array and keep an array of rows) * works about 15-20% slower (in case of inverse transform) */ export class LinearBytemap { constructor(width, height) { this.width = width; this.array = new Int16Array(width * height); } get(i, j) { return this.array[i * this.width + j]; } set(i, j, val) { this.array[i * this.width + j] = val; } sub(i, j, val) { this.array[i * this.width + j] -= val; } add(i, j, val) { this.array[i * this.width + j] += val; } } /** Needed for IWImageWriter - should be replaced there with the LinearBytemap too */ export class Bytemap extends Array { constructor(width, height) { super(height); this.height = height; this.width = width; for (var i = 0; i < height; i++) { this[i] = new Int16Array(width); } } } //блок - структурная единица исходного изображения export class Block { constructor(buffer, offset, withBuckets = false) { this.array = new Int16Array(buffer, offset, 1024); if (withBuckets) { // just for IWEncoder, чтобы не переписывать код this.buckets = new Array(64); for (var i = 0; i < 64; i++) { this.buckets[i] = new Int16Array(buffer, offset, 16); offset += 32; } } } setBucketCoef(bucketNumber, index, value) { this.array[(bucketNumber << 4) | index] = value; // index from 0 to 15 } getBucketCoef(bucketNumber, index) { return this.array[(bucketNumber << 4) | index]; // index from 0 to 15 } getCoef(n) { return this.array[n]; } setCoef(n, val) { this.array[n] = val; } /** * Функция создания массива блоков на основе одного буфера, более быстрого выделения памяти * @returns {Array} */ static createBlockArray(length) { var blocks = new Array(length); var buffer = new ArrayBuffer(length << 11); // выделяем память под все блоки for (var i = 0; i < length; i++) { blocks[i] = new Block(buffer, i << 11); } return blocks; } } class BlockMemoryManager { constructor() { this.buffer = null; this.offset = 0; this.retainedMemory = 0; this.usedMemory = 0; } ensureBuffer() { if (!this.buffer || this.offset >= this.buffer.byteLength) { this.buffer = new ArrayBuffer(10 << 20); // 10MB this.offset = 0; this.retainedMemory += this.buffer.byteLength; } return this.buffer; } allocateBucket() { this.ensureBuffer(); const array = new Int16Array(this.buffer, this.offset, 16); this.offset += 32; this.usedMemory += 32; return array; } } export class LazyBlock { constructor(memoryManager) { this.buckets = new Array(64); this.mm = memoryManager; } setBucketCoef(bucketNumber, index, value) { if (!this.buckets[bucketNumber]) { this.buckets[bucketNumber] = this.mm.allocateBucket(); } this.buckets[bucketNumber][index] = value; // index from 0 to 15 } getBucketCoef(bucketNumber, index) { return this.buckets[bucketNumber] ? this.buckets[bucketNumber][index] : 0; } getCoef(n) { return this.getBucketCoef(n >> 4, n & 15); } setCoef(n, val) { return this.setBucketCoef(n >> 4, n & 15, val); } /** * Функция создания массива блоков на основе одного буфера, более быстрого выделения памяти * @returns {Array} */ static createBlockArray(length) { const mm = new BlockMemoryManager(); const blocks = new Array(length); for (var i = 0; i < length; i++) { blocks[i] = new LazyBlock(mm); } return blocks; } } ================================================ FILE: library/src/jb2/JB2Codec.js ================================================ import { ZPDecoder } from '../ZPCodec'; import { Bitmap, NumContext } from './JB2Structures'; import { IFFChunk } from '../chunks/IFFChunks'; export default class JB2Codec extends IFFChunk { constructor(bs) { super(bs); this.zp = new ZPDecoder(this.bs); this.directBitmapCtx = new Uint8Array(1024); this.refinementBitmapCtx = new Uint8Array(2048); this.offsetTypeCtx = [0]; this.resetNumContexts(); } resetNumContexts() { this.recordTypeCtx = new NumContext(); this.imageSizeCtx = new NumContext(); this.symbolWidthCtx = new NumContext(); this.symbolHeightCtx = new NumContext(); this.inheritDictSizeCtx = new NumContext(); //гориз смещение this.hoffCtx = new NumContext(); //вертикальное смещение this.voffCtx = new NumContext(); //гориз смещение this.shoffCtx = new NumContext(); //вертикальное смещение this.svoffCtx = new NumContext(); this.symbolIndexCtx = new NumContext(); this.symbolHeightDiffCtx = new NumContext(); this.symbolWidthDiffCtx = new NumContext(); this.commentLengthCtx = new NumContext(); this.commentOctetCtx = new NumContext(); this.horizontalAbsLocationCtx = new NumContext(); this.verticalAbsLocationCtx = new NumContext(); } /*decodeNumX(low, high, numctx) { // this is my own implementation var v = 0; var decision = 0; var range = 0xffffffff; if (low === high) { return low; } //phase 1 decision = (low >= 0) || ((high >= 0) && this.zp.decode(numctx.ctx, 0)); // раскодировали знак var negative = !decision; numctx = negative ? numctx.left : numctx.right; if (negative) { // переводим границы в положительную полуось var temp = -low - 1; low = -high - 1; high = temp; } //phase 2 decision = (low > (v << 1) + 1) || ((high >= (v << 1) + 1) && this.zp.decode(numctx.ctx, 0)); while (decision) { v += v + 1; numctx = numctx.right; decision = (low > (v << 1) + 1) || ((high >= (v << 1) + 1) && this.zp.decode(numctx.ctx, 0)); } numctx = numctx.left; //phase 3 range = (v + 1) >> 1; while (range) { decision = (low > v) || ((high >= (v + range)) && this.zp.decode(numctx.ctx, 0)); v += decision ? range : 0; numctx = decision ? numctx.right : numctx.left; range >>= 1; } //phase 4 return negative ? (-v - 1) : v; }*/ decodeNum(low, high, numctx) { // this implementation was copied from DjVuLibre var negative = false; var cutoff; // Start all phases cutoff = 0; for (var phase = 1, range = 0xffffffff; range != 1;) { // encode var decision = (low >= cutoff) || ((high >= cutoff) && this.zp.decode(numctx.ctx, 0)); // context for new bit numctx = decision ? numctx.right : numctx.left; // phase dependent part switch (phase) { case 1: negative = !decision; if (negative) { var temp = - low - 1; low = - high - 1; high = temp; } phase = 2; cutoff = 1; break; case 2: if (!decision) { phase = 3; range = (cutoff + 1) / 2; if (range == 1) cutoff = 0; else cutoff -= range / 2; } else { cutoff += cutoff + 1; } break; case 3: range /= 2; if (range != 1) { if (!decision) cutoff -= range / 2; else cutoff += range / 2; } else if (!decision) { cutoff--; } break; } } return (negative) ? (- cutoff - 1) : cutoff; } decodeBitmap(width, height) { var bitmap = new Bitmap(width, height); for (var i = height - 1; i >= 0; i--) { for (var j = 0; j < width; j++) { var ind = this.getCtxIndex(bitmap, i, j); if (this.zp.decode(this.directBitmapCtx, ind)) { bitmap.set(i, j) }; } } return bitmap; } getCtxIndex(bm, i, j) { var index = 0; var r = i + 2; if (bm.hasRow(r)) { //index = ((bm.get(r, j - 1)) << 9) | (bm.get(r, j) << 8) | ((bm.get(r, j + 1)) << 7); index = (bm.getBits(r, j - 1, 3)) << 7; } r--; if (bm.hasRow(r)) { // index |= ((bm.get(r, j - 2)) << 6) | ((bm.get(r, j - 1)) << 5) | // (bm.get(r, j) << 4) | ((bm.get(r, j + 1)) << 3) | ((bm.get(r, j + 2)) << 2); index |= bm.getBits(r, j - 2, 5) << 2; } //index |= ((bm.get(i, j - 2)) << 1) | (bm.get(i, j - 1)); index |= bm.getBits(i, j - 2, 2); return index; } // don't forget to remove empty edges of the result bitmap before it is added to the dictionary decodeBitmapRef(width, height, mbm) { //current bitmap var cbm = new Bitmap(width, height); var alignInfo = this.alignBitmaps(cbm, mbm); for (var i = height - 1; i >= 0; i--) { for (var j = 0; j < width; j++) { this.zp.decode(this.refinementBitmapCtx, this.getCtxIndexRef(cbm, mbm, alignInfo, i, j)) ? cbm.set(i, j) : 0; } } return cbm; } getCtxIndexRef(cbm, mbm, alignInfo, i, j) { var index = 0; var r = i + 1; if (cbm.hasRow(r)) { //index = ((cbm.get(r, j - 1) || 0) << 10) | (cbm.get(r, j) << 9) | ((cbm.get(r, j + 1) || 0) << 8); index = cbm.getBits(r, j - 1, 3) << 8; } index |= cbm.get(i, j - 1) << 7; r = i + alignInfo.rowshift + 1; var c = j + alignInfo.colshift; index |= mbm.hasRow(r) ? mbm.get(r, c) << 6 : 0; r--; if (mbm.hasRow(r)) { //index |= ((mbm.get(r, c - 1) || 0) << 5) | (mbm.get(r, c) << 4) | ((mbm.get(r, c + 1) || 0) << 3); index |= mbm.getBits(r, c - 1, 3) << 3; } r--; if (mbm.hasRow(r)) { //index |= ((mbm.get(r, c - 1) || 0) << 2) | (mbm.get(r, c) << 1) | (mbm.get(r, c + 1) || 0); index |= mbm.getBits(r, c - 1, 3); } return index; } alignBitmaps(cbm, mbm) { var cwidth = cbm.width - 1; var cheight = cbm.height - 1; var crow, ccol, mrow, mcol; crow = cheight >> 1; ccol = cwidth >> 1; mrow = (mbm.height - 1) >> 1; mcol = (mbm.width - 1) >> 1; return { 'rowshift': mrow - crow, 'colshift': mcol - ccol }; } decodeComment() { var length = this.decodeNum(0, 262142, this.commentLengthCtx); var comment = new Uint8Array(length); for (var i = 0; i < length; comment[i++] = this.decodeNum(0, 255, this.commentOctetCtx)) { } return comment; } /** * Отладочная функция для просмотра символов. * TODO: tranfer it to outside the library */ drawBitmap(bm) { var image = document.createElement('canvas') .getContext('2d') .createImageData(bm.width, bm.height); for (var i = 0; i < bm.height; i++) { for (var j = 0; j < bm.width; j++) { var v = bm.get(i, j) ? 0 : 255; var index = ((bm.height - i - 1) * bm.width + j) * 4; image.data[index] = v; image.data[index + 1] = v; image.data[index + 2] = v; image.data[index + 3] = 255; } } // Globals.canvas.width = Globals.canvas.width; //Globals.canvasCtx.putImageData(image, 0, 0); Globals.drawImage(image); } } ================================================ FILE: library/src/jb2/JB2Dict.js ================================================ import JB2Codec from './JB2Codec'; export default class JB2Dict extends JB2Codec { constructor(bs) { super(bs); this.dict = []; this.isDecoded = false; } decode(djbz) { if (this.isDecoded) { return; } var type = this.decodeNum(0, 11, this.recordTypeCtx); if (type == 9) { // длина словаря var size = this.decodeNum(0, 262142, this.inheritDictSizeCtx); djbz.decode(); this.dict = djbz.dict.slice(0, size); //тип следующей записи (должен быть 0) type = this.decodeNum(0, 11, this.recordTypeCtx); //console.log(size); } this.decodeNum(0, 262142, this.imageSizeCtx); // image width this.decodeNum(0, 262142, this.imageSizeCtx); // image height // флаг всегда должен быть = 0 var flag = this.zp.decode([0], 0); if (flag) { throw new Error("Bad flag!!!"); } type = this.decodeNum(0, 11, this.recordTypeCtx); var width, widthdiff, heightdiff, symbolIndex; var height; var bm; while (type !== 11) { switch (type) { case 2: width = this.decodeNum(0, 262142, this.symbolWidthCtx); height = this.decodeNum(0, 262142, this.symbolHeightCtx); bm = this.decodeBitmap(width, height); this.dict.push(bm); //this.drawBitmap(bm); break; case 5: symbolIndex = this.decodeNum(0, this.dict.length - 1, this.symbolIndexCtx); widthdiff = this.decodeNum(-262143, 262142, this.symbolWidthDiffCtx); heightdiff = this.decodeNum(-262143, 262142, this.symbolHeightDiffCtx); var mbm = this.dict[symbolIndex]; var cbm = this.decodeBitmapRef(mbm.width + widthdiff, heightdiff + mbm.height, mbm); //this.drawBitmap(cbm); this.dict.push(cbm.removeEmptyEdges()); break; case 9: // Numcoder reset //this.decodeNum(0, 262142, this.inheritDictSizeCtx); console.log("RESET DICT"); this.resetNumContexts(); break; case 10: /*var comment = */this.decodeComment(); /*var str = ""; // TODO: test comments for (var i = 0; i < comment.length; i++) { var byte = comment[i]; str += String.fromCharCode(byte); }*/ break; default: throw new Error("Unsupported type in JB2Dict: " + type); } type = this.decodeNum(0, 11, this.recordTypeCtx); if (type > 11) { console.error("TYPE ERROR " + type); break; } } this.isDecoded = true; } } ================================================ FILE: library/src/jb2/JB2Image.js ================================================ import JB2Codec from './JB2Codec'; import { Baseline, Bitmap } from './JB2Structures'; import DjVu from '../DjVu'; export default class JB2Image extends JB2Codec { constructor(bs) { super(bs); this.dict = []; // dict of bitmaps this.initialDictLength = 0; // a number of bitmaps from a shared dict (if required) this.blitList = []; // "blit" = "block transfer" this.init(); } /** * Добавляет в список битмап и координаты левого нижнего угла в классической системе координат * @param {Bitmap} bitmap * @param {Number} x * @param {Number} y */ addBlit(bitmap, x, y) { this.blitList.push({ bitmap, x, y }); } //раскодируем первую запись в потоке init() { var type = this.decodeNum(0, 11, this.recordTypeCtx); if (type == 9) { // длина словаря this.initialDictLength = this.decodeNum(0, 262142, this.inheritDictSizeCtx); //тип следующей записи (должен быть 0) type = this.decodeNum(0, 11, this.recordTypeCtx); //console.log("Zero", type); } this.width = this.decodeNum(0, 262142, this.imageSizeCtx) || 200; this.height = this.decodeNum(0, 262142, this.imageSizeCtx) || 200; // инициализация когда будет надо this.bitmap = false; //позиции первого и предыдущего символа на строке this.lastLeft = 0; this.lastBottom = this.height - 1; this.firstLeft = -1; // получено экспериментально, чтобы не вычитать 1 каждый раз из x как это делается в javadjvu this.firstBottom = this.height - 1; // флаг всегда должен быть = 0 var flag = this.zp.decode([0], 0); if (flag) { throw new Error("Bad flag!!!"); } this.baseline = new Baseline(); } toString() { var str = super.toString(); str += "{width: " + this.width + ", height: " + this.height + '}\n'; return str; } decode(djbz) { // если затребован словарь if (this.initialDictLength) { //декодируем словарь (он может быть уже декодирован) djbz.decode(); //копируем затребованное число символов this.dict = djbz.dict.slice(0, this.initialDictLength); } var type = this.decodeNum(0, 11, this.recordTypeCtx); var width, hoff, voff, flag; var height, index; var bm; // var count = 0; // degug code //var maxInterationNumber = 2000; while (type !== 11 /*&& count < maxInterationNumber*/) { // 11 means "End of data" //count++; // DjVu.IS_DEBUG && console.log('count', count); // DjVu.IS_DEBUG && console.log(type); switch (type) { case 1: // New symbol, add to image and library width = this.decodeNum(0, 262142, this.symbolWidthCtx); height = this.decodeNum(0, 262142, this.symbolHeightCtx); bm = this.decodeBitmap(width, height); //this.drawBitmap(bm); var coords = this.decodeSymbolCoords(bm.width, bm.height); this.addBlit(bm, coords.x, coords.y); //this.copyToBitmap(bm, coords.x, coords.y); this.dict.push(bm.removeEmptyEdges()); //Globals.drawBitmapOnImageCanvas(bm, coords.x, coords.y, this); break; case 2: // New symbol, add to library only width = this.decodeNum(0, 262142, this.symbolWidthCtx); height = this.decodeNum(0, 262142, this.symbolHeightCtx); bm = this.decodeBitmap(width, height); this.dict.push(bm.removeEmptyEdges()); break; case 3: // New symbol, add to image only width = this.decodeNum(0, 262142, this.symbolWidthCtx); height = this.decodeNum(0, 262142, this.symbolHeightCtx); bm = this.decodeBitmap(width, height); //this.drawBitmap(bm); var coords = this.decodeSymbolCoords(bm.width, bm.height); this.addBlit(bm, coords.x, coords.y); //this.copyToBitmap(bm, coords.x, coords.y); break; case 4: // Matched symbol with refinement, add to image and library index = this.decodeNum(0, this.dict.length - 1, this.symbolIndexCtx); var widthdiff = this.decodeNum(-262143, 262142, this.symbolWidthDiffCtx); var heightdiff = this.decodeNum(-262143, 262142, this.symbolHeightDiffCtx); var mbm = this.dict[index]; var cbm = this.decodeBitmapRef(mbm.width + widthdiff, heightdiff + mbm.height, mbm); var coords = this.decodeSymbolCoords(cbm.width, cbm.height); this.addBlit(cbm, coords.x, coords.y); //this.copyToBitmap(cbm, coords.x, coords.y); this.dict.push(cbm.removeEmptyEdges()); break; case 5: // Matched symbol with refinement, add to library only index = this.decodeNum(0, this.dict.length - 1, this.symbolIndexCtx); widthdiff = this.decodeNum(-262143, 262142, this.symbolWidthDiffCtx); heightdiff = this.decodeNum(-262143, 262142, this.symbolHeightDiffCtx); var mbm = this.dict[index]; var cbm = this.decodeBitmapRef(mbm.width + widthdiff, heightdiff + mbm.height, mbm); this.dict.push(cbm.removeEmptyEdges()); break; case 6: // Matched symbol with refinement, add to image only index = this.decodeNum(0, this.dict.length - 1, this.symbolIndexCtx); var widthdiff = this.decodeNum(-262143, 262142, this.symbolWidthDiffCtx); var heightdiff = this.decodeNum(-262143, 262142, this.symbolHeightDiffCtx); var mbm = this.dict[index]; var cbm = this.decodeBitmapRef(mbm.width + widthdiff, heightdiff + mbm.height, mbm); var coords = this.decodeSymbolCoords(cbm.width, cbm.height); this.addBlit(cbm, coords.x, coords.y); //this.copyToBitmap(cbm, coords.x, coords.y); break; case 7: // Matched symbol, copy to image without refinement index = this.decodeNum(0, this.dict.length - 1, this.symbolIndexCtx); bm = this.dict[index]; var coords = this.decodeSymbolCoords(bm.width, bm.height); this.addBlit(bm, coords.x, coords.y); //this.copyToBitmap(bm, coords.x, coords.y); //this.drawBitmap(bm); break; case 8: // Non-symbol data width = this.decodeNum(0, 262142, this.symbolWidthCtx); height = this.decodeNum(0, 262142, this.symbolHeightCtx); bm = this.decodeBitmap(width, height); //this.drawBitmap(bm); var coords = this.decodeAbsoluteLocationCoords(bm.width, bm.height); this.addBlit(bm, coords.x, coords.y); //this.copyToBitmap(bm, coords.x, coords.y); break; case 9: // Numcoder reset console.log("RESET NUM CONTEXTS"); // it hasn't been checked, may work incorrectly this.resetNumContexts(); break; case 10: this.decodeComment(); // TODO: test comments break; default: throw new Error("Unsupported type in JB2Image: " + type); } type = this.decodeNum(0, 11, this.recordTypeCtx); /*if (DjVu.IS_DEBUG && count > maxInterationNumber) { console.error("Too many iterations!"); break; }*/ if (type > 11) { console.error("TYPE ERROR " + type); break; } } } decodeAbsoluteLocationCoords(width, height) { var left = this.decodeNum(1, this.width, this.horizontalAbsLocationCtx); var top = this.decodeNum(1, this.height, this.verticalAbsLocationCtx); return { x: left, y: top - height } } decodeSymbolCoords(width, height) { var flag = this.zp.decode(this.offsetTypeCtx, 0); // флаг новой строки var horizontalOffsetCtx = flag ? this.hoffCtx : this.shoffCtx; var verticalOffsetCtx = flag ? this.voffCtx : this.svoffCtx; var horizontalOffset = this.decodeNum(-262143, 262142, horizontalOffsetCtx); var verticalOffset = this.decodeNum(-262143, 262142, verticalOffsetCtx); var x, y; if (flag) { x = this.firstLeft + horizontalOffset; y = this.firstBottom + verticalOffset - height + 1; this.firstLeft = x; this.firstBottom = y; this.baseline.fill(y); } else { x = this.lastRight + horizontalOffset; y = this.baseline.getVal() + verticalOffset; } this.baseline.add(y); this.lastRight = x + width - 1; return { 'x': x, // не вычитаем 1, так как firstLeft инициализирован -1, а Baseline и так выдает верный результат 'y': y }; } // принимает битмап и координаты левого нижнего угла в обычной системе координат copyToBitmap(bm, x, y) { if (!this.bitmap) { this.bitmap = new Bitmap(this.width, this.height); } for (var i = y, k = 0; k < bm.height; k++ , i++) { for (var j = x, t = 0; t < bm.width; t++ , j++) { if (bm.get(k, t)) { this.bitmap.set(i, j); } } } } getBitmap() { if (!this.bitmap) { this.blitList.forEach(blit => this.copyToBitmap(blit.bitmap, blit.x, blit.y)); } return this.bitmap; } getMaskImage() { var imageData = new ImageData(this.width, this.height); var pixelArray = imageData.data; var time = performance.now(); pixelArray.fill(255); // все белым непрозрачным for (var blitIndex = 0; blitIndex < this.blitList.length; blitIndex++) { var blit = this.blitList[blitIndex]; var bm = blit.bitmap; for (var i = blit.y, k = 0; k < bm.height; k++ , i++) { for (var j = blit.x, t = 0; t < bm.width; t++ , j++) { if (bm.get(k, t)) { var pixelIndex = ((this.height - i - 1) * this.width + j) * 4; pixelArray[pixelIndex] = 0; } } } } DjVu.IS_DEBUG && console.log("JB2Image mask image creating time = ", performance.now() - time); return imageData; } /** * Создаем изображение из маски и палитры, если таковая имеется * @param {DjVuPalette} palette * @param {boolean} isMarkMaskPixels - чтобы понять какой пиксель брать из фона, а какой не трогать. * Нужно только при составлении изображения из двух слоев */ getImage(palette = null, isMarkMaskPixels = false) { if (palette && palette.getDataSize() !== this.blitList.length) { palette = null; // отбрасываем цвета если что-то не так. } var pixelArray = new Uint8ClampedArray(this.width * this.height * 4); var time = performance.now(); pixelArray.fill(255); // все белым непрозрачным var blackPixel = { r: 0, g: 0, b: 0 }; var alpha = isMarkMaskPixels ? 0 : 255; for (var blitIndex = 0; blitIndex < this.blitList.length; blitIndex++) { var blit = this.blitList[blitIndex]; var pixel = palette ? palette.getPixelByBlitIndex(blitIndex) : blackPixel; var bm = blit.bitmap; for (var i = blit.y, k = 0; k < bm.height; k++ , i++) { for (var j = blit.x, t = 0; t < bm.width; t++ , j++) { if (bm.get(k, t)) { var pixelIndex = ((this.height - i - 1) * this.width + j) << 2; pixelArray[pixelIndex] = pixel.r; pixelArray[pixelIndex | 1] = pixel.g; pixelArray[pixelIndex | 2] = pixel.b; pixelArray[pixelIndex | 3] = alpha; } } } } DjVu.IS_DEBUG && console.log("JB2Image creating time = ", performance.now() - time); return new ImageData(pixelArray, this.width, this.height); } getImageFromBitmap() { // debug function mostly this.getBitmap(); var time = performance.now(); var image = new ImageData(this.width, this.height); for (var i = 0; i < this.height; i++) { for (var j = 0; j < this.width; j++) { var v = this.bitmap.get(i, j) ? 0 : 255; var index = ((this.height - i - 1) * this.width + j) * 4; image.data[index] = v; image.data[index + 1] = v; image.data[index + 2] = v; image.data[index + 3] = 255; } } DjVu.IS_DEBUG && console.log("JB2Image creating time = ", performance.now() - time); return image; } } ================================================ FILE: library/src/jb2/JB2Structures.js ================================================ export class Bitmap { constructor(width, height) { var length = Math.ceil(width * height / 8); // число байт необходимых для кодировки черно-белого изображения this.height = height; this.width = width; this.innerArray = new Uint8Array(length); } getBits(i, j, bitNumber) { if (!this.hasRow(i) || j >= this.width) { return 0; } if (j < 0) { bitNumber += j; j = 0; } var tmp = i * this.width + j; var index = tmp >> 3; var bitIndex = tmp & 7; var mask = 32768 >>> bitIndex; var twoBytes = ((this.innerArray[index] << 8) | (this.innerArray[index + 1] || 0)); var existingBits = this.width - j; var border = bitNumber < existingBits ? bitNumber : existingBits; for (var k = 1; k < border; k++) { mask |= 32768 >>> (bitIndex + k) } return (twoBytes & mask) >>> (16 - bitIndex - bitNumber); } get(i, j) { if (!this.hasRow(i) || j < 0 || j >= this.width) { return 0; } var tmp = i * this.width + j; var index = tmp >> 3; var bitIndex = tmp & 7; var mask = 128 >> bitIndex; return (this.innerArray[index] & mask) ? 1 : 0; } set(i, j) { // сделать "пиксель" черным var tmp = i * this.width + j; var index = tmp >> 3; var bitIndex = tmp & 7; var mask = 128 >> bitIndex; this.innerArray[index] |= mask; return; } hasRow(r) { return r >= 0 && r < this.height; } removeEmptyEdges() { var bottomShift = 0; var topShift = 0; var leftShift = 0; var rightShift = 0; main_cycle: for (var i = 0; i < this.height; i++) { for (var j = 0; j < this.width; j++) { if (this.get(i, j)) { break main_cycle; } } bottomShift++; } main_cycle: for (var i = this.height - 1; i >= 0; i--) { for (var j = 0; j < this.width; j++) { if (this.get(i, j)) { break main_cycle; } } topShift++; } main_cycle: for (var j = 0; j < this.width; j++) { for (var i = 0; i < this.height; i++) { if (this.get(i, j)) { break main_cycle; } } leftShift++; } main_cycle: for (var j = this.width - 1; j >= 0; j--) { for (var i = 0; i < this.height; i++) { if (this.get(i, j)) { break main_cycle; } } rightShift++; } if (topShift || bottomShift || leftShift || rightShift) { var newWidth = this.width - leftShift - rightShift; var newHeight = this.height - topShift - bottomShift; var newBitMap = new Bitmap(newWidth, newHeight); for (var i = bottomShift, p = 0; p < newHeight; p++ , i++) { for (var j = leftShift, q = 0; q < newWidth; q++ , j++) { if (this.get(i, j)) { newBitMap.set(p, q); } } } return newBitMap; } return this; } } export class NumContext { constructor() { this.ctx = [0]; this._left = null; this._right = null; } get left() { if (!this._left) { this._left = new NumContext(); } return this._left; } get right() { if (!this._right) { this._right = new NumContext(); } return this._right; } } // структура для вычисления позиции символов на картинке export class Baseline { constructor() { this.arr = new Array(3); this.fill(0); // на всякий случай заполняем нулями, хотя вообще это не должно быть нужно this.index = -1; } add(val) { if (++this.index === 3) { this.index = 0; } this.arr[this.index] = val; } getVal() { // возвращает медианное значение if (this.arr[0] >= this.arr[1] && this.arr[0] <= this.arr[2] || this.arr[0] <= this.arr[1] && this.arr[0] >= this.arr[2]) { return this.arr[0]; } else if (this.arr[1] >= this.arr[0] && this.arr[1] <= this.arr[2] || this.arr[1] <= this.arr[0] && this.arr[1] >= this.arr[2]) { return this.arr[1]; } else { return this.arr[2]; } } fill(val) { // инициализируем все 3 значения положением 1 символа на строке (и пока не будет добавлено еще 2, это значение и будет медианным) this.arr[0] = this.arr[1] = this.arr[2] = val; } } ================================================ FILE: library/src/methods/bundle.js ================================================ import { loadPage, loadPageDependency, loadThumbnail } from './load'; import DjVuWriter from '../DjVuWriter'; import { pLimit } from '../DjVu'; /** * A method to download and bundle an indirect djvu document * @this import('../DjVuDocument').DjVuDocument */ export default async function bundle(progressCallback = () => { }) { const djvuWriter = new DjVuWriter(); djvuWriter.startDJVM(); const dirm = { dflags: this.dirm.dflags | 128, flags: [], names: [], titles: [], sizes: [], ids: [], }; const chunkByteStreams = []; const filesQuantity = this.dirm.getFilesQuantity(); const totalOperations = filesQuantity + 3; let pageNumber = 0; const limit = pLimit(4); let downloadedNumber = 0; const promises = []; for (let i = 0; i < filesQuantity; i++) { promises.push(limit(async () => { let bs; if (this.dirm.isPageIndex(i)) { pageNumber++; bs = await loadPage(pageNumber, this._getUrlByPageNumber(pageNumber)); } else if (this.dirm.isThumbnailIndex(i)) { bs = await loadThumbnail( this.baseUrl + this.dirm.getComponentNameByItsId(this.dirm.ids[i]), this.dirm.ids[i] ); } else { bs = await loadPageDependency( this.dirm.ids[i], this.dirm.getComponentNameByItsId(this.dirm.ids[i]), this.baseUrl, ); } downloadedNumber++; progressCallback(downloadedNumber / totalOperations); //await new Promise(resolve => setTimeout(resolve, 1000)); return { flags: this.dirm.flags[i], id: this.dirm.ids[i], name: this.dirm.names[i], title: this.dirm.titles[i], bs: bs, }; })); } for (const data of await Promise.all(promises)) { dirm.flags.push(data.flags); dirm.ids.push(data.id); dirm.names.push(data.names); dirm.titles.push(data.title); dirm.sizes.push(data.bs.length); chunkByteStreams.push(data.bs); } djvuWriter.writeDirmChunk(dirm); if (this.navm) { djvuWriter.writeChunk(this.navm); } progressCallback((totalOperations - 2) / totalOperations); for (let i = 0; i < chunkByteStreams.length; i++) { djvuWriter.writeFormChunkBS(chunkByteStreams[i]); chunkByteStreams[i] = null; // release memory } progressCallback((totalOperations - 1) / totalOperations); const newBuffer = djvuWriter.getBuffer(); progressCallback(1); return new this.constructor(newBuffer); } ================================================ FILE: library/src/methods/load.js ================================================ /** * Logic related to loading pages and dictionaries * for indirect djvu documents. */ import { loadFileViaXHR } from "../DjVu"; import ByteStream from '../ByteStream'; import { NetworkDjVuError, UnsuccessfulRequestDjVuError, CorruptedFileDjVuError } from "../DjVuErrors"; /** @returns {ByteStream} */ async function loadByteStream(url, errorData = {}) { let xhr; try { xhr = await loadFileViaXHR(url); } catch (e) { throw new NetworkDjVuError({ url: url, ...errorData }); } if (xhr.status && xhr.status !== 200) { throw new UnsuccessfulRequestDjVuError(xhr, { ...errorData }); } return new ByteStream(xhr.response); } function checkAndCropByteStream(bs, compositeChunkId = null, errorData = null) { if (bs.readStr4() !== 'AT&T') { throw new CorruptedFileDjVuError(`The byte stream isn't a djvu file.`, errorData); } if (!compositeChunkId) { return bs.fork(); // we should skip format id in the page byte stream } let chunkId = bs.readStr4(); const length = bs.getInt32(); chunkId += bs.readStr4(); if (chunkId !== compositeChunkId) { throw new CorruptedFileDjVuError( `Unexpected chunk id. Expected "${compositeChunkId}", but got "${chunkId}"`, errorData ); } return bs.jump(-12).fork(length + 8); } /** @returns {ByteStream} */ export async function loadPage(number, url) { const errorData = { pageNumber: number }; return checkAndCropByteStream(await loadByteStream(url, errorData), null, errorData); } /** @returns {ByteStream} */ export async function loadPageDependency(id, name, baseUrl, pageNumber = null) { const errorData = { pageNumber: pageNumber, dependencyId: id }; return checkAndCropByteStream(await loadByteStream(baseUrl + name, errorData), 'FORMDJVI', errorData); } /** @returns {ByteStream} */ export async function loadThumbnail(url, id = null) { const errorData = { thumbnailId: id }; return checkAndCropByteStream(await loadByteStream(url, errorData), 'FORMTHUM', errorData); } ================================================ FILE: library/tests/embed.html ================================================ Embed test page

boy.djvu

boy.DJVU

================================================ FILE: library/tests/tests.css ================================================ html, body { height: 100%; } #test_results_wrapper { font-family: monospace; box-shadow: 0 0 1px lightgray; padding: 0.2em; margin: 0.2em; display: flex; flex-wrap: wrap; flex-direction: column; align-content: flex-start; box-sizing: border-box; justify-content: flex-start; height: 95%; overflow: auto; } .test_block { box-shadow: 0 0 1px gray; padding: 0.5em; flex: 0 0 auto; width: 25em; white-space: normal; word-wrap: break-word; margin: 0.2em; } ================================================ FILE: library/tests/tests.html ================================================ DjVu.js Tests
================================================ FILE: library/tests/tests.js ================================================ 'use strict'; var djvuWorker = new DjVu.Worker(); var outputBlock = $('#test_results_wrapper'); function createBaseUrl(url) { const a = document.createElement('a'); a.href = url; const absoluteUrl = a.href; return new URL('./', absoluteUrl).href } // test invocations async function runAllTests() { var testNames = Object.keys(Tests); var inPrior = testNames.filter(name => name[0] === '$'); var usual = testNames.filter(name => !/^[$,_]/.test(name)); testNames = [...inPrior, ...usual]; var totalTime = 0; var total = testNames.length; var failed = 0; while (testNames.length) { var testName = testNames.shift(); TestHelper.writeLog(`${testName} started...`); var startTime = performance.now(); try { var result = await Tests[testName](); } catch (e) { console.error(e); result = e; } var testTime = performance.now() - startTime; totalTime += testTime; if (!result) { TestHelper.writeLog(`${testName} succeeded!`, "green"); } else if (result.isSuccess) { TestHelper.writeLog(`${testName} succeeded!`, "green"); if (result.messages) { result.messages.forEach(message => { TestHelper.writeLog(message, "orange"); }); } } else { failed++; TestHelper.writeLog(`Error: ${JSON.stringify(result)}`, "red"); TestHelper.writeLog(`${testName} failed!`, "red"); } TestHelper.writeLog(`It has taken ${Math.round(testTime)} milliseconds`, "blue"); TestHelper.endTestBlock(); } TestHelper.writeLog(`Total time = ${Math.round(totalTime)} milliseconds`, "blue"); TestHelper.writeLog(`Total number of test = ${total}`, "blue"); if (failed) { TestHelper.writeLog(`Number of failed tests = ${failed}`, "red"); } else { TestHelper.writeLog('All tests succeeded!', "green"); } } var TestHelper = { testBlock: null, renderImageData(imageData) { var canvas = document.createElement('canvas'); canvas.width = imageData.width; canvas.height = imageData.height; canvas.getContext('2d').putImageData(imageData, 0, 0); document.body.appendChild(canvas); }, writeLog(message, color = "black") { if (!this.testBlock) { this.testBlock = $('
'); outputBlock.append(this.testBlock); } this.testBlock.append(`
${message}
`); }, endTestBlock() { this.testBlock = null; }, getHashOfArray(array) { var hash = 0, i, chr; if (array.length === 0) return hash; for (i = 0; i < array.length; i++) { chr = array[i]; hash = ((hash << 5) - hash) + chr; hash |= 0; // Convert to 32bit integer } return hash; }, getImageDataByImageURI(imageURI, rotate = 0) { var image = new Image(); image.src = imageURI; return new Promise(resolve => { image.onload = () => { var canvas = document.createElement('canvas'); if (rotate === 0 || rotate === 180) { canvas.width = image.width; canvas.height = image.height; } else { canvas.width = image.height; canvas.height = image.width; } var ctx = canvas.getContext('2d'); if (rotate) { ctx.translate(canvas.width / 2, canvas.height / 2) ctx.rotate(rotate * Math.PI / 180); ctx.translate(-canvas.width / 2, -canvas.height / 2); // canvas.style.border = "1px solid black"; // document.body.appendChild(canvas); } ctx.drawImage(image, (canvas.width - image.width) / 2, (canvas.height - image.height) / 2); var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); resolve(imageData); }; }); }, compareArrayBuffers(canonicBuffer, resultBuffer) { var canonicArray = new Uint8Array(canonicBuffer); var resultArray = new Uint8Array(resultBuffer); if (canonicArray.length !== resultArray.length) { return `Несовпадение длины байтовых массивов! ${canonicArray.length} и ${resultArray.length}` } for (var i = 0; i < canonicArray.length; i++) { if (canonicArray[i] !== resultArray[i]) { return `Расхождение в байте номер ${i} !`; } } }, compareImageData(canonicImageData, resultImageData) { if (canonicImageData.width !== resultImageData.width) { return `Несовпадение ширины! ${canonicImageData.width} и ${resultImageData.width}`; } if (canonicImageData.height !== resultImageData.height) { return `Несовпадение высоты! ${canonicImageData.height} и ${resultImageData.height}`; } var strictCheck = () => { for (var i = 0; i < resultImageData.data.length; i++) { if ( canonicImageData.data[i] !== resultImageData.data[i] ) { return i; } } return null; }; var height = canonicImageData.height * 4; var width = canonicImageData.width * 4; var byteStep = 4; var luft1Check = () => { var luftCheck = (luft) => { for (var i = 0; i < resultImageData.data.length; i++) { if ( canonicImageData.data[i + luft] !== resultImageData.data[i] && canonicImageData.data[i] !== resultImageData.data[i] ) { return i; } } return null; }; var successLuft = null; [byteStep, -byteStep, width, width + byteStep, width - byteStep, -width, -width + byteStep, -width - byteStep].some(luft => { var index = luftCheck(luft); if (index === null) { successLuft = luft; return true; } }); return successLuft; }; var strictResult = strictCheck(); if (strictResult === null) { return null; } else { var luft1Result = luft1Check(); if (luft1Result !== null) { return `Нестрогая проверка пройдена luft = ${luft1Result}, однако имеется расхождение пикселей! Строгая проверка: ${strictResult}`; } else { return `Pасхождение пикселей! Строгая проверка: ${strictResult}`; } } } }; var Tests = { async _imageTest(djvuName, pageNumber, imageName = null, hash = null, rotate = 0) { return await this._imageTestX({ djvuUrl: '/assets/' + djvuName, pageNumber, imageUrl: imageName ? '/assets/' + imageName : imageName, hash, rotate }); }, async _imageTestX({ djvuUrl, baseUrl = null, pageNumber, imageUrl = null, hash = null, rotate = 0 }) { function checkByHash(data, message) { const calculatedHash = TestHelper.getHashOfArray(data); const isHashTheSame = calculatedHash === hash; return { isSuccess: isHashTheSame, messages: [ isHashTheSame ? "Hash is the same! Good" : `Hash is different! Calculated: ${calculatedHash}, required: ${hash}`, message ] }; } var buffer = await (await fetch(djvuUrl)).arrayBuffer(); await djvuWorker.createDocument(buffer, baseUrl ? { baseUrl } : undefined); const resultImageData = await djvuWorker.doc.getPage(pageNumber).getImageData().run(); if (imageUrl === null) { var result = checkByHash(resultImageData.data); return result.isSuccess ? null : result.messages[0]; } var canonicImageData = await TestHelper.getImageDataByImageURI(imageUrl, rotate); var result = TestHelper.compareImageData(canonicImageData, resultImageData); if (result !== null && hash) { result = checkByHash(resultImageData.data, result); } else if (!hash) { result += "... Hash is " + TestHelper.getHashOfArray(resultImageData.data); } return result; }, async _sliceTest(source, from, to, result) { const buffer = await (await fetch(source)).arrayBuffer(); await djvuWorker.createDocument(buffer); const resultBuffer = await djvuWorker.doc.slice(from, to).run(); const canonicBuffer = await (await fetch(result)).arrayBuffer(); return TestHelper.compareArrayBuffers(canonicBuffer, resultBuffer); }, /*test3LayerSiglePageDocument() { // отключен так как не ясен алгоритм масштабирования слоев return this._imageTest("happy_birthday.djvu", 0, "happy_birthday.png"); },*/ async _testText(djvuUrl, pageNumber, txtUrl) { const buffer = await (await fetch(djvuUrl)).arrayBuffer(); await djvuWorker.createDocument(buffer, { baseUrl: createBaseUrl(djvuUrl) }); const [resultString, binText] = await Promise.all([ pageNumber ? djvuWorker.doc.getPage(pageNumber).getText().run() : djvuWorker.doc.toString().run(), (await fetch(txtUrl)).arrayBuffer() ]); const canonicCharCodesArray = new Uint16Array(binText); for (var i = 0; i < canonicCharCodesArray.length; i++) { if (resultString.charCodeAt(i) !== canonicCharCodesArray[i]) { return "Text is incorrect!"; } } return canonicCharCodesArray.length ? null : "No canonic text!"; }, async _testTextUtf8(djvuUrl, pageNumber, txtUrl) { const buffer = await (await fetch(djvuUrl)).arrayBuffer(); await djvuWorker.createDocument(buffer, { baseUrl: createBaseUrl(djvuUrl) }); const [resultString, binText] = await Promise.all([ pageNumber ? djvuWorker.doc.getPage(pageNumber).getText().run() : djvuWorker.doc.toString().run(), (await fetch(txtUrl)).arrayBuffer() ]); if (resultString !== new TextDecoder().decode(binText)) { return 'Text is incorrect!'; } return null; }, async _testTextZones(djvuUrl, pageNumber, txtUrl, isNormalized = false) { const buffer = await (await fetch(djvuUrl)).arrayBuffer(); await djvuWorker.createDocument(buffer); const page = djvuWorker.doc.getPage(pageNumber); const [textZones, binText] = await Promise.all([ isNormalized ? page.getNormalizedTextZones().run() : page.getPageTextZone().run(), (await fetch(txtUrl)).arrayBuffer() ]); const resultString = JSON.stringify(textZones); const canonicCharCodesArray = new Uint16Array(binText); for (var i = 0; i < canonicCharCodesArray.length; i++) { if (resultString.charCodeAt(i) !== canonicCharCodesArray[i]) { return "Text Zones are incorrect!"; } } return canonicCharCodesArray.length ? null : "No canonic text zones!"; }, testIncorrectFileFormatError() { return fetch(`/assets/boy.png`).then(res => res.arrayBuffer()) .then(buffer => { return djvuWorker.createDocument(buffer); }).then(() => { return "No error! But there must be one!"; }).catch(e => { if (e.code === DjVu.ErrorCodes.INCORRECT_FILE_FORMAT) { return null; } else { return e; } }); }, async testNoSuchPageError() { const buffer = await (await fetch(`/assets/boy.djvu`)).arrayBuffer(); await djvuWorker.createDocument(buffer); try { var pageNumber = 100; await djvuWorker.doc.getPage(pageNumber).getImageData().run(); } catch (e) { if (e.code === DjVu.ErrorCodes.NO_SUCH_PAGE && e.pageNumber === pageNumber) { return null; } else { return e; } } return "No error! But there must be one!"; }, async testMetaDataOfDocWithShortINFOChunk() { return this._testTextUtf8('/assets/carte.djvu', null, '/assets/carte_metadata.bin'); }, testPageTextZone() { return this._testTextZones('/assets/DjVu3Spec.djvu', 1, '/assets/DjVu3Spec_1_page_text_zone.bin'); }, testNormalizedTextZones() { return this._testTextZones('/assets/DjVu3Spec.djvu', 1, '/assets/DjVu3Spec_1_normalized_text_zones.bin', true); }, async testContents() { const buffer = await (await fetch(`/assets/DjVu3Spec.djvu`)).arrayBuffer(); await djvuWorker.createDocument(buffer); const contents = await djvuWorker.doc.getContents().run(); var res = await fetch('/assets/DjVu3Spec_contents.json'); var canonicContents = await res.json(); if (JSON.stringify(canonicContents) === JSON.stringify(contents)) { return null; } else { console.log(canonicContents, contents); return "Contents are different!"; } }, async testPageUrlWithLeadingZero() { const buffer = await (await fetch(`/assets/djvu3spec+.djvu`)).arrayBuffer(); await djvuWorker.createDocument(buffer); const contents = await await djvuWorker.doc.getContents().run(); const url = contents[2].url; if (url !== '#002') { return `Incorrect url of a page! Got ${url}, while expected #002`; } const pageNumber = await djvuWorker.doc.getPageNumberByUrl(url).run(); if (pageNumber !== 2) { return `Incorrect page number was returned! Got ${pageNumber} for url ${url}`; } return null; }, async testGetPageNumberByUrl() { const buffer = await (await fetch(`/assets/DjVu3Spec.djvu`)).arrayBuffer(); await djvuWorker.createDocument(buffer); var pageNum = await djvuWorker.doc.getPageNumberByUrl('#p0069.djvu').run(); if (pageNum !== 69) { return `The url #p0069.djvu is targeted at 69 page but we got ${pageNum} !`; } pageNum = await djvuWorker.doc.getPageNumberByUrl('#57').run(); if (pageNum !== 57) { return `The url #57 is targeted at 57 page but we got ${pageNum} !`; } pageNum = await djvuWorker.doc.getPageNumberByUrl('#900').run(); if (pageNum !== null) { return `There is no page with the url #900, but we got ${pageNum} !`; } return null; }, async testCancelAllWorkerTasks() { const buffer = await (await fetch(`/assets/boy.djvu`)).arrayBuffer(); await djvuWorker.createDocument(buffer); try { var promises = []; for (var i = 2; i < 4; i++) { promises.push(djvuWorker.doc.getPage(i).getImageData().run()); } djvuWorker.cancelAllTasks(); promises.push(djvuWorker.doc.getPage(i).getImageData().run()); await Promise.race(promises); } catch (e) { if (e.code === DjVu.ErrorCodes.NO_SUCH_PAGE && e.pageNumber === i) { return null; } else { return e; } } return "No error! But there must be one!"; }, async testCancelOneWorkerTask() { const buffer = await (await fetch(`/assets/boy.djvu`)).arrayBuffer(); await djvuWorker.createDocument(buffer); try { var promises = []; for (var i = 2; i < 4; i++) { promises.push(djvuWorker.doc.getPage(i).getImageData().run()); } djvuWorker.cancelTask(promises[0]); promises.push(djvuWorker.doc.getPage(i).getImageData().run()); await Promise.race(promises); } catch (e) { if (e.code === DjVu.ErrorCodes.NO_SUCH_PAGE && e.pageNumber === 3) { return null; } else { return e; } } return "No error! But there must be one!"; }, testGetEnglishText() { return this._testText('/assets/DjVu3Spec.djvu', 1, '/assets/DjVu3Spec_1_text.bin'); }, testGetCzechText() { return this._testText('/assets/czech.djvu', 6, '/assets/czech_6_text.bin'); }, testGetIncorrectlyEncodedUtf8Text() { return this._testTextUtf8('/assets/century_dict/index08.djvu', 475, '/assets/century_dict/page475_text.bin'); }, testCreateDocumentFromPictures() { djvuWorker.startMultiPageDocument(90, 0, 0); return Promise.all([ TestHelper.getImageDataByImageURI(`/assets/boy.png`), TestHelper.getImageDataByImageURI(`/assets/chicken.png`) ]).then(imageDatas => { return Promise.all(imageDatas.map(imageData => djvuWorker.addPageToDocument(imageData))); }).then(() => { return Promise.all([ fetch(`/assets/boy_and_chicken.djvu`).then(res => res.arrayBuffer()), djvuWorker.endMultiPageDocument() ]); }).then(arrayBuffers => { return TestHelper.compareArrayBuffers(...arrayBuffers); }); }, async testBundleDocument() { const buffer = await (await fetch('/assets/DjVu3Spec_indirect/index.djvu')).arrayBuffer(); await djvuWorker.createDocument(buffer, { baseUrl: '/assets/DjVu3Spec_indirect' }); let counter = 88; let progress = 0; const resultBuffer = await djvuWorker.doc.bundle(p => { progress = p; counter--; }).run(); const canonicBuffer = await (await fetch('/assets/DjVu3Spec_bundled.djvu')).arrayBuffer(); const progressCheck = counter === 0 && progress === 1 ? null : "Проблемы с отслеживанием прогресса"; return progressCheck || TestHelper.compareArrayBuffers(canonicBuffer, resultBuffer); }, testSliceDocument() { return this._sliceTest(`/assets/DjVu3Spec.djvu`, 5, 10, `/assets/DjVu3Spec_5-10.djvu`); }, testSliceDocumentWithAnnotations() { return this._sliceTest(`/assets/czech.djvu`, 1, 3, `/assets/czech_1-3.djvu`); }, testSliceDocumentWithCyrillicIds() { return this._sliceTest(`/assets/history.djvu`, 2, 2, `/assets/history_2.djvu`); }, async testIndirectDjVu() { var buffer = await (await fetch('/assets/czech_indirect/index.djvu')).arrayBuffer(); djvuWorker.createDocument(buffer, { baseUrl: '/assets/czech_indirect/', memoryLimit: 0 }); async function checkPage(number, canonicHash) { var imageData = await djvuWorker.doc.getPage(number).getImageData().run(); //TestHelper.renderImageData(imageData); var hash = TestHelper.getHashOfArray(imageData.data); if (hash !== canonicHash) { throw "Hash of isn't the same!"; } } await checkPage(3, 400840825); var memoryUsage1 = await djvuWorker.doc.getMemoryUsage().run(); await checkPage(1, -769561152); var memoryUsage2 = await djvuWorker.doc.getMemoryUsage().run(); await checkPage(3, 400840825); var memoryUsage3 = await djvuWorker.doc.getMemoryUsage().run(); if (memoryUsage2 >= memoryUsage1) { throw "The memory wasn't released!"; } if (memoryUsage1 !== memoryUsage3) { throw "There is a memory leakage!"; } await djvuWorker.doc.setMemoryLimit(1000000).run(); await checkPage(1, -769561152); var memoryUsage4 = await djvuWorker.doc.getMemoryUsage().run(); if (memoryUsage4 <= memoryUsage3) { throw "The memory limit is ignored!"; } try { await djvuWorker.doc.getPage(2).getImageData().run(); throw "There is no error, but there must be one!"; } catch (e) { if (!( e.code === DjVu.ErrorCodes.UNSUCCESSFUL_REQUEST && e.status === 404 && e.pageNumber === 2 && !e.dependencyId )) { throw { message: "Different Error!", error: e }; } } try { await djvuWorker.doc.getPage(4).getImageData().run(); throw "There is no error, but there must be one!"; } catch (e) { if (!( e.code === DjVu.ErrorCodes.UNSUCCESSFUL_REQUEST && e.status === 404 && e.pageNumber === 4 && e.dependencyId === 'dict1085.iff' // the dependency was spoiled manually in the file )) { throw { message: "Different Error!", error: e }; } } }, testOpenIndirectDjVuPageDirectly() { return this._imageTestX({ djvuUrl: '/assets/czech_indirect/p0001.djvu', baseUrl: '/assets/czech_indirect/', imageUrl: '/assets/czech_indirect/p0001.png', pageNumber: 1, hash: 400840825, }); }, testOpenIndirectDjVuWithEmptyDjVi() { return this._imageTestX({ djvuUrl: '/assets/polish_indirect/index.djvu', baseUrl: '/assets/polish_indirect/', imageUrl: '/assets/polish_indirect/sw1-0002.png', pageNumber: 1, hash: -177861879, }); }, testPageWithEmptyLastChunk() { return this._imageTestX({ djvuUrl: '/assets/ccitt_2.djvu', imageUrl: '/assets/ccitt_2.png', pageNumber: 1, hash: -1646655329, }); }, testGrayscaleBG44() { return this._imageTest("boy.djvu", 1, "boy.png", -1560338846); }, testColorBG44() { return this._imageTest("chicken.djvu", 1, "chicken.png", 1973539465); }, testJB2Pure() { return this._imageTest("boy_jb2.djvu", 1, "boy_jb2.png", -650210314); }, testRotate90() { return this._imageTest("boy_jb2_rotate90.djvu", 1, "boy_jb2.png", -76276490, 90); }, testRotate180() { return this._imageTest("boy_jb2_rotate180.djvu", 1, "boy_jb2.png", -76276490, 180); }, testRotate270() { return this._imageTest("boy_jb2_rotate270.djvu", 1, "boy_jb2.png", -80336394, 270); }, testJB2WithBitOfBackground() { return this._imageTest("DjVu3Spec.djvu", 48, "DjVu3Spec_48.png", 1367724765); }, testJB2WhereRemovingOfEmptyEdgesOfBitmapsBeforeAddingToDictRequired() { return this._imageTest("problem_page.djvu", 1, "problem_page.png", 826528816); }, testFGbzColoredMask() { return this._imageTest("navm_fgbz.djvu", 3, "navm_fgbz_3.png", 1017482741); }, testPageWithCyrillicId() { return this._imageTest("history.djvu", 2, null, 1203480221); }, async testEmptyPage() { var buffer = await (await fetch(`/assets/malliavin.djvu`)).arrayBuffer(); await djvuWorker.createDocument(buffer); const imageData = await djvuWorker.doc.getPage(6).getImageData().run(); if (!imageData.data.every(byte => byte === 255)) { return "The page must be empty, but it isn't!"; } }, testDeutschBaseline() { // документ в котором Baseline считался неправильно и символы были не на своих местах return this._imageTestX({ djvuUrl: '/assets/deutsch.djvu', imageUrl: '/assets/deutsch_1.png', pageNumber: 1, hash: 2018317133, }); }, testNewJB2SymbolWithEmptyEdges() { return this._imageTestX({ djvuUrl: '/assets/vega.djvu', imageUrl: '/assets/vega_1.png', pageNumber: 1, hash: 1675742877, }); }, testFileWith2BZZEncodedBlocks() { return this._imageTestX({ djvuUrl: '/assets/irish.djvu', imageUrl: '/assets/irish_1.png', pageNumber: 1, hash: -371412505, }); }, /*test3LayerColorImage() { // отключен так как не ясен алгоритм масштабирования слоев return this._imageTest("colorbook.djvu", 3, "colorbook_4.png"); }*/ }; runAllTests(); ================================================ FILE: package.json ================================================ { "name": "DjVu.js_Project", "scripts": { "clean": "git clean -fdX --exclude=!/.*/", "install": "cd library && npm install && cd .. && cd viewer && npm install && cd ..", "build": "cd library && npm run build && cd .. && cd viewer && npm run build && cd .. && npm run copy", "copy": "node .js copy", "make": "npm run install && npm run build", "remake": "npm run clean && npm run install && npm run build", "_ext": "cd extension && npx web-ext build -n {name}-v{manifest_version}-{version}.zip -o", "ext2": "node .js v2 && npm run _ext", "ext3": "node .js v3 && npm run _ext", "ext": "npm run ext2 && npm run ext3" } } ================================================ FILE: viewer/.gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies /node_modules # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* /public/tmp /src/css ================================================ FILE: viewer/CHANGELOG.md ================================================ # DjVu.js Viewer's Changelog ## v.0.10.1 (30.05.2024) - Hide the "Analyze headers" option in the manifest v3 extension, because it's not supported there. ## v.0.10.0 (26.12.2023) - Fix: reset global styles provided by some CSS frameworks. - New translation: Ukrainian. - Chinese translation update. - Inner changes: dependency updates, E2E test fixes, new build and dev tools. ## v.0.9.2 (09.04.2023) - Support for "data:" URLs ## v.0.9.1 (01.02.2023) - Spanish translation update. - DjVu.js v.0.5.4: error messages are displayed again. ## v.0.9.0 (24.05.2022) - Feature: several pages in a row in the continuous scroll mode. - Fix: pinch-zoom works on mobile devices in the continuous scroll mode. - Style improvements. ## v.0.8.3 (14.11.2021) - French translation update. - Minor style fixes. ## v.0.8.2 (23.09.2021) - Support for big images (up to 20K * 20K pixels) in the single page view mode. - Dynamic letter spacing in the text layer to make text fully fill its zone. - API to change view mode programmatically. - Fixed: a previous page was shown for a short while when one switched from continuous scroll to single page view mode. - Getters, constants and action types are exposed as an escape hatch API for temporary solutions. ## v.0.8.1 (11.09.2021) - Fixed a bug from the previous release (the text layer couldn't be selected). - Updated Italian and Chinese translations. ## v.0.8.0 (06.09.2021) - Mobile version. - Fullscreen mode. - Image scaling via pinch zoom. ## v.0.7.1 (30.08.2021) - Toolbar can be pinned and unpinned. - Minor fixes and improvements. ## v.0.7.0 (20.08.2021) - Removed footer, added menu in order to use less space for controls. - Contents panel is animated and can be closed completely. - Continuous scroll mode performance improvement. ## v.0.6.2 (06.04.2021) - Spanish and Portuguese translations. - French translation update. ## v.0.6.1 (14.03.2021) - Fixed print CSS to avoid printing empty pages in Safari. ## v.0.6.0 (10.03.2021) - Print function. - New UI options to hide open, close, print and save buttons. ## v.0.5.6 (21.02.2021) - Simplified Chinese translation. - UI options: a notification/agreement can be shown when a user tries to save a document. - Save an indirect document after it's bundled with its original name. - UI improvement: a handle to resize the left panel. ## v.0.5.5 (18.02.2021) - Translation of error messages. - New UI options: `showContentsAutomatically` and `changePageOnScroll`. - Options window. - Support of absolute and relative links in the table of contents. - Italian translation. - Minor improvements and corrections. ## v.0.5.4 (16.01.2021) - French translation. - An option to hide the full-page mode switch. ## v.0.5.3 (08.12.2020) - Fix: styles in the help window. - Update of the Swedish translation. ## v.0.5.2 (06.12.2020) - Feature: bundle indirect djvu documents (suggestion in the save dialog). - Fix: hide the page's scrollbars in the full page mode. ## v.0.5.1 (19.11.2020) - Separate image and text errors for a page in order to show the part of data which is available. (Former behavior: if the text couldn't be decoded, an error page was shown, even if the image had been gotten) It's related only to the single-page view mode. ## v.0.5.0 (09.11.2020) - Dark and light color themes. - All CSS is built into the JS file. - Preference for the continuous-scroll view mode is saved in the options now. - Bug fixes and style improvements. ## v.0.4.1 (23.08.2020) - Swedish translation. - File name is extracted from the Content-Disposition header, if it's present. - Russian translation was corrected. ## v.0.4.0 (19.08.2020) - Multi-Language support. - Russian and English languages. ## v.0.3.6 (27.07.2020) - An ability to load a file by URL manually in the extension. - An extension option to analyze http headers to detect djvu files. ## v.0.3.5 (24.04.2020) - New DOCUMENT_CLOSED and DOCUMENT_CHANGED events to change the page title dynamically. - decodeURIComponent applied to a file's name derived from a URL. - Fixed a bug due to which there was no file names, but only ***. ## v.0.3.4 (30.03.2020) - Viewer's programmatic API enhancement: page number can be set via configure(), getPageNumber() and PAGE_NUMBER_CHANGED event were added. ## v.0.3.3 (03.11.2019) - Options. There is only one option to open all links to .djvu files via the viewer on click. It's available only in the extension. ## v.0.3.2 (26.10.2019) - Fixed a bug due to which it was impossible to create many instance of the viewer on the same page. ## v.0.3.1 (11.08.2019) - A page number can be set in the URL, e.g. some.djvu#page=10. ## v.0.3.0 (12.05.2019) - Continuous scroll mode. ## v.0.2.5 (30.03.2019) - Page scale can be set programmatically. ## v.0.2.4 (15.11.2018) - Loading layer with a short delay. - Improvement in pages caching logic. - Minor fixes. ## v.0.2.3 (12.10.2018) - Some errors are shown instead of pages rather than in a pop-up window. - Loading placeholder is shown when there is no image yet. - Minor changes to support indirect djvu. ## v.0.2.2 (27.08.2018) - Rotate pages 0, 90, 180, 270 degrees clockwise. - New API allowing to set the initial page rotation programmatically. - Page positioning improvement. ## v.0.2.1 (20.08.2018) - Turn over pages via scrolling. - Bug fixes. ## v.0.2.0 (05.08.2018) - Now it's possible to create many instances of the viewer. - Ctrl+S works even when the keyboard layout isn't English. ## v.0.1.7 (18.06.2018) - The height of the containing element (which the viewer renders into) isn't changed anymore. - DjVu global variable is encapsulated in a separate module. - Minor styles update. ## v.0.1.6 (02.06.2018) - Layout update: no tools panel on the initial screen. - Drag&Drop file zone on the initial screen. - A possibility to close a document and return to the initial screen. ## v.0.1.5 (25.05.2018) - A page text layer and two cursor modes. ## v.0.1.4 (16.05.2018) - Pages are cached now, so better user experience is provided, since pages are switched faster. ## v.0.1.3 (10.05.2018) - Help button and help window. - Layout update. - Save button near the file block. - Minor style improvements. ## v.0.1.2 (01.05.2018) - Program API: loadDocument and loadDocumentByUrl. - File loading screen with a progress bar. - Hotkeys: Ctrl+S to save the document, right/left arrows to go to next/previous pages. ## v.0.1.1 (20.04.2018) - Better error handling. - Table of contents styles improvements. - Now the page vertical scroll bar returns to the top, when a page is changed. ## v.0.1.0 (10.04.2018) - Open single-page and multi-page .djvu documents. - Show text of pages if it is provided. - Scale images of pages. - Drag images of pages. - Turn pages of documents, go to arbitrary page by its number. - Table of contents is shown, if it exists in the document. An ability to turn over pages of the document via links of the table of contents. - Full page mode. - Status bar shows when a task is being executed. ================================================ FILE: viewer/cypress/e2e/fullscreen_mode.cy.js ================================================ import { customId, customClass, renderViewer, loadDocument } from "../utils"; describe('Full page mode', () => { beforeEach(() => { cy.visit('/'); renderViewer(); }); it('Full page mode button works', () => { cy.window().then(win => { const check = (chainer) => { cy.get(customId('root')).then($el => $el.get(0).getBoundingClientRect()).as('boundingRect'); cy.get("@boundingRect").its("width").should(chainer, win.innerWidth); cy.get("@boundingRect").its("height").should(chainer, win.innerHeight); }; check('be.lessThan'); cy.get(customClass('full_page_button')).click(); check('eq'); cy.get(customClass('full_page_button')).click(); check('be.lessThan'); }) }); }); // Doesn't work in Cypress's Electron, only in Firefox describe.skip('Fullscreen mode unavailable', () => { beforeEach(() => { cy.visit('/'); renderViewer(); }); it('Fullscreen button on initial screen', () => { cy.get(customClass('fullscreen_button')).should('not.exist'); }); it('Fullscreen button in menu', () => { loadDocument(); cy.get(customId('menu_button')).click(); cy.get(customId('menu')).within(() => { cy.contains('Fullscreen mode').should('not.exist'); cy.get(customClass('fullscreen_button')).should('not.exist'); }); }); }); describe('Fullscreen mode', () => { beforeEach(() => { cy.visit('/'); cy.window().then(win => { win.parent.document .querySelector('.aut-iframe') .setAttribute('allow', 'fullscreen'); win.location.reload(); cy.document().its('fullscreenEnabled').should('be.true'); }); renderViewer(); }); // Browser doesn't allow to toggle fullscreen programmatically without a user gesture. it.skip('Fullscreen mode', () => { cy.window().then(win => { const check = (chainer) => { cy.get(customId('root')).then($el => $el.get(0).getBoundingClientRect()).as('boundingRect'); cy.get("@boundingRect").its("width").should(chainer, win.screen.width); cy.get("@boundingRect").its("height").should(chainer, win.screen.width); }; check('be.lessThan'); cy.get(customClass('fullscreen_button')).click(); cy.document().its('fullscreenElement').should('not.equal', null); cy.get(customClass('fullscreen_button')).click().wait(2000); check('eq'); cy.get(customClass('fullscreen_button')).click(); check('be.lessThan'); }) }); it('Fullscreen button on initial screen', () => { cy.get(customClass('fullscreen_button')).should('be.visible'); }); it('Fullscreen button in menu', () => { loadDocument(); cy.get(customId('menu_button')).click(); cy.get(customId('menu')).within(() => { cy.contains('Fullscreen mode').should('be.visible'); cy.get(customClass('fullscreen_button')).should('be.visible'); }); }); }); ================================================ FILE: viewer/cypress/e2e/initial_screen.cy.js ================================================ import { getByCustomId, haveCustomClass, hexToRGB, notHaveCustomClass, renderViewer } from "../utils"; import { initialScreenShouldBeVisible } from "../shared"; describe.only('Initial screen', () => { beforeEach(() => { cy.visit('/'); renderViewer(); }); it('Initial screen is visible', () => { initialScreenShouldBeVisible(); }); it('Dark and white theme', () => { getByCustomId('root').should('have.css', 'background-color', hexToRGB('#fcfcfc')); getByCustomId('light_theme_button').should(haveCustomClass('active')); getByCustomId('dark_theme_button').click(); getByCustomId('root').should('have.css', 'background-color', hexToRGB('#1e1e1e')); getByCustomId('light_theme_button').click(); getByCustomId('root').should('have.css', 'background-color', hexToRGB('#fcfcfc')); }); it('Language switch', () => { cy.contains('English').should(haveCustomClass('selected')); cy.contains('Русский').click().should(haveCustomClass('selected')); cy.contains('изменение настроек').should('be.visible'); cy.contains('English').should(notHaveCustomClass('selected')); }); }); ================================================ FILE: viewer/cypress/e2e/menu.cy.js ================================================ import { customClass, customId, loadDocument, renderViewer } from "../utils"; import { helpWindowShouldBeOpen, initialScreenShouldBeVisible, optionsWindowShouldBeOpen } from "../shared"; const menuShouldNotBeVisible = () => cy.get(customId('menu')).should('not.be.visible'); describe('Document menu opens and closes', () => { beforeEach(() => { cy.visit('/'); renderViewer(); loadDocument(); }); it('Menu opens and closes via menu button', () => { cy.contains("Menu").should('not.be.visible'); cy.get(customId('menu_button')).click(); cy.contains("Menu").should('be.visible'); cy.get(customId('menu_button')).click(); menuShouldNotBeVisible(); }); it('Menu can be closed with the close button', () => { cy.get(customId('menu_button')).click().wait(500); cy.get(customId('menu')).should('be.visible') .find(customClass('close_button')).first().click(); menuShouldNotBeVisible(); }); }); describe('Document menu controls', () => { beforeEach(() => { cy.visit('/'); renderViewer(); loadDocument(); cy.get(customId('menu_button')).click().wait(500); }); it('Options inside menu and menu closes', () => { cy.contains('Options').click(); optionsWindowShouldBeOpen(); menuShouldNotBeVisible(); }); it('Help button inside menu', () => { cy.contains('About').click(); helpWindowShouldBeOpen(); menuShouldNotBeVisible(); }); it('Print document', () => { cy.contains('Print').click(); menuShouldNotBeVisible(); cy.get(customClass('modal_window')).within(() => { cy.contains('Pages must be rendered before printing').should('be.visible'); cy.contains('From').should('be.visible'); cy.contains('to').should('be.visible'); cy.contains('Prepare pages for printing').click(); }); cy.contains('Prepare pages for printing').should('not.exist'); cy.get(customClass('modal_window')) .contains('Preparing pages for printing...').should('be.visible'); }); it('Close document', () => { cy.contains('test_document').should('be.visible'); cy.contains('Close').click(); cy.get(customId('menu')).should('not.exist'); initialScreenShouldBeVisible(); }); }); ================================================ FILE: viewer/cypress/e2e/mobile_version.cy.js ================================================ import { customClass, customId, loadDocument, renderViewer } from "../utils"; describe('Adaptive layout', () => { beforeEach(() => { cy.visit('/'); renderViewer(); loadDocument(); }); it('Dynamic layout change', () => { const checkVisibility = (hidden = false) => { cy.get(customId('toolbar')).within(() => { cy.get(customId('view_mode_buttons')).should(`be.${hidden ? 'not.' : ''}visible`); cy.get(customId('cursor_mode_buttons')).should(`be.${hidden ? 'not.' : ''}visible`); cy.get(customId('scale_gizmo')).should(`be.${hidden ? 'not.' : ''}visible`); cy.get(customId('rotation_control')).should(`be.${hidden ? 'not.' : ''}visible`); cy.get(customId('pin_button')).should(hidden ? 'not.exist' : `be.visible`); cy.get(customClass('right_panel') + '>' + customClass('full_page_button')) .should(hidden ? 'not.exist' : `be.visible`); }); cy.contains('Contents').should(`be.${hidden ? 'not.' : ''}visible`); } checkVisibility(); cy.viewport(700, 800); checkVisibility(true); cy.get(customId('contents_button')).should('be.visible'); cy.get(customId('page_number_block')).should('be.visible'); cy.get(customId('page_number_block')).should('be.visible'); cy.get(customId('hide_button')).should('be.visible'); cy.get(customId('menu_button')).should('be.visible'); }); }); describe('Mobile version', { viewportWidth: 700, viewportHeight: 800, }, () => { beforeEach(() => { cy.visit('/'); renderViewer(); loadDocument(); }); it('Hide button', () => { cy.get(customId('toolbar')).should('be.visible'); cy.get(customId('hide_button')).should('be.visible').click(); cy.get(customId('toolbar')).should('not.be.visible'); cy.get(customId('hide_button')).should('be.visible').click(); cy.get(customId('toolbar')).should('be.visible'); }); it('Contents button', () => { cy.contains('Contents').should('not.be.visible'); cy.get(customId('contents_button')).click(); cy.contains('Contents').should('be.visible'); cy.get(customId('contents_button')).click(); cy.contains('Contents').should('not.be.visible'); }); it('Mobile menu', () => { cy.get(customId('menu_button')).click(); cy.get(customId('menu')).should('be.visible').within(() => { cy.contains('View mode').should('be.visible'); cy.contains('Scale').should('be.visible'); cy.contains('Rotation').should('be.visible'); cy.contains('Cursor mode').should('be.visible'); cy.contains('Full page mode').should('be.visible'); }); }); }); ================================================ FILE: viewer/cypress/e2e/modal_windows.cy.js ================================================ import { customClass, customId, renderViewer } from "../utils"; import { closeModalWindow, helpWindowShouldBeOpen, optionsWindowShouldBeOpen } from "../shared"; describe('Modal windows', () => { beforeEach(() => { cy.visit('/'); renderViewer(); }); it('A click on the dark layer closes the modal window', () => { cy.get(customClass('help_button')).click(); cy.get(customClass('modal_window')).as('modal_window').should('be.visible'); cy.get(customId('root')).click(5, 5); cy.get('@modal_window').should('not.exist'); }); it('The close button closes the modal window', () => { cy.get(customClass('help_button')).click(); closeModalWindow(); cy.get("@modal_window").should('not.exist'); }); it('Options window', () => { cy.get(customClass('options_button')).click(); optionsWindowShouldBeOpen(); }); it('Help window', () => { cy.get(customClass('help_button')).click(); helpWindowShouldBeOpen(); }); }); ================================================ FILE: viewer/cypress/e2e/toolbar.cy.js ================================================ import { customId, loadDocument, renderViewer } from "../utils"; describe('Toolbar controls', () => { beforeEach(() => { cy.visit('/'); renderViewer(); loadDocument(); }); it('Pin/Unpin toolbar', () => { cy.get(customId('toolbar')).should('be.visible'); cy.get(customId('pin_button')).click(); cy.get(customId('toolbar')).trigger('mouseout').wait(500); cy.get(customId('toolbar')).should('not.be.visible'); cy.get(customId('root')).trigger('mouseover', 'bottom'); cy.get(customId('toolbar')).should('be.visible'); cy.get(customId('pin_button')).click(); cy.get(customId('toolbar')).trigger('mouseout').wait(500); cy.get(customId('toolbar')).should('be.visible'); }); it('Contents button works', () => { cy.contains("Contents").should('be.visible'); cy.get(customId('contents_button')).click().wait(500); cy.contains("Contents").should('not.be.visible'); cy.get(customId('contents_button')).click(); cy.contains("Contents").should('be.visible'); }); it('Go to the next/previous page', () => { cy.get(customId('page_number_block')).find('svg:first-of-type').as('prev'); cy.get(customId('page_number_block')).find('svg:last-of-type').as('next'); cy.contains('1 / 71').should('be.visible'); cy.get('@next').click(); cy.contains('2 / 71').should('be.visible'); cy.get('@prev').click(); cy.contains('1 / 71').should('be.visible'); cy.get('@prev').click(); cy.contains('71 / 71').should('be.visible'); cy.get('@next').click(); cy.contains('1 / 71').should('be.visible'); }); }); ================================================ FILE: viewer/cypress/shared.js ================================================ import { customClass } from "./utils"; export function helpWindowShouldBeOpen() { cy.get(customClass('modal_window')).should('be.visible').within(() => { cy.contains('DjVu.js Viewer'); cy.contains('Hotkeys'); cy.contains('Controls'); }); } export function optionsWindowShouldBeOpen() { cy.get(customClass('modal_window')).should('be.visible').within(() => { cy.contains('Options'); cy.contains('Language'); cy.contains('Color theme'); }); } export function closeModalWindow() { cy.get(customClass('modal_window')).as('modal_window').should('be.visible') .find(customClass('close_button')).click(); } export function initialScreenShouldBeVisible() { cy.contains("DjVu.js Viewer").should('be.visible'); cy.contains("powered with DjVu.js").should('be.visible'); cy.get(customClass('help_button')).should('be.visible'); cy.get(customClass('options_button')).should('be.visible'); } ================================================ FILE: viewer/cypress/utils.js ================================================ export const hexToRGB = (string) => { if ((string.length !== 4 && string.length !== 7) || string[0] !== '#') { throw new Error('Incorrect hex color string: ' + string); } const componentLength = string.length === 4 ? 1 : 2; const arr = []; for (let i = 0; i < 3; i++) { arr.push(string.slice(i * componentLength + 1, componentLength * (i + 1) + 1)); } const result = arr.map(color => Number.parseInt(color.length === 2 ? color : (color + color), 16)).join(', '); return `rgb(${result})`; } export const customId = id => `[data-djvujs-id="${id}"]`; export const customClass = className => `[data-djvujs-class~="${className}"]`; export const haveCustomId = id => $el => $el.is(customId(id)); export const haveCustomClass = className => $el => expect($el).to.match(customClass(className)); export const notHaveCustomClass = className => $el => expect($el).not.to.match(customClass(className)); export const getByCustomId = id => cy.get(customId(id)); export const getByCustomClass = className => cy.get(customClass(className)); export const renderViewer = () => { cy.window().then(win => { win.viewer && win.viewer.destroy(); win.viewer = new win.DjVu.Viewer({ language: 'en', theme: 'light' }); win.viewer.render(win.document.getElementById('root')); }); }; export function loadDocument() { cy.clearLocalStorage(); cy.window().then(win => { win.viewer.loadDocumentByUrl('DjVu3Spec.djvu', { name: "test_document", locale: 'en', }); }); } ================================================ FILE: viewer/cypress.config.js ================================================ import { defineConfig } from 'cypress' export default defineConfig({ viewportWidth: 1200, viewportHeight: 900, video: false, fixturesFolder: false, e2e: { setupNodeEvents(on, config) {}, baseUrl: 'http://localhost:8000/?tests=1', supportFile: false, }, }) ================================================ FILE: viewer/index.html ================================================ DjVu Viewer
================================================ FILE: viewer/jsconfig.json ================================================ { "compilerOptions": { "target": "ES6" }, "exclude": [ "node_modules", "**/node_modules/*" ] } ================================================ FILE: viewer/package.json ================================================ { "name": "DjVu.js_Viewer", "private": true, "type": "module", "devDependencies": { "@vitejs/plugin-react": "^4.0.4", "babel-plugin-styled-components": "^2.1.4", "cypress": "^12.17.4", "npm-run-all": "^4.1.5", "serve-static": "^1.15.0", "vite": "^4.4.9" }, "dependencies": { "content-disposition-header": "0.6.0", "eventemitter3": "^5.0.1", "memoize-one": "^6.0.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.10.1", "react-redux": "^8.1.2", "redux": "^4.2.1", "redux-saga": "^1.2.3", "redux-thunk": "^2.4.2", "reselect": "^4.1.8", "styled-components": "^6.0.7" }, "scripts": { "start": "vite --open", "build": "vite build", "test:open": "cypress open", "test": "cypress run", "test:visual": "cypress run --headed", "syncLocales": "node syncLocales.js" } } ================================================ FILE: viewer/public/manifest.json ================================================ { "short_name": "DjVu Viewer", "name": "DjVu Viewer", "icons": [ { "src": "djvu.src", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" } ], "start_url": "./index.html", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: viewer/src/App.test.js ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; it('renders without crashing', () => { const div = document.createElement('div'); ReactDOM.render(, div); }); ================================================ FILE: viewer/src/DjVu.js ================================================ /** * The module is required due to the bug https://bugzilla.mozilla.org/show_bug.cgi?id=1408996 * Because of which I can't address to the global object directly via window.DjVu * Also it encapsulates the logic of getting the global DjVu object. */ if (typeof DjVu !== 'object') { throw new Error("There is no DjVu object! You have to include the DjVu.js library first!"); } const djvu = DjVu; // eslint-disable-line export default djvu; ================================================ FILE: viewer/src/DjVuViewer.jsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux' import App from './components/App.jsx'; import Actions from './actions/actions'; import configureStore from './store'; import EventEmitter from 'eventemitter3'; import Constants, { constant, ActionTypes } from './constants'; import { get } from './reducers'; import dictionaries from './locales'; const Events = constant({ PAGE_NUMBER_CHANGED: null, DOCUMENT_CHANGED: null, DOCUMENT_CLOSED: null, }); export default class DjVuViewer extends EventEmitter { static VERSION = '0.10.1'; static Events = Events; static Constants = Constants; static ActionTypes = ActionTypes; static get = get; static getAvailableLanguages() { return Object.keys(dictionaries); }; /** * Technically, we can pass the same config as to the configure() method. * But some options are reset when a new document is loaded. */ constructor(config = null) { super(); this.store = configureStore(this.eventMiddleware); config && this.configure(config); } eventMiddleware = store => next => action => { let result; switch (action.type) { case Constants.SET_NEW_PAGE_NUMBER_ACTION: const oldPageNumber = this.getPageNumber(); result = next(action); const newPageNumber = this.getPageNumber(); if (oldPageNumber !== newPageNumber) { this.emit(Events.PAGE_NUMBER_CHANGED); } break; case Constants.DOCUMENT_CREATED_ACTION: result = next(action); this.emit(Events.DOCUMENT_CHANGED); break; case Constants.CLOSE_DOCUMENT_ACTION: result = next(action); this.emit(Events.DOCUMENT_CLOSED); break; case Constants.END_FILE_LOADING_ACTION: // use in this.loadDocumentByUrl only result = next(action); this.emit(Constants.END_FILE_LOADING_ACTION); break; default: result = next(action); break; } return result; }; getPageNumber() { return get.currentPageNumber(this.store.getState()); } getDocumentName() { return get.fileName(this.store.getState()); } _render() { this.reactRoot.render( ); } render(element) { this.unmount(); this.htmlElement = element; this.reactRoot = createRoot(element); //this.shadow = element.attachShadow({ mode: 'open' }); this._render(); } unmount() { this.reactRoot && this.reactRoot.unmount(); this.reactRoot = null; this.htmlElement = null; } destroy() { this.unmount(); this.store.dispatch({ type: ActionTypes.DESTROY }); } /** * The config object is destructed merely for the purpose of documentation * @param {number} pageNumber * @param {0|90|180|270} pageRotation * @param {'continuous'|'single'|'text'} viewMode * @param {number} pageScale * @param {string} language * @param {'dark'|'light'} theme * @param {{ hideFullPageSwitch: boolean, changePageOnScroll: boolean, showContentsAutomatically: boolean, hideOpenAndCloseButtons: boolean, hidePrintButton: boolean, hideSaveButton: boolean, }} uiOptions * @returns {DjVuViewer} */ configure({ pageNumber, pageRotation, viewMode, pageScale, language, theme, uiOptions, } = {}) { this.store.dispatch({ type: ActionTypes.CONFIGURE, pageNumber, pageRotation, viewMode, pageScale, language, theme, uiOptions, }); return this; } loadDocument(buffer, name = "***", config = {}) { return new Promise(resolve => { this.once(Events.DOCUMENT_CHANGED, () => resolve()); // the buffer is transferred to the worker, so we copy it this.store.dispatch(Actions.createDocumentFromArrayBufferAction(buffer.slice(0), name, config)); }); } loadDocumentByUrl(url, config = null) { return new Promise(resolve => { this.once(Constants.END_FILE_LOADING_ACTION, () => resolve()); this.store.dispatch({ type: ActionTypes.LOAD_DOCUMENT_BY_URL, url: url, config: config }); }); } } ================================================ FILE: viewer/src/actions/actions.js ================================================ import Constants, { ActionTypes } from '../constants'; import { get } from '../reducers'; import DjVu from '../DjVu'; const Actions = { dropPageAction: pageNumber => ({ type: Constants.DROP_PAGE_ACTION, pageNumber: pageNumber }), pagesSizesAreGottenAction: (pagesSizes) => ({ type: Constants.PAGES_SIZES_ARE_GOTTEN, sizes: pagesSizes, }), pageIsLoadedAction: (pageData, pageNumber) => ({ type: Constants.PAGE_IS_LOADED_ACTION, pageNumber: pageNumber, pageData: pageData, }), setPageRotationAction: rotation => dispatch => { if (rotation === 0 || rotation === 90 || rotation === 180 || rotation === 270) { dispatch({ type: Constants.SET_PAGE_ROTATION_ACTION, pageRotation: rotation }); } }, closeDocumentAction: () => ({ type: Constants.CLOSE_DOCUMENT_ACTION }), setCursorModeAction: cursorMode => ({ type: Constants.SET_CURSOR_MODE_ACTION, cursorMode: cursorMode }), closeHelpWindowAction: () => ({ type: Constants.CLOSE_HELP_WINDOW_ACTION }), showHelpWindowAction: () => ({ type: Constants.SHOW_HELP_WINDOW_ACTION }), tryToSaveDocument: () => (dispatch, getState) => { if (get.isIndirect(getState())) { dispatch({ type: ActionTypes.OPEN_SAVE_DIALOG }); } else { dispatch({ type: ActionTypes.SAVE_DOCUMENT }); } }, startFileLoadingAction: () => ({ type: Constants.START_FILE_LOADING_ACTION }), endFileLoadingAction: () => ({ type: Constants.END_FILE_LOADING_ACTION }), goToNextPageAction: () => (dispatch, getState) => { const state = getState(); if (get.currentPageNumber(state) < get.pagesQuantity(state)) { dispatch(Actions.setNewPageNumberAction(get.currentPageNumber(state) + 1, true)); } }, goToPreviousPageAction: () => (dispatch, getState) => { const state = getState(); if (get.currentPageNumber(state) > 1) { dispatch(Actions.setNewPageNumberAction(get.currentPageNumber(state) - 1, true)); } }, fileLoadingProgressAction: (loaded, total) => ({ type: Constants.FILE_LOADING_PROGRESS_ACTION, loaded: loaded, total: total }), errorAction: error => { console.error(error); return { type: ActionTypes.ERROR, payload: error, } }, createDocumentFromArrayBufferAction: (arrayBuffer, fileName = "***", config = {}) => ({ type: Constants.CREATE_DOCUMENT_FROM_ARRAY_BUFFER_ACTION, arrayBuffer: arrayBuffer, fileName: fileName, config: config, }), setNewPageNumberAction: (pageNumber, shouldScrollToPage = false) => ({ type: Constants.SET_NEW_PAGE_NUMBER_ACTION, pageNumber: pageNumber, shouldScrollToPage: shouldScrollToPage, }), setPageByUrlAction(url, closeContentsOnSuccess = false) { return { type: Constants.SET_PAGE_BY_URL_ACTION, url: url, closeContentsOnSuccess: closeContentsOnSuccess, }; }, setUserScaleAction: (scale) => ({ type: Constants.SET_USER_SCALE_ACTION, scale: scale < 0.1 ? 0.1 : scale > 6 ? 6 : scale }), toggleFullPageViewAction: (isFullPageView) => (dispatch) => { const disableScrollClass = 'disable_scroll_djvujs'; if (isFullPageView) { document.querySelector('html').classList.add(disableScrollClass); document.body.classList.add(disableScrollClass); } else { document.querySelector('html').classList.remove(disableScrollClass); document.body.classList.remove(disableScrollClass); } dispatch({ type: Constants.TOGGLE_FULL_PAGE_VIEW_ACTION, isFullPageView: isFullPageView }); }, }; export default Actions; ================================================ FILE: viewer/src/components/App.jsx ================================================ import React from 'react'; import { useSelector } from 'react-redux'; import { createGlobalStyle, css, /* StyleSheetManager */ } from 'styled-components'; import { get } from '../reducers'; import Toolbar from "./Toolbar/Toolbar"; import InitialScreen from './InitialScreen/InitialScreen'; import FileLoadingScreen from './FileLoadingScreen'; import ErrorWindow from './ModalWindows/ErrorWindow'; import HelpWindow from './ModalWindows/HelpWindow'; import { TranslationProvider } from './Translation'; import Main from './Main'; import SaveDialog from "./ModalWindows/SaveDialog"; import OptionsWindow from "./ModalWindows/OptionsWindow"; import PrintDialog from "./ModalWindows/PrintDialog"; import AppContextProvider from "./AppContext"; const GlobalStyle = createGlobalStyle` html.disable_scroll_djvujs, body.disable_scroll_djvujs { width: 100% !important; height: 100% !important; overflow: hidden !important; } /* Reset styles to get rid of default global styles provided by some frameworks, e.g. https://tailwindcss.com/docs/preflight that adds "svg {display: block}". The specificity is (0, 0, 2) for tags and (0, 1, 1) for pseudo elements to both override the default styles, but not override class-based styles from styled-components. :not(span) and :not(html) are added to increased the specificity. We cannot use "all: revert" for svg and its children, because it will override all svg attributes, including "d" prop of , which will make all icons invisible. */ :where(.djvujs-viewer-root) *:not(svg *):not(svg), div:not(span):where(.djvujs-viewer-root), :where(.djvujs-viewer-root, .djvujs-viewer-root *):not(html)::before, :where(.djvujs-viewer-root, .djvujs-viewer-root *):not(html)::after { all: revert; } :where(.djvujs-viewer-root) :is(svg:not(span), svg *) { display: revert; position: revert; vertical-align: revert; border: revert; box-sizing: revert; background: revert; margin: revert; padding: revert; } // -------------------------- end of styles reset -------------------------- `; const lightTheme = css` --background-color: #fcfcfc; --alternative-background-color: #eee; --modal-window-background-color: var(--background-color); --color: #000; --border-color: #555; --highlight-color: #084475; --scrollbar-track-color: var(--alternative-background-color); --scrollbar-thumb-color: #cccccc; `; const darkTheme = css` --background-color: #1e1e1e; --alternative-background-color: #333333; --modal-window-background-color: var(--background-color); --color: #CCCCCC; --border-color: #999999; --highlight-color: #d89416; --scrollbar-track-color: var(--alternative-background-color); --scrollbar-thumb-color: #858585; `; const style = css` font-family: Arial, Helvetica, sans-serif; font-size: ${p => p.theme.isMobile ? 10 : 14}px; overflow: hidden; position: relative; box-sizing: border-box; height: 100%; display: flex; flex-direction: column; align-items: center; line-height: initial; writing-mode: horizontal-tb; --app-padding: 5px; padding: var(--app-padding); background: var(--background-color); color: var(--color); a { color: var(--highlight-color); } *::-webkit-scrollbar { background-color: var(--scrollbar-track-color); } *::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb-color); } *::-webkit-scrollbar-corner { background-color: var(--background-color); } `; const fullPageStyle = css` top: 0; left: 0; position: fixed; width: 100%; height: 100%; z-index: 100; `; const AppRoot = React.forwardRef(({ shadowRoot }, ref) => { const isFileLoaded = useSelector(get.isDocumentLoaded); const isFileLoading = useSelector(get.isFileLoading); const isFullPageView = useSelector(get.isFullPageView); const theme = useSelector(get.options).theme; const isPrintDialogOpened = useSelector(get.isPrintDialogOpened); return (
{isFileLoading ? : !isFileLoaded ? :
} {/*{isFileLoading ? null :