[
  {
    "path": ".gitattributes",
    "content": "# .bin files containing text are used as test assets\n*.bin binary"
  },
  {
    "path": ".gitignore",
    "content": ".idea\n.vscode/\nnode_modules\n_src\nbuild\ndist\nextension/web-ext-artifacts\nextension/manifest.json"
  },
  {
    "path": ".js",
    "content": "/**\n * Used in npm scripts to copy files\n */\n\n'use strict';\n\nconst fs = require('fs');\n\nasync function copy() {\n    const buildFolder = 'build/';\n    const extensionFolder = 'extension/dist/'\n    if (!fs.existsSync(buildFolder)) fs.mkdirSync(buildFolder);\n    if (!fs.existsSync(extensionFolder)) fs.mkdirSync(extensionFolder);\n\n    const copyFile = (path) => {\n        const fileName = path.split('/').at(-1);\n        fs.copyFileSync(path, buildFolder + fileName);\n        fs.copyFileSync(path, extensionFolder + fileName);\n    }\n\n    copyFile('viewer/dist/djvu_viewer.js');\n    copyFile('library/dist/djvu.js');\n\n    console.info('Dist files are copied to the ./build/ and ./extension/ directories');\n}\n\nasync function prepareManifest(v = 2) {\n    fs.copyFileSync(`./extension/manifest_v${v}.json`, `./extension/manifest.json`);\n    console.info(`Copied manifest_v${v} to manifest.json`);\n}\n\nasync function main() {\n    const command = process.argv[2];\n\n    switch (command) {\n        case 'copy':\n            return await copy();\n        case 'v2':\n            return prepareManifest(2);\n        case 'v3':\n            return prepareManifest(3);\n        default:\n            throw new Error('Unsupported command: ' + command);\n    }\n\n}\n\nvoid main();"
  },
  {
    "path": "GNU_GPL_v2",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  <signature of Ty Coon>, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.\n"
  },
  {
    "path": "LICENSE.md",
    "content": "The DjVu.js Library (everything that is in `library/src` directory) is subject\nto, and may be distributed under,\nthe [GNU General Public License, Version 2](GNU_GPL_v2).\n\nEverything else in this repository (including the DjVu.js Viewer and the browser\nextension) is distributed under the terms of [The Unlicense](THE_UNLICENSE).\n\nThe choice of GNU GPL v2 for the library is conditioned by the fact DjVu.js\ncontains some code fragments copied from DjVuLibre or Java DjVu libraries, which\nare distributed under this very license.\n\nThe DjVu.js Library isn't a port of DjVuLibre or Java DjVu, but the published\nspecification of the DjVu format refers to DjVuLibre as the de facto standard of\nthe DjVu format and advises to study its code. Also, some parts of DjVu codecs\naren't described in details and can be gotten only from the source code of\nDjVuLibre.\n\nHere I put all the copyright notices, which I found in the source files of both\nDjVuLibre and Java DjVu.\n\n```\nDjVuLibre-3.5\nCopyright (c) 2002  Leon Bottou and Yann Le Cun.\nCopyright (c) 2001  AT&T\n\nDjVu (r) Reference Library (v. 3.5)\nCopyright (c) 1999-2001 LizardTech, Inc. All Rights Reserved.\n\nJava DjVu (r) (v. 0.8)\nCopyright (c) 2004-2005 LizardTech, Inc.  All Rights Reserved.\n```"
  },
  {
    "path": "README.md",
    "content": "# DjVu.js\n\n## About / О проекте\n\n**DjVu.js** is a program library for working with `.djvu` online. It's written\nin JavaScript and can be run in a web browser without any connection with a\nserver. DjVu.js can be used for splitting (and concatenation) of `.djvu` files,\nrendering pages of a `.djvu` document, converting (and compressing) images\ninto `.djvu` documents and for analyzing of metadata of `.djvu` documents.\n\n**DjVu.js Viewer** is an app which uses DjVu.js to render DjVu\ndocuments. The app may be easily included into any html page. You can look at it\nand try it out on the official website (the link is below).\n\n**DjVu.js Viewer browser extension**. By and large, it's a copy of the viewer,\nbut also it allows opening links to `.djvu` files right in the browser without\ndownloading them explicitly. The links to the extension are below.\n\n<hr>\n\n**DjVu.js** - это программная библиотека написанная на JavaScript и\nпредназначенная для работы с файлами формата `.djvu` онлайн. DjVu.js\nориентирована на исполнение в браузере пользователя без связи с сервером.\nБиблиотека может быть использована для разделения (объединения) файлов `.djvu`,\nпреобразования картинок в документы `.djvu`, отрисовки страниц\nдокументов `.djvu`, а также для анализа мета данных и структуры `.djvu`\nдокументов.\n\n**DjVu.js Viewer** - приложение, которое можно легко встроить в любую\nhtml-страницу. Данное приложение служит для просмотра документов DjVu\nнепосредственно в браузере. Вы можете ознакомиться с ним по ссылке ниже.\n\n**Расширение для браузера DjVu.js Viewer**. По большей части это копия\nприложения DjVu.js Viewer, однако также расширение позволяет открывать ссылки\nна `.djvu` файлы прямо в браузере, не скачивая их явно. Ссылки на расширение\nдоступны ниже.\n\n## Translation (localization)\n\nIf you want to add a new translation to the viewer [read here](TRANSLATION.md)\nhow to do it.\n\n## Tools and supported browsers\n\nYou need to have Node.js 18+ (although older versions should work too)\nand npm 9+ installed to work with the project.\n\nThe viewer and the library are supposed to run in a browser. Technically,\nit should not be difficult to update the library so that it could be used\nin Node.js projects - the main code is pure JS and doesn't rely on\nbrowser specific APIs.\n\nCurrently, the following browsers are supported:\n\n```\nChrome >= 88\nFirefox >= 78\nSafari >= 14\nEdge >= 88\n```\n\nThe list above is conditioned by the [default Vite settings](https://vitejs.dev/guide/build.html#browser-compatibility)\nand the support of the [`:where` CSS pseudo class](https://caniuse.com/mdn-css_selectors_where).\n\n## How to build it\n\nClone the repo and run:\n\n```sh\nnpm run make\n```` \n\nin the root folder of the repository. The command will install all dependencies\nand create bundles of the library and viewer (the `build` folder should\nappear).\n\nThere is another variant:\n\n```sh\nnpm run remake\n```\n\nIt does the same as `make`, but first it removes all git-ignored files \n(including dependencies).\n\n## How to run it locally\n\nIf you want to work with the library you should read [the library's README](./library/README.md).\n\nAs for the viewer, you have to build the library once and start the dev server.\nIt can be achieved with the following commands:\n\n```sh\nnpm run make # run it only once\ncd viewer\nnpm start\n```\n\nA page with the viewer will open automatically.\n\n### Tests\n\nOnce the dev server has been started, you can run E2E tests via `test*` npm scripts that you can find in\nthe `viewer/package.json` file.\n\n## How to pack the extension\n\nAfter the project has been built (`npm run make`), the extension\nfolder contains all the necessary files. However, there are two manifests:\nv2 and v3. You should copy and rename one of them to `manifest.json`.\n\nAfter it's done, the folder is an unpacked extension - it can be\ninstalled in the \"developer mode\".\n\nIf you want to pack the extension, you can either zip it yourself\nor run:\n\n```sh\nnpm run ext # it uses \"npx web-ext\", so you will be asked to install the package\n```\n\nIt will pack the extension with both v2 and v3 manifests.\n\n## Links\n\n- The **official website** with the DjVu.js Viewer demo is https://djvu.js.org\n- You may **download the library** and the viewer\n  on https://djvu.js.org/downloads\n- The **browser extension**\n  for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/djvu-js-viewer/)\n- The **browser extension**\n  for [Google Chrome](https://chrome.google.com/webstore/detail/djvujs-viewer/bpnedgjmphmmdgecmklcopblfcbhpefm)\n- The **technical documentation** of the library is\n  available [in the wiki](https://github.com/RussCoder/djvujs/wiki/DjVu.js-Documentation)\n- [CHANGELOG of the library](library/CHANGELOG.md)\n- [CHANGELOG of the viewer](viewer/CHANGELOG.md)\n\n## License / Лицензия\n\nThe DjVu.js Library is distributed under the terms of [GNU GPL v2](GNU_GPL_v2).\nEverything else in this repository (including the DjVu.js Viewer and the browser\nextension) is under [The Unlicense](THE_UNLICENSE). Read more in\nthe [LICENSE file](LICENSE.md).\n\n<hr>\n\nБиблиотека DjVu.js распространяется под лицензией [GNU GPL v2](GNU_GPL_v2). Все\nостальное в этом репозитории (включая DjVu.js Viewer и расширение для браузера)\nявляется общественным достоянием ([The Unlicense](THE_UNLICENSE)). Читайте\nподробнее в [файле лицензии](LICENSE.md)."
  },
  {
    "path": "THE_UNLICENSE",
    "content": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, compile, sell, or\ndistribute this software, either in source code form or as a compiled\nbinary, for any purpose, commercial or non-commercial, and by any\nmeans.\n\nIn jurisdictions that recognize copyright laws, the author or authors\nof this software dedicate any and all copyright interest in the\nsoftware to the public domain. We make this dedication for the benefit\nof the public at large and to the detriment of our heirs and\nsuccessors. We intend this dedication to be an overt act of\nrelinquishment in perpetuity of all present and future rights to this\nsoftware under copyright law.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR\nOTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,\nARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\nFor more information, please refer to <http://unlicense.org>\n"
  },
  {
    "path": "TRANSLATION.md",
    "content": "# How to add a new translation to the viewer or improve an existing one\n\nIf you want to add one more translation to the viewer, \nyou need to fulfill the following steps:\n\n1. Copy [the Russian dictionary file](viewer/src/locales/Russian.js) and rename\n   it according to the name of your language in English. Put your file into the\n   same directory, where the Russian file is.\n\n2. Then change all the Russian translations of English phrases with your own.\n   You can look at\n   the  [the English dictionary file](viewer/src/locales/English.js), which\n   essentially does not translate anything. But it may serve you as an\n   additional example.\n\n3. Pay attention to **the topmost comments** in the Russian dictionary file.\n   Especially, read about placeholders which start with #, e.g. #helpButton.\n   Other comments throughout the file will help you to find where a phrase is\n   used in the app. Some phrases you will not see if you start the viewer\n   locally (not as in the extension). While others you can see only if you\n   remove some phrases from a dictionary\n   (namely notifications that the translation isn't complete), and start the\n   viewer locally (it's written in [README](README.md) how to do it). But you do\n   not need to find all phrases, you can translate some of them blindly.\n\n4. If you want to **improve an existing translation**, the notification window\n   should have told you what phrases are missing. So find where those phrases\n   are placed in the Russian dictionary and add them with translations to the\n   dictionary you want to improve. However, most probably untranslated phrases\n   have been already added to the file, but with `null` values as placeholders.\n   In this case, replace all `null` values with corresponding translations.\n   Also, if there are missing phrases, but they are not present in the file, you\n   can add them via the command `npm run syncLocales`. It should be run\n   inside `viewer` directory.\n\nYou do not need to connect the dictionary to the code, I will do it myself.\nHowever, if you want you can find where it's connected in the code and add it\nthere.\n**But in general you need only to create a dictionary and nothing more.**\n\nIt's better to create **a pull request on GitHub**, but if you do not know how\nto do it, and do not want to learn how to do it, you can just send the\ndictionary at djvujs@yandex.ru and I will add it to the project myself.\n"
  },
  {
    "path": "extension/background.js",
    "content": "/**\n * The execution starts in the main() function\n */\n\n'use strict';\n\nfunction isManifestV3() {\n    return chrome.runtime.getManifest().manifest_version === 3;\n}\n\nconst extensionUrl = chrome.runtime.getURL('viewer.html');\nconst httpRedirectRuleId = 1;\nconst fileRedirectRuleId = 2;\n\nfunction updateContextMenu() {\n    chrome.contextMenus.removeAll();\n    chrome.contextMenus.create({\n        id: 'open_with',\n        title: 'Open with DjVu.js Viewer',\n        contexts: ['link'],\n        targetUrlPatterns: [\n            '*://*/*.djvu',\n            '*://*/*.djv',\n            '*://*/*.djvu?*',\n            '*://*/*.djv?*',\n\n            '*://*/*.DJVU',\n            '*://*/*.DJV',\n            '*://*/*.DJVU?*',\n            '*://*/*.DJV?*',\n        ]\n    });\n    chrome.contextMenus.onClicked.addListener(info => {\n        if (info.menuItemId === 'open_with') {\n            openViewerTab(info.linkUrl);\n        }\n    });\n}\n\nfunction promisify(func) {\n    return function (...args) {\n        return new Promise(resolve => {\n            func(...args, resolve);\n        });\n    };\n}\n\nconst getViewerUrl = (djvuUrl = null, djvuName = null) => {\n    const params = new URLSearchParams();\n    djvuUrl && params.set('url', djvuUrl);\n    djvuName && params.set('name', djvuName);\n    const queryString = params.toString();\n    return extensionUrl + (queryString ? '?' + queryString : '');\n};\n\nconst executeScript = (src, sender) => {\n    if (isManifestV3()) {\n        return chrome.scripting.executeScript({\n            files: [src],\n            target: {\n                tabId: sender.tab.id,\n                frameIds: [sender.frameId],\n            }\n        });\n    }\n\n    return promisify(chrome.tabs.executeScript)(sender.tab.id, {\n        frameId: sender.frameId,\n        file: src,\n        runAt: 'document_end'\n    });\n}\n\nfunction listenForMessages() {\n    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {\n        if (sender.tab && message === 'include_scripts') {\n            Promise.all([\n                executeScript('dist/djvu.js', sender),\n                executeScript('dist/djvu_viewer.js', sender),\n            ]).then(() => {\n                sendResponse();\n            })\n            return true; // do not send response immediately\n        }\n\n        if (message.command === 'open_viewer_tab') {\n            openViewerTab(message.url);\n        }\n\n        sendResponse();\n    });\n}\n\nfunction openViewerTab(djvuUrl = null) {\n    chrome.tabs.create({ url: getViewerUrl(djvuUrl) });\n}\n\nfunction enableFileOpeningInterception() {\n    if (isManifestV3()) {\n        chrome.declarativeNetRequest.updateDynamicRules({\n            removeRuleIds: [fileRedirectRuleId],\n            addRules: [{\n                id: fileRedirectRuleId,\n                action: {\n                    type: 'redirect',\n                    redirect: { regexSubstitution: `${extensionUrl}?url=\\\\0` },\n                },\n                condition: {\n                    isUrlFilterCaseSensitive: false,\n                    regexFilter: '^file:///.+\\\\.djvu?$',\n                    resourceTypes: ['main_frame'],\n                },\n            }],\n        });\n    } else {\n        chrome.webRequest.onBeforeRequest.addListener(details => {\n                return { redirectUrl: getViewerUrl(details.url) };\n            }, {\n                urls: [\n                    'file:///*/*.djvu',\n                    'file:///*/*.djvu?*',\n                    'file:///*/*.djv',\n                    'file:///*/*.djv?*',\n\n                    'file:///*/*.DJVU',\n                    'file:///*/*.DJVU?*',\n                    'file:///*/*.DJV',\n                    'file:///*/*.DJV?*',\n                ],\n                types: ['main_frame']\n            },\n            ['blocking']\n        );\n    }\n}\n\n// it shouldn't be the same function as the file opening interceptor,\n// since this event listener can be removed independently of the file opening interceptor\nconst requestInterceptor = details => {\n    // http://*/*.djvu also corresponds to \"http://localhost/page.php?file=doc.djvu\"\n    // so we have to add this additional check, because it's not a link to a file.\n    if (/\\.djvu?$/i.test(new URL(details.url).pathname)) {\n        return { redirectUrl: getViewerUrl(details.url) };\n    }\n}\n\n// it's \"undefined\" for manifest v3, because the \"webRequest\" permission isn't requested\nconst onBeforeRequest = chrome.webRequest?.onBeforeRequest;\nconst onHeadersReceived = chrome.webRequest?.onHeadersReceived;\n\n// Detect djvu only by URL\nconst enableHttpIntercepting = () => {\n    if (isManifestV3()) {\n        chrome.declarativeNetRequest.updateDynamicRules({\n            removeRuleIds: [httpRedirectRuleId],\n            addRules: [{\n                id: httpRedirectRuleId,\n                action: {\n                    type: 'redirect',\n                    redirect: { regexSubstitution: `${extensionUrl}?url=\\\\0` },\n                },\n                condition: {\n                    isUrlFilterCaseSensitive: false,\n                    regexFilter: '^https?://[^?]+\\\\.djvu?(\\\\?.*)?',\n                    resourceTypes: ['main_frame', 'sub_frame'],\n                },\n            }],\n        });\n    } else {\n        !onBeforeRequest.hasListener(requestInterceptor) && onBeforeRequest.addListener(requestInterceptor, {\n                urls: [\n                    'http://*/*.djvu',\n                    'http://*/*.djvu?*',\n                    'https://*/*.djvu',\n                    'https://*/*.djvu?*',\n                    'http://*/*.djv',\n                    'http://*/*.djv?*',\n                    'https://*/*.djv',\n                    'https://*/*.djv?*',\n\n                    'http://*/*.DJVU',\n                    'http://*/*.DJVU?*',\n                    'https://*/*.DJVU',\n                    'https://*/*.DJVU?*',\n                    'http://*/*.DJV',\n                    'http://*/*.DJV?*',\n                    'https://*/*.DJV',\n                    'https://*/*.DJV?*',\n                ],\n                types: ['main_frame', 'sub_frame'],\n            },\n            ['blocking']\n        );\n    }\n};\n\nconst disableHttpIntercepting = () => {\n    if (isManifestV3()) {\n        chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: [httpRedirectRuleId] });\n    } else {\n        onBeforeRequest.hasListener(requestInterceptor) && onBeforeRequest.removeListener(requestInterceptor);\n    }\n};\n\nconst headersAnalyzer = details => {\n    const getFileName = () => {\n        const contentDisposition = details.responseHeaders.find(item => item.name.toLowerCase() === 'content-disposition');\n        if (contentDisposition) {\n            // In fact, there may be also filename*= in the header, so perhaps, it will be needed for someone in the future\n            const matches = /(?:attachment|inline);\\s+filename=\"(.+\\.djvu?)\"/.exec(contentDisposition.value);\n            return matches && matches[1];\n        }\n    };\n\n    const contentType = details.responseHeaders.find(item => item.name.toLowerCase() === 'content-type');\n    if (contentType) {\n        if (contentType.value === 'image/vnd.djvu' || contentType.value === 'image/x.djvu') {\n            // analyse Content-Disposition only if there is no filename in the URL\n            return { redirectUrl: getViewerUrl(details.url, /\\.djvu?(?:\\?.*)?$/.test(details.url) ? null : getFileName()) };\n        } else if (contentType.value === 'application/octet-stream') {\n            const fileName = getFileName();\n            if (fileName) {\n                return { redirectUrl: getViewerUrl(details.url, fileName) };\n            }\n        }\n    }\n};\n\nconst enableHeadersAnalysis = () => {\n    !onHeadersReceived.hasListener(headersAnalyzer) && onHeadersReceived.addListener(headersAnalyzer, {\n        urls: [\n            'http://*/*',\n            'https://*/*',\n        ],\n        types: ['main_frame', 'sub_frame'],\n    }, ['blocking', 'responseHeaders']);\n};\n\nconst disableHeadersAnalysis = () => {\n    onHeadersReceived.hasListener(headersAnalyzer) && onHeadersReceived.removeListener(headersAnalyzer)\n};\n\nconst defaultOptions = Object.freeze({\n    // here we duplicated only the options, which are used by the extension code\n    interceptHttpRequests: true,\n    analyzeHeaders: false,\n});\n\nconst onOptionsChanged = json => {\n    let parsedOptions = {};\n    try {\n        parsedOptions = json ? JSON.parse(json) : {};\n    } catch (e) {\n        console.error('DjVu.js Extension: cannot parse options json from the storage. The json: \\n', json);\n        console.error(e);\n    }\n\n    try {\n        const options = { ...defaultOptions, ...parsedOptions };\n        if (options.interceptHttpRequests) {\n            enableHttpIntercepting();\n        } else {\n            disableHttpIntercepting();\n        }\n\n        if (isManifestV3()) return;\n\n        if (options.interceptHttpRequests && options.analyzeHeaders) {\n            enableHeadersAnalysis();\n        } else {\n            disableHeadersAnalysis();\n        }\n    } catch (e) {\n        console.error('DjVu.js Extension: some options might not have been applied due to an error.');\n        console.error(e);\n    }\n};\n\nfunction applySavedOptions() {\n    chrome.storage.local.get('djvu_js_options', options => onOptionsChanged(options['djvu_js_options']));\n}\n\nfunction listenForOptionChanges() {\n    chrome.storage.onChanged.addListener((changes, area) => {\n        if (area === 'local' && changes['djvu_js_options']) {\n            if (changes['djvu_js_options'].newValue) {\n                onOptionsChanged(changes['djvu_js_options'].newValue);\n            }\n        }\n    });\n}\n\nfunction main() {\n    // For manifest v3 onInstalled and onStartup events could be used to update the context menu\n    // and to register the file opening interception rules, but it seems to work well\n    // this way - it's updated every time the service worker is started.\n    updateContextMenu();\n    enableFileOpeningInterception();\n    chrome[isManifestV3() ? 'action' : 'browserAction'].onClicked.addListener(() => openViewerTab());\n    listenForMessages();\n    listenForOptionChanges();\n    applySavedOptions();\n}\n\nmain();\n"
  },
  {
    "path": "extension/content.js",
    "content": "(function () {\n    'use strict';\n\n    var includeScriptsPromise = null;\n    function includeScripts() {\n        return includeScriptsPromise || (includeScriptsPromise = new Promise(resolve => {\n            chrome.runtime.sendMessage(\"include_scripts\", resolve);\n        }));\n    }\n\n    function processTag(tag, src) {\n        function isJustNumber(value) {\n            return Number(value).toString() === String(value).trim();\n        }\n\n        var div = document.createElement('div');\n        div.style.minWidth = '600px'; // to fit the toolbar\n        div.style.minHeight = '200px';\n\n        if (tag.height) { // deliberately use attribute, not styles\n            div.style.height = isJustNumber(tag.height) ? Number(tag.height) + \"px\" : tag.height;\n        } else {\n            div.style.height = '90vh';\n            div.style.maxHeight = '90%';\n        }\n        if (tag.width) {\n            div.style.width = isJustNumber(tag.width) ? Number(tag.width) + \"px\" : tag.width;\n        }\n\n        div.style.overflow = \"hidden\";\n        div.className = \"djvu_js_viewer_container\";\n        tag.parentNode.replaceChild(div, tag);\n\n        var viewer = new DjVu.Viewer();\n        viewer.loadDocumentByUrl(src);\n        viewer.render(div);\n    }\n\n    const objects = document.querySelectorAll(\n        'object[classid=\"clsid:0e8d0700-75df-11d3-8b4a-0008c7450c4a\"]'\n        + ', object[type=\"image/x.djvu\"]'\n    );\n    if (objects.length) {\n        includeScripts().then(() => {\n            objects.forEach(object => {\n                var srcParam = object.querySelector('param[name=\"src\"]');\n                if (srcParam && srcParam.value) {\n                    processTag(object, srcParam.value);\n                }\n            });\n            processEmbeds();\n        })\n    } else {\n        processEmbeds();\n    }\n\n    function processEmbeds() { // should be processed after objects, since embeds may be nested in objects as a fallback\n        const embeds = document.querySelectorAll('embed[type=\"image/x-djvu\"], embed[type=\"image/vnd.djvu\"]');\n        if (embeds.length) {\n            includeScripts().then(() => {\n                embeds.forEach(embed => {\n                    processTag(embed, embed.src);\n                });\n            });\n        }\n    }\n})();\n"
  },
  {
    "path": "extension/initializer.js",
    "content": "'use strict';\n\nwindow.onload = () => {\n    const viewer = new DjVu.Viewer({\n        uiOptions: {\n            hideFullPageSwitch: true,\n        }\n    });\n    viewer.render(document.getElementById('root'));\n\n    viewer.on(DjVu.Viewer.Events.DOCUMENT_CHANGED, () => {\n        document.title = viewer.getDocumentName();\n    });\n\n    viewer.on(DjVu.Viewer.Events.DOCUMENT_CLOSED, () => {\n        document.title = 'DjVu.js Viewer';\n    });\n\n    const params = new URLSearchParams(location.search.slice(1));\n    if (params.get('url')) {\n        viewer.loadDocumentByUrl(params.get('url'), params.get('name') ? { name: params.get('name') } : undefined);\n    }\n};"
  },
  {
    "path": "extension/manifest_v2.json",
    "content": "{\n    \"manifest_version\": 2,\n    \"name\": \"DjVu.js Viewer\",\n    \"short_name\": \"DV\",\n    \"version\": \"0.10.1.0\",\n    \"author\": \"RussCoder\",\n    \"homepage_url\": \"https://github.com/RussCoder/djvujs\",\n    \"description\": \"Opens links to .djvu files. Allows opening files from a local disk. Processes <object> & <embed> tags.\",\n    \"background\": {\n        \"scripts\": [\n            \"background.js\"\n        ]\n    },\n    \"content_security_policy\": \"script-src 'self'; object-src 'self';\",\n    \"content_scripts\": [\n        {\n            \"matches\": [\n                \"*://*/*\"\n            ],\n            \"js\": [\n                \"content.js\"\n            ],\n            \"all_frames\": true,\n            \"run_at\": \"document_end\"\n        }\n    ],\n    \"permissions\": [\n        \"storage\",\n        \"webRequest\",\n        \"webRequestBlocking\",\n        \"<all_urls>\",\n        \"contextMenus\"\n    ],\n    \"web_accessible_resources\": [\n        \"viewer.html\"\n    ],\n    \"icons\": {\n        \"16\": \"djvu16.png\",\n        \"32\": \"djvu32.png\",\n        \"48\": \"djvu48.png\",\n        \"64\": \"djvu64.png\",\n        \"96\": \"djvu96.png\"\n    },\n    \"browser_action\": {\n        \"default_icon\": {\n            \"16\": \"djvu16.png\",\n            \"32\": \"djvu32.png\",\n            \"48\": \"djvu48.png\",\n            \"64\": \"djvu64.png\",\n            \"96\": \"djvu96.png\"\n        }\n    }\n}"
  },
  {
    "path": "extension/manifest_v3.json",
    "content": "{\n    \"manifest_version\": 3,\n    \"name\": \"DjVu.js Viewer\",\n    \"short_name\": \"DV\",\n    \"version\": \"0.10.1.0\",\n    \"author\": \"RussCoder\",\n    \"homepage_url\": \"https://github.com/RussCoder/djvujs\",\n    \"description\": \"Opens links to .djvu files. Allows opening files from a local disk. Processes <object> & <embed> tags.\",\n    \"background\": {\n        \"service_worker\": \"background.js\"\n    },\n    \"content_security_policy\": {\n        \"extension_pages\": \"script-src 'self'; object-src 'self';\"\n    },\n    \"content_scripts\": [\n        {\n            \"matches\": [\n                \"*://*/*\"\n            ],\n            \"js\": [\n                \"content.js\"\n            ],\n            \"all_frames\": true,\n            \"run_at\": \"document_end\"\n        }\n    ],\n    \"permissions\": [\n        \"storage\",\n        \"declarativeNetRequest\",\n        \"scripting\",\n        \"contextMenus\"\n    ],\n    \"host_permissions\": [\n        \"<all_urls>\"\n    ],\n    \"web_accessible_resources\": [\n        {\n            \"resources\": [\"viewer.html\"],\n            \"matches\": [\"<all_urls>\"]\n        }\n    ],\n    \"icons\": {\n        \"16\": \"djvu16.png\",\n        \"32\": \"djvu32.png\",\n        \"48\": \"djvu48.png\",\n        \"64\": \"djvu64.png\",\n        \"96\": \"djvu96.png\"\n    },\n    \"action\": {\n        \"default_icon\": {\n            \"16\": \"djvu16.png\",\n            \"32\": \"djvu32.png\",\n            \"48\": \"djvu48.png\",\n            \"64\": \"djvu64.png\",\n            \"96\": \"djvu96.png\"\n        }\n    }\n}"
  },
  {
    "path": "extension/viewer.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <link rel=\"shortcut icon\" href=\"djvu32.png\">\n    <title>DjVu.js Viewer</title>\n    <input type=\"hidden\" id=\"djvu_js_extension_main_page\">\n    <style>\n        html, body, #root {\n            height: 100% !important;\n            margin: 0;\n            padding: 0;\n            font-family: sans-serif;\n        }\n    </style>\n    <script id=\"djvu_js_lib\" src=\"dist/djvu.js\"></script>\n    <script src=\"dist/djvu_viewer.js\"></script>\n    <script src=\"initializer.js\"></script>\n</head>\n\n<body>\n    <noscript>\n        You need to enable JavaScript to run this app.\n    </noscript>\n    <div id=\"root\"></div>\n</body>\n\n</html>"
  },
  {
    "path": "library/.gitignore",
    "content": ".idea/\nnode_modules/\n/samples/\n.vscode/\naccess.log\nerror.log\nfavicon.ico\njsconfig.json"
  },
  {
    "path": "library/API.md",
    "content": "# DjVu.js Library API\n\n> The library is supposed to work in the browser. Theoretically, it should work\n> in Node.js too (with some limitations), but I have never did it, and the\n> current bundle is just an IIFE. \n\nThe whole API is available in two forms - synchronous, when all operations are\nrun in the main thread, and the asynchronous, when all operations are run in the\nWeb Worker. The last one is preferred in case of browsers, because it may take\nup to several seconds to render a page, and no one wants to freeze the UI for\nsuch a long time.\n\nHowever, the async API is mostly a wrapper around the sync one, so all methods are\ndescribed in their sync version.\n\nThe library adds one object to the global scope - `DjVu`. \n\n## Synchronous API \n\nThe sync API is represented via the `DjVu.Document` constructor:\n\n```js\nDjVu.Document(arrayBuffer, { baseUrl = null, memoryLimit = MEMORY_LIMIT } = {})\n```\n\nArguments:\n- `arrayBuffer` is an `ArrayBuffer` object representing the file. \n- `baseUrl` is the URL to the folder where the indirect djvu is stored. It's\n  required only in case of indirect djvu documents (the documents where each\n  page is a separate file) to construct an absolute URL to the pages (cause\n  inside the document all references are relative).\n- `memoryLimit` - shouldn't be provided at all in most cases. The default value\n  is 50 MB. This value is the upper border of the memory used to store pages of\n  an indirect djvu. If the total size of downloaded pages exceeds this limit,\n  the library removes some of them before downloading new pages. \n\nAnd example for a bundled (one file) djvu document:\n\n```js\nconst bundledDjVuArrayBuffer = await fetch('/bundled.djvu').then(r => r.arrayBuffer());\nconst doc = new DjVu.Document(bundledDjVuArrayBuffer);\n```\n\nAnd example for an indirect (multi-file) djvu document:\n\n```js\nconst indexFileBuffer = await fetch('/some_indirect_djvu/index.djvu').then(r => r.arrayBuffer());\nconst doc = new DjVu.Document(indexFileBuffer, { baseUrl: '/some_indirect_djvu' });\n```\n\nThe constructor creates a `DjVuDocument` instance which has the following methods:\n\n- `getPagesSizes(): Array<{width: number, height: number, dpi: number}>` -\n  returns an array of pages sizes. Needed for the continuous scroll view mode in\n  the viewer to determine the total height of the view area and of each page.\n- `isBundled(): boolean` - returns `true` if the document is bundled (one-file).\n  `false` if it's indirect (multi-file).\n- `getPagesQuantity(): number` - returns the total number of pages in the\n  document.\n- `getContents(): Array<Bookmark>`, where `Bookmark` is   \n  `{description: string, url: string, children?: Array<Bookmark>}` - returns the\n  table of contents, if it exists in the document.\n- `getMemoryUsage(): number` - returns the amount of the memory used to store\n   parts of an indirect djvu document.\n- `getMemoryLimit(): number` - returns the current memory limit for an indirect\n  djvu.\n- `setMemoryLimit(limit = MEMORY_LIMIT): void` - sets the memory limit.\n- `getPageNumberByUrl(url: string): ?number` - returns the page number\n  corresponding to the `url` from a `Bookmark`, that is, from the table of\n  contents. If the page cannot be found, `null` is returned. \n- `async getPage(number: number): Promise<DjVuPage>` - this method is async,\n  cause it works both in case of a single-file djvu and an indirect one, and in\n  the latter case the page and its dependencies have to be downloaded first. It\n  accepts the page number starting from 1 (not from 0). What's more, this method\n  automatically reset the previously requested page (read about it in the\n  methods of `DjVuPage`), which allows you not to care about memory leaks.\n- `getPageUnsafe(number: number): DjVuPage` - in case of a bundled djvu, you can\n  get a page synchronously. But you will have to `page.reset()` manually after\n  you finished working with the page. Otherwise, you risk overusing memory.\n  Prefer `getPage()` to this method.\n- `createObjectURL(): string` - creates a url to download the file (it should be\n  revoked afterwards).\n- `slice(from = 1, to = this.getPagesQuantity()): DjVuDocument` - creates a\n  document from a subset of pages, including the first and the last page. Pages\n  are counted from 1. This method isn't production-ready. It may work\n  incorrectly in some cases, and it doesn't split the table of contents, but\n  copies it completely to the new document.\n- `async bundle(progressCallback: (progress: number) => void): Promise<DjVuDocument>`\n  \\- downloads and bundles an indirect djvu into one-file document. Accepts a\n  callback which is invoked with a number parameter which takes values from 0 to\n  1 and provides an ability to track the progress.\n- `toString(): string` - returns metadata describing the structure of the\n  document. Useful if you are familiar with the DjVu Specification.\n\nThe most important method is `async getPage(number)` which returns `DjVuPage`\nwith the following methods: \n\n- `getWidth(): number` - width in pixels.\n- `getHeight(): number` - height in pixels.\n- `getDpi(): number` - returns the dpi value. This value is required to\n  determine the \"100%\" scale factor. E.g. a usual monitor has 96 dots per inch\n  (let's say 100). If a document has 300 dpi (more precisely, it was scanned\n  with the resolution of 300 dpi), it means that its \"real size\" is 300 / 100 =\n  3 times smaller than its full size in pixels.\n- `getRotation(): 0 | 90 | 180 | 270` - the rotation of the page. It's needed\n  only to show it properly to the user.\n- `getImageData(rotate = true): ImageData` - returns `ImageData` object\n  representing the page. By default, it has been already rotated (if it's\n  required), and you do not need `getRotation()` at all.\n- `async createPngObjectUrl(): Promise<PngObjectData>` - creates a PNG image of\n  the page, and forms a URL via `URL.createObjectURL()`. It means that you have\n  to `URL.revokeObjectURL(url)` (or `worker.revokeObjectURL(url)` in case of the\n  async API) once you need it no longer, otherwise there will be memory leaks.\n  The `PngObjectData` has the following structure:\n\n  ```ts\n  {\n    url: string, // do not forget to revoke it\n    byteLength: number, // the size of the PNG image retained by the URL\n    width: number,\n    height: number,\n    dpi: number,\n  }\n  ```\n  This method uses `OffscreenCanvas`, but if it's not available (as in Firefox)\n  it uses `png.js` library as a fallback. `png.js` is the only dependency of the\n  library, and it takes more than 50% of the eventual bundle.\n\n  The method itself is very useful, because a djvu page can easily take 30 MB of\n  memory (and more) as a raw `ImageData` object (4 bytes per a pixel), while the\n  same image in the PNG format takes less than 0.5 MB. Also, images are much\n  better scaled via CSS than canvases. The continuous scroll mode would be\n  impossible without this method, because it would take too much memory to\n  render many pages on canvases.\n\n- `getText(): string` - returns the page's text as one string, if it exists on\n  the page.\n- `getNormalizedTextZones(): ?Array<TextZone>` - returns the array of text zones\n  to form a text layer above the page's image. The `TextZone` object is the\n  following:\n\n  ```ts\n  {\n    x: number,\n    y: number,\n    width: number,\n    height: number,\n    text: string,\n  }\n  ```\n\n  Its coordinates are relative to the page's top left corner, that is, all zones\n  should be absolutely positioned.\n- `toString(): string` - returns metadata describing the structure of the page.\n  Useful if you are familiar with the DjVu Specification.\n- `reset(): void` - resets the page's inner structures. During the decoding\n  phase, which is called lazily when different parts of the page's data are\n  requested, a lot of temporary structures are allocated. To release the memory,\n  you have to reset the page. Otherwise, it will retain a lot of memory for that\n  structures. Page objects are created in the constructor of the document, so\n  they are not garbage collected, until the document is removed. If you get\n  pages via `await doc.getPage(number)` method, you can do nothing since it\n  takes care to reset a page when the next one is requested.\n\n\n## Asynchronous API\n\nThe asynchronous API is represented via the `DjVu.Worker` constructor: \n\n```js\nnew DjVu.Worker(urlToTheLibrary = DEFAULT_VALUE);\n```\n\nIt may accept a URL to the DjVu.js Library, but in a normal case you do not have\nto provide it explicitly at all, since the library creates an ObjectURL from its\ncode automatically. This param can be required only if you run the code in some\nenvironment which prohibits to execute code from ObjectURL or data URIs, e.g. in\ncase of a browser extension. But in case of a usual web page it's not needed.\n\nSo the example is:\n\n```js\nconst worker = new DjVu.Worker();\n```\n\nThe `DjVuWorker` instance has the following methods and props: \n\n- `async createDocument(buffer: ArrayBuffer, options: Object): Promise` -\n  invokes the\n  `DjVu.Document` constructor in the Web Worker. Accepts the same parameters.\n  Note that `buffer` is transferred to the Web Worker, so it will be unavailable\n  after you call his method.\n- `async run(): Promise` - a special methods to execute a `DjVuWorkerTask`\n  object (or several). Read about the `doc` property to understand how to use\n  it.\n- `get doc: DjVuWorkerTask` - a read only property which is the heart of the\n  async API. It mimics the `DjVuDocument` object, but in fact it's\n  a `DjVuWorkerTask` (which is a  `Proxy`), and you can call any method on it,\n  and it always returns another `DjVuWorkerTask` (until you call the `run()`\n  method). It's better to look at the examples first:\n\n  ```js\n  const [text, textZones] = await worker.run(\n    worker.doc.getPage(pageNumber).getText(),\n    worker.doc.getPage(pageNumber).getNormalizedTextZones(),\n  );\n\n  const pagesSizes = await worker.doc.getPagesSizes().run();\n  ```\n\n  In the first example two tasks are run in one bunch, and the array of results\n  is returned (inside a `Promise` of course). In the second example only one\n  task is executed via a special method `run()` which is the same as to do:\n\n  ```js\n  const pagesSizes = await worker.run(worker.doc.getPagesSizes());\n  ```\n\n  Using this API you can call any chain of methods on the `DjVuDocument` inside\n  the Web Worker. However, you should remember, that you **cannot get complex\n  objects like `DjVuPage`** (but you still **can pass callbacks to the worker**,\n  e.g. in case of the `bundle()` method). You can only get the eventual results\n  like  `ArrayBuffer`'s, `ImageData`'s, strings, plain objects and numbers.\n  Also, despite the fact `DjVuDocument.getPage()` method is async, you can use\n  in as a sync one in the methods chain. The same takes place in case of any\n  other async methods.\n\n  The fact we cannot access the `DjVuPage` directly via the async API conditions\n  the current architecture, due to which we have to `reset()` pages manually in\n  case of the sync API - otherwise two tasks in one bunch would require to\n  decode the page twice, while now it's decoded lazily and only once.\n\n  In essence, when you call methods on a `DjVuWorkerTask` object it just pushes\n  the method's name and its arguments into an array, which is passed to the Web\n  Worker when you call the `run()` method. All those methods are applied to the\n  `DjVuDocument` instance one by one, and the eventual result is passed back.\n\n- `cancelTask(promise: Promise): void` - cancels the task. Accept the promise\n  returned by the `run()` method. \n- `emptyTaskQueue(): void` - cancels all tasks except the current one.\n- `dropCurrentTask(): void` - forgets about the current task (it cannot be\n  really stopped once it began to execute).\n- `cancelAllTasks(): void` - invokes two previous methods.\n\n  It's worth saying that if you initiate a lot of tasks via the `run()` method,\n  they are not passed to the Web Worker at once, so they can be just deleted\n  from the queue. But when a task has been sent, there is no way to stop it,\n  except for the Web Worker termination and recreation, but in this case you\n  will lose the `DjVuDocument` created inside. \n\n  Since the library doesn't work too fast, these \"cancel task\" methods are\n  useful in some cases, e.g. the DjVu.js Viewer renders the current page, the\n  previous and the next in case of the single page mode, and 15 pages back and\n  forward in case of the continuous scroll mode. If a user looks at the\n  current page, the viewer at the same time may be rendering other pages to\n  show them quickly when the user turns a page over. But if he just jumps in\n  100 pages, there is no use in those pages which were to be rendered, so\n  those tasks have to be cancelled and new tasks are to be initiated. The same\n  need to cancel a task occurs when the user quickly clicks on the next page\n  button.\n\n- `isTaskInQueue(promise: Promise): boolean` - checks whether the task is in the\n  queue.\n- `isTaskInProcess(promise: Promise): boolean` - checks whether the task has\n  been already started.\n- `revokeObjectURL(url: string): void` - formerly, if an ObjectURL had been\n  created inside a worker it could be revoked only inside this very worker. I\n  checked it by myself, but now it seems that this behavior has been fixed and\n  usual `URL.revokeObjectURL()` works too. But this method is still available if\n  you wish to revoke the URL inside the worker in which it was created.\n- `reset(): void` - recreates the worker.\n\n## Additional Notes\n\nBesides `DjVu.Worker` and `DjVu.Document`, there are also:\n\n- `DjVu.VERSION` - a string with the current version.\n- `DjVu.ErrorCodes` - an object with all possible error codes created by the\n  library. Perhaps, it's worth describing them more profoundly. But for now,\n  just print it to the console to see the codes. These error codes I use mostly\n  in tests, they are not something too important.\n\nIf you want more practical examples of the library usage, you can take a look at\n`viewer/src/sagas` files. There are real examples of the async API usage.\n\nIf you want to know more about the inner DjVu structure, it's worth reading the\n[DjVu Specification](./assets/DjVu3Spec.djvu?raw=true).\n\nIf something isn't intelligible enough, feel free to create an issue."
  },
  {
    "path": "library/CHANGELOG.md",
    "content": "# DjVu.js Library's Changelog\n\n## v.0.5.4 (01.02.2023)\n\n- Fix: error messages are sent from the worker again.\n\n## v.0.5.3 (18.02.2021)\n\n- Error handlers for unhandled promise rejections and errors in the worker.\n- Object URL to the whole library code is created only once to avoid memory\n  leaks.\n\n## v.0.5.2 (18.02.2021)\n\n- Use DIRM registry to get a page number by its id. This approach works the same\n  for bundled and indirect documents.\n\n## v.0.5.1 (09.01.2021)\n\n- More robust memory management for document creation (usage of\n  `WebAssembly.Memory` with its `grow()` method instead of manual `ArrayBuffer`\n  expansion).\n- `'use strict';` in the Web Worker (typo correction).\n- Returned to the old behaviour: wait for the completion of a forgotten task,\n  before sending the next to the Web Worker.\n\n## v.0.5.0 (06.12.2020)\n\n- Feature: bundle indirect djvu documents.\n- Removed old redundant DjVuWorker's methods duplicating \"doc\" proxy API.\n- Now callbacks can be passed to the DjVuWorker.\n- Minor improvements.\n\n## v.0.4.5 (18.11.2020)\n\n- Use standard TextDecoder API to handle ill-formed utf-8 arrays.\n\n## v.0.4.4 (28.10.2020)\n\n- Significant reduction of memory consumption in IWDecoder (LazyBlock).\n- Automatic reset of temporary IW structures after the decoding phase, if the image is big.\n\n## v.0.4.3 (30.07.2020)\n\n- Fixed a bug due to which an empty DJVI chunk caused an error.\n\n## v.0.4.2 (30.06.2020)\n\n- Fixed an error, which took place when there is no location.origin (when a web page is opened directly in a browser).\n\n## v.0.4.1 (22.04.2020)\n\n- Wrapped some loop's bodies into functions to avoid code deoptimizations in Chrome in some cases.\n\n## v.0.4.0 (18.05.2019)\n\n- png.js was integrated into djvu.js to create png files (and Object URLs to them) of the pages inside a worker. \nIt's required for the continuous scroll mode, since a png file is much less than a raw ImageData object.\n\n## v.0.3.5 (03.04.2019)\n\n- Fixed a bug having taken place when there were more than 1 block in bzz encoded data.\n\n## v.0.3.4 (30.03.2019)\n\n- Now XHR is used instead of fetch(), since the latter can't load local files (i.e. file:/// urls).\n\n## v.0.3.3 (02.03.2019)\n\n- Fixed a bug. Now empty edges are removed for all symbols added to the dict.\n\n## v.0.3.2 (11.02.2019)\n\n- Fixed a bug when baseline (y coord) was computed incorrectly.\n\n## v.0.3.1 (15.11.2018)\n\n- New method for getting quantity of pages.\n- Correct processing of page urls with leading zeros (like \"#002\").\n\n## v.0.3.0 (12.10.2018)\n\n- The support of indirect djvu files.\n- Bug fixes.\n\n## v.0.2.2 (14.09.2018)\n\n- Rotation flags are processed now. A image of page is rotated by default if required.\n\n## v.0.2.1 (20.08.2018)\n\n- Empty pages are processed correctly.\n\n## v.0.2.0 (16.06.2018)\n\n- DjVuWorker is created from the Data URL which is generated automatically, so there is no need in explicit script URL. \n- Additional method run() for the DjVuWorkerTask (the proxy object which is return by the \"doc\" property of the worker).\n- Utils.loadFile() is deprecated now.\n- The whole script is available through the DjVu.DjVuScript() method, which is added as a wrapper in the build process. \n\n## v.0.1.9 (25.05.2018)\n\n- TXT* chunks are decoded completely - text zones are decoded.\n- Normalized text zones for the text layer of page. \n\n## v.0.1.8 (15.05.2018)\n\n- New universal Proxy-based DjVuWorker API, allowing to automatically use most of methods of DjVuDocument.\n\n## v.0.1.7 (19.04.2018)\n\n- UTF-8 ids of pages and dictionaries are supported.\n\n## v.0.1.6 (15.04.2018)\n\n- JB2 codec performance optimizations (more efficient memory access)\n\n## v.0.1.5 (05.04.2018)\n\n- Old files with INFO chunks less than 10 bytes are supported.\n- A specific error for corrupted files. \n\n## v.0.1.4 (27.03.2018)\n\n- A table of contents can be gotten.\n- A page number may be gotten by a url. \n\n## v.0.1.3 (25.03.2018)\n\n- All worker tasks-promises can be cancelled now.\n- A task is posted to the worker only after the previous one is fulfilled.\n\n## v.0.1.2 (24.03.2018)\n\n- Unified style of DjVuErrors, which are errors that are thrown manually, when a file is corrupted, there is no requested page and so on. \n- DjVuErrors are rather simple objects that may be copied between workers. \n\n## v.0.1.1 (19.03.2018)\n\n- UTF-8 strings are decoded correctly now.\n\n## v.0.1.0 (14.03.2018)\n\n- IW44, BZZ and ZP codecs are fully implemented with some constraints in case of BZZ codec.\n- JB2 codec is implemented only for decoding.\n- BGjp, FGjp, Smmr are not supported at all.\n- ANTa, ANTz, NAVM, FORM:THUM and TH44 are not supported, but there are dummies for them, so they are processed somehow.\n- Support of TXTz and TXTa is implemented partly, only pure text is decoded.\n- 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)."
  },
  {
    "path": "library/README.md",
    "content": "# DjVu.js Library\n\nThis file contains some information about the inner structure of the project and about how to use the library.\nIt may be useful for you, if you want to play with code or contribute to the project.\n\nIt's implied that you have run `npm install` and all dependencies are installed correctly.\n\n## Documentation\n\nIf you are interested only in the library's API [read it here](./API.md).\n\n## How to use it\n\nBesides the API docs, there is a good example script with many comments, which can give you a rather good understanding\nhow to use the library.\nIt's located at `library/debug/js/examples.js`.\nTo run it, you should clone the repository and in the root directory do the following:\n\n```\ncd library\nnpm install\nnpm start\n```\n\nAfter the debug server is run (usually on 9000 port) access `http://localhost:9000/examples.html` to see the results,\nand then read the code\nand the comments to understand how it works. You can edit the code (and the page will reload automatically).\n\nAlso, you can read source code of `DjVuDocument.js`, `DjVuPage.js` and `DjVuWorker.js` in the `src` directory to know\nmore about the API.\n\nIf you have more questions, feel free to create an issue.\n\n## The structure\n\nThere are the following directories:\n\n- `app` - contains an old application, which is poorly maintained now. It can split a djvu file, convert images to a\n  document, and show metadata of a document.\n- `assets` - contains test .djvu files and images. They are used in the automatic tests.\n- `debug` - contains css and js files for debugging, which are not the part of the source code of the library.\n- `dist` - a directory where the final bundle file is saved to (the eventual `djvu.js` file).\n- `src` - a main directory, containing the source code of the library. Its inner structure is self-descriptive, at least\n  I think so.\n- `tests` - a directory containing tests, which are run in a browser.\n\nThere are the following npm commands that may be run:\n\n- `start` - starts a local static server and runs a rollup watch command, which build the library and rebuild it on each\n  change. Also, on each change of the bundle or a file from `js` folder, the server sends a message to a client script\n  to reload the page.\n- `watch` - just runs a rollup watch method, which builds the library and rebuilds it on each change.\n- `build` - just builds the library once.\n\nSo if you don't know what to start with, run `npm start` and head to `http://localhost:9000/` - you will see the old\napp.  \n`http://localhost:9000/sync.html` - is a debug page, which I use most often.\n\nIf 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\ndone in case of `/tests/tests.html` and other pages, and your page will be reloaded on each change of the library source\ncode.\n\n## Tests\n\nThere are some automatic tests. In order to run them you should run `npm start` and then\nopen `http://localhost:9000/tests`.\n\nThe tests are run automatically when the page loads. If everything is ok, you will see that all messages are green. If\nyou see a green message with an orange message, it means that a test has passed, but your browser differs from mine. The\nthing is that different browsers differently render `.png` files, which are used for tests. So I use Opera, and all\ntests pass well in my case. In case of other browsers there may be some problems. So, if you use Mozilla, you should\nwrite `about:config` in the address line and then find the parameter `gfx.color_management.mode` and set it to `0`.\nAfter it, all tests should be green, except for one, which has an orange message as well.\n\nWhen you change the source code of the library, you may open the tests page (which reloads automatically) and check\nwhether your changes break the current functionality or not.\n\n## Build process\n\nThe library is built with Rollup. I chose it rather than Webpack, since Rollup creates a very simple and light bundle (\neventually it just copies all classes in one file in right order and wrap them with an anonymous function).\n\nAll files are es6 modules with corresponding import and export statements. So when you create a new file, it's\nautomatically added to the bundle (if you import something from it to the other files). "
  },
  {
    "path": "library/app/app.css",
    "content": "\n.func_menu_block {\n    display: flex;\n    justify-content: space-between;\n    flex-flow: row wrap;\n    color: gray;\n    max-width: 50em;\n    margin: 1em auto;\n    box-shadow: 0 0 1px gray;\n    padding: 1em;\n    overflow: hidden;\n}\n\n.wrapper {\n    color: gray;\n    max-width: 50em;\n    margin: 1em auto;\n    box-shadow: 0 0 1px gray;\n    padding: 1em;\n    overflow: hidden;\n}\n\n.djvu_version {\n    position: relative;\n    color: gray;\n    text-shadow: 0 0 1px lightgray;\n    top: 0;\n    left: 0;\n}\n\n.additionalBlock {\n    height: 5em;\n    text-align: center;\n}\n\n.funcelem, .disabledfunc {\n    font-size: 1.1em;\n    font-weight: bold; \n    flex: 0 0 auto;\n    box-sizing: content-box;\n    width: 10em;\n    height: 2em;\n    box-shadow: 0 0 1px gray;\n    text-align: center;   \n    color: gray;\n    margin: 1em;\n    padding: 1em;\n}\n\n.disabledfunc {\n    text-shadow: 0 0 1px gray;\n    background: #eee;\n}\n\n.funcelem:hover {\n    cursor: pointer;\n    background: #eee;\n}\n\n.inputext {\n    display: inline-block;\n    margin: 5px;\n    width: 150px;\n}\n\n#finput {\n    margin: 10px;\n}\n\n.filehref, #filehref {\n    text-decoration: none;\n    border: 1px solid orangered;\n    padding: 3px;\n    margin: 3px;\n    border-radius: 5px;\n    color: orangered;\n    display: none;\n}\n\n.filehref:hover, #filehref:hover {\n    text-shadow: 0 0 1px orangered;\n}\n\n#sliceblock, .funcblock {\n    display: none;\n}\n\n#warnmess {\n    color: orangered;\n    font-weight: bold;\n}\n#procmess {\n    color: blue;\n}\n\n#metaDataBlock #metadata {\n    color: black;\n    border: dotted 1px black;\n    padding: 5px;\n}\n\n.activebut {\n    cursor: pointer;\n    background: white;\n    text-decoration: none;\n    text-align: center;\n    border: 1px solid #0f0f0f;\n    padding: 0.3em;\n    margin: 0.1em;\n    border-radius: 5px;\n    color: #0f0f0f;\n    outline: none;\n\n}\n\n.activebut:hover {\n    box-shadow: 0 0 1px gray;\n}\n\n.activebut:active {\n    box-shadow: 0 0 1px gray inset;\n}\n\n.activebut:disabled {\n    cursor: not-allowed;\n    opacity: 0.5;\n    box-shadow: none;\n}\n\n#backbutton {\n    display: none;\n}\n\n.djvu_viewer {\n    overflow: hidden;\n    position: relative;\n    box-shadow: 0 0 4px gray;\n    margin: 1em auto;\n    padding: 1em;\n}\n\n.djvu_viewer .controls {\n    display: block;\n    position: absolute;\n    bottom: 0px;\n    margin: 1em;\n    width: 100%;\n    height: 5%;\n    text-align: center;\n}\n\n.djvu_viewer .controls .scale_label {\n    display: inline-block;\n    min-width: 3em;\n}\n\n.djvu_viewer .scale {\n    display: inline-block;\n    width: 10em;\n}\n\n.image_wrapper {\n    overflow: auto;\n    height: 95%;\n    text-align: center;\n}\n\n.djvu_viewer .image {\n    margin: 0.5em;\n    box-shadow: 0 0 1px lightgray;\n}\n\n.djvu_viewer .page_number {\n    width: 5em;\n}"
  },
  {
    "path": "library/app/app.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n    <link rel=\"shortcut icon\" href=\"/catfav.png\" type=\"image/x-icon\" />\n    <title>DjVu.js | Работа с DjVu файлами онлайн</title>\n    <script type=\"text/javascript\" src=\"jquery-3.2.1.min.js\"></script>\n    <script type=\"text/javascript\" src=\"../dist/djvu.js\"></script>\n    <script type=\"text/javascript\" src=\"app.js\" defer></script>\n    <link rel=\"stylesheet\" href=\"app.css\">\n</head>\n\n<body>\n    <div id=\"djvu_app\" class=\"djvu_app\">\n        <div class=\"additionalBlock\">\n            <input id=\"backbutton\" type=\"button\" class=\"activebut\" value=\"Назад к выбору функций\">\n        </div>\n        <div class=\"func_menu_block\" id=\"funcmenublock\">\n            <div id=\"slicefunc\" class=\"funcelem\" title=\"Разделить DjVu документ онлайн\">\n                Разделить DjVu\n            </div>\n            <div id=\"picturefunc\" class=\"funcelem\" title=\"Создать DjVu из картинок онлайн\">\n                Картинки в DjVu\n            </div>        \n            <div id=\"metadatafunc\" class=\"funcelem\">\n                Метаданные DjVu\n            </div>\n        </div>\n        <div id=\"funcblock\" class=\"wrapper funcblock\">\n            <input name=\"finput\" type=\"file\" class=\"activebut\" id=\"finput\">\n            <br>\n            <p id=\"warnmess\"></p>\n\n            <div class=\"funcblock\" id=\"sliceblock\">\n                <p class=\"describing\">\n                    Выберите djvu документ. Введите номер первой и последней страницы, которые Вы хотите поместить в новый документ.\n                </p>\n                <p class=\"info\"></p>\n                <p class=\"message\"></p>\n                <span class=\"inputext\">Номер первой страницы</span>\n                <input type=\"number\" id=\"firstnum\"><br>\n                <span class=\"inputext\">Номер последней страницы</span>\n                <input type=\"number\" id=\"secondnum\"><br>\n                <input type=\"button\" class=\"activebut\" value=\"Разделить файл\" id=\"slicebut\" disabled>\n            </div>\n\n            <div class=\"funcblock\" id=\"pictureblock\">\n                <p class=\"describing\">\n                    Выберите одну или несколько картинок для создания djvu документа. Можно настраивать качество изображение (влияет на размер\n                    файла).\n                </p>\n                <p class=\"info\"></p>\n                <p>Выбетите качество кодирования. При хорошем качестве изображение в djvu весит примерно вдвое меньше, чем в\n                    формате jpeg. Другие варианты, еще более экономичны.</p>\n                <form>\n                    <input name=\"imagequality\" type=\"radio\" value=\"100\">Хорошее\n                    <input name=\"imagequality\" type=\"radio\" value=\"90\" checked>Среднее\n                    <input name=\"imagequality\" type=\"radio\" value=\"80\">Плохое\n                </form><br><br>\n                <input type=\"checkbox\" id=\"grayscale\" value=\"1\">Серое изображение (отбросить цвета при кодировании)\n                <br><br>\n                <input type=\"button\" class=\"activebut\" value=\"Создать документ\" id=\"picturebut\" disabled>\n            </div>\n\n            <div class=\"funcblock\" id=\"metaDataBlock\">\n                <p class=\"describing\">\n                    Выберите djvu документ. Метаданные представляют структуру djvu файла. Каждая единица (порция) данных или Data Chunk расположены\n                    в том порядке, в котором они встречаются в файле. Некоторые порции описаны подробно в соответствии с\n                    их назначение и устройством, другие же характеризуются лишь заголовком и длиной, так как библиотека способна\n                    читать не все порции данных, или же не представляется возможным вывести информацию в текстовом виде кратко.\n                    Перед каждой страницей или словарем выводится id машинного оглавления.\n                </p>\n                <p class=\"info\"></p>\n                <p id=\"metadata\"></p>\n            </div>\n            <br>\n            <p id=\"procmess\"></p>\n            <a href=\"\" id=\"filehref\" download=\"djvujs_file.djvu\"> Сохранить файл </a>\n        </div>\n\n    </div>\n\n</body>\n\n</html>"
  },
  {
    "path": "library/app/app.js",
    "content": "'use strict';\n\nvar djvuWorker = new DjVu.Worker();\n\nfunction initDjVuApplication() {\n    $('#backbutton').click(reset);\n    $('.funcelem').on('click', () => {\n        $('#backbutton').show(400);\n    });\n    $('#slicefunc').click(sliceFuncPrepare);\n    $('#picturefunc').click(pictureFuncPrepare);\n    $('#metadatafunc').click(metaDataFuncPrepare);\n\n    if (DjVu.VERSION) {\n        $('#djvu_app').prepend('<div class=\"djvu_version\">djvu.js version: ' + DjVu.VERSION + '</div>');\n    }\n}\n\nfunction reset(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    $('.funcblock').hide(400);\n    $('#backbutton').hide(400);\n    $('#funcmenublock').show(400);\n    $('#finput').wrap('<form>').closest('form').get(0).reset();\n    $('#finput').unwrap().removeAttr('multiple').off('change');\n    $('.info').text('');\n    $('#procmess').text('');\n    $('#filehref').hide();\n    djvuWorker && djvuWorker.reset();\n}\n\nfunction metaDataFuncPrepare() {\n    $('#funcmenublock').hide(400);\n    $('#funcblock').show(400);\n    var mtblock = $('#metaDataBlock').show(400);\n    $(\"#finput\").change(metaDataFunc);\n    $(\"#procmess\").text(\"\");\n    $(\"#metaDataBlock #metadata\").html('');\n}\n\nfunction metaDataFunc() {\n    $(\"#procmess\").text(\"\");\n    $(\"#metaDataBlock #metadata\").html('');\n    if (this.files.length) {\n        if (this.files[0].name.substr(-5) !== '.djvu') {\n            $('#warnmess').text(\"Расширение файла не .djvu !!!\");\n            return;\n        }\n        $('#warnmess').text(\"\");\n        var fr = new FileReader();\n        fr.readAsArrayBuffer($(\"#finput\")[0].files[0]);\n        $(\"#procmess\").text('Загрузка документа ...');\n        fr.onload = () => {\n            var buf = fr.result;\n            djvuWorker.createDocument(buf)\n\n                .then(() => {\n                    $(\"#procmess\").text('Задание выполняется ...');\n                    return djvuWorker.doc.toString(true).run();\n                })\n\n                .then(str => {\n                    $(\"#procmess\").text(\"Задание выполнено !\");\n                    $(\"#metaDataBlock #metadata\").html(str);\n                })\n\n                .catch(() => {\n                    $(\"#procmess\").text(\"Ошибка при обработке файла !!!\");\n                });\n        }\n    }\n}\n\nfunction pictureFuncPrepare() {\n    $('#funcmenublock').hide(400);\n    $('#funcblock').show(400);\n    var picuture = $('#pictureblock').show(400);\n    $('#finput').prop('multiple', true).off('change').change(function () {\n        $('#filehref').hide();\n        $('#picturebut').prop('disabled', false);\n\n    });\n    $('#picturebut').click(readImagesAndCreateDocument);\n}\n\nfunction readImagesAndCreateDocument() {\n    var delayInit = 0;\n    var slices = +$('input[name=imagequality]:checked').val();\n    var grayscale = $('#grayscale').prop('checked') ? 1 : 0;\n\n    var files = $('#finput')[0].files;\n    djvuWorker.startMultyPageDocument(slices, delayInit, grayscale);\n    $('#filehref').hide();\n    var i = 0;\n    var canvas = document.createElement('canvas');\n    var ctx = canvas.getContext('2d');\n    $(\"#procmess\").text(\"Задание выполняется ...\");\n\n    var func = () => {\n        createImageBitmap(files[i])\n            .then((image) => {\n                canvas.width = image.width;\n                canvas.height = image.height;\n                ctx.drawImage(image, 0, 0);\n                var imageData = ctx.getImageData(0, 0, image.width, image.height);\n                return djvuWorker.addPageToDocument(imageData);\n            },\n                (e) => {\n                    $(\"#procmess\").text(\"Ошибка при загрузке файлов! \" + e.message);\n                })\n            .then(() => {\n                if (++i < files.length) {\n                    $(\"#procmess\").text(\"Задание выполняется ... \" + Math.round(i / files.length * 100) + ' %');\n                    func();\n                }\n                else {\n                    $(\"#procmess\").text(\"Сборка файла ... \");\n                    djvuWorker.endMultyPageDocument()\n                        .then((buffer) => {\n                            $(\"#procmess\").text(\"Задание выполненено !!!\");\n                            $('#filehref').prop('href', URL.createObjectURL(new Blob([buffer]))).show(400);\n                        });\n                }\n            });\n    }\n    func();\n}\n\n\nfunction createPicDocument(imageArray) {\n    var delayInit = 0;\n    var slices = +$('input[name=imagequality]:checked').val();\n    var grayscale = $('#grayscale').prop('checked') ? 1 : 0;\n\n    djvuWorker.createDocumentFromPictures(imageArray, slices, delayInit, grayscale)\n        .then((buffer) => {\n            $(\"#procmess\").text(\"Задание выполненено !!!\");\n            $('#filehref').prop('href', URL.createObjectURL(new Blob([buffer]))).show(400);\n        },\n            () => {\n                $(\"#procmess\").text(\"Ошибка при обработке файла !!!\");\n            });\n    djvuWorker.onprocess = (percent) => {\n        $(\"#procmess\").text(\"Задание выполняется ... \" + (percent * 100 >> 0) + '%');\n    }\n}\n\n\nfunction sliceFuncPrepare() {\n    $('#funcmenublock').hide(400);\n    $('#funcblock').show(400);\n    var sliceblock = $('#sliceblock').show(400);\n    $(\"#finput\").off('change').change(function () {\n        $('#filehref').hide();\n        if (this.files.length) {\n            if (this.files[0].name.substr(-5) !== '.djvu') {\n                $('#warnmess').text(\"Расширение файла не .djvu !!!\");\n                return;\n            }\n            $('#warnmess').text(\"\");\n\n            sliceblock.find('.info').text('');\n            var fr = new FileReader();\n            fr.readAsArrayBuffer($(\"#finput\")[0].files[0]);\n            fr.onload = () => {\n                var buf = fr.result;\n                djvuWorker.createDocument(buf)\n                    .then(() => djvuWorker.doc.getPagesQuantity().run())\n                    .then(pageCount => {\n                        $(\"#procmess\").text('');\n                        sliceblock.find('.info').text('Документ содержит ' + pageCount\n                            + ' страниц. Вы можете ввести значение от 1 до ' + pageCount);\n                        $('#slicebut').off('off').click(sliceFunc).prop('disabled', false);\n                    },\n                        () => {\n                            $(\"#procmess\").text(\"Ошибка при обработке файла !!!\");\n                        });\n            }\n        }\n        else {\n            $('#slicebut').prop('disabled', true);\n        }\n    });\n}\n\nfunction sliceFunc() {\n    $(\"#procmess\").text(\"Задание выполняется ...\");\n    $('#filehref').hide();\n    var from = +$(\"#firstnum\").val();\n    var to = +$(\"#secondnum\").val();\n    djvuWorker.slice(from, to)\n        .then((buffer) => {\n            $(\"#procmess\").text(\"Задание выполненено !!!\");\n            $('#filehref').prop('href', URL.createObjectURL(new Blob([buffer]))).show(400);\n        },\n            (e) => { // reject\n                console.error(e);\n                $(\"#procmess\").text(\"Ошибка при обработке файла !!!\");\n            });\n}\n\ninitDjVuApplication();"
  },
  {
    "path": "library/assets/DjVu3Spec_contents.json",
    "content": "[{\"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\"}]}]}]"
  },
  {
    "path": "library/debug/async.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n    <title>DjVu.js</title>\n    <script type=\"text/javascript\" src=\"js/reloader.js\"></script>\n    <script type=\"text/javascript\" src=\"dist/djvu.js\"></script>\n    <script type=\"text/javascript\" src=\"js/DjVuGlobals.js\" defer></script>\n    <script type=\"text/javascript\" src=\"js/debug.js\"></script>\n    <script type=\"text/javascript\" src=\"js/async.js\" defer></script>\n    <link rel=\"stylesheet\" href=\"css/style.css\">\n</head>\n\n<body>\n    <!-- <div id=\"viewer\" class=\"djvu_viewer\">\n        <div class=\"image_wrapper\">\n            <img class=\"image\" />\n            <canvas class=\"image\"></canvas>\n        </div>\n        <div class=\"controls\">\n            <input type=\"button\" class=\"navbut prev\" value=\"&#9668;\">\n            <input class=\"page_number\" type=\"number\">\n            <input type=\"button\" class=\"navbut next\" value=\"&#9658;\">\n            <input class=\"scale\" type=\"range\" min=\"0\" max=\"200\" step=\"1\" value=\"100\"><span class=\"scale_label\">100</span>%\n        </div>\n    </div> -->\n\n    <div class=\"control_block\">\n        <input type=\"button\" id=\"rerun\" value=\"rerun\">\n        <input type=\"button\" id=\"redraw\" value=\"redraw\">\n\n        <input type=\"button\" id=\"next\" value=\"next\">\n        <input type=\"button\" id=\"prev\" value=\"prev\">\n\n        <span id=\"time_output\"></span>\n        <span id=\"render_time_output\"></span>\n    </div>\n\n    <canvas width=\"192\" height=\"256\" id=\"canvas\"></canvas>\n    <img id=\"img\" src=\"\" /><br>\n    <a id=\"dochref\" href=\"#\" download=\"file.djvu\">Скачать</a>\n    <canvas width=\"200\" height=\"260\" id=\"canvas2\"></canvas>\n    <div id=\"output2\"></div>\n    <input type=\"file\" multiple onchange=\"main(this.files)\" />\n    <div id=\"output\"></div>\n\n</body>\n\n</html>"
  },
  {
    "path": "library/debug/css/style.css",
    "content": ".control_block{\n    box-shadow: 0 0 1px gray;\n    padding: 0.5em;\n    margin: 0.5em;\n}\n\n#time_output {\n    color: blue;\n}\n\n#canvas, #canvas2 {\n    box-shadow: 0 0 1px gray;\n}\n#img {\n    box-shadow: 0 0 1px gold;\n}\n#dochref {\n   text-decoration: none;\n   border: 1px solid blue;\n   padding: 3px;\n   margin: 3px;\n   border-radius: 5px;\n   color: blue;\n   display: inline-block;\n}\n\n#dochref:hover {\n    color: white;\n    background: blue;\n}\n\n.djvu_viewer {\n    overflow: hidden;\n    position: relative;\n    box-shadow: 0 0 4px gray;\n    margin: 1em auto;\n    padding: 1em;\n}\n\n.djvu_viewer .controls {\n    display: block;\n    position: absolute;\n    bottom: 0px;\n    margin: 1em;\n    width: 100%;\n    height: 5%;\n    text-align: center;\n}\n\n.djvu_viewer .controls .scale_label {\n    display: inline-block;\n    min-width: 3em;\n}\n\n.image_wrapper {\n    overflow: auto;\n    height: 95%;\n    text-align: center;\n}\n\n.djvu_viewer .image {\n    margin: 0.5em;\n    box-shadow: 0 0 1px lightgray;\n}\n\n.djvu_viewer .page_number {\n    width: 5em;\n}"
  },
  {
    "path": "library/debug/examples.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta charset=\"UTF-8\">\n    <title>DjVu.js usage examples</title>\n    <link rel=\"shortcut icon\" href=\"/catfav.png\" type=\"image/x-icon\" />\n    <script type=\"text/javascript\" src=\"/js/reloader.js\"></script>\n    <script type=\"text/javascript\" src=\"/dist/djvu.js\"></script>\n    <script type=\"text/javascript\" src=\"/js/examples.js\"></script>\n    <style>\n        img,\n        canvas {\n            max-width: 90%;\n            height: auto;\n            border: 1px solid black;\n        }\n    </style>\n</head>\n\n<body>\n    <h1>DjVu.js library usage examples</h1>\n    <h2>(open the console and read the source code to know how it works)</h2>\n    <h3>Canvas gotten with sync interface</h3>\n    <canvas id=\"sync-canvas\"></canvas>\n    <h3>Canvas gotten with async interface</h3>\n    <canvas id=\"async-canvas\"></canvas>\n    <h3>Image gotten with async interface (as an image URL)</h3>\n    <img id=\"async-image\" />\n</body>\n\n</html>"
  },
  {
    "path": "library/debug/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n    <link rel=\"shortcut icon\" href=\"/catfav.png\" type=\"image/x-icon\" />\n    <title>DjVu.js | Работа с DjVu файлами онлайн</title>\n\n    <style>\n        html, body {\n            height: 100%;\n            margin: 0;\n            padding: 0;\n        }\n        iframe {\n            display: block;\n            height: 94%;\n            width: 100%;\n            border: none;\n            box-sizing: border-box;\n        }\n        .header, .footer {\n            height: 3%;\n            background-color: gray;\n        }\n\n\n    </style>\n</head>\n\n<body style=\"height: 100%\">\n    <div class=\"header\"></div>\n    <iframe src=\"app/app.html\"></iframe>\n    <div class=\"footer\"></div>\n</body>\n\n</html>"
  },
  {
    "path": "library/debug/js/DjVuGlobals.js",
    "content": "'use strict';\n\n/**\n * Just a set of debug functions. \n */\n\nfunction writeln(str) {\n    str = str || \"\";\n    output.innerHTML += str + \"<br>\";\n}\n\nfunction write(str) {\n    output.innerHTML += str;\n}\nfunction clear() {\n    output.innerHTML = \"\";\n}\n\n// вспомогательный класс для быстрого доступа к разделяемым ресурсам\n/**\n * @type {DjVuGlobals}\n */\nvar Globals = {\n\n    init() {\n        var canvas = document.getElementById('canvas');\n        var c = canvas.getContext('2d');\n        this.defaultDPI = 100; // число точек на дюйм для монитора, в реальности 96.\n        this.Timer = new DebugTimer();\n        this.canvas = canvas;\n        this.canvasCtx = c;\n        this.dict = [];\n        this.img = document.getElementById('img');\n        this.counter = 0;\n    },\n\n    clearCanvas() {\n        this.canvasCtx.fillStyle = 'white';\n        this.canvasCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n    },\n\n    /**\n     * @returns {Promise<ArrayBuffer>}\n     */\n    loadFile(url) {\n        return new Promise(resolve => {\n            var xhr = new XMLHttpRequest();\n            xhr.open(\"GET\", url);\n            xhr.responseType = \"arraybuffer\";\n            xhr.onload = (e) => {\n                DjVu.IS_DEBUG && console.log(\"File loaded: \", e.loaded);\n                resolve(xhr.response);\n            };\n            xhr.send();\n        });\n    },\n\n    drawImage(image, dpi) {\n        var tmp;\n        var scale = dpi ? dpi / Globals.defaultDPI : 1;\n        var time = performance.now();\n        Globals.canvas.width = image.width / scale;\n        this.canvas.height = image.height / scale;\n\n        var oc = document.createElement('canvas');\n        var octx = oc.getContext('2d');\n        oc.width = image.width;\n        oc.height = image.height;\n        octx.putImageData(image, 0, 0);\n\n        var tmpH, tmpW, tmpH2, tmpW2;\n        tmpH = tmpH2 = oc.height;\n        tmpW = tmpW2 = oc.width;\n\n        if (scale > 4) {\n            tmpH = oc.height / scale * 4;\n            tmpW = oc.width / scale * 4;\n            //первое сжатие\n            octx.drawImage(oc, 0, 0, tmpW, tmpH);\n        }\n        if (scale > 2) {\n            tmpH2 = oc.height / scale * 2;\n            tmpW2 = oc.width / scale * 2;\n            //второе сжатие\n            octx.drawImage(oc, 0, 0, tmpW, tmpH, 0, 0, tmpW2, tmpH2);\n        }\n        //итоговое сжатие\n        //this.canvasCtx.translate(- this.canvas.width / 2, - this.canvas.height / 2);\n        // this.canvasCtx.translate(this.canvas.width / 2, this.canvas.height / 2);\n        // this.canvasCtx.rotate(180* Math.PI / 180);\n        // this.canvasCtx.translate(-this.canvas.width / 2, -this.canvas.height / 2);\n        this.canvasCtx.drawImage(oc, 0, 0, tmpW2, tmpH2,\n            0, 0, canvas.width, canvas.height);\n        DjVu.IS_DEBUG && console.log(\"Canvas resizing time = \", performance.now() - time);\n        //this.canvasCtx.setTransform(1, 0, 0, 1, 0, 0);\n    },\n\n    drawImageNS(image, dpi) {\n        Globals.Timer.start('drawImageNS');\n        var tmp;\n        var scale = dpi ? Globals.defaultDPI / dpi : 1;\n        var time = performance.now();\n        Globals.canvas.width = image.width / scale;\n        this.canvas.height = image.height / scale;\n\n        var oc = document.createElement('canvas');\n        var octx = oc.getContext('2d');\n        oc.width = image.width;\n        oc.height = image.height;\n        octx.putImageData(image, 0, 0);\n        var resImg = downScaleCanvas(oc, scale);\n        this.canvas.width = resImg.width;\n        this.canvas.height = resImg.height;\n        this.canvasCtx.putImageData(resImg, 0, 0);\n        Globals.Timer.end('drawImageNS');\n    },\n\n    drawImageSmooth(image, dpi) {\n        var time = performance.now();\n        var tmp;\n        var scale = dpi ? dpi / Globals.defaultDPI : 1;\n\n        Globals.canvas.width = image.width;\n        this.canvas.height = image.height;\n\n        Globals.canvasCtx.putImageData(image, 0, 0);\n\n        this.img.src = this.canvas.toDataURL();\n        DjVu.IS_DEBUG && console.log(\"DataURL creating time = \", performance.now() - time);\n        this.img.width = image.width / scale;\n        (tmp = this.canvas.parentNode) ? tmp.removeChild(this.canvas) : 0;\n        DjVu.IS_DEBUG && console.log(\"DataURL creating time = \", performance.now() - time);\n    },\n\n    /** рисует символ на хосте с учетом его координат (можно посимвольно рисовать картинку) - отладочная функция*/\n    drawBitmapOnImageCanvas(bm, x, y, jb2Image) {\n        if (this._drawTime && (Date.now() - this._drawTime) < 100) { // если не под отладчиком, то не рисовать\n            return;\n        } else {\n            this._drawTime = Date.now();\n        }\n        if (!this.testImageData) {\n            console.warn(\"Debug draw function is enabled!\");\n            this.testImageData = document.createElement('canvas')\n                .getContext('2d')\n                .createImageData(jb2Image.width, jb2Image.height);\n            this.testImageData.data.fill(255); // все белым непрозрачным\n        }\n        var pixelArray = this.testImageData.data;\n        for (var i = y, k = 0; k < bm.height; k++ , i++) {\n            for (var j = x, t = 0; t < bm.width; t++ , j++) {\n                if (bm.get(k, t)) {\n                    var pixelIndex = ((jb2Image.height - i - 1) * jb2Image.width + j) * 4;\n                    pixelArray[pixelIndex] = 0;\n                    pixelArray[pixelIndex + 1] = 0;\n                    pixelArray[pixelIndex + 2] = 0;\n                }\n            }\n        }\n        Globals.drawImage(this.testImageData, 300);\n    }\n};\n\nfunction downScaleCanvas(cv, scale) {\n    if (!(scale < 1) || !(scale > 0))\n        throw ('scale must be a positive number <1 ');\n    var sqScale = scale * scale;\n    // square scale = area of source pixel within target\n    var sw = cv.width;\n    // source image width\n    var sh = cv.height;\n    // source image height\n    var tw = Math.floor(sw * scale);\n    // target image width\n    var th = Math.floor(sh * scale);\n    // target image height\n    var sx = 0\n        , sy = 0\n        , sIndex = 0;\n    // source x,y, index within source array\n    var tx = 0\n        , ty = 0\n        , yIndex = 0\n        , tIndex = 0;\n    // target x,y, x,y index within target array\n    var tX = 0\n        , tY = 0;\n    // rounded tx, ty\n    var w = 0\n        , nw = 0\n        , wx = 0\n        , nwx = 0\n        , wy = 0\n        , nwy = 0;\n    // weight / next weight x / y\n    // weight is weight of current source point within target.\n    // next weight is weight of current source point within next target's point.\n    var crossX = false;\n    // does scaled px cross its current px right border ?\n    var crossY = false;\n    // does scaled px cross its current px bottom border ?\n    var sBuffer = cv.getContext('2d').\n        getImageData(0, 0, sw, sh).data;\n    // source buffer 8 bit rgba\n    var tBuffer = new Float32Array(3 * tw * th);\n    // target buffer Float32 rgb\n    var sR = 0\n        , sG = 0\n        , sB = 0;\n    // source's current point r,g,b\n    /* untested !\n    var sA = 0;  //source alpha  */\n\n    for (sy = 0; sy < sh; sy++) {\n        ty = sy * scale;\n        // y src position within target\n        tY = 0 | ty;\n        // rounded : target pixel's y\n        yIndex = 3 * tY * tw;\n        // line index within target array\n        crossY = (tY != (0 | ty + scale));\n        if (crossY) {\n            // if pixel is crossing botton target pixel\n            wy = (tY + 1 - ty);\n            // weight of point within target pixel\n            nwy = (ty + scale - tY - 1);\n            // ... within y+1 target pixel\n        }\n        for (sx = 0; sx < sw; sx++ ,\n            sIndex += 4) {\n            tx = sx * scale;\n            // x src position within target\n            tX = 0 | tx;\n            // rounded : target pixel's x\n            tIndex = yIndex + tX * 3;\n            // target pixel index within target array\n            crossX = (tX != (0 | tx + scale));\n            if (crossX) {\n                // if pixel is crossing target pixel's right\n                wx = (tX + 1 - tx);\n                // weight of point within target pixel\n                nwx = (tx + scale - tX - 1);\n                // ... within x+1 target pixel\n            }\n            sR = sBuffer[sIndex];\n            // retrieving r,g,b for curr src px.\n            sG = sBuffer[sIndex + 1];\n            sB = sBuffer[sIndex + 2];\n\n            /* !! untested : handling alpha !!\n               sA = sBuffer[sIndex + 3];\n               if (!sA) continue;\n               if (sA != 0xFF) {\n                   sR = (sR * sA) >> 8;  // or use /256 instead ??\n                   sG = (sG * sA) >> 8;\n                   sB = (sB * sA) >> 8;\n               }\n            */\n            if (!crossX && !crossY) {\n                // pixel does not cross\n                // just add components weighted by squared scale.\n                tBuffer[tIndex] += sR * sqScale;\n                tBuffer[tIndex + 1] += sG * sqScale;\n                tBuffer[tIndex + 2] += sB * sqScale;\n            } else if (crossX && !crossY) {\n                // cross on X only\n                w = wx * scale;\n                // add weighted component for current px\n                tBuffer[tIndex] += sR * w;\n                tBuffer[tIndex + 1] += sG * w;\n                tBuffer[tIndex + 2] += sB * w;\n                // add weighted component for next (tX+1) px                \n                nw = nwx * scale\n                tBuffer[tIndex + 3] += sR * nw;\n                tBuffer[tIndex + 4] += sG * nw;\n                tBuffer[tIndex + 5] += sB * nw;\n            } else if (crossY && !crossX) {\n                // cross on Y only\n                w = wy * scale;\n                // add weighted component for current px\n                tBuffer[tIndex] += sR * w;\n                tBuffer[tIndex + 1] += sG * w;\n                tBuffer[tIndex + 2] += sB * w;\n                // add weighted component for next (tY+1) px                \n                nw = nwy * scale\n                tBuffer[tIndex + 3 * tw] += sR * nw;\n                tBuffer[tIndex + 3 * tw + 1] += sG * nw;\n                tBuffer[tIndex + 3 * tw + 2] += sB * nw;\n            } else {\n                // crosses both x and y : four target points involved\n                // add weighted component for current px\n                w = wx * wy;\n                tBuffer[tIndex] += sR * w;\n                tBuffer[tIndex + 1] += sG * w;\n                tBuffer[tIndex + 2] += sB * w;\n                // for tX + 1; tY px\n                nw = nwx * wy;\n                tBuffer[tIndex + 3] += sR * nw;\n                tBuffer[tIndex + 4] += sG * nw;\n                tBuffer[tIndex + 5] += sB * nw;\n                // for tX ; tY + 1 px\n                nw = wx * nwy;\n                tBuffer[tIndex + 3 * tw] += sR * nw;\n                tBuffer[tIndex + 3 * tw + 1] += sG * nw;\n                tBuffer[tIndex + 3 * tw + 2] += sB * nw;\n                // for tX + 1 ; tY +1 px\n                nw = nwx * nwy;\n                tBuffer[tIndex + 3 * tw + 3] += sR * nw;\n                tBuffer[tIndex + 3 * tw + 4] += sG * nw;\n                tBuffer[tIndex + 3 * tw + 5] += sB * nw;\n            }\n        }\n        // end for sx \n    }\n    // end for sy\n\n    // create result canvas\n    var resCV = document.createElement('canvas');\n    resCV.width = tw;\n    resCV.height = th;\n    var resCtx = resCV.getContext('2d');\n    var imgRes = resCtx.getImageData(0, 0, tw, th);\n    var tByteBuffer = imgRes.data;\n    // convert float32 array into a UInt8Clamped Array\n    var pxIndex = 0;\n    //  \n    for (sIndex = 0,\n        tIndex = 0; pxIndex < tw * th; sIndex += 3,\n        tIndex += 4,\n        pxIndex++) {\n        tByteBuffer[tIndex] = Math.ceil(tBuffer[sIndex]);\n        tByteBuffer[tIndex + 1] = Math.ceil(tBuffer[sIndex + 1]);\n        tByteBuffer[tIndex + 2] = Math.ceil(tBuffer[sIndex + 2]);\n        tByteBuffer[tIndex + 3] = 255;\n    }\n    // writing result to canvas.\n    resCtx.putImageData(imgRes, 0, 0);\n    return imgRes;\n}\n"
  },
  {
    "path": "library/debug/js/DjVuViewer.js",
    "content": "'use strict';\n\n/**\n * Old Viewer with manual DOM manipulation. Saved just for history. \n * Isn't used anymore. \n */\n\nclass DjVuViewer {\n    constructor(selector, worker) {\n        this.defaultDPI = 100;\n        this.selector = selector;\n        this.element = document.querySelector(selector);\n        this.fileReader = new FileReader();\n        this.tmpCanvas = document.createElement('canvas');\n        this.tmpCanvasCtx = this.tmpCanvas.getContext('2d');\n        this.prevBut = this.element.querySelector('.controls .navbut.prev');\n        this.nextBut = this.element.querySelector('.controls .navbut.next');\n        this.pageNumberBox = this.element.querySelector('.controls .page_number');\n        this.scaleSlider = this.element.querySelector('.controls .scale');\n        this.scaleLabel = this.element.querySelector('.controls .scale_label');\n        this.img = this.element.querySelector('.image_wrapper img');\n        this.img.style.display = 'none';\n        this.canvas = this.element.querySelector('.image_wrapper canvas');\n        this.canvasCtx = this.canvas.getContext('2d');\n        this.imgWrapper = this.element.querySelector('.image_wrapper');\n        this._curPage = 0;\n        this.pageNumber = null;\n        this.stdWidth\n        /** @type {DjVuWorker} */\n        this.worker = worker || new DjVuWorker();\n\n        this.isCanvasMode = true;\n\n        this.element.style.width = window.innerWidth * 0.9 + 'px';\n        this.element.style.height = window.innerHeight * 0.9 + 'px';\n\n        this.nextBut.onclick = () => this.showNextPage();\n        this.prevBut.onclick = () => this.showPrevPage();\n        this.pageNumberBox.onblur = (e) => this.renderEnteredPage(e);\n        this.pageNumberBox.onkeypress = (e) => this.renderEnteredPageByEnter(e);\n        this.scaleSlider.oninput = () => this.changeScale();\n    }\n\n    reset() {\n        clearTimeout(this.improveImageTimeout);\n        if (this.nextBut) {\n            this.nextBut.onclick = null;\n            this.prevBut.onclick = null;\n            this.pageNumberBox.onblur = null;\n            this.pageNumberBox.onkeypress = null;\n            this.scaleSlider.oninput = null;\n            this.worker = null;\n            this.img.src = '';\n        }\n    }\n\n    changeScale() {\n        this.scaleLabel.innerText = this.scaleSlider.value;\n        this.isCanvasMode ? this.drawImageOnCanvas() : this.rescaleImageOnImg();\n    }\n\n    renderEnteredPageByEnter(e) {\n        if (e.keyCode === 13) {\n            this.pageNumberBox.blur(); // it will call showEnteredPage() as the event handler\n        }\n    }\n\n    renderEnteredPage(e) {\n        var page = +this.pageNumberBox.value;\n        this.curPage = page;\n    }\n\n    get curPage() {\n        return this._curPage + 1;\n    }\n\n    set curPage(value) {\n        this.setPage(value);\n    }\n\n    showNextPage() {\n        this.curPage += 1;\n    }\n\n    showPrevPage() {\n        this.curPage -= 1;\n    }\n\n    lockNavButtons() {\n        this.nextBut.disabled = true;\n        this.prevBut.disabled = true;\n    }\n\n    unlockNavButtons() {\n        this.nextBut.disabled = false;\n        this.prevBut.disabled = false;\n    }\n\n    getScaledImageWidth() {\n        return this.imageData.width / this.standardScale * (+this.scaleSlider.value / 100);\n    }\n\n    renderCurPage() {\n        clearTimeout(this.improveImageTimeout);\n        return this.worker.getPageImageDataWithDPI(this._curPage).then(obj => {\n            this.imageData = obj.imageData;\n            this.imageDPI = obj.dpi;\n            this.standardScale = this.imageDPI ? this.imageDPI / this.defaultDPI : 1;\n            this.drawImageOnCanvas();\n\n            this.improveImageTimeout = setTimeout(() => {\n                this.drawImageViaImg();\n            }, 1000);\n        });\n    }\n\n    /**\n     * @returns {Promise<ArrayBuffer>}\n     */\n    loadFile(url) {\n        return new Promise(resolve => {\n            var xhr = new XMLHttpRequest();\n            xhr.open(\"GET\", url);\n            xhr.responseType = \"arraybuffer\";\n            xhr.onload = (e) => {\n                DjVu.IS_DEBUG && console.log(\"File loaded: \", e.loaded);\n                resolve(xhr.response);\n            };\n            xhr.send();\n        });\n    }\n\n    loadDjVu(url) { // debug functions\n        return this.loadFile(url)\n            .then(buffer => this.worker.createDocument(buffer))\n            .then(() => this.worker.getPageNumber())\n            .then(number => {\n                this.pageNumber = number;\n                this.curPage = 1;\n            });\n    }\n\n    loadDjVuFromBuffer(buffer) {\n        return this.worker.createDocument(buffer)\n            .then(() => this.worker.getPageNumber())\n            .then(number => {\n                this.pageNumber = number;\n                this.curPage = 1;\n            });\n    }\n\n    relockNavButtons() {\n        this.unlockNavButtons();\n        if (this._curPage === 0) {\n            this.prevBut.disabled = true;\n        }\n        if (this._curPage === this.pageNumber - 1) {\n            this.nextBut.disabled = true;\n        }\n    }\n\n    setPage(page) {\n        page--;\n        if (page < 0) {\n            page = 0;\n        } else if (page > this.pageNumber - 1) {\n            page = this.pageNumber - 1;\n        }\n        this._curPage = page;\n        this.relockNavButtons();\n        this.pageNumberBox.value = this.curPage;\n        this.lockNavButtons();\n        this.renderCurPage().then(() => {\n            this.relockNavButtons();\n        });\n    }\n\n    _switchToImageMode() {\n        this.img.style.display = 'block';\n        this.canvas.style.display = 'none';\n        this.isCanvasMode = false;\n    }\n\n    _switchToCanvasMode() {\n        this.img.style.display = 'none';\n        this.canvas.style.display = 'block';\n        this.isCanvasMode = true;\n    }\n\n    getImageDataURL() {\n        this.tmpCanvas.width = this.imageData.width;\n        this.tmpCanvas.height = this.imageData.height;\n        this.tmpCanvasCtx.putImageData(this.imageData, 0, 0);\n        return this.tmpCanvas.toDataURL();\n    }\n\n    getImageDataURLAsync() {\n        return new Promise(resolve => {\n            this.tmpCanvas.width = this.imageData.width;\n            this.tmpCanvas.height = this.imageData.height;\n            this.tmpCanvasCtx.putImageData(this.imageData, 0, 0);\n            this.tmpCanvas.toBlob(imageBlob => {\n                this.fileReader.onload = event => {\n                    resolve(event.target.result);\n                };\n                this.fileReader.readAsDataURL(imageBlob);\n            });\n        });\n    }\n\n    drawImageViaImg() {\n        this.img.src = this.getImageDataURL();\n        this.rescaleImageOnImg();\n        this._switchToImageMode();\n    }\n\n    rescaleImageOnImg() {\n        this.img.width = this.getScaledImageWidth();\n    }\n\n    drawImageOnCanvas() {\n        //var time = performance.now();\n        var image = this.imageData;\n        var scale = this.imageDPI ? this.imageDPI / this.defaultDPI : 1;\n        scale /= (+this.scaleSlider.value / 100)\n        this.stdWidth = image.width / scale * (+this.scaleSlider.value / 100);\n        this.stdHeight = image.height / scale * (+this.scaleSlider.value / 100);\n\n        this.tmpCanvas.width = image.width;\n        this.tmpCanvas.height = image.height;\n        this.tmpCanvasCtx.putImageData(image, 0, 0);\n\n        var tmpH, tmpW, tmpH2, tmpW2;\n        tmpH = tmpH2 = this.tmpCanvas.height;\n        tmpW = tmpW2 = this.tmpCanvas.width;\n\n        if (scale > 4) {\n            tmpH = this.tmpCanvas.height / scale * 4;\n            tmpW = this.tmpCanvas.width / scale * 4;\n            //первое сжатие\n            this.tmpCanvasCtx.drawImage(this.tmpCanvas, 0, 0, tmpW, tmpH);\n        }\n        if (scale > 2) {\n            tmpH2 = this.tmpCanvas.height / scale * 2;\n            tmpW2 = this.tmpCanvas.width / scale * 2;\n            //второе сжатие\n            this.tmpCanvasCtx.drawImage(this.tmpCanvas, 0, 0, tmpW, tmpH, 0, 0, tmpW2, tmpH2);\n        }\n        //итоговое сжатие\n        this.canvas.width = image.width / scale;\n        this.canvas.height = image.height / scale;\n        this.canvasCtx.drawImage(this.tmpCanvas, 0, 0, tmpW2, tmpH2,\n            0, 0, this.canvas.width, this.canvas.height);\n        this._switchToCanvasMode();\n        //console.log('Render time', performance.now() - time, scale);\n    }\n}"
  },
  {
    "path": "library/debug/js/async.js",
    "content": "\"use strict\";\n\n/**\n * Скрипт для тестирования библиотеки через Web Worker\n */\n\nvar fileSize = 0;\nvar output;\nvar worker;\n\nvar timeOutput = document.querySelector('#time_output');\nvar renderTimeOutput = document.querySelector('#render_time_output');\nvar rerunButton = document.querySelector('#rerun');\nrerunButton.onclick = rerun;\ndocument.querySelector('#redraw').onclick = redrawPage;\n\nvar pageNumber = 1;\nvar djvuUrl = '/assets/DjVu3Spec_indirect/index.djvu';\nvar baseUrl = '/assets/DjVu3Spec_indirect/';\n\ndocument.querySelector('#next').onclick = () => {\n    pageNumber++;\n    redrawPage();\n};\n\ndocument.querySelector('#prev').onclick = () => {\n    pageNumber--;\n    redrawPage();\n};\n\nwindow.onload = function () {\n    output = document.getElementById(\"output\");\n    var canvas = document.getElementById('canvas');\n    var c = canvas.getContext('2d');\n    Globals.defaultDPI = 100;\n    Globals.Timer = new DebugTimer();\n    Globals.canvas = canvas;\n    Globals.canvasCtx = c;\n    Globals.dict = [];\n    Globals.img = document.getElementById('img');\n    // testFunc();\n    //loadPicture();\n    renderDjVu();\n    //initViewer();\n    //Globals.loadFile('samples/csl.djvu').then(buf => showMetaData(buf));\n}\n\nfunction initViewer() {\n    /** @type {DjVuViewer} */\n    var viewer = new DjVuViewer('.djvu_viewer');\n    viewer.loadDjVu('samples/csl.djvu');\n}\n\nasync function renderDjVu() {\n    /** @type {DjVuWorker} */\n    worker = new DjVu.Worker();\n    const buffer = await fetch(djvuUrl).then(r => r.arrayBuffer());\n    await worker.createDocument(buffer, { baseUrl });\n\n    // const bundle = await worker.doc.bundle(progress => {\n    //     console.log(progress);\n    // }).run();\n\n    await redrawPage();\n}\n\nasync function redrawPage() {\n    console.log('**** Render Page ****');\n    var time = performance.now();\n    var [imageData, dpi] = await worker.run(\n        worker.doc.getPage(pageNumber).getImageData(),\n        worker.doc.getPage(pageNumber).getDpi()\n    );\n    Globals.drawImage(imageData, dpi * 1.5);\n    time = performance.now() - time;\n    console.log(\"Redraw time\", time);\n    console.log('**** ***** **** ****');\n    renderTimeOutput.innerText = Math.round(time);\n}\n\nfunction loadPicture() {\n    var xhr = new XMLHttpRequest();\n    xhr.open(\"GET\", \"samples/bear.jpg\");\n    xhr.responseType = \"arraybuffer\";\n    xhr.onload = function (e) {\n        console.log(e.loaded);\n        fileSize = e.loaded;\n        var buf = xhr.response;\n        readPicture(buf);\n    }\n    xhr.send();\n}\nfunction readPicture(buffer) {\n\n    createImageBitmap(new Blob([buffer])).then(function (image) {\n        var pictureTotalTime = performance.now();\n        var canvas = document.getElementById('canvas2');\n        var c = canvas.getContext('2d');\n        canvas.width = image.width;\n        canvas.height = image.height;\n\n        c.drawImage(image, 0, 0);\n        var imageData = c.getImageData(0, 0, image.width, image.height);\n        var iwiw = new IWImageWriter(90, 0, 0);\n        var doc = iwiw.createMultyPageDocument([imageData, imageData, imageData]);\n        // var doc = iwiw.createOnePageDocument(imageData);\n        console.log('docCreateTime = ', performance.now() - pictureTotalTime);\n        var link = document.querySelector('#dochref');\n        link.href = doc.createObjectURL();\n\n        c.putImageData(doc.pages[0].getImage(), 0, 0);\n        console.log('Counter', Globals.counter);\n        //console.log('PZP', Globals.pzp.log.length, ' ', Globals.pzp.offset );\n        writeln(doc.toString());\n        console.log('pictureTotalTime = ', performance.now() - pictureTotalTime);\n    });\n\n}\n\nfunction showMetaData(buffer) {\n    var worker = new DjVuWorker();\n    worker.createDocument(buffer)\n        .then(() => worker.getDocumentMetaData(true))\n        .then(text => writeln(text));\n}\n\nfunction readDjvu(buf) {\n    console.log(\"DJ1\");\n    var link = document.querySelector('#dochref');\n    var time = performance.now();\n    console.log(\"Buffer length = \" + buf.byteLength);\n    //var doc = new DjVuDocument(buf);\n    Globals.counter = 0;\n    var worker = new DjVuWorker();\n\n    setTimeout(() => {\n        Globals.Timer.start('TotalTime');\n        worker.createDocument(buf)\n            .then(() => {\n                Globals.Timer.end('TotalTime', true);\n                return worker.getDocumentMetaData(true);\n            })\n            .then((str) => {\n                //link.href = URL.createObjectURL(new Blob([buffer]))\n                writeln(str);\n                Globals.Timer.end('TotalTime', true);\n            });\n\n    }, 1000);\n\n\n    console.log(Globals.Timer.toString());\n    console.log(\"Total execution time = \", performance.now() - time);\n}\n\n/**\n * Функция для работы с файлами загруженными вручную.\n */\nfunction main(files) {\n    clear();\n    console.log(files.length);\n    //readFile(file);\n    var fileReader = new FileReader();\n    var doc1, doc2;\n    fileReader.onload = function () {\n        if (!doc1) {\n            doc1 = new DjVuDocument(this.result);\n            fileReader.readAsArrayBuffer(files[1]);\n            return;\n        }\n\n        doc2 = new DjVuDocument(this.result);\n        testFunc(doc1, doc2);\n\n    };\n    if (files.length > 0) {\n        fileReader.readAsArrayBuffer(files[0]);\n    }\n}\n\nfunction testFunc(doc1, doc2) {\n    var doc = DjVuDocument.concat(doc1, doc2);\n    Globals.drawImageSmooth(doc.pages[0].getImage(), 600);\n    writeln(doc.toString());\n    var link = document.querySelector('#dochref');\n    link.href = doc.createObjectURL();\n\n}\n"
  },
  {
    "path": "library/debug/js/debug.js",
    "content": "'use strict';\n\n/**\n * One more set of debug funcitions that was used in development of the library.\n * Saved mostly for history. \n */\n\n//класс для измерения времени с точностью до микросекунд\nclass DebugTimer {\n    constructor() {\n        this.timers = {};\n    }\n    start(id) {\n        var timer;\n        if (this.timers[id]) {\n            timer = this.timers[id];\n        } else {\n            timer = {\n                totalTime: 0,\n                timeArray: [],\n                startTime: 0\n            };\n            this.timers[id] = timer;\n        }\n        timer.startTime = performance.now();\n    }\n    end(id, print) {\n        if (!this.timers[id]) {\n            console.log(\"Несуществующий таймер: \", id);\n        }\n        var timer = this.timers[id];\n        var time = performance.now() - timer.startTime\n        timer.totalTime += time;\n        timer.timeArray.push(time);\n        if (print) {\n            console.log(\"Timer '\", id, \"'\", time);\n        }\n    }\n    toString() {\n        var str = '**DebugTimer**\\n';\n        for (var p in this.timers) {\n            str += \">>\" + p + \" \" + this.timers[p].totalTime + \"\\n\" + /*JSON.stringify(this.timers[p].timeArray) + '\\n'*/ + '<<\\n';\n        }\n        str += \"**DebugTimer**\\n\";\n        return str;\n    }\n}\n/*\n* Псевдо ZPСoder для того чтобы видеть битовый поток.\n*/\nclass PseudoZP {\n    constructor() {\n        this.log = [];\n        this.offset = 0;\n    }\n    encode(bit, ctx, n) {\n        bit = +bit;\n        if (ctx) {\n            var tmp = {\n                bit: bit,\n                ctx: ctx[n],\n                off: n,\n                len: this.log.length\n            };\n        } else {\n            var tmp = {\n                bit: bit,\n                ctx: -1,\n                off: -1,\n                len: this.log.length\n            };\n        }\n        this.log.push(tmp);\n    }\n    decode(ctx, n) {\n        var tmp = this.log[this.offset++];\n        if(!tmp) { Globals.counter++; return 1;}\n        if (ctx) {\n            var cv = ctx[n];\n            if (!(tmp.ctx === cv && n === tmp.off && tmp.len === (this.offset - 1))) {\n                4;\n                throw new Error(\"Context dismatch\");\n            }\n        } else {\n            if (!(tmp.ctx === -1 && tmp.off === -1 && tmp.len === (this.offset - 1))) {\n                throw new Error(\"Context dismatch\");\n            }\n        }\n        return tmp.bit;\n    }\n    eflush() {\n        console.log(\"PseudoZP eflushed\");\n    }\n}\nfunction tmpFunc(doc) {\n    var writer = new DjVuWriter(1000000);\n    writer.writeStr(\"AT&T\");\n    writer.writeStr(\"FORM\");\n    //todo переделать\n    writer.writeInt32(0);\n    writer.writeStr(\"DJVU\");\n    var page = doc.pages[3];\n    writer.writeChunk(page.info);\n    for (var i = 0; i < page.bg44arr.length; i++) {\n        writer.writeChunk(page.bg44arr[i]);\n    }\n    var bs = writer.getByteStream();\n    console.log(bs.readStr4());\n    var link = document.querySelector('#dochref');\n    var nb = writer.getBuffer();\n    var blob = new Blob([nb]);\n    var url = URL.createObjectURL(blob);\n    link.href = url;\n    var dd = new DjVuDocument(nb);\n    Globals.drawImage(dd.pages[0].getImage())\n}\nfunction ZPtest() {\n    var bsw = new ByteStreamWriter(100000);\n    var zp = new ZPEncoder(bsw);\n    var n = 64;\n    var ctx = [0];\n    var arr = [];\n    for (var i = 0; i < n; arr.push(Math.random() * 2 >> 0),\n    i++) {}\n    for (i = 0; i < n; i++) {\n        var byte = arr[i];\n        var mask = 128;\n        for (var j = 7; j >= 0; j--) {\n            var bit = (byte & mask) >> j;\n            mask >>= 1;\n            zp.encode(bit, ctx, 0);\n        }\n    }\n    zp.eflush();\n    console.log(arr);\n    ctx = [0];\n    var bs = new ByteStream(bsw.getBuffer());\n    var zp = new ZPDecoder(bs);\n    for (i = 0; i < n; i++) {\n        var byte = 0;\n        for (var j = 7; j >= 0; j--) {\n            var bit = zp.decode(ctx, 0);\n            byte = (byte << 1) | bit;\n        }\n        arr[i] = byte;\n    }\n    console.log(arr);\n    console.log(\"Full length = \", n, \" Coded length = \", bs.length);\n}\nfunction BZZtest() {\n    var bs = new ByteStreamWriter();\n    var zp = new ZPEncoder(bs);\n    var pzp = new PseudoZP();\n    var bzz = new BZZEncoder(zp);\n    var data = Uint8Array.of(11, 3, 2, 10, 2, 10, 2, 0);\n    bzz.encode(data.buffer);\n    var bsbs = new ByteStream(bs.getBuffer());\n    var zp2 = new ZPDecoder(bsbs);\n    zp2.pzp = zp.pzp;\n    var bzz = new BZZCodec(zp2);\n    bzz.decode();\n    var bsz = bzz.getByteStream();\n    data = new Uint8Array(bsz.buffer);\n    console.log(data);\n}\n/*\nfunction tmpFunc() {\n    var zigzagRow = [];\n    var zigzagCol = [];\n    for (var i = 0; i < 1024; i++) {\n        var bits = [];\n        for (let j = 0; j < 10; j++) {\n            bits.push((i & Math.pow(2, j)) >> j);\n        }\n        let row = 16 * bits[1] + 8 * bits[3] + 4 * bits[5] + 2 * bits[7] + bits[9];\n        let col = 16 * bits[0] + 8 * bits[2] + 4 * bits[4] + 2 * bits[6] + bits[8];\n        zigzagRow.push(row);\n        zigzagCol.push(col);\n    }\n    //console.log(JSON.stringify(zigzagRow));\n    //console.log(JSON.stringify(zigzagCol));\n    let r = \"[\";\n    let c = \"[\";\n    let k = 0;\n    for (let i = 0; i < 1024; i++) {\n        r += zigzagRow[i] + ',';\n        c += zigzagCol[i] + ',';\n        k++;\n        if (k === 16) {\n            k = 0;\n            r += '\\n';\n            c += '\\n';\n        }\n    }\n    r += ']';\n    c += ']';\n    console.log(r);\n    console.log(c);\n\n}*/\n"
  },
  {
    "path": "library/debug/js/examples.js",
    "content": "/**\n * This code serves as an example of the API provided by the DjVu.js library.\n * This very API is used by the DjVu.js Viewer.\n * \n * Start to read the code form the main() function in the same direction as it is executed.\n */\n\n'use strict';\n\nasync function syncInterfaceExamples(djvuDocument) {\n    // The method is async just because it can be used for indirect djvu files, and then \n    // different parts of a document are loaded lazily. \n    // In case of bundled djvu (one file djvu) there are no async operations actually.\n    const djvuPage = await djvuDocument.getPage(1); // note that pages start with 1 NOT WITH 0\n\n    // we can get page size even before it's decoded\n    console.log(\"Page width\", djvuPage.getWidth());\n    console.log(\"Page height\", djvuPage.getHeight());\n\n    // get the standard ImageData object representing the page\n    const imageData = djvuPage.getImageData(); // it's the longest operation, here the page is decoded and image is created.\n\n    // Let's draw the ImageData on a canvas\n    const canvas = document.querySelector('#sync-canvas');\n    canvas.width = imageData.width;\n    canvas.height = imageData.height;\n    const ctx = canvas.getContext('2d');\n    ctx.putImageData(imageData, 0, 0); // But it will be much \n\n    // In fact, our image is very large, so we need to scale it somehow. Let's use its DPI.\n    // DPI is needed to render an image in so called 100% scale.\n    // A usual monitor has 96 dpi (let's say 100). Thus, if an image save with 300 dpi, then\n    // on a usual monitor you should decrease it 300 / 100 = 3 times.\n    // The DjVu.js Viewer uses css scaling only for the initial render. Then it rewrites imageData\n    // on the canvas several times, decreasing it 2 times on each render (or less on the last render). \n    // Such manual scaling gives much better quality, than css scaling. \n    // In case of continuous scroll mode, it uses <img>'s rather than canvases, and imgs are scaled via css much better.\n    // But here we use only css scaling on a canvas.\n    const imageDpi = djvuPage.getDpi();\n    canvas.style.width = imageData.width / (imageDpi / 100) + 'px';\n\n    // when a document has a contents table you can get it\n    const contents = djvuDocument.getContents();\n    console.log('DjVu Document contents \\n\\n', contents);\n\n    // if you want a raw text of the page. This text is used in the DjVu.js Viewer's text mode.\n    const text = djvuPage.getText();\n    console.log('DjVu Page text \\n\\n', text);\n\n    // if you want a structured text zones to create a text layer over an image.\n    const topTextZone = djvuPage.getPageTextZone();\n    console.log('DjVu Page top text zone \\n\\n', topTextZone);\n\n    // or another variant (more convenient for absolute positioning, look at the structure of output to understand the difference) \n    const textZones = djvuPage.getNormalizedTextZones();\n    console.log('DjVu Page text zones \\n\\n', textZones);\n\n    // get the number of page in a document\n    const pageCount = djvuDocument.getPagesQuantity();\n    console.log(\"There are \", pageCount, \" pages in the document\");\n\n    // we can get sizes of all pages too. E.g. to render empty pages of an appropriate size while they are being loaded.\n    const pageSizes = djvuDocument.getPagesSizes();\n    console.log(\"Pages sizes \", pageSizes);\n}\n\nasync function asyncInterfaceExamples(djvuWorker) {\n    // In case of the worker, you cannot get a DjVuPage object explicitly.\n    // You can get only eventual results. \n    // The async interface is similar to the sync one. \n    // djvuWorker.doc is a proxy object, called DjVuTask, which remembers what methods you have called.\n    // Each method of the task returns another task. When it's run, the consequence of methods is executed on \n    // a DjVuDocument object which is created inside the Web Worker, and the result is transferred to the main thread.\n\n    // You can execute task in batches (it's faster than one by one)\n    // You will get an array of results.\n    const [width, height] = await djvuWorker.run(\n        djvuWorker.doc.getPage(60).getWidth(),\n        djvuWorker.doc.getPage(60).getHeight(),\n        // here you can add more tasks\n    );\n\n    console.log('Page size is', width, height)\n\n    // But there is a convenience method .run() to execute one task\n    const imageData = await djvuWorker.doc.getPage(60).getImageData().run();\n\n    // Let's draw the ImageData on a canvas\n    const canvas = document.querySelector('#async-canvas');\n    canvas.width = imageData.width;\n    canvas.height = imageData.height;\n    const ctx = canvas.getContext('2d');\n    ctx.putImageData(imageData, 0, 0); // But it will be much \n\n    // You can execute only one task via djvuWorker.run() too.\n    // In this case you will get only the result, not an array of results. \n    const imageDpi = await djvuWorker.run(djvuWorker.doc.getPage(60).getDpi()); // actually could be executed in one batch with getImageData()\n    canvas.style.width = imageData.width / (imageDpi / 100) + 'px';\n\n    // let's execute more operations \n    const [contents, count, sizes, test, textZones] = await djvuWorker.run(\n        djvuWorker.doc.getContents(),\n        djvuWorker.doc.getPagesQuantity(),\n        djvuWorker.doc.getPagesSizes(),\n        djvuWorker.doc.getPage(60).getText(),\n        djvuWorker.doc.getPage(60).getNormalizedTextZones(),\n    );\n\n    console.log('More data from async interface: ', contents, count, sizes, test, textZones);\n\n    // What's more is that the async interface allow you to create image URLs in a Web Worker\n    // asynchronously. If you get an ImageData and then create a URL from it via a canvas element,\n    // you still execute a rather costly operation on the main thread. So it's better to do it on the background thread.\n    // Namely for this feature, DjVu.js is bundled with Png.js. In Chrome, an image URL can be created via OffscreenCanvas\n    // in a web worker, but since Firefox hasn't supported OffscreenCanvas yet, Png.js is used as a polyfill.\n\n    // The Viewer's continuous scroll mode is based on this very feature.\n    // This method returns not a simple URL, but an object with some additional data\n    // to simplify rendering and memory management.\n    const pageData = await djvuWorker.doc.getPage(21).createPngObjectUrl().run();\n    console.log('Page image URL with some data', pageData);\n\n    const img = document.querySelector('#async-image');\n    img.src = pageData.url;\n    img.style.width = pageData.width / (pageData.dpi / 100) + 'px'; // scale it as well as canvases\n\n    // Internally URL.createObjectURL is used. It doesn't create a dataURI, but a short url to a blob inside the worker's memory.\n    // It's much faster than to create a dataURI string, but such a URL should be revoked manually after it is used, \n    // otherwise you will get a memory leak. Also, as practice has shown, you can't revoke a URL, created in a web worker,\n    // on the main thread, so you should revoke it in a web worker via djvuWorker.revokeObjectURL() method.\n    // The byteLength property allow you to count how much memory image blobs occupy in the memory now. Since png format is used,\n    // blobs are rather small compared to raw ImageData objects, but still if you don't revoke URLs at all you can get a\n    // noticeable memory leak. It should be mentioned, that you will not see this memory leak in a JS profiler - it can be seen\n    // only in an OS task manager.\n    img.onload = () => {\n        djvuWorker.revokeObjectURL(pageData.url);\n        console.log(pageData.byteLength, 'bytes were released in the worker memory after the image URL was revoked');\n    }\n\n    // In fact this feature with Object URL of pages can be used in the sync interface too.\n    // However there is not much sense in it, because it uses OffscreenCanvas or Png.js, while on the main thread you can\n    // just use a normal canvas element and control the process by yourself.\n}\n\nasync function main() {\n    // A DjVuDocument is built on top of an ArrayBuffer representing a file. \n    // In case of one-file bundled djvu an ArrayBuffer is all you need to construct a document.\n    const arrayBuffer = await fetch('/assets/DjVu3Spec.djvu').then(res => res.arrayBuffer());\n\n    // The sync interface is represented by DjVu.Document.\n    // Usually, you should use DjVu.Document only for debug purposes or maybe on server side (don't know will it work there).\n    // Synchronous operations on the main thread block UI and it spoils user experience badly.\n    // The DjVu.js Viewer uses only async interface (DjVu.Worker).\n    // But since the async API is based on the sync one, you should look at the sync interface first.\n    console.log('%c\\n\\n\\n Sync Interface results \\n\\n\\n\\n', \"font-size: 3em; color: blue\");\n    const djvuDocument = new DjVu.Document(arrayBuffer);\n\n    await syncInterfaceExamples(djvuDocument);\n\n    // The async interface is represented by DjVu.Worker.\n    // DjVu.Worker implicitly creates a Web Worker and all operations are executed on a background thread, \n    // not blocking the UI. For this reason, in case of the browser, \n    // it's by all means recommended to always use DjVu.Worker instead of DjVu.Document.\n    // We copy the array buffer, since the buffer is transferred to the worker and will not be available on the main thread anymore.\n    // Read about Transferable object here https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage\n    console.log('%c\\n\\n\\n Async Interface results \\n\\n\\n\\n', \"font-size: 3em; color: green\");\n    const arrayBufferCopy = arrayBuffer.slice(0);\n    const djvuWorker = new DjVu.Worker(); // do not pass anything to the constructor!\n    await djvuWorker.createDocument(arrayBufferCopy);\n\n    await asyncInterfaceExamples(djvuWorker);\n\n    // After all operation are done or you want to load another document to the worker, \n    // you may reset it to free inner structure and recreate a Web Worker. \n    // Right now there is no method to destroy it, but you can do it via djvuWorker.worker.terminate().\n    djvuWorker.reset();\n\n    // When you work with both async or sync interfaces you should remember about one thing.\n    // When a page is decoded, a lot of memory is allocated for inner structures during a decoding process.\n    // So a decoded page object takes about 30 MB of memory \n    // (by the way, an ImageData of 2539 * 3295 pixels takes 2539 * 3295 * 4 / 1024 / 1024 = 31.9 MB too).\n    // So if you decode 10 pages you get an overhead of 300 MB. It is pretty much.\n    // For this reason, .getPage() method saves the last requested page and reset it (clearing all inner buffers),\n    // when another page is requested. Thus, if you access pages only via .getPage() you will not get 300 MB overhead.\n    // But such behaviour means, that you should avoid accessing pages arbitrary. In other words, try to get all required data\n    // from one page before getting the second, otherwise a page will be decoded several times, every time you access it, and \n    // it's a rather long process, it can take from 100 to 1100 ms depending on a document.\n    // Actually, if you want to get something like width, height or dpi of a page, it's not decoded fully, and this metadata\n    // is extracted rather quickly, but you should remember: once you requested another page your current one is reset, and needs a full\n    // decoding to create an ImageData one more time.\n}\n\nvoid main();\n\n"
  },
  {
    "path": "library/debug/js/handler.js",
    "content": "'use strict';\n(function() {\nvar canvas = document.getElementById(\"canvas\");\nvar output = document.getElementById(\"output2\");\nfunction write(str) {\n    output.innerText = str;\n}\ncanvas.onclick = function (e) {\n    var rect = this.getBoundingClientRect();\n    write((e.clientX - rect.left) + \" \" + (e.clientY - rect.top));\n}\n})();"
  },
  {
    "path": "library/debug/js/initScript.js",
    "content": "\"use strict\";\n\n/**\n * Скрипт для тестирования библиотеки непосредственно в синхронном режиме\n */\n\nDjVu.setDebugMode(true);\n\nvar fileSize = 0;\nvar output;\nvar djvuArrayBuffer;\nvar djvuDocument;\nvar timeOutput = document.querySelector('#time_output');\nvar renderTimeOutput = document.querySelector('#render_time_output');\nvar rerunButton = document.querySelector('#rerun');\nrerunButton.onclick = rerun;\ndocument.querySelector('#redraw').onclick = redrawPage;\n\nvar pageNumber = 1;\nvar djvuUrl = 'assets/DjVu3Spec_5-10.djvu';\n// var djvuUrl = 'assets/carte.djvu';\nvar baseUrl = 'assets/DjVu3Spec_indirect/';\n\ndocument.querySelector('#next').onclick = () => {\n    pageNumber++;\n    redrawPage();\n};\n\ndocument.querySelector('#prev').onclick = () => {\n    pageNumber--;\n    redrawPage();\n}\n\nfunction saveStringAsFile(string) {\n    var link = document.createElement('a');\n    link.download = 'string.txt';\n    var blob = new Blob([string], { type: 'text/plain' });\n    link.href = window.URL.createObjectURL(blob);\n    link.click();\n}\n\nfunction saveImage(imageData) {\n    var canvas = document.createElement('canvas');\n    canvas.width = imageData.width;\n    canvas.height = imageData.height;\n    var ctx = canvas.getContext('2d');\n    ctx.putImageData(imageData, 0, 0);\n    var link = document.createElement('a');\n    link.download = 'image.png';\n    link.href = canvas.toDataURL();\n    link.click();\n}\n\nfunction saveStringAsBinFile(string) {\n    var link = document.createElement('a');\n    link.download = 'string.bin';\n    var array = new Uint16Array(string.length);\n    for (var i = 0; i < string.length; i++) {\n        array[i] = string.charCodeAt(i);\n    }\n    var blob = new Blob([array], { type: 'application/octet-binary' });\n    link.href = window.URL.createObjectURL(blob);\n    link.click();\n}\n\nfunction rerun() {\n    Globals.init();\n    Globals.clearCanvas();\n\n    setTimeout(async () => {\n        var start = performance.now();\n        await readDjvu(djvuArrayBuffer);\n        var time = performance.now() - start;\n        timeOutput.innerText = Math.round(time);\n    }, 0);\n}\n\nwindow.onload = function () {\n    output = document.getElementById(\"output\");\n    Globals.init();\n    // testFunc();\n    loadDjVu();\n    //loadPicture(); \n}\n\nfunction loadDjVu() {\n    var xhr = new XMLHttpRequest();\n    xhr.open(\"GET\", djvuUrl);\n    xhr.responseType = \"arraybuffer\";\n    xhr.onload = function (e) {\n        console.log(e.loaded);\n        fileSize = e.loaded;\n        djvuArrayBuffer = xhr.response;\n        rerun();\n        //splitDjvu(buf);\n    }\n    xhr.send();\n}\n\nfunction loadPicture() {\n    Globals.loadFile('samples/csl.djvu').then(buffer => {\n        readDjvu(buffer);\n    });\n}\n\nfunction readPicture(buffer) {\n\n    createImageBitmap(new Blob([buffer])).then(function (image) {\n        var pictureTotalTime = performance.now();\n        var canvas = document.getElementById('canvas2');\n        var c = canvas.getContext('2d');\n        canvas.width = image.width;\n        canvas.height = image.height;\n\n        c.drawImage(image, 0, 0);\n        var imageData = c.getImageData(0, 0, image.width, image.height);\n        var iwiw = new IWImageWriter(90, 0, 1);\n        // var doc = iwiw.createMultyPageDocument([imageData, imageData, imageData]);\n        iwiw.startMultyPageDocument();\n        iwiw.addPageToDocument(imageData);\n        //for (var i = 0; i < 5; i++) \n        var buffer = iwiw.endMultyPageDocument();\n        //var doc = new DjVuDocument(buffer);\n        // var doc = iwiw.createOnePageDocument(imageData);\n        console.log('docCreateTime = ', performance.now() - pictureTotalTime);\n        var link = document.querySelector('#dochref');\n        link.href = URL.createObjectURL(new Blob([buffer]));\n\n        // c.putImageData(doc.pages[0].getImage(), 0, 0);\n        console.log('Counter', Globals.counter);\n        //console.log('PZP', Globals.pzp.log.length, ' ', Globals.pzp.offset );\n        // writeln(doc.toString());\n        console.log('pictureTotalTime = ', performance.now() - pictureTotalTime);\n    });\n}\n\nconst sleep = (timeout = 0) => new Promise(resolve => setTimeout(resolve, timeout));\n\nasync function readDjvu(buf) {\n    console.log('redraw');\n    var link = document.querySelector('#dochref');\n    var time = performance.now();\n    console.log(\"Buffer length = \" + buf.byteLength);\n    djvuDocument = new DjVu.Document(buf, { baseUrl: baseUrl });\n    //console.log(djvuDocument.toString());\n    Globals.counter = 0;\n\n    // console.time('Document bundle');\n    // const bundle = await djvuDocument.bundle(p => {\n    //     console.log(p  * 100);\n    // });\n    // console.timeEnd('Document bundle');\n\n    // link.href = bundle.createObjectURL();\n\n    //writeln(djvuDocument.toString(true));\n\n    //saveStringAsFile(djvuDocument.toString())\n    //return;\n    //saveStringAsFile(JSON.stringify(djvuDocument.getContents()));\n\n    // const text = (await djvuDocument.getPage(pageNumber)).getText();\n    // console.log(text.length, text);\n    // writeln(text);\n    await redrawPage();\n    //saveStringAsBinFile(djvuDocument.toString());\n    // doc.countFiles();\n    //console.log(Globals.Timer.toString());\n    console.log(\"Total execution time = \", performance.now() - time);\n}\n\nasync function redrawPage() {\n    console.log('**** Render Page ****');\n    var time = performance.now();\n    var page = await djvuDocument.getPage(pageNumber);\n    var imageData = page.getImageData();\n    const dpi = page.getDpi();\n    //page.reset();\n    //saveImage(imageData);\n    Globals.drawImage(\n        imageData,\n        dpi * 4\n    );\n    //console.log(doc.pages[pageNumber].getText());\n    time = performance.now() - time;\n    console.log(\"Redraw time\", time);\n    console.log('**** ***** **** ****');\n\n    renderTimeOutput.innerText = Math.round(time);\n\n    // setTimeout(() => {\n    //     console.log('**** Refine Page ****');\n    //     var time = performance.now();\n    //     Globals.drawImage(\n    //         page.getImageData(),\n    //         page.getDpi() * 1.5\n    //     );\n    //     time = performance.now() - time;\n    //     console.log(\"Refine time\", time);\n    //     console.log('**** ***** **** ****');\n    // }, 50);\n\n}\n\nfunction prepareIframe() {\n    const iframe = document.createElement('iframe');\n    iframe.style.cssText = `\n        width: 0;\n        height: 0;\n        position: absolute;\n        left: 0;\n        top: 0;\n        opacity: 0;\n    `;\n    document.body.appendChild(iframe);\n    return iframe;\n}\n\ndocument.querySelector(('#print_button')).onclick = async () => {\n    const iframe = prepareIframe();\n\n    // await sleep(1);\n\n    console.log(iframe.contentWindow);\n    const promises = [];\n    for (let i = 1; i <= 2; i++) {\n        const page = await djvuDocument.getPage(i);\n        const image = await page.createPngObjectUrl();\n\n        const img = iframe.contentWindow.document.createElement('img');\n        promises.push(new Promise(resolve => img.onload = resolve));\n        img.style.display = 'block';\n        img.style.breakAfter = 'page';\n        img.style.breakInside = 'avoid';\n        img.style.margin = '0 auto';\n        img.src = image.url;\n        img.width = image.width;\n        img.height = image.height;\n        img.style.width = (image.width / image.dpi) + 'in';\n        img.style.height = (image.height / image.dpi) + 'in';\n        iframe.contentWindow.document.body.appendChild(img);\n    }\n\n    window.w = iframe.contentWindow;\n\n    if (/Firefox/.test(navigator.userAgent)) {\n        iframe.contentWindow.print();\n    } else {\n        await Promise.all(promises);\n        iframe.contentWindow.print();\n    }\n}\n\nfunction splitDjvu(buf) {\n    var link = document.querySelector('#dochref');\n    console.log(\"Buffer length = \" + buf.byteLength);\n    var doc = new DjVuDocument(buf);\n    var slice = doc.slice(0, 11);\n    link.href = slice.createObjectURL();\n}\n\n/**\n * Функция для работы с файлами загруженными вручную.\n */\nfunction main(files) {\n    clear();\n    console.log(files.length);\n    //readFile(file);\n    var fileReader = new FileReader();\n    var doc1, doc2;\n    fileReader.onload = function () {\n        if (!doc1) {\n            doc1 = new DjVuDocument(this.result);\n            fileReader.readAsArrayBuffer(files[1]);\n            return;\n        }\n\n        doc2 = new DjVuDocument(this.result);\n        testFunc(doc1, doc2);\n\n    };\n    if (files.length > 0) {\n        fileReader.readAsArrayBuffer(files[0]);\n    }\n}\n\nfunction testFunc(doc1, doc2) {\n    var doc = DjVuDocument.concat(doc1, doc2);\n    Globals.drawImageSmooth(doc.pages[0].getImage(), 600);\n    writeln(doc.toString());\n    var link = document.querySelector('#dochref');\n    link.href = doc.createObjectURL();\n}\n"
  },
  {
    "path": "library/debug/js/reloader.js",
    "content": "/**\n * A simple websocket client reloading a page when a bundle is updated.\n */\n(function () {\n    'use strict';\n    function setConnection() {\n        var address = 'ws://' + location.host;\n\n        console.log(`%cTrying to open a connention with ${address} ...`, \"color: blue\");\n        var ws = new WebSocket(address);\n        ws.onopen = () => console.info(`%cConnection is opened with ${address}. The page will be reloaded on each update.`, \"color: green\");\n\n        ws.onmessage = message => {\n            if (message.data === 'reload') {\n                window.location.reload();\n            }\n        };\n\n        ws.onclose = (e) => {\n            console.info(`%cConnection is closed!`, 'color: red');\n            setConnection();\n        }\n    }\n\n    setConnection();\n\n})();"
  },
  {
    "path": "library/debug/sync.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n    <title>DjVu.js</title>\n    <link rel=\"shortcut icon\" href=\"/catfav.png\" type=\"image/x-icon\" />\n    <script type=\"text/javascript\" src=\"/js/reloader.js\"></script>\n    <script type=\"text/javascript\" src=\"/dist/djvu.js\"></script>\n    <script type=\"text/javascript\" src=\"/js/DjVuGlobals.js\" defer></script>\n    <script type=\"text/javascript\" src=\"/js/debug.js\"></script>\n    <script type=\"text/javascript\" src=\"/js/initScript.js\" defer></script>\n    <link rel=\"stylesheet\" href=\"/css/style.css\">\n</head>\n\n<body>\n    <div class=\"control_block\">\n        <input type=\"button\" id=\"rerun\" value=\"rerun\">\n        <input type=\"button\" id=\"redraw\" value=\"redraw\">\n\n        <input type=\"button\" id=\"next\" value=\"next\">\n        <input type=\"button\" id=\"prev\" value=\"prev\">\n\n        <span id=\"time_output\"></span>\n        <span id=\"render_time_output\"></span>\n    </div>\n    <pre id=\"output\"></pre>\n    <canvas width=\"192\" height=\"256\" id=\"canvas\"></canvas>\n    <img id=\"img\" src=\"\" /><br>\n    <a id=\"dochref\" href=\"#\" download=\"file.djvu\">Скачать</a>\n    <a id=\"print_button\" href=\"#\">Печать</a>\n    <canvas width=\"200\" height=\"260\" id=\"canvas2\"></canvas>\n    <div id=\"output2\"></div>\n    <input type=\"file\" multiple onchange=\"main(this.files)\" />\n\n</body>\n\n</html>"
  },
  {
    "path": "library/package.json",
    "content": "{\n  \"name\": \"DjVu.js_Library\",\n  \"scripts\": {\n    \"start\": \"node server.js\",\n    \"watch\": \"rollup --config --watch\",\n    \"build\": \"rollup --config\"\n  },\n  \"devDependencies\": {\n    \"colors\": \"^1.4.0\",\n    \"express\": \"^4.18.2\",\n    \"rollup\": \"^3.28.0\",\n    \"rollup-plugin-cleanup\": \"^3.2.1\",\n    \"rollup-plugin-commonjs\": \"^10.1.0\",\n    \"rollup-plugin-node-resolve\": \"^5.2.0\",\n    \"ws\": \"^8.13.0\"\n  },\n  \"dependencies\": {\n    \"pngjs\": \"5.0.0\"\n  }\n}\n"
  },
  {
    "path": "library/rollup.config.js",
    "content": "'use strict';\n\nconst cleanup = require('rollup-plugin-cleanup');\nconst resolve = require('rollup-plugin-node-resolve');\nconst commonjs = require('rollup-plugin-commonjs');\n\nconst outputTemplate = {\n    format: 'iife',\n    name: 'DjVu',\n    intro: \"function DjVuScript() {\\n'use strict';\",\n    outro: \"}\\nreturn Object.assign(DjVuScript(), {DjVuScript});\"\n};\n\nmodule.exports = {\n    input: './src/index.js',\n    output: [\n        Object.assign({ file: 'dist/djvu.js' }, outputTemplate),\n        Object.assign({ file: '../viewer/public/tmp/djvu.js' }, outputTemplate),\n        Object.assign({ file: '../extension/dist/djvu.js' }, outputTemplate)\n    ],\n    plugins: [\n        resolve(),\n        commonjs(),\n        cleanup()\n    ]\n};"
  },
  {
    "path": "library/server.js",
    "content": "/**\n * A server used for debugging.\n */\n\n'use strict';\n\nconst http = require('http');\nconst fs = require('fs');\nconst WebSocket = require('ws');\nrequire('colors');\nconst path = require('path');\nconst rollup = require('rollup');\nconst express = require('express');\nconst rollupConfig = require('./rollup.config.js');\n\n// you can pass the parameter in the command line. e.g. node server.js 3000\nconst port = process.argv[2] || 9000;\n\nconst app = express();\n\napp.use(express.static(__dirname));\napp.use(express.static(__dirname + '/debug'));\napp.use('/tests', express.static('./tests', { index: 'tests.html' }));\n\n// used to debug the request interception in the extension according to the headers\napp.get('/file_without_extension', (req, res) => {\n    res.sendFile(path.resolve('./assets/DjVu3Spec.djvu'), {\n        headers: {\n            'Content-Type': 'application/octet-stream',\n            'Content-Disposition': 'inline; filename=\"TestFileName.djvu\"',\n        }\n    });\n});\n\napp.get('/get_embed_djvu_html', (req, res) => {\n    res.send(`\n<!DOCTYPE html>\n<html lang=\"en\">\n   <body><embed type=\"image/x-djvu\" src=\"${req.query.file}\" width=\"600\"></body>     \n</html>\n`);\n});\n\napp.use((req, res) => {\n    res.status(404).end('No such a page! 404 error!');\n});\n\nconst server = http.createServer(app);\n\nconst wss = new WebSocket.Server({ server: server });\nwss.broadcast = function (data) {\n    this.clients.forEach(client => {\n        client.send(data);\n    });\n}\n\nfs.watch('./debug/', { recursive: true }, () => wss.broadcast('reload')); // watch debug .js files\nfs.watch('./tests/', () => wss.broadcast('reload')); // watch tests files\n\nconst watcher = rollup.watch(rollupConfig);\n\nwatcher.on('event', event => {\n    switch (event.code) {\n        case 'BUNDLE_START':\n            console.log('Start building ...'.blue.bold);\n            break;\n        case 'BUNDLE_END':\n            console.log(`Bundle was created in ${event.duration} ms! \\n`.green.bold);\n            wss.broadcast('reload');\n            break;\n        case 'ERROR':\n            console.log('\\n Error occured! \\n'.red.bold);\n            console.log(event);\n            console.log('\\n');\n            break;\n        case 'FATAL':\n            console.log('\\n Fatal Error occured! \\n'.red.bold);\n            console.log(event);\n            process.exit();\n            break;\n    }\n});\n\nserver.listen(parseInt(port));\nconsole.log(`Http and WebSocket servers are listening on port ${port}`);"
  },
  {
    "path": "library/src/ByteStream.js",
    "content": "import { createStringFromUtf8Array } from './DjVu'\n\n/** @typedef {ByteStream} ByteStream */\n\n/**\n * Объект байтового потока. Предоставляет API для чтения сырого ArrayBuffer как потока байт.\n * После вызова каждого метода чтения, внутренний указатель смещается автоматически.\n * Можно читать числа, строки, массив байт разной длины. \n */\nexport default class ByteStream {\n    constructor(buffer, offsetx, length) {\n        this._buffer = buffer;\n        this.offsetx = offsetx || 0;\n        this.offset = 0;\n        this._length = length || buffer.byteLength;\n        if (this._length + offsetx > buffer.byteLength) {\n            this._length = buffer.byteLength - offsetx;\n            console.error(\"Incorrect length in ByteStream!\");\n        }\n        this.viewer = new DataView(this._buffer, this.offsetx, this._length);\n    }\n\n    /** @returns {number} */\n    get length() { return this._length; }\n\n    /** @returns {ArrayBuffer} */\n    get buffer() { return this._buffer; }\n\n    // \"читает\" следующие length байт в массив, возвращает массив основанный на том же ArrayBuffer\n    getUint8Array(length = this.remainingLength()) {\n        var off = this.offset;\n        this.offset += length;\n        return new Uint8Array(this._buffer, this.offsetx + off, length);\n    }\n\n    // возвращает массив полностью представляющий весь поток\n    toUint8Array() {\n        return new Uint8Array(this._buffer, this.offsetx, this._length);\n    }\n\n    remainingLength() {\n        return this._length - this.offset;\n    }\n\n    reset() {\n        this.offset = 0;\n    }\n\n    byte() { // the function is used inside other codecs (look at ZPCodec)\n        if (this.offset >= this._length) {\n            this.offset++;\n            return 0xff;\n        }\n        return this.viewer.getUint8(this.offset++);\n    }\n\n    getInt8() {\n        return this.viewer.getInt8(this.offset++);\n    }\n    getInt16() {\n        var tmp = this.viewer.getInt16(this.offset);\n        this.offset += 2;\n        return tmp;\n    }\n    getUint16() {\n        var tmp = this.viewer.getUint16(this.offset);\n        this.offset += 2;\n        return tmp;\n    }\n    getInt32() {\n        var tmp = this.viewer.getInt32(this.offset);\n        this.offset += 4;\n        return tmp;\n    }\n    getUint8() {\n        return this.viewer.getUint8(this.offset++);\n    }\n\n    getInt24() {\n        var uint = this.getUint24();\n        return (uint & 0x800000) ? (0xffffff - val + 1) * -1 : uint\n    }\n\n    getUint24() {\n        return (this.byte() << 16) | (this.byte() << 8) | this.byte();\n    }\n\n    jump(length) {\n        this.offset += length;\n        return this;\n    }\n\n    setOffset(offset) {\n        this.offset = offset;\n    }\n\n    readStr4() { // used to read chunk names, just ASCII characters\n        return String.fromCharCode(...this.getUint8Array(4));\n    }\n\n    readStrNT() {\n        var array = [];\n        var byte = this.getUint8();\n        while (byte) {\n            array.push(byte);\n            byte = this.getUint8();\n        }\n        return createStringFromUtf8Array(new Uint8Array(array));\n    }\n\n    readStrUTF(byteLength) {\n        return createStringFromUtf8Array(this.getUint8Array(byteLength));\n    }\n\n    fork(length = this.remainingLength()) {\n        return new ByteStream(this._buffer, this.offsetx + this.offset, length);\n    }\n\n    clone() {\n        return new ByteStream(this._buffer, this.offsetx, this._length);\n    }\n\n    isEmpty() {\n        return this.offset >= this._length;\n    }\n}\n"
  },
  {
    "path": "library/src/ByteStreamWriter.js",
    "content": "import { stringToCodePoints, codePointsToUtf8 } from './DjVu';\n\nconst pageSize = 64 * 1024;\nconst growthLimit = 20 * 1024 * 1024 / pageSize;\n\nexport default class ByteStreamWriter {\n    constructor(length = 0) {\n        // As the practice has shown, usage of WebAssembly.Memory and its grow() method\n        // is more robust than the manual expansion of ArrayBuffer \n        // via `new Uint8Array(newBuffer).set(new Uint8Array(oldBuffer))`.\n        // In particular, with WebAssembly.Memory it's possible to download and bundle\n        // a document that is about 1.7 GB in size, while with raw ArrayBuffers \n        // a browser tab crashes (in Chrome) when the buffer reaches about 1.5 GB \n        // (or there is an error that a buffer cannot be allocated).\n        this.memory = new WebAssembly.Memory({ initial: Math.ceil(length / pageSize), maximum: 65536 });\n        this.assignBufferFromMemory();\n\n        this.offset = 0;\n        this.offsetMarks = {};\n    }\n\n    assignBufferFromMemory() {\n        this.buffer = this.memory.buffer;\n        this.viewer = new DataView(this.buffer);\n    }\n\n    /**\n     * Переводит смещение на начало и зачищает сохраненные смещения\n     */\n    reset() {\n        this.offset = 0;\n        this.offsetMarks = {};\n    }\n\n    saveOffsetMark(mark) {\n        this.offsetMarks[mark] = this.offset;\n        return this;\n    }\n\n    writeByte(byte) {\n        this.checkOffset(1);\n        this.viewer.setUint8(this.offset++, byte);\n        return this;\n    }\n\n    writeStr(str) {\n        this.writeArray(codePointsToUtf8(stringToCodePoints(str)));\n        return this;\n    }\n\n    writeInt32(val) {\n        this.checkOffset(4);\n        this.viewer.setInt32(this.offset, val);\n        this.offset += 4;\n        return this;\n    }\n\n    /**\n     * Перезапись числа. Принимает смещение или метку смещения и число\n     */\n    rewriteInt32(off, val) {\n        var xoff = off;\n        if (typeof (xoff) === 'string') {\n            xoff = this.offsetMarks[off];\n            this.offsetMarks[off] += 4;\n        }\n        this.viewer.setInt32(xoff, val);\n    }\n\n\n    /**\n     * Перезапись размера в 4 байта по сохраненной метке\n     */\n    rewriteSize(offmark) {\n        if (!this.offsetMarks[offmark]) throw new Error('Unexisting offset mark');\n        var xoff = this.offsetMarks[offmark];\n        this.viewer.setInt32(xoff, this.offset - xoff - 4);\n    }\n\n    getBuffer() {\n        if (this.offset === this.buffer.byteLength) {\n            return this.buffer;\n        }\n        return this.buffer.slice(0, this.offset);\n    }\n\n    checkOffset(requiredBytesNumber = 0) {\n        const bool = this.offset + requiredBytesNumber > this.buffer.byteLength;\n        if (bool) {\n            this._expand(requiredBytesNumber);\n        }\n        return bool;\n    }\n\n    _expand(requiredBytesNumber) {\n        this.memory.grow(Math.max(\n            Math.ceil(requiredBytesNumber / pageSize),\n            Math.min(this.memory.buffer.byteLength / pageSize, growthLimit)\n        ));\n        this.assignBufferFromMemory();\n    }\n\n    //смещение на length байт\n    jump(length) {\n        length = +length;\n        if (length > 0) {\n            this.checkOffset(length);\n        }\n        this.offset += length;\n        return this;\n    }\n\n    writeByteStream(bs) {\n        this.writeArray(bs.toUint8Array());\n    }\n\n    writeArray(arr) {\n        while (this.checkOffset(arr.length)) { }\n        new Uint8Array(this.buffer).set(arr, this.offset);\n        this.offset += arr.length;\n    }\n\n    writeBuffer(buffer) {\n        this.writeArray(new Uint8Array(buffer));\n    }\n\n    writeStrNT(str) {\n        this.writeStr(str);\n        this.writeByte(0);\n    }\n\n    writeInt16(val) {\n        this.checkOffset(2);\n        this.viewer.setInt16(this.offset, val);\n        this.offset += 2;\n        return this;\n    }\n\n    writeUint16(val) {\n        this.checkOffset(2);\n        this.viewer.setUint16(this.offset, val);\n        this.offset += 2;\n        return this;\n    }\n\n    writeInt24(val) {\n        this.writeByte((val >> 16) & 0xff)\n            .writeByte((val >> 8) & 0xff)\n            .writeByte(val & 0xff);\n        return this;\n    }\n}\n"
  },
  {
    "path": "library/src/DjVu.js",
    "content": "var DjVu = {\n    VERSION: '0.5.4',\n    IS_DEBUG: false,\n    setDebugMode: (flag) => DjVu.IS_DEBUG = flag\n};\n\nexport function pLimit(limit = 4) {\n    const queue = [];\n    let running = 0;\n\n    const runNext = async () => {\n        if (!queue.length || running >= limit) return;\n        const func = queue.shift();\n\n        try {\n            running++;\n            await func();\n        } finally {\n            running--;\n            runNext();\n        }\n    };\n\n    return func => new Promise((resolve, reject) => {\n        queue.push(() => func().then(resolve, reject));\n        runNext();\n    });\n}\n\n/**\n *  @returns {Promise<ArrayBuffer>}\n */\nexport function loadFileViaXHR(url, responseType = 'arraybuffer') {\n    return new Promise((resolve, reject) => {\n        var xhr = new XMLHttpRequest();\n        xhr.open(\"GET\", url);\n        xhr.responseType = responseType;\n        xhr.onload = (e) => resolve(xhr);\n        xhr.onerror = (e) => reject(xhr);\n        xhr.send();\n    });\n}\n\nconst utf8Decoder = self.TextDecoder ? new self.TextDecoder() : {\n    decode(utf8array) {\n        const codePoints = utf8ToCodePoints(utf8array);\n        return String.fromCodePoint(...codePoints);\n    }\n};\n\nexport function createStringFromUtf8Array(utf8array) {\n    return utf8Decoder.decode(utf8array);\n}\n\n/**\n * Creates an array of Unicode code points from an array, representing a utf8 encoded string\n * The code assumes that the utf-8 input is well formed. Otherwise, can produce illegal code \n * points. As the practice has shown, there are ill-formed utf-8 arrays in some djvu files.\n * \n * This function should be removed in the future. The standard TextDecoder/TextEncoder should\n * be used instead. Its was initially written only for the old Edge browser \n * which didn't support TextDecoder.\n */\nexport function utf8ToCodePoints(utf8array) {\n    var i, c;\n    var codePoints = [];\n\n    i = 0;\n    while (i < utf8array.length) {\n        c = utf8array[i++];\n        switch (c >> 4) {\n            case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:\n                // 0xxx xxxx\n                codePoints.push(c);\n                break;\n            case 12: case 13:\n                // 110x xxxx   10xx xxxx\n                codePoints.push(((c & 0x1F) << 6) | (utf8array[i++] & 0x3F));\n                break;\n            case 14:\n                // 1110 xxxx  10xx xxxx  10xx xxxx      \n                codePoints.push(\n                    ((c & 0x0F) << 12) |\n                    ((utf8array[i++] & 0x3F) << 6) |\n                    (utf8array[i++] & 0x3F)\n                );\n                break;\n            case 15:\n                // 1111 0xxx  10xx xxxx  10xx xxxx  10xx xxxx\n                codePoints.push(\n                    ((c & 0x07) << 18) |\n                    ((utf8array[i++] & 0x3F) << 12) |\n                    ((utf8array[i++] & 0x3F) << 6) |\n                    (utf8array[i++] & 0x3F)\n                );\n                break;\n        }\n    }\n    return codePoints.map(codePoint => {\n        return codePoint > 0x10FFFF ? 120 : codePoint; // replace all bad code points with \"x\"\n    });\n}\n\nexport function codePointsToUtf8(codePoints) {\n    var utf8array = [];\n    codePoints.forEach(codePoint => {\n        if (codePoint < 0x80) {\n            utf8array.push(codePoint);\n        } else if (codePoint < 0x800) {\n            utf8array.push(0xC0 | (codePoint >> 6));\n            utf8array.push(0x80 | (codePoint & 0x3F));\n        } else if (codePoint < 0x10000) {\n            utf8array.push(0xE0 | (codePoint >> 12));\n            utf8array.push(0x80 | ((codePoint >> 6) & 0x3F));\n            utf8array.push(0x80 | (codePoint & 0x3F));\n        } else {\n            utf8array.push(0xF0 | (codePoint >> 18));\n            utf8array.push(0x80 | ((codePoint >> 12) & 0x3F));\n            utf8array.push(0x80 | ((codePoint >> 6) & 0x3F));\n            utf8array.push(0x80 | (codePoint & 0x3F));\n        }\n    });\n\n    return new Uint8Array(utf8array);\n}\n\nexport function stringToCodePoints(str) {\n    var codePoints = [];\n    for (var i = 0; i < str.length; i++) {\n        var code = str.codePointAt(i);\n        codePoints.push(code);\n        if (code > 65535) {\n            i++; // skip the second part of 4 byte symbol\n        }\n    }\n\n    return codePoints;\n}\n\n// unicode test: symbols of 1, 2, 4 and 3 bytes (in utf8 encoding) are encoded and decoded\n// var str = 'szвф' + String.fromCodePoint(0x1F702) + String.fromCodePoint(0x1F704) + String.fromCodePoint(0x2C00) + String.fromCodePoint(0x2C08);\n// var str2 = String.fromCodePoint(...utf8ToCodePoints(codePointsToUtf8(stringToCodePoints(str))));\n// console.log(str, str2, str === str2);\n\nexport default DjVu;"
  },
  {
    "path": "library/src/DjVuDocument.js",
    "content": "import DjViChunk from './chunks/DjViChunk';\nimport DjVuPage from './DjVuPage';\nimport DIRMChunk from './chunks/DirmChunk';\nimport NAVMChunk from './chunks/NavmChunk';\nimport DjVuWriter from './DjVuWriter';\nimport DjVu from './DjVu';\nimport ThumChunk from './chunks/ThumChunk';\nimport ByteStream from './ByteStream';\nimport { loadPageDependency, loadPage } from './methods/load';\nimport {\n    IncorrectFileFormatDjVuError,\n    NoSuchPageDjVuError,\n    CorruptedFileDjVuError,\n    NoBaseUrlDjVuError,\n} from './DjVuErrors';\n\n/** @typedef {DjVuDocument} DjVuDocument */\n\nconst MEMORY_LIMIT = 50 * 1024 * 1024; // 50 MB\n\nexport default class DjVuDocument {\n    constructor(arraybuffer, { baseUrl = null, memoryLimit = MEMORY_LIMIT } = {}) {\n        this.buffer = arraybuffer;\n        this.baseUrl = baseUrl && baseUrl.trim();\n        if (typeof this.baseUrl === 'string') {\n            if (this.baseUrl[this.baseUrl.length - 1] !== '/') {\n                this.baseUrl += '/';\n            }\n            if (!/^[A-Za-z]+:\\/\\//.test(this.baseUrl)) { // a relative URL\n                // all URL in a worker should be absolute\n                // in case of a local web page opened as file:/// there is no location.origin.\n                this.baseUrl = location.origin && (new URL(this.baseUrl, location.origin).href);\n            }\n        }\n        this.memoryLimit = memoryLimit; // required to limit the size of cache in case of indirect djvu\n\n        this.djvi = {}; //разделяемые ресурсы. Могут потребоваться и в случае одностраничного документа\n        this.getINCLChunkCallback = id => this.djvi[id].innerChunk;\n\n        this.bs = new ByteStream(arraybuffer);\n        this.formatID = this.bs.readStr4();\n        if (this.formatID !== 'AT&T') {\n            throw new IncorrectFileFormatDjVuError();\n        }\n        this.id = this.bs.readStr4();\n        this.length = this.bs.getInt32();\n        this.id += this.bs.readStr4();\n        if (this.id === 'FORMDJVM') {\n            this._initMultiPageDocument();\n        } else if (this.id === 'FORMDJVU') {\n            this.bs.jump(-12);\n            this.pages = [new DjVuPage(this.bs.fork(this.length + 8), this.getINCLChunkCallback)];\n        } else {\n            throw new CorruptedFileDjVuError(\n                `The id of the first chunk of the document should be either FORMDJVM or FORMDJVU, but there is ${this.id}`\n            );\n        }\n    }\n\n    _initMultiPageDocument() { // for FORMDJVM\n        this._readMetaDataChunk();\n        this._readContentsChunkIfExists();\n\n        /**\n         * @type {Array<DjVuPage>}\n         */\n        this.pages = []; //страницы FORMDJVU\n        this.thumbs = [];\n\n        if (this.dirm.isBundled) {\n            this._parseComponents();\n        } else {\n            this.pages = new Array(this.dirm.getPagesQuantity()); // fixed length array in order to know what pages are loaded and what are not.\n            this.memoryUsage = this.bs.buffer.byteLength;\n            this.loadedPageNumbers = [];\n        }\n    }\n\n    _readMetaDataChunk() { // DIRM chunk\n        var id = this.bs.readStr4();\n        if (id !== 'DIRM') {\n            throw new CorruptedFileDjVuError(\"The DIRM chunk must be the first but there is \" + id + \" instead!\");\n        }\n        var length = this.bs.getInt32();\n        this.bs.jump(-8);\n        this.dirm = new DIRMChunk(this.bs.fork(length + 8)); // document directory, metadata for multi-page documents\n        this.bs.jump(8 + length + (length & 1 ? 1 : 0));\n    }\n\n    _readContentsChunkIfExists() { // NAVM chunk\n        this.navm = null; // человеческое оглавление \n        if (this.bs.remainingLength() > 8) {\n            var id = this.bs.readStr4();\n            var length = this.bs.getInt32();\n            this.bs.jump(-8);\n            if (id === 'NAVM') {\n                this.navm = new NAVMChunk(this.bs.fork(length + 8))\n            }\n        }\n    }\n\n    _parseComponents() {\n        // all chunks of the file in the order which they are listed in the DIRM chunk\n        this.dirmOrderedChunks = new Array(this.dirm.getFilesQuantity());\n\n        for (var i = 0; i < this.dirm.offsets.length; i++) {\n            this.bs.setOffset(this.dirm.offsets[i]);\n            var id = this.bs.readStr4();\n            var length = this.bs.getInt32();\n            id += this.bs.readStr4();\n            this.bs.jump(-12);\n            switch (id) {\n                case \"FORMDJVU\":\n                    this.pages.push(this.dirmOrderedChunks[i] = new DjVuPage(\n                        this.bs.fork(length + 8),\n                        this.getINCLChunkCallback\n                    ));\n                    break;\n                case \"FORMDJVI\":\n                    //через строчку id chunk INCL ссылается на нужный ресурс\n                    this.dirmOrderedChunks[i] = this.djvi[this.dirm.ids[i]] = new DjViChunk(this.bs.fork(length + 8));\n                    break;\n                case \"FORMTHUM\":\n                    this.thumbs.push(this.dirmOrderedChunks[i] = new ThumChunk(this.bs.fork(length + 8)));\n                    break;\n                default:\n                    console.error(\"Incorrect chunk ID: \", id);\n            }\n        }\n    }\n\n    /**\n     * @returns {Array<{ width: number, height: number, dpi: number }>} \n     */\n    getPagesSizes() {\n        var sizes = this.pages.map(page => {\n            return {\n                width: page.getWidth(),\n                height: page.getHeight(),\n                dpi: page.getDpi(),\n            };\n        });\n        this.pages.forEach(page => page.reset());\n        return sizes;\n    }\n\n    isBundled() {\n        return this.dirm ? this.dirm.isBundled : true;\n    }\n\n    getPagesQuantity() {\n        return this.dirm ? this.dirm.getPagesQuantity() : 1;\n    }\n\n    /** @returns {import('./chunks/NavmChunk').Contents} */\n    getContents() {\n        return this.navm ? this.navm.getContents() : null;\n    }\n\n    getMemoryUsage() {\n        return this.memoryUsage;\n    }\n\n    getMemoryLimit() {\n        return this.memoryLimit;\n    }\n\n    setMemoryLimit(limit = MEMORY_LIMIT) {\n        this.memoryLimit = limit;\n    }\n\n    getPageNumberByUrl(url) {\n        if (url[0] !== '#') {\n            return null;\n        }\n\n        const ref = url.slice(1);\n        let pageNumber = this.dirm.getPageNumberByItsId(ref);\n        if (!pageNumber) {\n            const num = Math.round(Number(ref));\n            if (num >= 1 && num <= this.pages.length) { // there can be refs like \"#057\";\n                pageNumber = num;\n            }\n        }\n\n        return pageNumber || null;\n    }\n\n    releaseMemoryIfRequired(preservedDependencies = null) {\n        if (this.memoryUsage <= this.memoryLimit) {\n            //console.log(`%c Memory wasnt released  ${this.memoryUsage}, ${this.memoryLimit}, ${this.loadedPageNumbers.length}, ${Object.keys(this.djvi).length}`, \"color: green\");\n            return;\n        }\n        //var was = this.memoryUsage;\n        while (this.memoryUsage > this.memoryLimit && this.loadedPageNumbers.length) {\n            var number = this.loadedPageNumbers.shift();\n            this.memoryUsage -= this.pages[number].bs.buffer.byteLength;\n            this.pages[number] = null;\n        }\n\n        if (this.memoryUsage > this.memoryLimit && !this.loadedPageNumbers.length) { // remove all dictionaries, if there is no pages\n            this.resetLastRequestedPage();\n\n            var newDjVi = {};\n            if (preservedDependencies) {\n                preservedDependencies.forEach(id => {\n                    newDjVi[id] = this.djvi[id];\n                    this.memoryUsage += newDjVi[id].bs.buffer.byteLength; // will be subtracted back further\n                });\n            }\n            Object.keys(this.djvi).forEach(key => {\n                this.memoryUsage -= this.djvi[key].bs.buffer.byteLength;\n            });\n\n            this.djvi = newDjVi;\n        }\n        //console.log(`%c Memory was released ${was}, ${this.memoryUsage}, ${this.loadedPageNumbers.length}, ${Object.keys(this.djvi).length}`, \"color: red\");\n    }\n\n    _getUrlByPageNumber(number) {\n        return this.baseUrl + this.dirm.getPageNameByItsNumber(number);\n    }\n\n    async getPage(number) {\n        var page = this.pages[number - 1];\n        if (this.lastRequestedPage && this.lastRequestedPage !== page) {\n            this.lastRequestedPage.reset();\n        }\n        this.lastRequestedPage = page;\n\n        if (!page) {\n            if (number < 1 || number > this.pages.length || this.isBundled()) {\n                throw new NoSuchPageDjVuError(number);\n            } else {\n                if (this.baseUrl === null) {\n                    throw new NoBaseUrlDjVuError();\n                }\n                const bs = await loadPage(\n                    number,\n                    this._getUrlByPageNumber(number)\n                );\n\n                const page = new DjVuPage(bs, this.getINCLChunkCallback);\n                this.memoryUsage += bs.buffer.byteLength;\n                await this._loadDependencies(page.getDependencies(), number);\n\n                this.releaseMemoryIfRequired(page.getDependencies()); // should be called before the page are added to the pages array\n                this.pages[number - 1] = page;\n                this.loadedPageNumbers.push(number - 1);\n                this.lastRequestedPage = page;\n            }\n        } else if (!this.isOnePageDependenciesLoaded && this.id === \"FORMDJVU\") { // single page document\n            var dependencies = page.getDependencies();\n            if (dependencies.length) {\n                await this._loadDependencies(dependencies, 1);\n            }\n            this.isOnePageDependenciesLoaded = true;\n        }\n\n        return this.lastRequestedPage;\n    }\n\n    async _loadDependencies(dependencies, pageNumber = null) {\n        var unloadedDependencies = dependencies.filter(id => !this.djvi[id]);\n        if (!unloadedDependencies.length) {\n            return;\n        }\n        await Promise.all(unloadedDependencies.map(async id => {\n            const bs = await loadPageDependency(\n                id,\n                this.dirm ? this.dirm.getComponentNameByItsId(id) : id,\n                this.baseUrl,\n                pageNumber\n            );\n\n            this.djvi[id] = new DjViChunk(bs);\n            this.memoryUsage += bs.buffer.byteLength;\n        }));\n    }\n\n    getPageUnsafe(number) {\n        return this.pages[number - 1];\n    }\n\n    resetLastRequestedPage() {\n        this.lastRequestedPage && this.lastRequestedPage.reset();\n        this.lastRequestedPage = null;\n    }\n\n    /** A debug function, isn't actually used */\n    countFiles() {\n        var count = 0;\n        var bs = this.bs.clone();\n        bs.jump(16);\n        while (!bs.isEmpty()) {\n            var id = bs.readStr4();\n            var length = bs.getInt32();\n            // перепрыгнули к следующей порции\n            bs.jump(length + (length & 1 ? 1 : 0));\n            if (id === 'FORM') {\n                count++;\n            }\n        }\n        return count;\n    }\n\n    /**\n     * Возвращает метаданные документа. \n     * @param {Boolean} html - заменять ли \\n на <br>\n     * @returns {string} строка метаданных\n     */\n    toString(html) {\n        var str = this.formatID + '\\n';\n        if (this.dirm) { // multi page document\n            str += this.id + \" \" + this.length + '\\n\\n';\n            str += this.dirm.toString();\n            str += this.navm ? this.navm.toString() : '';\n            if (this.isBundled()) {\n                this.dirmOrderedChunks.forEach((chunk, i) => {\n                    str += this.dirm.getMetadataStringByIndex(i) + chunk.toString();\n                });\n            } else {\n                for (let i = 0; i < this.dirm.getFilesQuantity(); i++) {\n                    str += this.dirm.getMetadataStringByIndex(i);\n                }\n            }\n        } else { // single page document\n            str += this.pages[0].toString();\n        }\n\n        return html ? str.replace(/\\n/g, '<br>').replace(/\\s/g, '&nbsp;') : str;\n    }\n\n    /**\n     * Создает ссылку для скачивания документа\n     */\n    createObjectURL() {\n        var blob = new Blob([this.bs.buffer]);\n        var url = URL.createObjectURL(blob);\n        return url;\n    }\n\n    /**\n     *  Creates a new DjVuDocument with pages from \"from\" to \"to\", including first and last pages.\n     */\n    slice(from = 1, to = this.pages.length) {\n        const djvuWriter = new DjVuWriter();\n        djvuWriter.startDJVM();\n        const dirm = {\n            dflags: this.dirm.dflags,\n            flags: [],\n            names: [],\n            titles: [],\n            sizes: [],\n            ids: [],\n        };\n        const chunkByteStreams = [];\n        const totalPageCount = to - from + 1;\n        // все зависимости страниц в новом документе\n        // нужно чтобы не копировать лишние словари\n        const dependencies = {};\n        const filesQuantity = this.dirm.getFilesQuantity();\n\n        // находим все зависимости в первом проходе\n        for (\n            let i = 0, pageIndex = 0, addedPageCount = 0;\n            i < filesQuantity && addedPageCount < totalPageCount;\n            i++\n        ) {\n            const isPage = (this.dirm.flags[i] & 63) === 1;\n            if (!isPage) continue;\n            pageIndex++;\n            if (pageIndex < from) continue;\n\n            addedPageCount++;\n            const pageByteStream = new ByteStream(this.buffer, this.dirm.offsets[i], this.dirm.sizes[i]);\n            const deps = new DjVuPage(pageByteStream).getDependencies();\n            for (const dependencyId of deps) {\n                dependencies[dependencyId] = 1;\n            }\n        }\n\n        // теперь все словари и страницы, которые нужны\n        for (\n            let i = 0, pageIndex = 0, addedPageCount = 0;\n            // ?? maybe dicts can go after pages and we should check all chunks (remove addedPageCount < totalPageCount)\n            i < filesQuantity && addedPageCount < totalPageCount;\n            i++\n        ) {\n            const isPage = (this.dirm.flags[i] & 63) === 1;\n            if (isPage) {\n                pageIndex++;\n                //если она не входит в заданный диапазон\n                if (pageIndex < from) continue;\n                addedPageCount++;\n            }\n\n\n            //копируем страницы и словари. Эскизы пропускаем - пока что это не реализовано\n            if ((this.dirm.ids[i] in dependencies) || isPage) {\n                dirm.flags.push(this.dirm.flags[i]);\n                dirm.sizes.push(this.dirm.sizes[i]);\n                dirm.ids.push(this.dirm.ids[i]);\n                dirm.names.push(this.dirm.names[i]);\n                dirm.titles.push(this.dirm.titles[i]);\n                chunkByteStreams.push(\n                    new ByteStream(this.buffer, this.dirm.offsets[i], this.dirm.sizes[i])\n                );\n            }\n        }\n\n        djvuWriter.writeDirmChunk(dirm);\n        if (this.navm) {\n            djvuWriter.writeChunk(this.navm);\n        }\n\n        for (const chunkByteStream of chunkByteStreams) {\n            djvuWriter.writeFormChunkBS(chunkByteStream);\n        }\n        const newBuffer = djvuWriter.getBuffer();\n        DjVu.IS_DEBUG && console.log(\"New Buffer size = \", newBuffer.byteLength);\n\n        return new DjVuDocument(newBuffer);\n    }\n\n    /**\n     * Функция склейки двух документов\n     */\n    static concat(doc1, doc2) {\n        var dirm = {};\n        var length = doc1.pages.length + doc2.pages.length;\n        dirm.dflags = 129;\n        dirm.flags = [];\n        dirm.sizes = [];\n        dirm.ids = [];\n        var pages = [];\n        var idset = new Set(); // чтобы убрать повторяющиеся id\n\n        if (!doc1.dirm) { // тогда  записываем свой id\n            dirm.flags.push(1);\n            dirm.sizes.push(doc1.pages[0].bs.length);\n            dirm.ids.push('single');\n            idset.add('single');\n            pages.push(doc1.pages[0]);\n        }\n        else {\n            for (var i = 0; i < doc1.pages.length; i++) {\n                dirm.flags.push(doc1.dirm.flags[i]);\n                dirm.sizes.push(doc1.dirm.sizes[i]);\n                dirm.ids.push(doc1.dirm.ids[i]);\n                idset.add(doc1.dirm.ids[i]);\n                pages.push(doc1.pages[i]);\n            }\n        }\n        if (!doc2.dirm) { // тогда  записываем свой id\n            dirm.flags.push(1);\n            dirm.sizes.push(doc2.pages[0].bs.length);\n\n            var newid = 'single2';\n            var tmp = 0;\n            while (idset.has(newid)) { // генерируем уникальный id\n                newid = 'single2' + tmp.toString();\n                tmp++;\n            }\n            dirm.ids.push(newid);\n            pages.push(doc2.pages[0]);\n        }\n        else {\n            for (var i = 0; i < doc2.pages.length; i++) {\n                dirm.flags.push(doc2.dirm.flags[i]);\n                dirm.sizes.push(doc2.dirm.sizes[i]);\n                var newid = doc2.dirm.ids[i];\n                var tmp = 0;\n                while (idset.has(newid)) { // генерируем уникальный id\n                    newid = doc2.dirm.ids[i] + tmp.toString();\n                    tmp++;\n                }\n                dirm.ids.push(newid);\n                idset.add(newid);\n                pages.push(doc2.pages[i]);\n            }\n        }\n\n        var dw = new DjVuWriter();\n        dw.startDJVM();\n        dw.writeDirmChunk(dirm);\n        for (var i = 0; i < length; i++) {\n            dw.writeFormChunkBS(pages[i].bs);\n        }\n\n        return new DjVuDocument(dw.getBuffer());\n    }\n}\n\nimport bundle from './methods/bundle';\n\nObject.assign(DjVuDocument.prototype, {\n    bundle,\n});"
  },
  {
    "path": "library/src/DjVuErrors.js",
    "content": "/**\n * Простейший класс ошибки, не содержит рекурсивных данных, чтобы иметь возможность копироваться\n * между потоками в сообщениях\n */\nexport class DjVuError {\n    constructor(code, message, additionalData = null) {\n        this.code = code;\n        this.message = message;\n        if (additionalData) this.additionalData = additionalData;\n    }\n}\n\nexport class IncorrectFileFormatDjVuError extends DjVuError {\n    constructor() {\n        super(DjVuErrorCodes.INCORRECT_FILE_FORMAT, \"The provided file is not a .djvu file!\");\n    }\n}\n\nexport class NoSuchPageDjVuError extends DjVuError {\n    constructor(pageNumber) {\n        super(DjVuErrorCodes.NO_SUCH_PAGE, \"There is no page with the number \" + pageNumber + \" !\");\n        this.pageNumber = pageNumber;\n    }\n}\n\nexport class CorruptedFileDjVuError extends DjVuError {\n    constructor(message = \"\", data = null) {\n        super(DjVuErrorCodes.FILE_IS_CORRUPTED, \"The file is corrupted! \" + message, data);\n    }\n}\n\nexport class UnableToTransferDataDjVuError extends DjVuError {\n    constructor(tasks) {\n        super(DjVuErrorCodes.DATA_CANNOT_BE_TRANSFERRED,\n            \"The data cannot be transferred from the worker to the main page! \" +\n            \"Perhaps, you requested a complex object like DjVuPage, but only simple objects can be transferred between workers.\"\n        );\n        this.tasks = tasks;\n    }\n}\n\nexport class IncorrectTaskDjVuError extends DjVuError {\n    constructor(task) {\n        super(DjVuErrorCodes.INCORRECT_TASK, \"The task contains an incorrect sequence of functions!\");\n        this.task = task;\n    }\n}\n\nexport class NoBaseUrlDjVuError extends DjVuError {\n    constructor() {\n        super(DjVuErrorCodes.NO_BASE_URL,\n            \"The base URL is required for the indirect djvu to load components,\" +\n            \" but no base URL was provided to the document constructor!\"\n        );\n    }\n}\n\nfunction getErrorMessageByData(data) {\n    var message = '';\n    if (data.pageNumber) {\n        if (data.dependencyId) {\n            message = `A dependency ${data.dependencyId} for the page number ${data.pageNumber} can't be loaded!\\n`;\n        } else {\n            message = `The page number ${data.pageNumber} can't be loaded!`;\n        }\n    } else if (data.dependencyId) {\n        message = `A dependency ${data.dependencyId} can't be loaded!\\n`;\n    }\n    return message;\n}\n\nexport class UnsuccessfulRequestDjVuError extends DjVuError {\n    constructor(xhr, data = { pageNumber: null, dependencyId: null }) {\n        var message = getErrorMessageByData(data);\n        super(DjVuErrorCodes.UNSUCCESSFUL_REQUEST,\n            message + '\\n' +\n            `The request to ${xhr.responseURL} wasn't successful.\\n` +\n            `The response status is ${xhr.status}.\\n` +\n            `The response status text is: \"${xhr.statusText}\".`\n        );\n        this.status = xhr.status;\n        this.statusText = xhr.statusText;\n        this.url = xhr.responseURL;\n        if (data.pageNumber) {\n            this.pageNumber = data.pageNumber;\n        }\n        if (data.dependencyId) {\n            this.dependencyId = data.dependencyId;\n        }\n    }\n}\n\nexport class NetworkDjVuError extends DjVuError {\n    constructor(data = { pageNumber: null, dependencyId: null, url: null }) {\n        super(DjVuErrorCodes.NETWORK_ERROR,\n            getErrorMessageByData(data) + '\\n' +\n            \"A network error occurred! Check your network connection!\"\n        );\n        if (data.pageNumber) {\n            this.pageNumber = data.pageNumber;\n        }\n        if (data.dependencyId) {\n            this.dependencyId = data.dependencyId;\n        }\n        if (data.url) {\n            this.url = data.url;\n        }\n    }\n}\n\nexport const DjVuErrorCodes = Object.freeze({\n    FILE_IS_CORRUPTED: 'FILE_IS_CORRUPTED',\n    INCORRECT_FILE_FORMAT: 'INCORRECT_FILE_FORMAT',\n    NO_SUCH_PAGE: 'NO_SUCH_PAGE',\n    UNEXPECTED_ERROR: 'UNEXPECTED_ERROR',\n    DATA_CANNOT_BE_TRANSFERRED: 'DATA_CANNOT_BE_TRANSFERRED',\n    INCORRECT_TASK: 'INCORRECT_TASK',\n    NO_BASE_URL: 'NO_BASE_URL',\n    NETWORK_ERROR: 'NETWORK_ERROR',\n    UNSUCCESSFUL_REQUEST: 'UNSUCCESSFUL_REQUEST',\n});"
  },
  {
    "path": "library/src/DjVuPage.js",
    "content": "import { INCLChunk, ColorChunk, CIDaChunk, IFFChunk, INFOChunk, CompositeChunk, ErrorChunk } from './chunks/IFFChunks';\nimport JB2Dict from './jb2/JB2Dict';\nimport JB2Image from './jb2/JB2Image';\nimport DjVuPalette from './chunks/DjVuPalette';\nimport IWImage from './iw44/IWImage';\nimport DjVuText from './chunks/DjVuText';\nimport { ZPDecoder } from './ZPCodec';\nimport DjVu from './DjVu';\nimport { CorruptedFileDjVuError } from './DjVuErrors';\nimport png from 'pngjs/browser';\n\nconst offscreenCanvas = self.OffscreenCanvas ? new OffscreenCanvas(0, 0) : null;\nconst ctx = offscreenCanvas ? offscreenCanvas.getContext('2d') : null;\n\nasync function createBlobFromImageData(imageData) {\n    if (!offscreenCanvas) {\n        return null;\n    }\n    offscreenCanvas.width = imageData.width;\n    offscreenCanvas.height = imageData.height;\n    ctx.putImageData(imageData, 0, 0);\n    const blob = await offscreenCanvas.convertToBlob();\n    offscreenCanvas.width = 0;\n    offscreenCanvas.height = 0;\n    return blob;\n}\n\n/**\n * Страница документа\n */\nexport default class DjVuPage extends CompositeChunk {\n\n    /**\n     * @param {import('./ByteStream').ByteStream} bs\n     * @param {Function} getINCLChunkCallback\n     */\n    constructor(bs, getINCLChunkCallback) {\n        super(bs);\n        this.getINCLChunkCallback = getINCLChunkCallback; // метод для получения глобальной порции данных (словарь обычно) от документа по id\n        this.reset();\n    }\n\n    reset() {\n        this.bs.setOffset(12); // skip id, length and secondary id\n        this.djbz = null;\n        this.bg44arr = new Array();\n        this.fg44 = null;\n\n        /**\n         * @type {?IWImage}\n         */\n        this.bgimage = null;\n        /**\n         * @type {?IWImage}\n         */\n        this.fgimage = null;\n        /**\n         * @type {?JB2Image}\n         */\n        this.sjbz = null;\n        /**\n         * @type {?DjVuPalette}\n         */\n        this.fgbz = null;\n\n        /** @type {?DjVuText} */\n        this.text = null;\n\n        this.decoded = false;\n        this.isBackgroundCompletelyDecoded = false;\n        this.isFirstBgChunkDecoded = false;\n        this.info = null;\n\n\n        // список всех порций данных - для toString\n        this.iffchunks = [];\n        // id разделяемых данных (в частности словарей)\n        this.dependencies = null;\n        //this.init();\n    }\n\n    /**\n     * Свойство необходимое для корректного отображения страницы - влияет на 100% масштаб.\n     */\n    getDpi() {\n        if (this.info) {\n            return this.info.dpi;\n        } else {\n            return this.init().info.dpi;\n        }\n    }\n\n    getHeight() {\n        return this.info ? this.info.height : this.init().info.height;\n    }\n\n    getWidth() {\n        return this.info ? this.info.width : this.init().info.width;\n    }\n\n    async createPngObjectUrl() {\n        var time = performance.now();\n        var imageData = this.getImageData();\n        var imageBlob = await createBlobFromImageData(imageData);\n        if (!imageBlob) {\n            const pngImage = png.PNG.sync.write(this.getImageData())\n            imageBlob = new Blob([pngImage.buffer]);\n        }\n        DjVu.IS_DEBUG && console.log(\"Png creation time = \", performance.now() - time);\n        var url = URL.createObjectURL(imageBlob);\n        return {\n            //url: URL.createObjectURL(new Blob([new ArrayBuffer(10 * 1024 * 1024)])),\n            url: url,\n            byteLength: imageBlob.size,\n            width: this.getWidth(),\n            height: this.getHeight(),\n            dpi: this.getDpi(),\n        };\n    }\n\n    // метод поиска зависимостей, то есть INCLChunk\n    // возвращает массив id\n    /** @returns {Array<string>} */\n    getDependencies() {\n        //чтобы не вызывалось более 1 раза\n        if (this.info || this.dependencies) {\n            return this.dependencies;\n        }\n        this.dependencies = [];\n        var bs = this.bs.fork();\n        while (!bs.isEmpty()) {\n            var chunk;\n            var id = bs.readStr4();\n            var length = bs.getInt32();\n            bs.jump(-8);\n            // вернулись назад\n            var chunkBs = bs.fork(length + 8);\n            bs.jump(8 + length + (length & 1 ? 1 : 0));\n            // перепрыгнули к следующей порции\n            if (id === \"INCL\") {\n                chunk = new INCLChunk(chunkBs);\n                this.dependencies.push(chunk.ref);\n            }\n        }\n        return this.dependencies;\n    }\n\n    /**\n     * Метод предварительного разбора страницы.\n     * Вызывается вручную или автоматически\n     * @returns {DjVuPage}\n     */\n    init() {\n        //чтобы не вызывалось более 1 раза\n        if (this.info) {\n            return this;\n        }\n        this.dependencies = [];\n\n        var id = this.bs.readStr4();\n        if (id !== 'INFO') {\n            throw new CorruptedFileDjVuError(\"The very first chunk must be INFO chunk, but we got \" + id + '!')\n        }\n        var length = this.bs.getInt32();\n        this.bs.jump(-8);\n        this.info = new INFOChunk(this.bs.fork(length + 8));\n        this.bs.jump(8 + length + (this.info.length & 1));\n        this.iffchunks.push(this.info);\n\n        while (!this.bs.isEmpty()) {\n            var chunk;\n            var id = this.bs.readStr4();\n            var length = this.bs.getInt32();\n\n            this.bs.jump(-8); // вернулись назад\n            var chunkBs = this.bs.fork(length + 8); // создали поток включающий только 1 порцию\n            this.bs.jump(8 + length + (length & 1)); // перепрыгнули к следующей порции\n\n            if (!length) { // empty chunk\n                chunk = new IFFChunk(chunkBs); // save it for metadata\n            } else if (id == \"FG44\") {\n                chunk = this.fg44 = new ColorChunk(chunkBs);\n            } else if (id == \"BG44\") {\n                this.bg44arr.push(chunk = new ColorChunk(chunkBs));\n            } else if (id == 'Sjbz') {\n                chunk = this.sjbz = new JB2Image(chunkBs);\n            } else if (id === \"INCL\") {\n                chunk = this.incl = new INCLChunk(chunkBs);\n                var inclChunk = this.getINCLChunkCallback(this.incl.ref);\n                if (inclChunk) { // it takes place in case of polish_indirect, where shared_anno.iff is empty\n                    inclChunk.id === \"Djbz\" ? this.djbz = inclChunk : this.iffchunks.push(inclChunk);\n                }\n                this.dependencies.push(chunk.ref);\n            } else if (id === \"CIDa\") {\n                try {\n                    chunk = new CIDaChunk(chunkBs);\n                } catch (e) {\n                    chunk = new ErrorChunk('CIDa', e);\n                }\n            } else if (id === 'Djbz') {\n                chunk = this.djbz = new JB2Dict(chunkBs);\n            } else if (id === 'FGbz') {\n                chunk = this.fgbz = new DjVuPalette(chunkBs);\n            } else if (id === 'TXTa' || id === 'TXTz') {\n                chunk = this.text = new DjVuText(chunkBs);\n            } else {\n                chunk = new IFFChunk(chunkBs);\n            }\n            //тут все порции в том порядке, в каком встретились при разборе \n            this.iffchunks.push(chunk);\n        }\n        return this;\n    }\n\n    getRotation() {\n        switch (this.info.flags) {\n            case 5: return 90;\n            case 2: return 180;\n            case 6: return 270;\n            default: return 0;\n        }\n    }\n\n    rotateIfRequired(imageData) {\n        if (this.info.flags === 5 || this.info.flags === 6) {\n            var newImageData = new ImageData(this.info.height, this.info.width);\n            var newPixelArray = new Uint32Array(newImageData.data.buffer);\n            var oldPixelArray = new Uint32Array(imageData.data.buffer);\n            var height = this.info.height;\n            var width = this.info.width;\n\n            if (this.info.flags === 6) { // 270\n                for (var i = 0; i < width; i++) {\n                    var rowOffset = (width - i - 1) * height;\n                    var to = height + rowOffset;\n                    for (var newIndex = rowOffset, oldIndex = i; newIndex < to; newIndex++, oldIndex += width) {\n                        newPixelArray[newIndex] = oldPixelArray[oldIndex];\n                    }\n                }\n            } else { // 90\n                for (var i = 0; i < width; i++) {\n                    var rowOffset = i * height;\n                    var from = height + rowOffset - 1;\n                    for (var newIndex = from, oldIndex = i; newIndex >= rowOffset; newIndex--, oldIndex += width) {\n                        newPixelArray[newIndex] = oldPixelArray[oldIndex];\n                    }\n                }\n            }\n\n            return newImageData;\n        }\n\n        if (this.info.flags === 2) { // 180\n            new Uint32Array(imageData.data.buffer).reverse();\n            return imageData;\n        }\n\n        return imageData;\n    }\n\n    getImageData(rotate = true) {\n        const image = this._getImageData();\n        const rotatedImage = rotate ? this.rotateIfRequired(image) : image;\n\n        // In the decoding phase, each pixel is stored in 6 bytes in YCbCr \n        // and converted to 4 bytes RGBA ImageData, that means that in this moment about\n        // (4 + 6) * width * height bytes of RAM are used.\n        // 10 000 000 pixels corresponds to about 95 MB of RAM. If more we should\n        // reset the page to free all the memory retained by YCbCr pixels.\n        // (however this very measure doesn't help reduce the peak memory usage)\n        if (image.width * image.height > 10000000) {\n            this.reset();\n        }\n\n        return rotatedImage;\n    }\n\n    /**\n     * Метод генерации изображения для общего случая (все 3 слоя) без разворота\n     * @returns {ImageData}\n     */\n    _getImageData() {\n        this.decode();\n        var time = performance.now();\n        //достаем маску\n        if (!this.sjbz) {\n            //если только фоновый слой\n            if (this.bgimage) {\n                return this.bgimage.getImage();\n            }//это вряд ли может быть но на всякий случай   \n            else if (this.fgimage) {\n                return this.fgimage.getImage();\n            } else {\n                var emptyImage = new ImageData(this.info.width, this.info.height);\n                emptyImage.data.fill(255);\n                return emptyImage;\n            }\n        }\n        if (!this.bgimage && !this.fgimage) {\n            return this.sjbz.getImage(this.fgbz);\n        }\n\n        var fgscale, bgscale, fgpixelmap, bgpixelmap;\n\n        function fakePixelMap(r, g, b) { // ??? нужно ли это вообще ??? Пока что не встречал таких примеров\n            return {\n                writePixel(index, pixelArray, pixelIndex) {\n                    pixelArray[pixelIndex] = r;\n                    pixelArray[pixelIndex | 1] = g;\n                    pixelArray[pixelIndex | 2] = b;\n                }\n            }\n        }\n\n        if (this.bgimage) {\n            //масштабы на случай если закодированы в более меньшем разрешении\n            bgscale = Math.round(this.info.width / this.bgimage.info.width);\n            bgpixelmap = this.bgimage.pixelmap;\n        } else {\n            bgscale = 1;\n            bgpixelmap = fakePixelMap(255, 255, 255);\n        }\n\n        if (this.fgimage) {\n            //масштабы на случай если закодированы в более меньшем разрешении\n            fgscale = Math.round(this.info.width / this.fgimage.info.width);\n            fgpixelmap = this.fgimage.pixelmap;\n        } else {\n            fgscale = 1;\n            fgpixelmap = fakePixelMap(0, 0, 0);\n        }\n\n\n        var image;\n        if (!this.fgbz) { // если нет палитры\n            image = this.createImageFromMaskImageAndPixelMaps(\n                this.sjbz.getMaskImage(),\n                fgpixelmap,\n                bgpixelmap,\n                fgscale,\n                bgscale\n            );\n        } else { // тут уже предполагается, что переднего плана нет, а только палитра (in DjVu_Tech_Primer it is so)\n            image = this.createImageFromMaskImageAndBackgroundPixelMap(\n                this.sjbz.getImage(this.fgbz, true),\n                bgpixelmap,\n                bgscale\n            );\n        }\n\n        DjVu.IS_DEBUG && console.log(\"DataImage creating time = \", performance.now() - time);\n        return image;\n    }\n\n    createImageFromMaskImageAndPixelMaps(maskImage, fgpixelmap, bgpixelmap, fgscale, bgscale) {\n        var image = maskImage;\n        var pixelArray = image.data;\n        //набираем изображение по пикселям\n        var rowIndexOffset = ((this.info.height - 1) * this.info.width) << 2;\n        var width4 = this.info.width << 2;\n        for (var i = 0; i < this.info.height; i++) {\n            var bis = i / bgscale >> 0;\n            var fis = i / fgscale >> 0;\n            var bgIndexOffset = bgpixelmap.width * bis;\n            var fgIndexOffset = fgpixelmap.width * fis;\n\n            var index = rowIndexOffset;\n            for (var j = 0; j < this.info.width; j++) {\n                if (pixelArray[index]) {\n                    bgpixelmap.writePixel(bgIndexOffset + (j / bgscale >> 0), pixelArray, index);\n                } else {\n                    fgpixelmap.writePixel(fgIndexOffset + (j / fgscale >> 0), pixelArray, index);\n                }\n                index += 4;\n            }\n            rowIndexOffset -= width4;\n        }\n\n        return image;\n    }\n\n    createImageFromMaskImageAndBackgroundPixelMap(coloredMaskImage, bgpixelmap, bgscale) {\n        var pixelArray = coloredMaskImage.data;\n        //набираем изображение по пикселям\n        var rowOffset = (this.info.height - 1) * this.info.width << 2;\n        var width4 = this.info.width << 2;\n        for (var i = 0; i < this.info.height; i++) {\n            var bgRowOffset = (i / bgscale >> 0) * bgpixelmap.width;\n            var index = rowOffset;\n            for (var j = 0; j < this.info.width; j++) {\n                if (pixelArray[index | 3]) {\n                    bgpixelmap.writePixel(bgRowOffset + (j / bgscale >> 0), pixelArray, index);\n                } else {\n                    pixelArray[index | 3] = 255;\n                }\n                index += 4;\n            }\n            rowOffset -= width4;\n        }\n\n        return coloredMaskImage;\n    }\n\n    decodeForeground() {\n        if (this.fg44) {\n            this.fgimage = new IWImage();\n            var zp = new ZPDecoder(this.fg44.bs);\n            this.fgimage.decodeChunk(zp, this.fg44.header);\n            var pixelMapTime = performance.now();\n            this.fgimage.createPixelmap();\n            DjVu.IS_DEBUG && console.log(\"Foreground pixelmap creating time = \", performance.now() - pixelMapTime);\n        }\n    }\n\n    /**\n     * Decoding of the only first chunk was an experimental feature.\n     * Now it's not used at all.\n     */\n    decodeBackground(isOnlyFirstChunk = false) {\n        if (this.isBackgroundCompletelyDecoded || this.isFirstBgChunkDecoded && isOnlyFirstChunk) {\n            return;\n        }\n\n        if (this.bg44arr.length) {\n            this.bgimage = this.bgimage || new IWImage();\n            var to = isOnlyFirstChunk ? 1 : this.bg44arr.length;\n            var from = this.isFirstBgChunkDecoded ? 1 : 0;\n            for (var i = from; i < to; i++) {\n                var chunk = this.bg44arr[i];\n                var zp = new ZPDecoder(chunk.bs);\n                var time = performance.now();\n                this.bgimage.decodeChunk(zp, chunk.header);\n                DjVu.IS_DEBUG && console.log(\"Background chunk decoding time = \", performance.now() - time);\n            }\n\n            var pixelMapTime = performance.now();\n            this.bgimage.createPixelmap();\n            DjVu.IS_DEBUG && console.log(\"Background pixelmap creating time = \", performance.now() - pixelMapTime);\n\n            if (isOnlyFirstChunk) {\n                this.isFirstBgChunkDecoded = true;\n            } else {\n                this.isBackgroundCompletelyDecoded = true;\n            }\n        }\n    }\n\n    /**\n     * Раскодирование всех 3 слоев изображения страницы, вызыват init()\n     * @returns {DjVuPage}\n     */\n    decode() {\n        if (this.decoded) {\n            this.decodeBackground();\n            return this;\n        }\n        this.init();\n\n        var time = performance.now();\n        this.sjbz ? this.sjbz.decode(this.djbz) : 0;\n        DjVu.IS_DEBUG && console.log(\"Mask decoding time = \", performance.now() - time);\n\n        time = performance.now();\n        this.decodeForeground();\n        DjVu.IS_DEBUG && console.log(\"Foreground decoding time = \", performance.now() - time);\n\n        time = performance.now();\n        this.decodeBackground();\n        DjVu.IS_DEBUG && console.log(\"Background decoding time = \", performance.now() - time);\n\n        this.decoded = true;\n        return this;\n    }\n\n    /**\n     * Фоновой слой\n     * @returns {ImageData}\n     */\n    getBackgroundImageData() {\n        this.decode();\n        if (this.bg44arr.length) {\n            this.bg44arr.forEach((chunk) => {\n                var zp = new ZPDecoder(chunk.bs);\n                this.bgimage.decodeChunk(zp, chunk.header);\n            }\n            );\n            return this.bgimage.getImage();\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * @returns {ImageData}\n     */\n    getForegroundImageData() {\n        this.decode();\n        if (this.fg44) {\n            this.fgimage = new IWImage();\n            var zp = new ZPDecoder(this.fg44.bs);\n            this.fgimage.decodeChunk(zp, this.fg44.header);\n            return this.fgimage.getImage();\n        } else {\n            return null;\n        }\n    }\n\n    /** @return {ImageData} */\n    getMaskImageData() {\n        this.decode();\n        return this.sjbz && this.sjbz.getImage(this.fgbz);\n    }\n\n    getText() {\n        this.init();\n        return this.text ? this.text.getText() : \"\";\n    }\n\n    /** @returns {?import('./chunks/DjVuText').RawTextZone} */\n    getPageTextZone() { // returns the top text zone of the whole page (which contains nested zones)\n        this.init();\n        return this.text ? this.text.getPageZone() : null;\n    }\n\n    /** @returns {?Array<import('./chunks/DjVuText').TextZoneF} */\n    getNormalizedTextZones() { // returns a flat array of zones without nested zones\n        this.init();\n        return this.text ? this.text.getNormalizedZones() : null;\n    }\n\n    toString() {\n        this.init();\n        var str = this.iffchunks.reduce((str, chunk) => str + chunk.toString(), '');\n        return super.toString(str);\n    }\n}\n"
  },
  {
    "path": "library/src/DjVuWorker.js",
    "content": "/**\n * @typedef {{\n *      command: string,\n *      data?: {funcs: string[], args: any[][]}[]\n *  } & Partial<Record<string, any>>} CommandObject\n */\n\n/**\n * DjVuScript is a function containing the whole library.\n * It's a wrapper added in the build process. Look at the build config file.\n */\nfunction getLinkToTheWholeLibrary() {\n    if (!getLinkToTheWholeLibrary.url) {\n        getLinkToTheWholeLibrary.url = URL.createObjectURL(new Blob(\n            [\"(\" + DjVuScript.toString() + \")();\"],\n            { type: 'application/javascript' }\n        ));\n    }\n    return getLinkToTheWholeLibrary.url;\n}\n\n/**\n * Класс создающий фоновый поток. Предоставляет интерфейс и инкапсулирует логику связи с\n * объектом DjVuDocument в фоновом потоке выполнения.\n */\nexport default class DjVuWorker {\n    constructor(path = getLinkToTheWholeLibrary()) {\n        this.path = path;\n        this.reset();\n    }\n\n    reset() {\n        this.terminate();\n        this.worker = new Worker(this.path);\n        this.worker.onmessage = (e) => this.messageHandler(e);\n        this.worker.onerror = (e) => this.errorHandler(e);\n        this.promiseCallbacks = null;\n        this.currentPromise = null;\n        this.promiseMap = new Map();\n        this.isWorking = false;\n        this.commandCounter = 0;\n        this.currentCommandId = null;\n\n        // Hyper callback is a callback working even from inside the Worker :)\n        this.hyperCallbacks = {};\n        this.hyperCallbackCounter = 0;\n    }\n\n    registerHyperCallback(func) {\n        const id = this.hyperCallbackCounter++;\n        this.hyperCallbacks[id] = func;\n        return { hyperCallback: true, id: id };\n    }\n\n    unregisterHyperCallback(id) {\n        delete this.hyperCallbacks[id];\n    }\n\n    terminate() {\n        this.worker && this.worker.terminate();\n    }\n\n    get doc() {\n        return DjVuWorkerTask.instance(this);\n    }\n\n    errorHandler(event) {\n        console.error(\"DjVu.js Worker error!\", event);\n    }\n\n    cancelTask(promise) {\n        if (!this.promiseMap.delete(promise)) {\n            if (this.currentPromise === promise) {\n                this.dropCurrentTask();\n            }\n        }\n    }\n\n    dropCurrentTask() {\n        this.currentPromise = null;\n        this.promiseCallbacks = null;\n        this.currentCommandId = null;\n    }\n\n    emptyTaskQueue() {\n        this.promiseMap.clear();\n    }\n\n    cancelAllTasks() {\n        this.emptyTaskQueue();\n        this.dropCurrentTask();\n    }\n\n    /**\n     * @param {CommandObject} commandObj\n     * @param {Array<Transferable>} transferList\n     */\n    createNewPromise(commandObj, transferList = undefined) {\n        var callbacks;\n        var promise = new Promise((resolve, reject) => {\n            callbacks = { resolve, reject };\n        });\n        this.promiseMap.set(promise, { callbacks, commandObj, transferList });\n        this.runNextTask();\n        return promise;\n    }\n\n    /**\n     * Replaces functions with special \"hyper callback\" objects - it allows invoking callbacks from the web worker\n     * (asynchronously of course, but it's mostly used to track progress)\n     * @param {CommandObject} commandObj\n     * @returns {CommandObject}\n     */\n    prepareCommandObject(commandObj) {\n        if (!(commandObj.data instanceof Array)) return commandObj;\n\n        const hyperCallbackIds = [];\n\n        for (const { args: argsList } of commandObj.data) {\n            for (const args of argsList) {\n                for (let i = 0; i < args.length; i++) {\n                    if (typeof args[i] === 'function') {\n                        const hyperCallback = this.registerHyperCallback(args[i]);\n                        args[i] = hyperCallback;\n                        hyperCallbackIds.push(hyperCallback.id);\n                    }\n                }\n            }\n        }\n\n        if (hyperCallbackIds.length) {\n            commandObj.sendBackData = {\n                ...commandObj.sendBackData,\n                hyperCallbackIds\n            };\n        }\n\n        return commandObj;\n    }\n\n    runNextTask() {\n        if (this.isWorking) {\n            return;\n        }\n        var next = this.promiseMap.entries().next().value;\n        if (next) {\n            const [promise, { callbacks, commandObj, transferList }] = next;\n            this.promiseCallbacks = callbacks;\n            this.currentPromise = promise;\n            this.currentCommandId = this.commandCounter++;\n            commandObj.sendBackData = {\n                commandId: this.currentCommandId,\n            };\n            this.worker.postMessage(this.prepareCommandObject(commandObj), transferList);\n            this.isWorking = true;\n            this.promiseMap.delete(promise);\n        } else {\n            this.dropCurrentTask();\n        }\n    }\n\n    isTaskInProcess(promise) {\n        return this.currentPromise === promise;\n    }\n\n    isTaskInQueue(promise) {\n        return this.promiseMap.has(promise) || this.isTaskInProcess(promise);\n    }\n\n    processAction(obj) { // usually progress messages, not the commands' finish\n        switch (obj.action) {\n            case 'Process':\n                this.onprocess ? this.onprocess(obj.percent) : 0;\n                break;\n            case 'hyperCallback':\n                if (this.hyperCallbacks[obj.id]) this.hyperCallbacks[obj.id](...obj.args);\n                break;\n        }\n    }\n\n    messageHandler({ data: obj }) {\n        if (obj.action) return this.processAction(obj);\n\n        this.isWorking = false;\n        const callbacks = this.promiseCallbacks;\n        const commandId = obj.sendBackData && obj.sendBackData.commandId;\n\n        // either a result or a forgotten command returned\n        if (commandId === this.currentCommandId || this.currentCommandId === null) {\n            // in fact, this invocation is essential, since this.isWorking\n            // isn't reset when all tasks are cancelled.\n            // So we still wait for a cancelled task to finish - it's important, because otherwise\n            // cancelAllTasks() would have no sense - the real worker's queue would be overwhelmed with \"current\" tasks,\n            // which cannot be cancelled once sent, while now it's possible to really cancel all tasks several times\n            // while some other task is being processed in the worker.\n            // Real example: a user is quickly turning over pages in the single page mode in the viewer.\n            // commandIds only prevent us from forgetting current task\n            // in case when something comes from the worker and it's not an action\n            // (it shouldn't happen, just an additional measure)\n            this.runNextTask();\n        } else {\n            // it shouldn't happen, it means that one more task has been already sent\n            // without waiting for a forgotten one. Or an action is sent incorrectly.\n            // Or there is an unhandled promise rejection or an error.\n            if (obj === \"unhandledrejection\" || obj === \"error\") {\n                console.warn(\"DjVu.js: \" + obj + \" occurred in the worker!\");\n                this.runNextTask();\n            } else {\n                console.warn('DjVu.js: Something strange came from the worker.', obj);\n            }\n            return;\n        }\n\n        if (!callbacks) return; // in case of all tasks cancellation\n\n        const { resolve, reject } = callbacks;\n        switch (obj.command) {\n            case 'Error':\n                reject(obj.error);\n                break;\n            case 'createDocument':\n            case 'startMultiPageDocument':\n            case 'addPageToDocument':\n                resolve();\n                break;\n            case 'createDocumentFromPictures':\n            case 'endMultiPageDocument':\n                resolve(obj.buffer);\n                break;\n            case 'run':\n                var restoredResult = !obj.result ? obj.result :\n                    obj.result.length && obj.result.map ? obj.result.map(result => this.restoreValueAfterTransfer(result)) :\n                        this.restoreValueAfterTransfer(obj.result);\n                resolve(restoredResult);\n                break;\n            default:\n                console.error(\"Unexpected message from DjVuWorker: \", obj);\n        }\n\n        if (obj.sendBackData && obj.sendBackData.hyperCallbackIds) {\n            obj.sendBackData.hyperCallbackIds.forEach(id => this.unregisterHyperCallback(id));\n        }\n    }\n\n    restoreValueAfterTransfer(value) {\n        if (value) {\n            if (value.width && value.height && value.buffer) {\n                return new ImageData(new Uint8ClampedArray(value.buffer), value.width, value.height);\n            }\n        }\n        return value;\n    }\n\n    /** @param {DjVuWorkerTask} tasks */\n    run(...tasks) {\n        const data = tasks.map(task => task._);\n        return this.createNewPromise({\n            command: 'run',\n            data: data,\n            //time: Date.now(),\n        });\n    }\n\n    revokeObjectURL(url) { // if an ObjectURL was created inside a worker it can be revoked only inside this very worker\n        this.worker.postMessage({\n            action: this.revokeObjectURL.name,\n            url: url,\n        });\n    }\n\n    startMultiPageDocument(slicenumber, delayInit, grayscale) {\n        return this.createNewPromise({\n            command: 'startMultiPageDocument',\n            slicenumber: slicenumber,\n            delayInit: delayInit,\n            grayscale: grayscale\n        });\n    }\n\n    addPageToDocument(imageData) {\n        var simpleImage = {\n            buffer: imageData.data.buffer,\n            width: imageData.width,\n            height: imageData.height\n        };\n        return this.createNewPromise({\n            command: 'addPageToDocument',\n            simpleImage: simpleImage\n        }, [simpleImage.buffer]);\n    }\n\n    endMultiPageDocument() {\n        return this.createNewPromise({ command: 'endMultiPageDocument' });\n    }\n\n    createDocument(buffer, options) {\n        return this.createNewPromise({ command: 'createDocument', buffer: buffer, options: options }, [buffer]);\n    }\n\n    createDocumentFromPictures(imageArray, slicenumber, delayInit, grayscale) {\n        var simpleImages = new Array(imageArray.length);\n        var buffers = new Array(imageArray.length);\n        for (var i = 0; i < imageArray.length; i++) {\n            // разлагаем картинки для передачи в фоновый поток по частям\n            simpleImages[i] = {\n                buffer: imageArray[i].data.buffer,\n                width: imageArray[i].width,\n                height: imageArray[i].height\n            };\n            buffers[i] = imageArray[i].data.buffer;\n        }\n\n        return this.createNewPromise({\n            command: 'createDocumentFromPictures',\n            images: simpleImages,\n            slicenumber: slicenumber,\n            delayInit: delayInit,\n            grayscale: grayscale\n        }, buffers);\n    }\n}\n\nclass DjVuWorkerTask {\n    /**\n     * @property {{funcs: string[], args: any[][]}} _\n     */\n\n    /**\n     * @param {DjVuWorker} worker\n     * @param {string[]} funcs\n     * @param {any[][]} args\n     */\n    static instance(worker, funcs = [], args = []) {\n        var proxy = new Proxy(DjVuWorkerTask.emptyFunc, {\n            get: (target, key) => {\n                switch (key) {\n                    case '_':\n                        return { funcs, args };\n                    case 'run':\n                        return () => worker.run(proxy);\n                    default:\n                        return DjVuWorkerTask.instance(worker, [...funcs, key], args);\n                }\n            },\n            apply: (target, that, _args) => { // when method is called, just add args to the array\n                return DjVuWorkerTask.instance(worker, funcs, [...args, _args]);\n            }\n        });\n        return proxy;\n    }\n\n    static emptyFunc() { }\n}"
  },
  {
    "path": "library/src/DjVuWorkerScript.js",
    "content": "import DjVuDocument from './DjVuDocument';\nimport IWImageWriter from './iw44/IWImageWriter';\nimport { DjVuError, DjVuErrorCodes, IncorrectTaskDjVuError, UnableToTransferDataDjVuError } from './DjVuErrors';\n\n/**\n * Это скрипт для выполнения в фоновом потоке.\n */\nexport default function initWorker() {\n\n    /** @type {DjVuDocument} */\n    var djvuDocument; // главный объект документа\n    /** @type {IWImageWriter} */\n    var iwiw; // объект записи документов\n\n    addEventListener(\"error\", e => {\n        console.error(e);\n        postMessage(\"error\");\n    });\n\n    addEventListener(\"unhandledrejection\", e => {\n        console.error(e);\n        postMessage(\"unhandledrejection\");\n    });\n\n    // обработчик приема событий\n    onmessage = async function ({ data: obj }) {\n        if (obj.action) return handlers[obj.action](obj); // action that doesn't require response\n\n        try { // отлавливаем все исключения\n            const { data, transferList } = await handlers[obj.command](obj) || {};\n            try {\n                postMessage({\n                    command: obj.command,\n                    ...data,\n                    ...(obj.sendBackData ? { sendBackData: obj.sendBackData } : null),\n                }, transferList && transferList.length ? transferList : undefined);\n            } catch (e) {\n                throw new UnableToTransferDataDjVuError(obj.data);\n            }\n        } catch (error) {\n            console.error(error);\n            // we can't pass the native Error object between workers, so only several properties are copied\n            var errorObj = error instanceof DjVuError ? error : {\n                code: DjVuErrorCodes.UNEXPECTED_ERROR,\n                name: error.name,\n                message: error.message\n            };\n            errorObj.commandObject = obj;\n            postMessage({\n                command: 'Error',\n                error: errorObj,\n                ...(obj.sendBackData ? { sendBackData: obj.sendBackData } : null),\n            });\n        }\n    };\n\n    function processValueBeforeTransfer(value, transferList) {\n        if (value instanceof ArrayBuffer) {\n            transferList.push(value);\n            return value;\n        }\n        if (value instanceof ImageData) {\n            transferList.push(value.data.buffer);\n            return {\n                width: value.width,\n                height: value.height,\n                buffer: value.data.buffer\n            };\n        }\n        if (value instanceof DjVuDocument) {\n            transferList.push(value.buffer);\n            return value.buffer;\n        }\n        return value;\n    }\n\n    function restoreHyperCallbacks(args) {\n        // we should not change the initial array,\n        // cause it is sent back in case of error, and a function cannot be sent\n        return args.map(arg => {\n            if (arg && (typeof arg === 'object') && arg.hyperCallback) {\n                return (...params) => postMessage({\n                    action: 'hyperCallback',\n                    id: arg.id,\n                    args: params\n                });\n            }\n            return arg;\n        });\n    }\n\n    var handlers = {\n\n        /* A universal command which handles all tasks created via doc proxy property of the DjVuWorker class */\n        async run(obj) {\n            //console.log(\"Got task request\", Date.now() - obj.time);\n            const results = await Promise.all(obj.data.map(async task => {\n                var res = djvuDocument;\n                for (var i = 0; i < task.funcs.length; i++) {\n                    if (typeof res[task.funcs[i]] !== 'function') {\n                        throw new IncorrectTaskDjVuError(task);\n                    }\n                    res = await res[task.funcs[i]](...restoreHyperCallbacks(task.args[i]));\n                }\n                return res;\n            }));\n\n            //var time = Date.now();\n            var transferList = [];\n            var processedResults = results.map(result => processValueBeforeTransfer(result, transferList));\n\n            return {\n                data: {\n                    result: processedResults.length === 1 ? processedResults[0] : processedResults\n                },\n                transferList\n            };\n        },\n\n        revokeObjectURL(obj) {\n            URL.revokeObjectURL(obj.url);\n        },\n\n        startMultiPageDocument(obj) {\n            iwiw = new IWImageWriter(obj.slicenumber, obj.delayInit, obj.grayscale);\n            iwiw.startMultiPageDocument();\n        },\n\n        addPageToDocument(obj) {\n            var imageData = new ImageData(new Uint8ClampedArray(obj.simpleImage.buffer), obj.simpleImage.width, obj.simpleImage.height);\n            iwiw.addPageToDocument(imageData);\n        },\n\n        endMultiPageDocument(obj) {\n            var buffer = iwiw.endMultiPageDocument();\n            return {\n                data: { buffer: buffer },\n                transferList: [buffer]\n            };\n        },\n\n        createDocumentFromPictures(obj) {\n            var sims = obj.images;\n            var imageArray = new Array(sims.length);\n            // собираем объекты ImageData\n            for (var i = 0; i < sims.length; i++) {\n                imageArray[i] = new ImageData(new Uint8ClampedArray(sims[i].buffer), sims[i].width, sims[i].height);\n            }\n            var iw = new IWImageWriter(obj.slicenumber, obj.delayInit, obj.grayscale);\n            iw.onprocess = (percent) => {\n                postMessage({ action: 'Process', percent: percent });\n            };\n            var ndoc = iw.createMultyPageDocument(imageArray);\n            return {\n                data: { buffer: ndoc.buffer },\n                transferList: [ndoc.buffer]\n            };\n        },\n\n        createDocument(obj) {\n            djvuDocument = new DjVuDocument(obj.buffer, obj.options);\n        },\n    };\n}"
  },
  {
    "path": "library/src/DjVuWriter.js",
    "content": "import ByteStreamWriter from './ByteStreamWriter';\nimport { ZPEncoder } from './ZPCodec';\nimport BZZEncoder from './bzz/BZZEncoder';\n\n\n/**\n * Класс предназначенный для создания итогового файла. \n * Определяет более высокоуровневые функции, нежели ByteStreamWriter\n */\nexport default class DjVuWriter {\n    constructor(length) {\n        this.bsw = new ByteStreamWriter(length || 1024 * 1024);\n    }\n\n    startDJVM() {\n        // пропускаем 4 байта для длины файла\n        this.bsw.writeStr('AT&T').writeStr('FORM').saveOffsetMark('fileSize')\n            .jump(4).writeStr('DJVM');\n    }\n\n    writeDirmChunk(dirm) {\n        this.dirm = dirm;\n        this.bsw.writeStr('DIRM').saveOffsetMark('DIRMsize').jump(4);\n        this.dirm.offsets = [];\n\n        this.bsw.writeByte(dirm.dflags)\n            .writeInt16(dirm.flags.length)\n            .saveOffsetMark('DIRMoffsets') // will be written in getBuffer() method\n            .jump(4 * dirm.flags.length);\n\n        //начинаем фазу кодирования bzz;\n\n        var tmpBS = new ByteStreamWriter();\n        for (var i = 0; i < dirm.sizes.length; i++) {\n            tmpBS.writeInt24(dirm.sizes[i]);\n        }\n        for (var i = 0; i < dirm.flags.length; i++) {\n            tmpBS.writeByte(dirm.flags[i]);\n        }\n        for (var i = 0; i < dirm.ids.length; i++) {\n            tmpBS.writeStrNT(dirm.ids[i]);\n            if (dirm.flags[i] & 128) { // has name flag\n                tmpBS.writeStrNT(dirm.names[i]);\n            }\n            if (dirm.flags[i] & 64) { // has title flag\n                tmpBS.writeStrNT(dirm.titles[i]);\n            }\n        }\n        //todo для BWT конечный символ EOB - временный код\n        tmpBS.writeByte(0);\n\n        var tmpBuffer = tmpBS.getBuffer();\n\n        var bzzBS = new ByteStreamWriter();\n        var zp = new ZPEncoder(bzzBS);\n        var bzz = new BZZEncoder(zp);\n        bzz.encode(tmpBuffer);\n        var encodedBuffer = bzzBS.getBuffer();\n\n        //записываем полученный буфер в основной поток\n        this.bsw.writeBuffer(encodedBuffer);\n\n        //записали длину \n        this.bsw.rewriteSize('DIRMsize');\n    }\n\n    get offset() {\n        return this.bsw.offset;\n    }\n\n    writeByte(byte) {\n        this.bsw.writeByte(byte);\n        return this;\n    }\n\n    writeStr(str) {\n        this.bsw.writeStr(str);\n        return this;\n    }\n\n    writeInt32(val) {\n        this.bsw.writeInt32(val);\n        return this;\n    }\n\n    writeFormChunkBS(bs) {\n        //проверка на четную границу\n        if (this.bsw.offset & 1) {\n            this.bsw.writeByte(0);\n        }\n        var off = this.bsw.offset;\n        this.dirm.offsets.push(off);\n        this.bsw.writeByteStream(bs);\n\n    }\n\n    writeFormChunkBuffer(buffer) {\n        //проверка на четную границу\n        if (this.bsw.offset & 1) {\n            this.bsw.writeByte(0);\n        }\n        var off = this.bsw.offset;\n        this.dirm.offsets.push(off);\n        this.bsw.writeBuffer(buffer);\n    }\n\n    writeChunk(chunk) {\n        //проверка на четную границу\n        if (this.bsw.offset & 1) {\n            this.bsw.writeByte(0);\n        }\n        this.bsw.writeByteStream(chunk.bs);\n    }\n\n    /*getByteStream() {\n        var bs = new ByteStream(this.buffer);\n        return bs;\n    }*/\n\n    getBuffer() {\n        //пишем длину файла\n        this.bsw.rewriteSize('fileSize');\n        if (this.dirm.offsets.length !== (this.dirm.flags.length)) {\n            throw new Error(\"Записаны не все страницы и словари !!!\");\n        }\n        for (var i = 0; i < this.dirm.offsets.length; i++) {\n            this.bsw.rewriteInt32('DIRMoffsets', this.dirm.offsets[i]);\n        }\n        return this.bsw.getBuffer();\n    }\n}"
  },
  {
    "path": "library/src/ZPCodec.js",
    "content": "import ByteStreamWriter from './ByteStreamWriter';\n\nexport class ZPEncoder {\n    constructor(bsw) {\n        //byteStreamWriter\n        this.bsw = bsw || new ByteStreamWriter();\n        this.a = 0;\n        this.scount = 0;\n        this.byte = 0;\n        this.delay = 25;\n        this.subend = 0;\n        this.buffer = 0xffffff;\n        this.nrun = 0;\n\n        //this.pzp = new PseudoZP();\n    }\n\n    outbit(bit) {\n        if (this.delay > 0) {\n            if (this.delay < 0xff)\n                // delay=0xff suspends emission forever\n                this.delay -= 1;\n        }\n        else {\n            /* Insert a bit */\n            this.byte = (this.byte << 1) | bit;\n            /* Output a byte */\n            if (++this.scount == 8) {\n                this.bsw.writeByte(this.byte);\n                this.scount = 0;\n                this.byte = 0;\n            }\n        }\n    }\n\n    zemit(b) {\n        /* Shift new bit into 3bytes buffer */\n        this.buffer = (this.buffer << 1) + b;\n        /* Examine bit going out of the 3bytes buffer */\n        b = (this.buffer >> 24);\n        this.buffer = (this.buffer & 0xffffff);\n        switch (b) {\n            /* Similar to WN&C upper renormalization */\n            case 1:\n                this.outbit(1);\n                while (this.nrun-- > 0)\n                    this.outbit(0);\n                this.nrun = 0;\n                break;\n            /* Similar to WN&C lower renormalization */\n            case 0xff:\n                this.outbit(0);\n                while (this.nrun-- > 0)\n                    this.outbit(1);\n                this.nrun = 0;\n                break;\n            /* Similar to WN&C central renormalization */\n            case 0:\n                this.nrun += 1;\n                break;\n            default:\n                //assert(0);\n                throw new Exception('ZPEncoder::zemit() error!');\n        }\n    }\n\n    //откопировано из djvulibre\n    encode(bit, ctx, n) {\n        //this.pzp.encode(bit, ctx, n);\n        bit = +bit;\n        if (!ctx) {\n            //return this.IWencode(bit);\n            // можно было бы использовать IWencode всегда, но так сделано в djvulibre видимо для оптимизации\n            return this._ptencode(bit, 0x8000 + (this.a >> 1));\n        }\n        var z = this.a + this.p[ctx[n]];\n        if (bit != (ctx[n] & 1)) {\n            //encode_lps(ctx, z);\n            var d = 0x6000 + ((z + this.a) >> 2);\n            if (z > d) {\n                z = d;\n            }\n            /* Adaptation */\n            ctx[n] = this.dn[ctx[n]];\n            /* Code LPS */\n            z = 0x10000 - z;\n            this.subend += z;\n            this.a += z;\n\n        } else if (z >= 0x8000) {\n            //encode_mps\n            var d = 0x6000 + ((z + this.a) >> 2);\n            if (z > d) {\n                z = d;\n            }\n            /* Adaptation */\n            if (this.a >= this.m[ctx[n]])\n                ctx[n] = this.up[ctx[n]];\n            /* Code MPS */\n            this.a = z;\n\n        } else {\n            this.a = z;\n            // чтобы выйти тут\n            return;\n        }\n\n        /* Export bits */// выполнится только для первых 2 случаев\n        while (this.a >= 0x8000) {\n            this.zemit(1 - (this.subend >> 15));\n            // 0xffff & ... вместо (unsigned short) в С++\n            this.subend = 0xffff & (this.subend << 1);\n            this.a = 0xffff & (this.a << 1);\n        }\n    }\n\n    //используется для кодирования изображений, может всегда использоваться как показала практика\n    IWencode(bit) {\n        //this.pzp.encode(bit);\n        this._ptencode(bit, 0x8000 + ((this.a + this.a + this.a) >> 3));\n    }\n\n    // тут скопировано с IWEncoder() может нужен просто Encoder()\n    _ptencode(bit, z) {\n        // IWEncoder()\n        //var z = 0x8000 + ((this.a + this.a + this.a) >> 3);\n        // просто Encoder()\n        //var z = 0x8000 + (this.a >> 1);\n\n        if (bit) {\n            //encode_lps_simple(z);\n            /* Code LPS */\n            z = 0x10000 - z;\n            this.subend += z;\n            this.a += z;\n        } else {\n            //encode_mps_simple(z);\n            /* Code MPS */\n            this.a = z;\n        }\n        /* Export bits */// выполнится только для первыйх 2 случаев\n        while (this.a >= 0x8000) {\n            this.zemit(1 - (this.subend >> 15));\n            // 0xffff & ... вместо (unsigned short) в С++\n            this.subend = 0xffff & (this.subend << 1);\n            this.a = 0xffff & (this.a << 1);\n        }\n    }\n\n    // функция выполняемая в деструкторе в С++. Надо вызывать вручную в js чтобы записать последние байты\n    eflush() {\n        /* adjust subend */\n        if (this.subend > 0x8000)\n            this.subend = 0x10000;\n        else if (this.subend > 0)\n            this.subend = 0x8000;\n        /* zemit many mps bits */\n        while (this.buffer != 0xffffff || this.subend) {\n            this.zemit(1 - (this.subend >> 15));\n            this.subend = 0xffff & (this.subend << 1);\n        }\n        /* zemit pending run */\n        this.outbit(1);\n        while (this.nrun-- > 0)\n            this.outbit(0);\n        this.nrun = 0;\n        /* zemit 1 until full byte */\n        while (this.scount > 0)\n            this.outbit(1);\n        /* prevent further emission */\n        this.delay = 0xff;\n    }\n}\n\n\nexport class ZPDecoder {\n    constructor(bs) {\n        this.bs = bs;\n        this.a = 0x0000;\n        this.c = this.bs.byte();\n        //code\n        this.c <<= 8;\n        var tmp = this.bs.byte();\n        this.c |= tmp;\n        this.z = 0;\n        this.d = 0;\n        //fence\n        this.f = Math.min(this.c, 0x7fff);\n        this.ffzt = new Int8Array(256);\n        // Create machine independent ffz table\n        for (var i = 0; i < 256; i++) {\n            this.ffzt[i] = 0;\n            for (var j = i; j & 0x80; j <<= 1)\n                this.ffzt[i] += 1;\n        }\n        /* Preload buffer */\n        this.delay = 25;\n        this.scount = 0;\n        this.buffer = 0;\n        // буфер на 4 байта\n        this.preload();\n    }\n\n    preload() {\n        // загрузка байтов из потока в буфер\n        while (this.scount <= 24) {\n            var byte = this.bs.byte();\n            this.buffer = (this.buffer << 8) | byte;\n            this.scount += 8;\n        }\n    }\n\n    ffz(x) {\n        return (x >= 0xff00) ? (this.ffzt[x & 0xff] + 8) : (this.ffzt[(x >> 8) & 0xff]);\n    }\n\n    /* Функции реализованы не как в документации, а скопированы из djvulibre */\n    decode(ctx, n) {\n        if (!ctx) {\n            //упрощенный декодер, но можно было использовать IWdecode\n            return this._ptdecode(0x8000 + (this.a >> 1));\n        }\n        this.b = ctx[n] & 1;\n        this.z = this.a + this.p[ctx[n]];\n        if (this.z <= this.f) {\n            this.a = this.z;\n            //console.log(\"123\");\n\n            /* if (this.pzp) {\n                 var tmp = this.pzp.decode(ctx, n);\n                 if (tmp != this.b) {\n                     throw new Exception('Bit dismatch');\n                 }\n             }*/\n\n            return this.b;\n        }\n        this.d = 0x6000 + ((this.a + this.z) >> 2);\n\n        if (this.z > this.d) {\n            this.z = this.d;\n        }\n\n        if (this.z > this.c) {\n            this.b = 1 - this.b;\n            /*if (this.pzp) {\n                var tmp = this.pzp.decode(ctx, n);\n                if (tmp != this.b) {\n                    throw new Exception('Bit dismatch');\n                }\n            }*/\n            this.z = 0x10000 - this.z;\n            this.a += this.z;\n            this.c += this.z;\n            ctx[n] = this.dn[ctx[n]];\n\n            var shift = this.ffz(this.a);\n            this.scount -= shift;\n            this.a = 0xffff & (this.a << shift);\n            this.c = 0xffff & (\n                (this.c << shift) | (this.buffer >> this.scount) & ((1 << shift) - 1)\n            );\n        }\n        else {\n            /*if (this.pzp) {\n                var tmp = this.pzp.decode(ctx, n);\n                if (tmp != this.b) {\n                    throw new Exception('Bit dismatch');\n                }\n            }*/\n            if (this.a >= this.m[ctx[n]]) {\n                ctx[n] = this.up[ctx[n]];\n            }\n            this.scount--;\n            this.a = 0xffff & (this.z << 1);\n            this.c = 0xffff & (\n                (this.c << 1) | ((this.buffer >> this.scount) & 1)\n            );\n        }\n\n        if (this.scount < 16)\n            this.preload();\n        this.f = Math.min(this.c, 0x7fff);\n\n        return this.b;\n    }\n\n    // для раскодирования картинок, но вообще можно всегда использовать\n    IWdecode() {\n        return this._ptdecode(0x8000 + ((this.a + this.a + this.a) >> 3));\n    }\n\n    _ptdecode(z) {\n        //z = 0x8000 + ((this.a + this.a + this.a) >> 3);\n        this.b = 0;\n        if (z > this.c) {\n            this.b = 1;\n            z = 0x10000 - z;\n            this.a += z;\n            this.c += z;\n\n            var shift = this.ffz(this.a);\n            this.scount -= shift;\n            this.a = 0xffff & (this.a << shift);\n            this.c = 0xffff & (\n                (this.c << shift) | (this.buffer >> this.scount) & ((1 << shift) - 1)\n            );\n        }\n        else {\n            this.b = 0;\n            this.scount--;\n            this.a = 0xffff & (z << 1);\n            this.c = 0xffff & (\n                (this.c << 1) | ((this.buffer >> this.scount) & 1)\n            );\n        }\n        if (this.scount < 16)\n            this.preload();\n        this.f = Math.min(this.c, 0x7fff);\n\n        /* if (this.pzp) {\n             var tmp = this.pzp.decode();\n             if (tmp != this.b) {\n                 throw new Exception('Bit dismatch');\n             }\n         }*/\n\n        return this.b;\n    }\n\n    /*decodex(ctx, n) {\n        if (!ctx) {\n            return this.ptdecode();\n        }\n        this.b = ctx[n] & 1;\n        this.z = this.a + this.p[ctx[n]];\n        if (this.z <= this.f) {\n            this.a = this.z;\n            //console.log(\"123\");\n            return this.b;\n        }\n        this.d = 0x6000 + ((this.a + this.z) >> 2);\n        \n        if (this.z > this.d) {\n            this.z = this.d;\n        }\n        if (this.c > this.z) {\n            if (this.a > this.m[ctx[n]]) {\n                ctx[n] = this.up[ctx[n]];\n            }\n            this.a = this.z;\n        } \n        else {\n            this.b = 1 - this.b;\n            this.z = 0x10000 - this.z;\n            this.a += this.z;\n            this.c += this.z;\n            ctx[n] = this.dn[ctx[n]];\n        }\n        var flag = 0;\n        while (this.a > 0x8000) {\n            flag = 1;\n            this.a += this.a - 0x10000;\n            this.c += this.c - 0x10000 + this.bs.bit();\n            //console.log(\"+\");\n        }\n        if (flag) {\n            this.f = Math.min(this.c, 0x7fff);\n        } \n        else {\n           // console.log(\"()\");\n        }\n        return this.b;\n    }*/\n\n    /*ptdecodex() {\n        this.z = 0x8000 + ((this.a + this.a + this.a) >> 3);\n        if (this.c > this.z) {\n            this.b = 0;\n            this.a = this.z;\n        } \n        else {\n            this.b = 1;\n            this.z = 0x10000 - this.z;\n            this.a += this.z;\n            this.c += this.z;\n        }\n        while (this.a > 0x8000) {\n            this.a += this.a - 0x10000;\n            this.c += this.c - 0x10000 + this.bs.bit();\n        }\n        return this.b;\n    }*/\n}\n\n\nZPEncoder.prototype.p = ZPDecoder.prototype.p = Uint16Array.of(\n    0x8000, 0x8000, 0x8000, 0x6bbd, 0x6bbd, 0x5d45, 0x5d45, 0x51b9, 0x51b9, 0x4813,\n    0x4813, 0x3fd5, 0x3fd5, 0x38b1, 0x38b1, 0x3275, 0x3275, 0x2cfd, 0x2cfd, 0x2825,\n    0x2825, 0x23ab, 0x23ab, 0x1f87, 0x1f87, 0x1bbb, 0x1bbb, 0x1845, 0x1845, 0x1523,\n    0x1523, 0x1253, 0x1253, 0xfcf, 0xfcf, 0xd95, 0xd95, 0xb9d, 0xb9d, 0x9e3,\n    0x9e3, 0x861, 0x861, 0x711, 0x711, 0x5f1, 0x5f1, 0x4f9, 0x4f9, 0x425,\n    0x425, 0x371, 0x371, 0x2d9, 0x2d9, 0x259, 0x259, 0x1ed, 0x1ed, 0x193,\n    0x193, 0x149, 0x149, 0x10b, 0x10b, 0xd5, 0xd5, 0xa5, 0xa5, 0x7b,\n    0x7b, 0x57, 0x57, 0x3b, 0x3b, 0x23, 0x23, 0x13, 0x13, 0x7,\n    0x7, 0x1, 0x1, 0x5695, 0x24ee, 0x8000, 0xd30, 0x481a, 0x481, 0x3579,\n    0x17a, 0x24ef, 0x7b, 0x1978, 0x28, 0x10ca, 0xd, 0xb5d, 0x34, 0x78a,\n    0xa0, 0x50f, 0x117, 0x358, 0x1ea, 0x234, 0x144, 0x173, 0x234, 0xf5,\n    0x353, 0xa1, 0x5c5, 0x11a, 0x3cf, 0x1aa, 0x285, 0x286, 0x1ab, 0x3d3,\n    0x11a, 0x5c5, 0xba, 0x8ad, 0x7a, 0xccc, 0x1eb, 0x1302, 0x2e6, 0x1b81,\n    0x45e, 0x24ef, 0x690, 0x2865, 0x9de, 0x3987, 0xdc8, 0x2c99, 0x10ca, 0x3b5f,\n    0xb5d, 0x5695, 0x78a, 0x8000, 0x50f, 0x24ee, 0x358, 0xd30, 0x234, 0x481,\n    0x173, 0x17a, 0xf5, 0x7b, 0xa1, 0x28, 0x11a, 0xd, 0x1aa, 0x34,\n    0x286, 0xa0, 0x3d3, 0x117, 0x5c5, 0x1ea, 0x8ad, 0x144, 0xccc, 0x234,\n    0x1302, 0x353, 0x1b81, 0x5c5, 0x24ef, 0x3cf, 0x2b74, 0x285, 0x201d, 0x1ab,\n    0x1715, 0x11a, 0xfb7, 0xba, 0xa67, 0x1eb, 0x6e7, 0x2e6, 0x496, 0x45e,\n    0x30d, 0x690, 0x206, 0x9de, 0x155, 0xdc8, 0xe1, 0x2b74, 0x94, 0x201d,\n    0x188, 0x1715, 0x252, 0xfb7, 0x383, 0xa67, 0x547, 0x6e7, 0x7e2, 0x496,\n    0xbc0, 0x30d, 0x1178, 0x206, 0x19da, 0x155, 0x24ef, 0xe1, 0x320e, 0x94,\n    0x432a, 0x188, 0x447d, 0x252, 0x5ece, 0x383, 0x8000, 0x547, 0x481a, 0x7e2,\n    0x3579, 0xbc0, 0x24ef, 0x1178, 0x1978, 0x19da, 0x2865, 0x24ef, 0x3987, 0x320e,\n    0x2c99, 0x432a, 0x3b5f, 0x447d, 0x5695, 0x5ece, 0x8000, 0x8000, 0x5695, 0x481a, 0x481a\n);\n\nZPEncoder.prototype.m = ZPDecoder.prototype.m = Uint16Array.of(\n    0x0, 0x0, 0x0, 0x10a5, 0x10a5, 0x1f28, 0x1f28, 0x2bd3, 0x2bd3, 0x36e3,\n    0x36e3, 0x408c, 0x408c, 0x48fd, 0x48fd, 0x505d, 0x505d, 0x56d0, 0x56d0, 0x5c71,\n    0x5c71, 0x615b, 0x615b, 0x65a5, 0x65a5, 0x6962, 0x6962, 0x6ca2, 0x6ca2, 0x6f74,\n    0x6f74, 0x71e6, 0x71e6, 0x7404, 0x7404, 0x75d6, 0x75d6, 0x7768, 0x7768, 0x78c2,\n    0x78c2, 0x79ea, 0x79ea, 0x7ae7, 0x7ae7, 0x7bbe, 0x7bbe, 0x7c75, 0x7c75, 0x7d0f,\n    0x7d0f, 0x7d91, 0x7d91, 0x7dfe, 0x7dfe, 0x7e5a, 0x7e5a, 0x7ea6, 0x7ea6, 0x7ee6,\n    0x7ee6, 0x7f1a, 0x7f1a, 0x7f45, 0x7f45, 0x7f6b, 0x7f6b, 0x7f8d, 0x7f8d, 0x7faa,\n    0x7faa, 0x7fc3, 0x7fc3, 0x7fd7, 0x7fd7, 0x7fe7, 0x7fe7, 0x7ff2, 0x7ff2, 0x7ffa,\n    0x7ffa, 0x7fff, 0x7fff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,\n    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0\n);\n\nZPEncoder.prototype.up = ZPDecoder.prototype.up = Uint8Array.of(\n    84, 3, 4, 5, 6, 7, 8, 9, 10, 11,\n    12, 13, 14, 15, 16, 17, 18, 19, 20, 21,\n    22, 23, 24, 25, 26, 27, 28, 29, 30, 31,\n    32, 33, 34, 35, 36, 37, 38, 39, 40, 41,\n    42, 43, 44, 45, 46, 47, 48, 49, 50, 51,\n    52, 53, 54, 55, 56, 57, 58, 59, 60, 61,\n    62, 63, 64, 65, 66, 67, 68, 69, 70, 71,\n    72, 73, 74, 75, 76, 77, 78, 79, 80, 81,\n    82, 81, 82, 9, 86, 5, 88, 89, 90, 91,\n    92, 93, 94, 95, 96, 97, 82, 99, 76, 101,\n    70, 103, 66, 105, 106, 107, 66, 109, 60, 111,\n    56, 69, 114, 65, 116, 61, 118, 57, 120, 53,\n    122, 49, 124, 43, 72, 39, 60, 33, 56, 29,\n    52, 23, 48, 23, 42, 137, 38, 21, 140, 15,\n    142, 9, 144, 141, 146, 147, 148, 149, 150, 151,\n    152, 153, 154, 155, 70, 157, 66, 81, 62, 75,\n    58, 69, 54, 65, 50, 167, 44, 65, 40, 59,\n    34, 55, 30, 175, 24, 177, 178, 179, 180, 181,\n    182, 183, 184, 69, 186, 59, 188, 55, 190, 51,\n    192, 47, 194, 41, 196, 37, 198, 199, 72, 201,\n    62, 203, 58, 205, 54, 207, 50, 209, 46, 211,\n    40, 213, 36, 215, 30, 217, 26, 219, 20, 71,\n    14, 61, 14, 57, 8, 53, 228, 49, 230, 45,\n    232, 39, 234, 35, 138, 29, 24, 25, 240, 19,\n    22, 13, 16, 13, 10, 7, 244, 249, 10, 89, 230\n);\n\nZPEncoder.prototype.dn = ZPDecoder.prototype.dn = Uint8Array.of(\n    145, 4, 3, 1, 2, 3, 4, 5, 6, 7,\n    8, 9, 10, 11, 12, 13, 14, 15, 16, 17,\n    18, 19, 20, 21, 22, 23, 24, 25, 26, 27,\n    28, 29, 30, 31, 32, 33, 34, 35, 36, 37,\n    38, 39, 40, 41, 42, 43, 44, 45, 46, 47,\n    48, 49, 50, 51, 52, 53, 54, 55, 56, 57,\n    58, 59, 60, 61, 62, 63, 64, 65, 66, 67,\n    68, 69, 70, 71, 72, 73, 74, 75, 76, 77,\n    78, 79, 80, 85, 226, 6, 176, 143, 138, 141,\n    112, 135, 104, 133, 100, 129, 98, 127, 72, 125,\n    102, 123, 60, 121, 110, 119, 108, 117, 54, 115,\n    48, 113, 134, 59, 132, 55, 130, 51, 128, 47,\n    126, 41, 62, 37, 66, 31, 54, 25, 50, 131,\n    46, 17, 40, 15, 136, 7, 32, 139, 172, 9,\n    170, 85, 168, 248, 166, 247, 164, 197, 162, 95,\n    160, 173, 158, 165, 156, 161, 60, 159, 56, 71,\n    52, 163, 48, 59, 42, 171, 38, 169, 32, 53,\n    26, 47, 174, 193, 18, 191, 222, 189, 218, 187,\n    216, 185, 214, 61, 212, 53, 210, 49, 208, 45,\n    206, 39, 204, 195, 202, 31, 200, 243, 64, 239,\n    56, 237, 52, 235, 48, 233, 44, 231, 38, 229,\n    34, 227, 28, 225, 22, 223, 16, 221, 220, 63,\n    8, 55, 224, 51, 2, 47, 87, 43, 246, 37,\n    244, 33, 238, 27, 236, 21, 16, 15, 8, 241,\n    242, 7, 10, 245, 2, 1, 83, 250, 2, 143, 246\n);\n"
  },
  {
    "path": "library/src/bzz/BZZDecoder.js",
    "content": "import { ZPDecoder } from '../ZPCodec';\nimport ByteStreamWriter from '../ByteStreamWriter';\nimport ByteStream from '../ByteStream';\n\nexport default class BZZDecoder {\n    constructor(zp) {\n        this.zp = zp;\n        // this.minblock = 10; // нигде не используется, оставлено для документации\n        this.maxblock = 4096;\n        this.FREQMAX = 4;\n        this.CTXIDS = 3;\n        this.ctx = new Uint8Array(300);\n        this.size = 0;\n        this.blocksize = 0;\n        this.data = null;\n    }\n\n    decode_raw(bits) {\n        var n = 1;\n        var m = (1 << bits);\n        while (n < m) {\n            var b = this.zp.decode();\n            n = (n << 1) | b;\n        }\n        return n - m;\n    }\n\n    decode_binary(ctxoff, bits) {\n        var n = 1;\n        var m = (1 << bits);\n        ctxoff--;\n\n        while (n < m) {\n            var b = this.zp.decode(this.ctx, ctxoff + n);\n            n = (n << 1) | b;\n        }\n\n        return n - m;\n    }\n\n    _decode() {\n        this.size = this.decode_raw(24);\n        if (!this.size) {\n            //сработать должно если читать несколько блоков\n            return 0;\n        }\n        if (this.size > this.maxblock * 1024) {\n            throw new Error(\"Too big block. Error\");\n        }\n        // Allocate\n        if (this.blocksize < this.size) {\n            this.blocksize = this.size;\n            this.data = new Uint8Array(this.blocksize);\n        } else if (this.data == null) {\n            this.data = new Uint8Array(this.blocksize);\n        }\n\n        // Decode Estimation Speed\n        var fshift = 0;\n\n        if (this.zp.decode()) {\n            fshift++;\n\n            if (this.zp.decode()) {\n                fshift++;\n            }\n        }\n\n        // Prepare Quasi MTF\n        var mtf = new Uint8Array(256);\n        for (var i = 0; i < 256; i++) {\n            mtf[i] = i;\n        }\n\n        var freq = new Array(this.FREQMAX);\n\n        for (var i = 0; i < this.FREQMAX; freq[i++] = 0);\n\n        var fadd = 4;\n\n        // Decode\n        var mtfno = 3;\n        var markerpos = -1;\n\n        for (var i = 0; i < this.size; i++) {\n            var ctxid = this.CTXIDS - 1;\n\n            if (ctxid > mtfno) {\n                ctxid = mtfno;\n            }\n\n            var ctxoff = 0;\n\n            switch (0) // чтобы можно было использовать break\n            {\n                default:\n\n                    if (this.zp.decode(this.ctx, ctxoff + ctxid) != 0) {\n                        mtfno = 0;\n                        this.data[i] = mtf[mtfno];\n                        break;\n                    }\n\n                    ctxoff += this.CTXIDS;\n\n                    if (this.zp.decode(this.ctx, ctxoff + ctxid) != 0) {\n                        mtfno = 1;\n                        this.data[i] = mtf[mtfno];\n                        break;\n                    }\n\n                    ctxoff += this.CTXIDS;\n\n                    if (this.zp.decode(this.ctx, ctxoff + 0) != 0) {\n                        mtfno = 2 + this.decode_binary(ctxoff + 1, 1);\n                        this.data[i] = mtf[mtfno];\n                        break;\n                    }\n\n                    ctxoff += (1 + 1);\n\n                    if (this.zp.decode(this.ctx, ctxoff + 0) != 0) {\n                        mtfno = 4 + this.decode_binary(ctxoff + 1, 2);\n                        this.data[i] = mtf[mtfno];\n                        break;\n                    }\n\n                    ctxoff += (1 + 3);\n\n                    if (this.zp.decode(this.ctx, ctxoff + 0) != 0) {\n                        mtfno = 8 + this.decode_binary(ctxoff + 1, 3);\n                        this.data[i] = mtf[mtfno];\n                        break;\n                    }\n\n                    ctxoff += (1 + 7);\n\n                    if (this.zp.decode(this.ctx, ctxoff + 0) != 0) {\n                        mtfno = 16 + this.decode_binary(ctxoff + 1, 4);\n                        this.data[i] = mtf[mtfno];\n                        break;\n                    }\n\n                    ctxoff += (1 + 15);\n\n                    if (this.zp.decode(this.ctx, ctxoff + 0) != 0) {\n                        mtfno = 32 + this.decode_binary(ctxoff + 1, 5);\n                        this.data[i] = mtf[mtfno];\n                        break;\n                    }\n\n                    ctxoff += (1 + 31);\n\n                    if (this.zp.decode(this.ctx, ctxoff + 0) != 0) {\n                        mtfno = 64 + this.decode_binary(ctxoff + 1, 6);\n                        this.data[i] = mtf[mtfno];\n                        break;\n                    }\n\n                    ctxoff += (1 + 63);\n\n                    if (this.zp.decode(this.ctx, ctxoff + 0) != 0) {\n                        mtfno = 128 + this.decode_binary(ctxoff + 1, 7);\n                        this.data[i] = mtf[mtfno];\n                        break;\n                    }\n\n                    mtfno = 256;\n                    this.data[i] = 0;\n                    markerpos = i;\n                    continue;\n            }\n\n            // Rotate mtf according to empirical frequencies (new!)\n            // Adjust frequencies for overflow\n            var k;\n            fadd = fadd + (fadd >> fshift);\n\n            if (fadd > 0x10000000) {\n                fadd >>= 24;\n                freq[0] >>= 24;\n                freq[1] >>= 24;\n                freq[2] >>= 24;\n                freq[3] >>= 24;\n\n                for (k = 4; k < this.FREQMAX; k++) {\n                    freq[k] >>= 24;\n                }\n            }\n\n            // Relocate new char according to new freq\n            var fc = fadd;\n\n            if (mtfno < this.FREQMAX) {\n                fc += freq[mtfno];\n            }\n\n            for (k = mtfno; k >= this.FREQMAX; k--) {\n                mtf[k] = mtf[k - 1];\n            }\n\n            for (; (k > 0) && ((0xffffffff & fc) >= (0xffffffff & freq[k - 1])); k--) {\n                mtf[k] = mtf[k - 1];\n                freq[k] = freq[k - 1];\n            }\n\n            mtf[k] = this.data[i];\n            freq[k] = fc;\n        }\n\n        /////////////////////////////////\n        ////////// Reconstruct the string\n        if ((markerpos < 1) || (markerpos >= this.size)) {\n            throw new Error(\"BZZ byte stream is corrupted\");\n        }\n\n        // Allocate poleters\n        var pos = new Uint32Array(this.size);\n\n        for (var j = 0; j < this.size; pos[j++] = 0);\n\n        // Prepare count buffer\n        var count = new Array(256);\n\n        for (var i = 0; i < 256; count[i++] = 0);\n\n        // Fill count buffer\n        for (var i = 0; i < markerpos; i++) {\n            var c = this.data[i];\n            pos[i] = (c << 24) | (count[0xff & c] & 0xffffff);\n            count[0xff & c]++;\n        }\n\n        for (var i = markerpos + 1; i < this.size; i++) {\n            var c = this.data[i];\n            pos[i] = (c << 24) | (count[0xff & c] & 0xffffff);\n            count[0xff & c]++;\n        }\n\n        // Compute sorted char positions\n        var last = 1;\n\n        for (var i = 0; i < 256; i++) {\n            var tmp = count[i];\n            count[i] = last;\n            last += tmp;\n        }\n\n        // Undo the sort transform\n        var j = 0;\n        last = this.size - 1;\n\n        while (last > 0) {\n            var n = pos[j];\n            var c = pos[j] >> 24;\n            this.data[--last] = 0xff & c;\n            j = count[0xff & c] + (n & 0xffffff);\n        }\n\n        // Free and check\n        if (j != markerpos) {\n            throw new Error(\"BZZ byte stream is corrupted\");\n        }\n\n        return this.size;\n    }\n\n    /** @return {ByteStream} */\n    getByteStream() {\n        var bsw, size;\n        while (size = this._decode()) {\n            if (!bsw) {\n                bsw = new ByteStreamWriter(size - 1);\n            }\n            // From specification: \"The array DATA[0...BLOCKSIZE-2] then contains the decoded bytes of the block.\" So size - 1; \n            var arr = new Uint8Array(this.data.buffer, 0, size - 1);\n            bsw.writeArray(arr);\n        }\n        // для высвобождения памяти.\n        this.data = null;\n        return new ByteStream(bsw.getBuffer());\n    }\n\n    /**\n     * @param {ByteStream} bs\n     * @return {ByteStream}\n     */\n    static decodeByteStream(bs) {\n        return new BZZDecoder(new ZPDecoder(bs)).getByteStream();\n    }\n\n}\n"
  },
  {
    "path": "library/src/bzz/BZZEncoder.js",
    "content": "import { ZPEncoder } from '../ZPCodec';\n\n/*\n* Предполагается, что все данные будут закодированы одним блоком.\n* Причем блок уже будет оканчиваться дополнительным 0 в качестве конечного символа\n*/\nexport default class BZZEncoder {\n    constructor(zp) {\n        this.zp = zp || new ZPEncoder();\n        // this.minblock = 10; // оставлено для документации, сейчас не используется\n        // this.maxblock = 4096;\n        this.FREQMAX = 4;\n        this.CTXIDS = 3;\n        this.ctx = new Uint8Array(300);\n        this.size = 0;\n        this.blocksize = 0;\n        this.FREQS0 = 100000;\n        this.FREQS1 = 1000000;\n    }\n\n    // сортировка на основе встроенной функции, может быть не очень оптимальна\n    blocksort(arr) {\n        var length = arr.length;\n        //массив смещений\n        var offs = new Array(arr.length);\n        //var markerpos = this.markerpos;\n        for (var i = 0; i < length; offs[i] = i++) { }\n        // сортируем массив смещений\n        offs.sort((a, b) => {\n            for (var i = 0; i < length; i++) {\n                // <EOB> конечный символ предполагается самым маленьким, \n                // то есть идет первым при сортировке по возрастанию\n                if (a === this.markerpos) {\n                    return -1;\n                }\n                else if (b === this.markerpos) {\n                    return 1;\n                }\n                var res = arr[a % length] - arr[b % length];\n                if (res) {\n                    return res;\n                }\n                a++;\n                b++;\n            }\n            return 0;\n        });\n\n        var narr = new Uint8Array(length);\n        for (var i = 0; i < length; i++) {\n            var pos = offs[i] - 1;\n            if (pos >= 0) {\n                narr[i] = arr[pos];\n            }\n            else {\n                narr[i] = 0;\n                this.markerpos = i;\n            }\n        }\n\n        return narr;\n    }\n\n    encode_raw(bits, x) {\n        var n = 1;\n        var m = (1 << bits);\n        while (n < m) {\n            x = (x & (m - 1)) << 1;\n            var b = (x >> bits);\n            this.zp.encode(b);\n            n = (n << 1) | b;\n        }\n    }\n\n    encode_binary(cxtoff, bits, x) {\n        // Require 2^bits-1  contexts\n        var n = 1;\n        var m = (1 << bits);\n        cxtoff--;\n        while (n < m) {\n            x = (x & (m - 1)) << 1;\n            var b = (x >> bits);\n            this.zp.encode(b, this.ctx, cxtoff + n);\n            n = (n << 1) | b;\n        }\n    }\n\n    encode(buffer) {\n        /////////////////////////////////\n        ////////////  Block Sort Tranform\n        var data = new Uint8Array(buffer);\n        var size = data.length;\n        var markerpos = size - 1;\n        this.markerpos = markerpos;\n        data = this.blocksort(data);\n        markerpos = this.markerpos;\n\n        /////////////////////////////////\n        //////////// Encode Output Stream\n\n        // Header\n        this.encode_raw(24, size);\n        // Determine and Encode Estimation Speed\n        var fshift = 0;\n        if (size < this.FREQS0) {\n            fshift = 0;\n            this.zp.encode(0);\n        }\n        else if (size < this.FREQS1) {\n            fshift = 1;\n            this.zp.encode(1);\n            this.zp.encode(0);\n        }\n        else {\n            fshift = 2;\n            this.zp.encode(1);\n            this.zp.encode(1);\n        }\n        // MTF\n        var mtf = new Uint8Array(256);\n        var rmtf = new Uint8Array(256);\n        var freq = new Uint32Array(this.FREQMAX);\n        var m = 0;\n        for (m = 0; m < 256; m++)\n            mtf[m] = m;\n        for (m = 0; m < 256; m++)\n            rmtf[mtf[m]] = m;\n        var fadd = 4;\n        for (m = 0; m < this.FREQMAX; m++)\n            freq[m] = 0;\n        // Encode\n        var i;\n        var mtfno = 3;\n        for (i = 0; i < size; i++) {\n            // Get MTF data\n            var c = data[i];\n            var ctxid = this.CTXIDS - 1;\n            if (ctxid > mtfno)\n                ctxid = mtfno;\n            mtfno = rmtf[c];\n            if (i == markerpos)\n                mtfno = 256;\n            // Encode using ZPEncoder\n            var b;\n            //вместо BitContext *cx = ctx; на С++\n            var ctxoff = 0;\n\n            switch (0) // чтобы можно было использовать break\n            {\n                default:\n                    b = (mtfno == 0);\n                    this.zp.encode(b, this.ctx, ctxoff + ctxid);\n                    if (b)\n                        // вместо goto rotate;\n                        break;\n                    ctxoff += this.CTXIDS;\n                    b = (mtfno == 1);\n                    this.zp.encode(b, this.ctx, ctxoff + ctxid);\n                    if (b)\n                        break;\n                    ctxoff += this.CTXIDS;\n                    b = (mtfno < 4);\n                    this.zp.encode(b, this.ctx, ctxoff);\n                    if (b) {\n                        this.encode_binary(ctxoff + 1, 1, mtfno - 2);\n                        break;\n                    }\n                    ctxoff += 1 + 1;\n                    b = (mtfno < 8);\n                    this.zp.encode(b, this.ctx, ctxoff);\n                    if (b) {\n                        this.encode_binary(ctxoff + 1, 2, mtfno - 4);\n                        break;\n                    }\n                    ctxoff += 1 + 3;\n                    b = (mtfno < 16);\n                    this.zp.encode(b, this.ctx, ctxoff);\n                    if (b) {\n                        this.encode_binary(ctxoff + 1, 3, mtfno - 8);\n                        break;\n                    }\n                    ctxoff += 1 + 7;\n                    b = (mtfno < 32);\n                    this.zp.encode(b, this.ctx, ctxoff);\n                    if (b) {\n                        this.encode_binary(ctxoff + 1, 4, mtfno - 16);\n                        break;\n                    }\n                    ctxoff += 1 + 15;\n                    b = (mtfno < 64);\n                    this.zp.encode(b, this.ctx, ctxoff);\n                    if (b) {\n                        this.encode_binary(ctxoff + 1, 5, mtfno - 32);\n                        break;\n                    }\n                    ctxoff += 1 + 31;\n                    b = (mtfno < 128);\n                    this.zp.encode(b, this.ctx, ctxoff);\n                    if (b) {\n                        this.encode_binary(ctxoff + 1, 6, mtfno - 64);\n                        break;\n                    }\n                    ctxoff += 1 + 63;\n                    b = (mtfno < 256);\n                    this.zp.encode(b, this.ctx, ctxoff);\n                    if (b) {\n                        this.encode_binary(ctxoff + 1, 7, mtfno - 128);\n                        break;\n                    }\n                    continue;\n                // Rotate MTF according to empirical frequencies (new!)\n            }\n            // Adjust frequencies for overflow\n            fadd = fadd + (fadd >> fshift);\n            if (fadd > 0x10000000) {\n                fadd = fadd >> 24;\n                freq[0] >>= 24;\n                freq[1] >>= 24;\n                freq[2] >>= 24;\n                freq[3] >>= 24;\n                for (var k = 4; k < this.FREQMAX; k++)\n                    freq[k] = freq[k] >> 24;\n            }\n            // Relocate new char according to new freq\n            var fc = fadd;\n            if (mtfno < this.FREQMAX)\n                fc += freq[mtfno];\n            var k;\n            for (k = mtfno; k >= this.FREQMAX; k--) {\n                mtf[k] = mtf[k - 1];\n                rmtf[mtf[k]] = k;\n            }\n            for (; k > 0 && fc >= freq[k - 1]; k--) {\n                mtf[k] = mtf[k - 1];\n                freq[k] = freq[k - 1];\n                rmtf[mtf[k]] = k;\n            }\n            mtf[k] = c;\n            freq[k] = fc;\n            rmtf[mtf[k]] = k;\n        }\n\n        // Encode EOF marker\n        this.encode_raw(24, 0);\n        this.zp.eflush();\n        // Terminate\n        return 0;\n    }\n}\n"
  },
  {
    "path": "library/src/chunks/DirmChunk.js",
    "content": "import { IFFChunk } from './IFFChunks';\nimport BZZDecoder from '../bzz/BZZDecoder';\n\n/**\n * Порция данных машинного оглавления документа. \n * Содержит сведения о структуре многостраничного документа\n */\nexport default class DIRMChunk extends IFFChunk {\n    constructor(bs) {\n        super(bs);\n        this.dflags = bs.byte(); // saved just to copy to a new file in the DjVuDocument::slice() method (look at DjVuWriter)\n        this.isBundled = this.dflags >> 7;\n        this.nfiles = bs.getInt16();\n\n        if (this.isBundled) {\n            this.offsets = new Int32Array(this.nfiles);\n            for (var i = 0; i < this.nfiles; i++) {\n                this.offsets[i] = bs.getInt32();\n            }\n        }\n\n        this.sizes = new Uint32Array(this.nfiles);\n        this.flags = new Uint8Array(this.nfiles);\n        this.ids = new Array(this.nfiles);\n        this.names = new Array(this.nfiles);\n        this.titles = new Array(this.nfiles);\n        var bsz = BZZDecoder.decodeByteStream(bs.fork());\n\n        for (var i = 0; i < this.nfiles; i++) {\n            this.sizes[i] = bsz.getUint24();\n        }\n        for (var i = 0; i < this.nfiles; i++) {\n            this.flags[i] = bsz.byte();\n        }\n\n        this.pagesIds = [];\n        this.idToNameRegistry = {};\n        for (var i = 0; i < this.nfiles && !bsz.isEmpty(); i++) {\n            this.ids[i] = bsz.readStrNT();\n            this.names[i] = this.flags[i] & 128 ? bsz.readStrNT() : this.ids[i]; // check hasname flag\n            this.titles[i] = this.flags[i] & 64 ? bsz.readStrNT() : this.ids[i]; // check hastitle flag\n\n            if (this.isPageIndex(i)) {\n                this.pagesIds.push(this.ids[i]);\n            }\n            this.idToNameRegistry[this.ids[i]] = this.names[i];\n        }\n    }\n\n    isPageIndex(i) {\n        return (this.flags[i] & 63) === 1;\n    }\n\n    isThumbnailIndex(i) {\n        return (this.flags[i] & 63) === 2;\n    }\n\n    /*isDependencyIndex(i) { // function just for documentation\n        return (this.flags[i] & 63) === 0;\n    }*/\n\n    getPageNameByItsNumber(number) {\n        return this.getComponentNameByItsId(this.pagesIds[number - 1]);\n    }\n\n    getPageNumberByItsId(id) {\n        const index = this.pagesIds.indexOf(id);\n        return index === -1 ? null : (index + 1);\n    }\n\n    getComponentNameByItsId(id) {\n        return this.idToNameRegistry[id];\n    }\n\n    getPagesQuantity() {\n        return this.pagesIds.length;\n    }\n\n    getFilesQuantity() {\n        return this.nfiles;\n    }\n\n    getMetadataStringByIndex(i) {\n        return (\n            `[id: \"${this.ids[i]}\", flag: ${this.flags[i]}, ` +\n            `offset: ${this.offsets ? this.offsets[i] : 'indirect'}, size: ${this.sizes[i]}]\\n`\n        );\n    }\n\n    toString() {\n        var str = super.toString();\n        str += (this.isBundled ? 'Bundled' : 'Indirect') + '\\n';\n        str += \"FilesCount: \" + this.nfiles + '\\n';\n        return str + '\\n';\n    }\n}"
  },
  {
    "path": "library/src/chunks/DjViChunk.js",
    "content": "import JB2Dict from '../jb2/JB2Dict';\nimport { IFFChunk, CompositeChunk } from './IFFChunks';\nimport DjVuAnno from './DjVuAnno';\n\nexport default class DjViChunk extends CompositeChunk {\n    constructor(bs) {\n        super(bs);\n        this.innerChunk = null;\n        this.init();\n    }\n\n    init() {\n        // In some cases there maybe only DJVI headers and nothing else (assets/polish_indirect/shared_anno.iff),\n        // so the innerChunk can remain null\n        while (!this.bs.isEmpty()) {\n            var id = this.bs.readStr4();\n            var length = this.bs.getInt32();\n            this.bs.jump(-8);\n            // вернулись назад\n            var chunkBs = this.bs.fork(length + 8);\n            // перепрыгнули к следующей порции\n            this.bs.jump(8 + length + (length & 1 ? 1 : 0));\n            switch (id) {\n                case 'Djbz':\n                    this.innerChunk = new JB2Dict(chunkBs);\n                    break;\n\n                case 'ANTa':\n                case 'ANTz':\n                    this.innerChunk = new DjVuAnno(chunkBs);\n                    break;\n\n                default:\n                    this.innerChunk = new IFFChunk(chunkBs);\n                    console.error(\"Unsupported chunk inside the DJVI chunk: \", id);\n                    break;\n            }\n        }\n    }\n\n    toString() {\n        return super.toString(this.innerChunk.toString());\n    }\n}\n"
  },
  {
    "path": "library/src/chunks/DjVuAnno.js",
    "content": "import { IFFChunk } from './IFFChunks';\n\nexport default class DjVuAnno extends IFFChunk { }"
  },
  {
    "path": "library/src/chunks/DjVuPalette.js",
    "content": "import { IFFChunk } from './IFFChunks';\nimport BZZDecoder from '../bzz/BZZDecoder';\nimport DjVu from '../DjVu';\n\nexport default class DjVuPalette extends IFFChunk {\n\n    /** @param {ByteStream} bs */\n    constructor(bs) {\n        var time = performance.now();\n        super(bs);\n        this.pixel = { r: 0, g: 0, b: 0 };\n\n        this.version = bs.getUint8();\n        if (this.version & 0x7f) {\n            throw \"Bad Djvu Pallete version!\";\n        }\n        this.palleteSize = bs.getInt16();\n        if (this.palleteSize < 0 || this.palleteSize > 65535) {\n            throw \"Bad Djvu Pallete size!\";\n        }\n        this.colorArray = bs.getUint8Array(this.palleteSize * 3);\n        if (this.version & 0x80) {\n            this.dataSize = bs.getInt24();\n            if (this.dataSize < 0) {\n                throw \"Bad Djvu Pallete data size!\";\n            }\n            var bsz = BZZDecoder.decodeByteStream(bs.fork());\n            this.colorIndices = new Int16Array(this.dataSize);\n            for (var i = 0; i < this.dataSize; i++) {\n                var index = bsz.getInt16();\n                if (index < 0 || index >= this.palleteSize) {\n                    throw \"Bad Djvu Pallete index! \" + index;\n                }\n                this.colorIndices[i] = index;\n            }\n        }\n        DjVu.IS_DEBUG && console.log('DjvuPalette time ', performance.now() - time);\n    }\n\n    getDataSize() {\n        return this.dataSize;\n    }\n\n    getPixelByBlitIndex(index) {\n        var colorIndex = this.colorIndices[index] * 3;\n\n        this.pixel.r = this.colorArray[colorIndex + 2];\n        this.pixel.g = this.colorArray[colorIndex + 1];\n        this.pixel.b = this.colorArray[colorIndex];\n\n        return this.pixel;\n    }\n\n    toString() {\n        var str = super.toString();\n        str += \"Pallete size: \" + this.palleteSize + \"\\n\";\n        str += \"Data size: \" + this.dataSize + \"\\n\";\n        return str;\n    }\n}"
  },
  {
    "path": "library/src/chunks/DjVuText.js",
    "content": "import { IFFChunk } from './IFFChunks';\nimport BZZDecoder from '../bzz/BZZDecoder';\nimport { createStringFromUtf8Array } from '../DjVu';\n\n/**\n * @typedef {Object} TextZone\n * @property {number} x - top left corner x coordinate relative to the page\n * @property {number} y - top left corner y coordinate relative to the page\n * @property {number} width\n * @property {number} height\n * @property {string} text\n */\n\n/**\n * @typedef {Object} RawTextZone\n * @property {number} type\n * @property {number} x - top left corner x coordinate relative to the page\n * @property {number} y - top left corner y coordinate relative to the page\n * @property {number} width\n * @property {number} height\n * @property {number} textStart - offset of text in bytes in the raw UTF8 array.\n * @property {number} textLength - length of text in bytes in the raw UTF8 array.\n * @property {Array<RawTextZone>} [children] - nested raw text zones.\n */\n\n/**\n * Класс для порций TXTa и TXTz.\n */\nexport default class DjVuText extends IFFChunk {\n    constructor(bs) {\n        super(bs);\n        this.isDecoded = false;\n        /** @type {import('../ByteStream').ByteStream} */\n        this.dbs = this.id === 'TXTz' ? null : this.bs; // decoded byte stream\n    }\n\n    decode() {\n        if (this.isDecoded) {\n            return;\n        }\n        if (!this.dbs) {\n            this.dbs = BZZDecoder.decodeByteStream(this.bs);\n        }\n\n        this.textLength = this.dbs.getInt24();\n        this.utf8array = this.dbs.getUint8Array(this.textLength);\n\n        this.version = this.dbs.getUint8();\n        if (this.version !== 1) {\n            console.warn(\"The version in \" + this.id + \" isn't equal to 1!\");\n        }\n\n        this.pageZone = this.dbs.isEmpty() ? null : this.decodeZone();\n\n        this.isDecoded = true;\n    }\n\n    /** @returns {RawTextZone} */\n    decodeZone(parent = null, prev = null) {\n\n        var type = this.dbs.getUint8();\n        var x = this.dbs.getUint16() - 0x8000;\n        var y = this.dbs.getUint16() - 0x8000;\n        var width = this.dbs.getUint16() - 0x8000;\n        var height = this.dbs.getUint16() - 0x8000;\n        var textStart = this.dbs.getUint16() - 0x8000; // must be always 0\n        var textLength = this.dbs.getInt24();\n\n        if (prev) {\n            if (type === 1 /*PAGE*/ || type === 4 /*PARAGRAPH*/ || type === 5 /*LINE*/) {\n                x = x + prev.x;\n                y = prev.y - (y + height);\n            } else // Either COLUMN or WORD or CHARACTER\n            {\n                x = x + prev.x + prev.width;\n                y = y + prev.y;\n            }\n            textStart += prev.textStart + prev.textLength;\n        } else if (parent) {\n            x = x + parent.x;\n            y = parent.y + parent.height - (y + height);\n            textStart += parent.textStart;\n        }\n\n        var zone = { type, x, y, width, height, textStart, textLength };\n\n        var childrenCount = this.dbs.getInt24();\n        if (childrenCount) {\n            var children = new Array(childrenCount);\n            var childZone = null;\n            for (var i = 0; i < childrenCount; i++) {\n                childZone = this.decodeZone(zone, childZone);\n                children[i] = childZone;\n            }\n            zone.children = children;\n        }\n\n        return zone;\n    }\n\n    /** @returns {string} */\n    getText() {\n        this.decode();\n        this.text = this.text || createStringFromUtf8Array(this.utf8array);\n        return this.text;\n    }\n\n    /** @returns {?RawTextZone} */\n    getPageZone() {\n        this.decode();\n        return this.pageZone;\n    }\n\n    /** @returns {?Array<TextZone} */\n    getNormalizedZones() {\n        this.decode();\n\n        if (!this.pageZone) {\n            return null;\n        }\n\n        if (this.normalizedZones) {\n            return this.normalizedZones;\n        }\n\n        this.normalizedZones = [];\n        var registry = {};\n\n        const process = (zone) => {\n            if (zone.children) {\n                zone.children.forEach(zone => process(zone));\n            } else {\n                var key = zone.x.toString() + zone.y + zone.width + zone.height;\n                var zoneText = createStringFromUtf8Array(this.utf8array.slice(zone.textStart, zone.textStart + zone.textLength));\n                if (registry[key]) { // unite text of the same zone\n                    registry[key].text += zoneText\n                } else {\n                    registry[key] = {\n                        x: zone.x,\n                        y: zone.y,\n                        width: zone.width,\n                        height: zone.height,\n                        text: zoneText\n                    };\n                    this.normalizedZones.push(registry[key]);\n                }\n            }\n        }\n\n        process(this.pageZone);\n\n        return this.normalizedZones;\n    }\n\n    toString() {\n        this.decode();\n        var st = \"Text length = \" + this.textLength + \"\\n\";\n        return super.toString() + st;\n    }\n}"
  },
  {
    "path": "library/src/chunks/IFFChunks.js",
    "content": "import { CorruptedFileDjVuError } from '../DjVuErrors';\n\n/** @typedef {import('../ByteStream').ByteStream} ByteStream */\n\n// простейший шаблон порции данных\nexport class IFFChunk {\n\n    /** @param {ByteStream} bs */\n    constructor(bs) {\n        this.id = bs.readStr4();\n        this.length = bs.getInt32();\n        this.bs = bs;\n    }\n    toString() {\n        return this.id + \" \" + this.length + '\\n';\n    }\n}\n\nexport class CompositeChunk extends IFFChunk {\n\n    /** @param {ByteStream} bs */\n    constructor(bs) {\n        super(bs);\n        this.id += ':' + bs.readStr4(); // read secondary id\n    }\n\n    toString(innerString = '') {\n        return super.toString() + '    ' + innerString.replace(/\\n/g, '\\n    ') + '\\n';\n    }\n}\n\nexport class ColorChunk extends IFFChunk {\n\n    /** @param {ByteStream} bs */\n    constructor(bs) {\n        super(bs);\n        this.header = new ColorChunkDataHeader(bs);\n    }\n    toString() {\n        return this.id + \" \" + this.length + this.header.toString();\n    }\n}\n\n/**\n * Порция данных содержащая в себе параметры изображения (всей страницы)\n */\nexport class INFOChunk extends IFFChunk {\n\n    /** @param {ByteStream} bs */\n    constructor(bs) {\n        super(bs);\n        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\n            throw new CorruptedFileDjVuError(\"The INFO chunk is shorter than 5 bytes!\")\n        }\n        this.width = bs.getInt16();\n        this.height = bs.getInt16();\n        this.minver = bs.getInt8();\n        this.majver = this.length > 5 ? bs.getInt8() : 0;\n\n        if (this.length > 7) {\n            this.dpi = bs.getUint8();\n            this.dpi |= bs.getUint8() << 8;\n        } else {\n            this.dpi = 300;\n        }\n        this.gamma = this.length > 8 ? bs.getInt8() : 22;\n        this.flags = this.length > 9 ? bs.getInt8() : 0;\n\n        // Fixup - copied from DjVuLibre\n        if (this.dpi < 25 || this.dpi > 6000) {\n            this.dpi = 300;\n        }\n        if (this.gamma < 3) {\n            this.gamma = 3;\n        }\n        if (this.gamma > 50) {\n            this.gamma = 50;\n        }\n    }\n\n    toString() {\n        var str = super.toString();\n        str += \"{\" + 'width:' + this.width + ', '\n            + 'height:' + this.height + ', '\n            + 'minver:' + this.minver + ', '\n            + 'majver:' + this.majver + ', '\n            + 'dpi:' + this.dpi + ', '\n            + 'gamma:' + this.gamma + ', '\n            + 'flags:' + this.flags + '}\\n';\n        return str;\n    }\n}\n\n/**\n * Заголовок порции цветовых порций данных. Содержит сведения о закодированном изображении.\n * Предоставляет основную информацию о порции данных.\n */\nclass ColorChunkDataHeader {\n\n    /** @param {ByteStream} bs */\n    constructor(bs) {\n        this.serial = bs.getUint8(); // номер порции \n        this.slices = bs.getUint8(); // количество кусочков данных\n        if (!this.serial) { // если это первая порция данных изображения\n            this.majver = bs.getUint8(); // номер версии кодироващика (первая цифра) вообще 1\n            this.grayscale = this.majver >> 7; // серое ли изображение\n            this.minver = bs.getUint8(); // номер версии кодировщика (вторая цифра) вообще 2\n            // ширина (высота) изображения.\n            // должна быть равна ширине(высоте) в INFOChunk или быть от 2 до 12 раз меньше\n            this.width = bs.getUint16();\n            this.height = bs.getUint16();\n\n            var byte = bs.getUint8();\n            // задержка декодирования цветовой информации (старший бит должен быть 1, но вообще игнорируется)\n            this.delayInit = byte & 127;\n            if (!byte & 128) {\n                console.warn('Old image reconstruction should be applied!');\n            }\n\n        }\n    }\n    toString() {\n        return '\\n' + JSON.stringify(this) + \"\\n\";\n    }\n}\n\nexport class INCLChunk extends IFFChunk {\n\n    /** @param {ByteStream} bs */\n    constructor(bs) {\n        super(bs);\n        this.ref = this.bs.readStrUTF();\n    }\n    toString() {\n        var str = super.toString();\n        str += \"{Reference: \" + this.ref + '}\\n';\n        return str;\n    }\n}\n\n/**\n * Нестандартная порция данных. \n * Обычно содержит в себе информацию о программе-кодировщике\n */\nexport class CIDaChunk extends INCLChunk { }\n\nexport class ErrorChunk {\n    constructor(id, e) {\n        this.id = id;\n        this.e = e;\n    }\n\n    toString() {\n        return `Error creating ${this.id}: ${this.e.toString()}\\n`;\n    }\n}\n"
  },
  {
    "path": "library/src/chunks/NavmChunk.js",
    "content": "import { IFFChunk } from './IFFChunks';\nimport BZZDecoder from '../bzz/BZZDecoder';\n\n/**\n * @typedef {Object} Bookmark\n * @property {string} description\n * @property {string} url\n * @property {Array<Bookmark>} [children]\n */\n\n/** @typedef {Array<Bookmark>} Contents */\n\n/**\n * Оглавление человеко-читаемое\n */\nexport default class NAVMChunk extends IFFChunk {\n    constructor(bs) {\n        super(bs);\n        this.isDecoded = false;\n        this.contents = [];\n        this.decodedBookmarkCounter = 0;\n    }\n\n    /**\n     * @returns {Contents}\n     */\n    getContents() {\n        this.decode();\n        return this.contents;\n    }\n\n    decode() {\n        if (this.isDecoded) {\n            return;\n        }\n        var dbs = BZZDecoder.decodeByteStream(this.bs);\n        var bookmarksCount = dbs.getUint16();\n        while (this.decodedBookmarkCounter < bookmarksCount) {\n            this.contents.push(this.decodeBookmark(dbs));\n        }\n        this.isDecoded = true;\n    }\n\n    /** \n     * @param {import('../ByteStream').ByteStream} bs\n     * @returns {Bookmark}\n     */\n    decodeBookmark(bs) {\n        var childrenCount = bs.getUint8();\n        var descriptionLength = bs.getInt24();\n        var description = descriptionLength ? bs.readStrUTF(descriptionLength) : '';\n        var urlLength = bs.getInt24();\n        var url = urlLength ? bs.readStrUTF(urlLength) : '';\n        this.decodedBookmarkCounter++;\n\n        var bookmark = { description, url };\n        if (childrenCount) {\n            var children = new Array(childrenCount);\n            for (var i = 0; i < childrenCount; i++) {\n                children[i] = this.decodeBookmark(bs);\n            }\n            bookmark.children = children;\n        }\n\n        return bookmark;\n    }\n\n    toString() {\n        this.decode();\n        var indent = '    ';\n\n        function stringifyBookmark(bookmark, indentSize = 0) {\n            var str = indent.repeat(indentSize) + `${bookmark.description} (${bookmark.url})\\n`;\n            if (bookmark.children) {\n                str = bookmark.children.reduce((str, bookmark) => str + stringifyBookmark(bookmark, indentSize + 1), str);\n            }\n            return str;\n        }\n\n        var str = this.contents.reduce((str, bookmark) => str + stringifyBookmark(bookmark), super.toString());\n        return str + '\\n';\n    }\n}"
  },
  {
    "path": "library/src/chunks/ThumChunk.js",
    "content": "import { CompositeChunk } from './IFFChunks';\n\nexport default class ThumChunk extends CompositeChunk { }"
  },
  {
    "path": "library/src/index.js",
    "content": "/**\n * Throughout the code mostly vars are used, not consts or lets. \n * It's because of that in 2015-2016, when the library was created, \n * Chrome couldn't optimize ES6 code properly, which resulted in a big performance decrease,\n * about 3 times or even more. Now, in 2020, it seems than ES6 variable declarations \n * don't have that devastating impact anymore, so they can be used.\n */\n\nimport DjVu from \"./DjVu\";\nimport DjVuDocument from \"./DjVuDocument\";\nimport DjVuWorker from \"./DjVuWorker\";\nimport initWorker from './DjVuWorkerScript';\nimport { DjVuErrorCodes } from './DjVuErrors';\n\nif (!self.document) { // if inside a Worker\n    initWorker();\n}\n\nexport default Object.assign({}, DjVu, {\n    Worker: DjVuWorker,\n    Document: DjVuDocument,\n    ErrorCodes: DjVuErrorCodes\n});"
  },
  {
    "path": "library/src/iw44/IWCodecBaseClass.js",
    "content": "\n//класс общих данных для кодирования и декодирования картинки\n/**\n * There are 4 magic values: \n * 1 for ZERO // this coeff never hits this bit\n * 2 for ACTIVE // this coeff is already active активный\n * 4 for NEW // this coeff is becoming active при закодировании используется, когда собираемся закодировать\n * 8 for UNK // потенциальный флаг\n * these 4 are flags. It turned out that it works much faster with raw constats \n * rather than with const variables or properties from the prototype. \n */\nexport default class IWCodecBaseClass {\n    constructor() {\n        this.quant_lo = Uint32Array.of(\n            0x004000, 0x008000, 0x008000, 0x010000, 0x010000,\n            0x010000, 0x010000, 0x010000, 0x010000, 0x010000,\n            0x010000, 0x010000, 0x020000, 0x020000, 0x020000, 0x020000\n        );\n        this.quant_hi = Uint32Array.of(\n            0, 0x020000, 0x020000, 0x040000, 0x040000,\n            0x040000, 0x080000, 0x040000, 0x040000, 0x080000\n        );\n        this.bucketstate = new Uint8Array(16);\n        this.coeffstate = new Array(16);\n        var buffer = new ArrayBuffer(256);\n        for (var i = 0; i < 16; i++) {\n            this.coeffstate[i] = new Uint8Array(buffer, i << 4, 16)\n        }\n        //for (var i = 0; i < 16; this.coeffstate[i++] = new Uint8Array(16)) { }\n        this.bbstate = 0;\n        this.decodeBucketCtx = new Uint8Array(1);\n        this.decodeCoefCtx = new Uint8Array(80);\n        this.activateCoefCtx = new Uint8Array(16);\n        this.inreaseCoefCtx = new Uint8Array(1);\n        this.curband = 0;\n    }\n\n    getBandBuckets(band) {\n        return this.bandBuckets[band];\n    }\n\n    //проверяем надо ли вообще что либо делать или просто уменьшить шаг\n    is_null_slice() {\n        if (this.curband == 0) // для нулевой группы шаги разные, поэтому надо проверить все\n        {\n            var is_null = 1;\n            for (var i = 0; i < 16; i++) {\n                var threshold = this.quant_lo[i];\n                //чтобы не проверять потом этот коэффициент\n                this.coeffstate[0][i] = 1/*ZERO*/;\n                if (threshold > 0 && threshold < 0x8000) {\n                    this.coeffstate[0][i] = 8/*UNK*/;\n                    is_null = 0;\n                }\n            }\n            return is_null;\n        } else // иначе просто смотрим шаг группы\n        {\n            var threshold = this.quant_hi[this.curband];\n            return (!(threshold > 0 && threshold < 0x8000));\n        }\n    }\n    //уменьшение шага после обработки одной порции данных\n    // todo использовать curbit\n    finish_code_slice() {\n        this.quant_hi[this.curband] = this.quant_hi[this.curband] >> 1;\n        if (this.curband === 0) {\n            for (var i = 0; i < 16; i++)\n                this.quant_lo[i] = this.quant_lo[i] >> 1;\n        }\n        this.curband++;\n        if (this.curband === 10) {\n            this.curband = 0;\n        }\n    }\n}\n// this coeff never hits this bit\nIWCodecBaseClass.prototype.ZERO = 1;\n// this coeff is already active активный\nIWCodecBaseClass.prototype.ACTIVE = 2;\n// this coeff is becoming active при закодировании используется, когда собираемся закодировать\nIWCodecBaseClass.prototype.NEW = 4;\n// потенциальный флаг\nIWCodecBaseClass.prototype.UNK = 8;\nIWCodecBaseClass.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);\nIWCodecBaseClass.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);\n\n// массив соответсвия номера группы и номеров сегментов (bucket'ов)\nIWCodecBaseClass.prototype.bandBuckets = [\n    { from: 0, to: 0 },\n    { from: 1, to: 1 },\n    { from: 2, to: 2 },\n    { from: 3, to: 3 },\n    { from: 4, to: 7 },\n    { from: 8, to: 11 },\n    { from: 12, to: 15 },\n    { from: 16, to: 31 },\n    { from: 32, to: 47 },\n    { from: 48, to: 63 }\n];\n"
  },
  {
    "path": "library/src/iw44/IWDecoder.js",
    "content": "import IWCodecBaseClass from './IWCodecBaseClass';\nimport { LinearBytemap, Block, LazyBlock } from './IWStructures';\nimport DjVu from '../DjVu';\n\nexport default class IWDecoder extends IWCodecBaseClass {\n\n    constructor() {\n        super();\n    }\n\n    init(imageinfo) {\n        // инициализируем на первой порции данных\n        this.info = imageinfo;\n        var blockCount = Math.ceil(this.info.width / 32) * Math.ceil(this.info.height / 32);\n        this.blocks = LazyBlock.createBlockArray(blockCount);\n    }\n\n    decodeSlice(zp, imageinfo) {\n        if (!this.info) {\n            this.init(imageinfo);\n        }\n\n        this.zp = zp;\n        if (!this.is_null_slice()) {\n            // по блокам идем            \n            this.blocks.forEach(block => {\n                this.preliminaryFlagComputation(block);\n                // четыре подхода декодирования\n                if (this.blockBandDecodingPass()) {\n                    this.bucketDecodingPass(block, this.curband);\n                    this.newlyActiveCoefficientDecodingPass(block, this.curband);\n                }\n                this.previouslyActiveCoefficientDecodingPass(block);\n            });\n        }\n        // уменьшаем шаги \n        this.finish_code_slice();\n    }\n\n    previouslyActiveCoefficientDecodingPass(block) {\n        var boff = 0;\n        var step = this.quant_hi[this.curband];\n        var indices = this.getBandBuckets(this.curband);\n        for (var i = indices.from; i <= indices.to; i++, boff++) {\n            for (var j = 0; j < 16; j++) {\n                if (this.coeffstate[boff][j] & 2 /*ACTIVE*/) {\n                    if (!this.curband) {\n                        step = this.quant_lo[j];\n                    }\n                    var des = 0;\n                    var coef = block.getBucketCoef(i, j);\n                    var absCoef = Math.abs(coef);\n                    if (absCoef <= 3 * step) {\n                        des = this.zp.decode(this.inreaseCoefCtx, 0);\n                        absCoef += step >> 2;\n                    } else {\n                        des = this.zp.IWdecode();\n                    }\n                    if (des) {\n                        absCoef += step >> 1;\n                    } else {\n                        absCoef += -step + (step >> 1);\n                    }\n                    block.setBucketCoef(i, j, coef < 0 ? -absCoef : absCoef);\n                }\n            }\n        }\n    }\n\n    newlyActiveCoefficientDecodingPass(block, band) {\n        //bucket offset\n        var boff = 0;\n        var indices = this.getBandBuckets(band);\n        //проверка на 0 группу позже\n        var step = this.quant_hi[this.curband];\n        for (var i = indices.from; i <= indices.to; i++, boff++) {\n            if (this.bucketstate[boff] & 4/*NEW*/) {\n                var shift = 0;\n                if (this.bucketstate[boff] & 2/*ACTIVE*/) {\n                    shift = 8;\n                }\n                var np = 0;\n                for (var j = 0; j < 16; j++) {\n                    if (this.coeffstate[boff][j] & 8/*UNK*/) {\n                        np++;\n                    }\n                }\n\n                for (var j = 0; j < 16; j++) {\n                    if (this.coeffstate[boff][j] & 8/*UNK*/) {\n                        var ip = Math.min(7, np);\n                        var des = this.zp.decode(this.activateCoefCtx, shift + ip);\n                        if (des) {\n                            var sign = this.zp.IWdecode() ? -1 : 1;\n                            np = 0;\n                            if (!this.curband) {\n                                step = this.quant_lo[j];\n                            }\n                            //todo сравнить нужно ли 2 слагаемое\n                            block.setBucketCoef(i, j, sign * (step + (step >> 1) - (step >> 3)));\n                        }\n                        if (np) {\n                            np--;\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    bucketDecodingPass(block, band) {\n        var indices = this.getBandBuckets(band);\n        // смещение сегмента\n        var boff = 0;\n        for (var i = indices.from; i <= indices.to; i++, boff++) {\n            // проверка потенциального флага сегмента         \n            if (!(this.bucketstate[boff] & 8/*UNK*/)) {\n                continue;\n            }\n            //вычисляем номер контекста\n            var n = 0;\n            if (band) {\n                var t = 4 * i;\n                for (var j = t; j < t + 4; j++) {\n                    if (block.getCoef(j)) {\n                        n++;\n                    }\n                }\n                if (n === 4) {\n                    n--;\n                }\n            }\n            if (this.bbstate & 2/*ACTIVE*/) {\n                //как и + 4\n                n |= 4;\n            }\n            if (this.zp.decode(this.decodeCoefCtx, n + band * 8)) {\n                this.bucketstate[boff] |= 4/*NEW*/;\n            }\n        }\n    }\n\n    blockBandDecodingPass() {\n        var indices = this.getBandBuckets(this.curband);\n        var bcount = indices.to - indices.from + 1;\n        if (bcount < 16 || (this.bbstate & 2/*ACTIVE*/)) {\n            this.bbstate |= 4 /*NEW*/;\n        } else if (this.bbstate & 8/*UNK*/) {\n            if (this.zp.decode(this.decodeBucketCtx, 0)) {\n                this.bbstate |= 4/*NEW*/;\n            }\n        }\n        return this.bbstate & 4/*NEW*/;\n    }\n\n    preliminaryFlagComputation(block) {\n        this.bbstate = 0;\n        var bstatetmp = 0;\n        var indices = this.getBandBuckets(this.curband);\n        if (this.curband) {\n            //смещение сегмента в массиве флагов\n            var boff = 0;\n            for (var j = indices.from; j <= indices.to; j++, boff++) {\n                bstatetmp = 0;\n                for (var k = 0; k < 16; k++) {\n                    if (block.getBucketCoef(j, k) === 0) {\n                        this.coeffstate[boff][k] = 8/*UNK*/;\n                    } else {\n                        this.coeffstate[boff][k] = 2/*ACTIVE*/;\n                    }\n                    bstatetmp |= this.coeffstate[boff][k];\n                }\n                this.bucketstate[boff] = bstatetmp;\n                this.bbstate |= bstatetmp;\n            }\n        } else {\n            //если нулевая группа            \n            for (var k = 0; k < 16; k++) {\n                //если шаг в допустимых пределах\n                if (this.coeffstate[0][k] !== 1/*ZERO*/) {\n                    if (block.getBucketCoef(0, k) === 0) {\n                        this.coeffstate[0][k] = 8/*UNK*/;\n                    } else {\n                        this.coeffstate[0][k] = 2/*ACTIVE*/;\n                    }\n                }\n                bstatetmp |= this.coeffstate[0][k];\n            }\n            this.bucketstate[0] = bstatetmp;\n            this.bbstate |= bstatetmp;\n        }\n    }\n\n    getBytemap() {\n        var time = performance.now();\n\n        var fullWidth = Math.ceil(this.info.width / 32) * 32;\n        var fullHeight = Math.ceil(this.info.height / 32) * 32;\n        var blockRows = Math.ceil(this.info.height / 32);\n        var blockCols = Math.ceil(this.info.width / 32);\n\n        // (this.blocks[0] instanceof LazyBlock) && console.log('Memory usage ',\n        //     this.blocks[0].mm.retainedMemory / 1024 / 1024,\n        //     this.blocks[0].mm.usedMemory / 1024 / 1024);\n\n        var bm = new LinearBytemap(fullWidth, fullHeight);\n        for (var r = 0; r < blockRows; r++) {\n            for (var c = 0; c < blockCols; c++) {\n                var block = this.blocks[r * blockCols + c];\n                for (var i = 0; i < 1024; i++) {\n                    /*var bits = [];\n                    for (var j = 0; j < 10; j++) {\n                        bits.push((i & Math.pow(2, j)) >> j);\n                    }\n                    var row = 16 * bits[1] + 8 * bits[3] + 4 * bits[5] + 2 * bits[7] + bits[9];\n                    var col = 16 * bits[0] + 8 * bits[2] + 4 * bits[4] + 2 * bits[6] + bits[8];*/\n                    // bitmap[this.zigzagRow[i] + 32 * r][this.zigzagCol[i] + 32 * c] = block.getCoef(i);\n                    bm.set(this.zigzagRow[i] + (r << 5), this.zigzagCol[i] + (c << 5), block.getCoef(i));\n                }\n            }\n        }\n\n        DjVu.IS_DEBUG && console.time(\"inverseTime\");\n        this.inverseWaveletTransform(bm);\n        DjVu.IS_DEBUG && console.timeEnd(\"inverseTime\");\n\n        DjVu.IS_DEBUG && console.log(\"getBytemap time = \", performance.now() - time);\n\n        return bm;\n    }\n\n    /**\n     * Алгоритм для строк и для столбцов по сути один и тот же. Разница в том,\n     * что индексы переставлены местами.\n     * \n     * The variant when the algorithm is extracted as a separate function \n     * and used both for columns with the bytemap object and for rows via a shim object \n     * (which swaps i and j in all methods' of the Bytemap, even \n     * an optimized version working with the inner array directly)\n     * works slower than the current variant. \n     */\n    inverseWaveletTransform(bitmap) {\n        var height = this.info.height;\n        var width = this.info.width;\n        var a, c, kmax, k, i, border;\n        var prev3, prev1, next1, next3;\n\n        for (var s = 16, sDegree = 4; s !== 0; s >>= 1, sDegree--) { // 2^4 === 16\n            //для столбцов\n            kmax = (height - 1) >> sDegree;\n            border = kmax - 3;\n            for (i = 0; i < width; i += s) {\n                //Lifting\n\n                k = 0;\n                prev1 = 0; next1 = 0;\n                next3 = 1 > kmax ? 0 : bitmap.get(1 << sDegree, i);\n\n                for (k = 0; k <= kmax; k += 2) {\n                    prev3 = prev1; prev1 = next1; next1 = next3;\n                    next3 = (k + 3) > kmax ? 0 : bitmap.get((k + 3) << sDegree, i);\n\n                    a = prev1 + next1;\n                    c = prev3 + next3;\n                    bitmap.sub(k << sDegree, i, ((a << 3) + a - c + 16) >> 5);\n                }\n\n                //Prediction \n\n                k = 1;\n                prev1 = bitmap.get((k - 1) << sDegree, i);\n                if (k + 1 <= kmax) {\n                    next1 = bitmap.get((k + 1) << sDegree, i);\n                    bitmap.add(k << sDegree, i, (prev1 + next1 + 1) >> 1);\n                } else {\n                    bitmap.add(k << sDegree, i, prev1);\n                }\n\n                if (border >= 3) {\n                    next3 = bitmap.get((k + 3) << sDegree, i);\n                }\n\n                for (k = 3; k <= border; k += 2) {\n                    prev3 = prev1; prev1 = next1; next1 = next3;\n                    next3 = bitmap.get((k + 3) << sDegree, i);\n\n                    a = prev1 + next1;\n                    bitmap.add(k << sDegree, i,\n                        ((a << 3) + a - (prev3 + next3) + 8) >> 4\n                    );\n                }\n\n                for (; k <= kmax; k += 2) {\n                    prev1 = next1; next1 = next3; next3 = 0;\n                    if (k + 1 <= kmax) {\n                        bitmap.add(k << sDegree, i, (prev1 + next1 + 1) >> 1);\n                    } else {\n                        bitmap.add(k << sDegree, i, prev1);\n                    }\n                }\n            }\n\n            //для строк\n            kmax = (width - 1) >> sDegree;\n            border = kmax - 3;\n\n            for (i = 0; i < height; i += s) {\n\n                //Lifting\n                k = 0;\n                prev1 = 0;\n                next1 = 0;\n                next3 = 1 > kmax ? 0 : bitmap.get(i, 1 << sDegree);\n\n                for (k = 0; k <= kmax; k += 2) {\n                    prev3 = prev1; prev1 = next1; next1 = next3;\n                    next3 = k + 3 > kmax ? 0 : bitmap.get(i, (k + 3) << sDegree);\n\n                    a = prev1 + next1;\n                    c = prev3 + next3;\n                    bitmap.sub(i, k << sDegree, ((a << 3) + a - c + 16) >> 5);\n                }\n\n                // Выполняется медленнее с этой оптимизацией.\n\n                // for (; k <= kmax; k += 2) {\n                //     prev3 = prev1; prev1 = next1; next1 = next3; next3 = 0;\n\n                //     a = prev1 + next1;\n                //     c = prev3 + next3;\n                //     bitmap.sub(i, k << sDegree, ((a << 3) + a - c + 16) >> 5);\n                // }\n\n                //Prediction\n\n                k = 1\n                prev1 = bitmap.get(i, (k - 1) << sDegree);\n                if (k + 1 <= kmax) {\n                    next1 = bitmap.get(i, (k + 1) << sDegree);\n                    bitmap.add(i, k << sDegree, (prev1 + next1 + 1) >> 1);\n                } else {\n                    bitmap.add(i, k << sDegree, prev1);\n                }\n\n                if (border >= 3) {\n                    next3 = bitmap.get(i, (k + 3) << sDegree);\n                }\n\n                for (k = 3; k <= border; k += 2) {\n                    prev3 = prev1; prev1 = next1; next1 = next3;\n                    next3 = bitmap.get(i, (k + 3) << sDegree);\n\n                    a = prev1 + next1;\n                    bitmap.add(i, k << sDegree,\n                        ((a << 3) + a - (prev3 + next3) + 8) >> 4\n                    );\n                }\n\n                for (; k <= kmax; k += 2) {\n                    prev1 = next1; next1 = next3; next3 = 0;\n                    if (k + 1 <= kmax) {\n                        bitmap.add(i, k << sDegree, (prev1 + next1 + 1) >> 1);\n                    } else {\n                        bitmap.add(i, k << sDegree, prev1);\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "library/src/iw44/IWEncoder.js",
    "content": "import IWCodecBaseClass from './IWCodecBaseClass';\nimport { Block } from './IWStructures';\n\nexport default class IWEncoder extends IWCodecBaseClass {\n\n    constructor(bytemap) {\n        super();\n        this.width = bytemap.width;\n        this.height = bytemap.height;\n        this.inverseWaveletTransform(bytemap);\n        this.createBlocks(bytemap);\n    }\n\n    /**\n     * Выполняет волновое преобразование\n     */\n    inverseWaveletTransform(bytemap) {\n        // LOOP ON SCALES\n        for (var scale = 1; scale < 32; scale <<= 1) {\n            //сначала строки\n            this.filter_fh(scale, bytemap);\n            //потом столбцы\n            this.filter_fv(scale, bytemap);\n        }\n        return bytemap;\n    }\n    // по сути то же преобразование что и при раскодировании, только в обратном порядке\n    filter_fv(s, bitmap) {\n        //для столбцов\n        var kmax = Math.floor((bitmap.height - 1) / s);\n        for (var i = 0; i < bitmap.width; i += s) {\n            //Prediction \n            for (var k = 1; k <= kmax; k += 2) {\n                if ((k - 3 >= 0) && (k + 3 <= kmax)) {\n                    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;\n                } else if (k + 1 <= kmax) {\n                    bitmap[k * s][i] -= (bitmap[(k - 1) * s][i] + bitmap[(k + 1) * s][i] + 1) >> 1;\n                } else {\n                    bitmap[k * s][i] -= bitmap[(k - 1) * s][i];\n                }\n            }\n            //Lifting\n            for (var k = 0; k <= kmax; k += 2) {\n                var a, b, c, d;\n                //-------------\n                if (k - 1 < 0) {\n                    a = 0;\n                } else {\n                    a = bitmap[(k - 1) * s][i];\n                }\n                //-------------\n                if (k - 3 < 0) {\n                    c = 0;\n                } else {\n                    c = bitmap[(k - 3) * s][i];\n                }\n                //-------------\n                if (k + 1 > kmax) {\n                    b = 0;\n                } else {\n                    b = bitmap[(k + 1) * s][i];\n                }\n                //-------------\n                if (k + 3 > kmax) {\n                    d = 0;\n                } else {\n                    d = bitmap[(k + 3) * s][i];\n                }\n                //-------------\n                bitmap[k * s][i] += (9 * (a + b) - (c + d) + 16) >> 5;\n            }\n        }\n    }\n    filter_fh(s, bitmap) {\n        //для строк\n        var kmax = Math.floor((bitmap.width - 1) / s);\n        for (var i = 0; i < bitmap.height; i += s) {\n            //Prediction \n            for (var k = 1; k <= kmax; k += 2) {\n                if ((k - 3 >= 0) && (k + 3 <= kmax)) {\n                    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;\n                } else if (k + 1 <= kmax) {\n                    bitmap[i][k * s] -= (bitmap[i][(k - 1) * s] + bitmap[i][(k + 1) * s] + 1) >> 1;\n                } else {\n                    bitmap[i][k * s] -= bitmap[i][(k - 1) * s];\n                }\n            }\n            //Lifting\n            for (var k = 0; k <= kmax; k += 2) {\n                var a, b, c, d;\n                if (k - 1 < 0) {\n                    a = 0;\n                } else {\n                    a = bitmap[i][(k - 1) * s];\n                }\n                if (k - 3 < 0) {\n                    c = 0;\n                } else {\n                    c = bitmap[i][(k - 3) * s];\n                }\n                if (k + 1 > kmax) {\n                    b = 0;\n                } else {\n                    b = bitmap[i][(k + 1) * s];\n                }\n                if (k + 3 > kmax) {\n                    d = 0;\n                } else {\n                    d = bitmap[i][(k + 3) * s];\n                }\n                bitmap[i][k * s] += (9 * (a + b) - (c + d) + 16) >> 5;\n            }\n        }\n    }\n\n    // переводим матрицу в блоки\n    createBlocks(bitmap) {\n        var blockRows = Math.ceil(this.height / 32);\n        var blockCols = Math.ceil(this.width / 32);\n        var length = blockRows * blockCols;\n        var buffer = new ArrayBuffer(length << 11);  // выделяем память под все блоки\n        // блоки исходного изображения\n        this.blocks = []; // TODO: переделать через Block.createBlockArray()\n        for (var r = 0; r < blockRows; r++) {\n            for (var c = 0; c < blockCols; c++) {\n                var block = new Block(buffer, (r * blockCols + c) << 11, true);\n                for (var i = 0; i < 1024; i++) {\n                    /*var bits = [];\n                    for (var j = 0; j < 10; j++) {\n                        bits.push((i & Math.pow(2, j)) >> j);\n                    }\n                    var row = 16 * bits[1] + 8 * bits[3] + 4 * bits[5] + 2 * bits[7] + bits[9];\n                    var col = 16 * bits[0] + 8 * bits[2] + 4 * bits[4] + 2 * bits[6] + bits[8];*/\n                    var val = 0;\n                    //проверк если нацело на 32 не делится ширина или высота\n                    if (bitmap[this.zigzagRow[i] + 32 * r]) {\n                        val = bitmap[this.zigzagRow[i] + 32 * r][this.zigzagCol[i] + 32 * c];\n                        // чтобы не было undefined \n                        val = val || 0;\n                    }\n                    block.setCoef(i, val);\n                }\n                this.blocks.push(block);\n            }\n        }\n        buffer = new ArrayBuffer(length << 11); // выделяем память под все блоки\n        // блоки в которые будем класть закодированные биты\n        this.eblocks = new Array(length);\n        for (var i = 0; i < length; i++) {\n            this.eblocks[i] = new Block(buffer, i << 11, true);\n        }\n    }\n\n    encodeSlice(zp) {\n        this.zp = zp;\n        if (!this.is_null_slice()) {\n            // по блокам идем        \n            for (var i = 0; i < this.blocks.length; i++) {\n                var block = this.blocks[i];\n                var eblock = this.eblocks[i];\n                this.preliminaryFlagComputation(block, eblock);\n                // четыре подхода декодирования\n                if (this.blockBandEncodingPass()) {\n                    this.bucketEncodingPass(eblock);\n                    this.newlyActiveCoefficientEncodingPass(block, eblock);\n                }\n                this.previouslyActiveCoefficientEncodingPass(block, eblock);\n            }\n        }\n        // уменьшаем шаги \n        return this.finish_code_slice();\n    }\n    previouslyActiveCoefficientEncodingPass(block, eblock) {\n        var boff = 0;\n        var step = this.quant_hi[this.curband];\n        var indices = this.getBandBuckets(this.curband);\n        for (var i = indices.from; i <= indices.to; i++ ,\n            boff++) {\n            for (var j = 0; j < 16; j++) {\n                if (this.coeffstate[boff][j] & this.ACTIVE) {\n                    if (!this.curband) {\n                        step = this.quant_lo[j];\n                    }\n                    var des = 0;\n                    var coef = Math.abs(block.buckets[i][j]);\n                    // и так всегда > 0 в процессе кодирования \n                    var ecoef = eblock.buckets[i][j];\n                    var pix = coef >= ecoef ? 1 : 0;\n                    if (ecoef <= 3 * step) {\n                        this.zp.encode(pix, this.inreaseCoefCtx, 0);\n                        //djvulibre не делает этого при кодировании\n                        //coef += step >> 2;\n                    } else {\n                        this.zp.IWencode(pix);\n                    }\n                    eblock.buckets[i][j] = ecoef - (pix ? 0 : step) + (step >> 1);\n                }\n            }\n        }\n    }\n    newlyActiveCoefficientEncodingPass(block, eblock) {\n        //bucket offset\n        var boff = 0;\n        var indices = this.getBandBuckets(this.curband);\n        //проверка на 0 группу позже\n        var step = this.quant_hi[this.curband];\n        for (var i = indices.from; i <= indices.to; i++ ,\n            boff++) {\n            if (this.bucketstate[boff] & this.NEW) {\n                var shift = 0;\n                if (this.bucketstate[boff] & this.ACTIVE) {\n                    shift = 8;\n                }\n                var bucket = block.buckets[i];\n                var ebucket = eblock.buckets[i];\n                var np = 0;\n                for (var j = 0; j < 16; j++) {\n                    if (this.coeffstate[boff][j] & this.UNK) {\n                        np++;\n                    }\n                }\n                for (var j = 0; j < 16; j++) {\n                    if (this.coeffstate[boff][j] & this.UNK) {\n                        var ip = Math.min(7, np);\n                        this.zp.encode((this.coeffstate[boff][j] & this.NEW) ? 1 : 0, this.activateCoefCtx, shift + ip);\n                        if (this.coeffstate[boff][j] & this.NEW) {\n                            //кодируем знак\n                            this.zp.IWencode((bucket[j] < 0) ? 1 : 0);\n                            np = 0;\n                            if (!this.curband) {\n                                step = this.quant_lo[j];\n                            }\n                            //todo сравнить нужно ли 2 слагаемое\n                            ebucket[j] = (step + (step >> 1) - (step >> 3));\n                            ebucket[j] = (step + (step >> 1));\n                        }\n                        if (np) {\n                            np--;\n                        }\n                    }\n                }\n            }\n        }\n    }\n    bucketEncodingPass(eblock) {\n        var indices = this.getBandBuckets(this.curband);\n        // смещение сегмента\n        var boff = 0;\n        for (var i = indices.from; i <= indices.to; i++ ,\n            boff++) {\n            // проверка потенциального флага сегмента         \n            if (!(this.bucketstate[boff] & this.UNK)) {\n                continue;\n            }\n            //вычисляем номер контекста\n            var n = 0;\n            if (this.curband) {\n                var t = 4 * i;\n                for (var j = t; j < t + 4; j++) {\n                    if (eblock.getCoef(j)) {\n                        n++;\n                    }\n                }\n                if (n === 4) {\n                    n--;\n                }\n            }\n            if (this.bbstate & this.ACTIVE) {\n                //как и + 4\n                n |= 4;\n            }\n            this.zp.encode((this.bucketstate[boff] & this.NEW) ? 1 : 0, this.decodeCoefCtx, n + this.curband * 8);\n        }\n    }\n    blockBandEncodingPass() {\n        var indices = this.getBandBuckets(this.curband);\n        var bcount = indices.to - indices.from + 1;\n        if (bcount < 16 || (this.bbstate & this.ACTIVE)) {\n            this.bbstate |= this.NEW;\n        } else if (this.bbstate & this.UNK) {\n            //this.bbstate может быть NEW на этапе preliminaryFlagComputation\n            this.zp.encode(this.bbstate & this.NEW ? 1 : 0, this.decodeBucketCtx, 0);\n        }\n        return this.bbstate & this.NEW;\n    }\n    // принимает исходный блок и кодируемый блок. Взято из djvulibre\n    preliminaryFlagComputation(block, eblock) {\n        this.bbstate = 0;\n        var bstatetmp = 0;\n        var indices = this.getBandBuckets(this.curband);\n        var step = this.quant_hi[this.curband];\n        if (this.curband) {\n            //смещение сегмента (bucket'а - bucket offset) в массиве флагов\n            var boff = 0;\n            for (var j = indices.from; j <= indices.to; j++ , boff++) {\n                bstatetmp = 0;\n                var bucket = block.buckets[j];\n                var ebucket = eblock.buckets[j];\n                for (var k = 0; k < bucket.length; k++) {\n                    //var index = k + 16 * boff;\n                    if (ebucket[k]) {\n                        this.coeffstate[boff][k] = this.ACTIVE;\n                    } else if (bucket[k] >= step || bucket[k] <= -step) {\n                        this.coeffstate[boff][k] = this.UNK | this.NEW;\n                    } else {\n                        this.coeffstate[boff][k] = this.UNK;\n                    }\n                    bstatetmp |= this.coeffstate[boff][k];\n                }\n                this.bucketstate[boff] = bstatetmp;\n                this.bbstate |= bstatetmp;\n            }\n        } else {\n            //если нулевая группа            \n            var bucket = block.buckets[0];\n            var ebucket = eblock.buckets[0];\n            for (var k = 0; k < bucket.length; k++) {\n                step = this.quant_lo[k];\n                //если шаг в допустимых пределах\n                if (this.coeffstate[0][k] !== this.ZERO) {\n                    if (ebucket[k]) {\n                        this.coeffstate[0][k] = this.ACTIVE;\n                    } else if (bucket[k] >= step || bucket[k] <= -step) {\n                        this.coeffstate[0][k] = this.UNK | this.NEW;\n                    } else {\n                        this.coeffstate[0][k] = this.UNK;\n                    }\n                }\n                bstatetmp |= this.coeffstate[0][k];\n            }\n            this.bucketstate[0] = bstatetmp;\n            this.bbstate |= bstatetmp;\n        }\n    }\n}\n"
  },
  {
    "path": "library/src/iw44/IWImage.js",
    "content": "import IWDecoder from './IWDecoder';\nimport { LazyPixelmap, Pixelmap } from './IWStructures';\nimport DjVu from '../DjVu';\n\nexport default class IWImage {\n    constructor() {\n        this.info = null;\n        this.pixelmap = null;\n        this.resetCodecs();\n    }\n\n    resetCodecs() {\n        this.ycodec = new IWDecoder();\n        this.crcodec = this.crcodec ? new IWDecoder() : null;\n        this.cbcodec = this.cbcodec ? new IWDecoder() : null;\n        this.cslice = 0; // current slice\n    }\n\n    decodeChunk(zp, header) {\n        if (!this.info) {\n            this.info = header;\n            if (!header.grayscale) {\n                this.crcodec = new IWDecoder();\n                this.cbcodec = new IWDecoder();\n            }\n        } else {\n            this.info.slices = header.slices;\n        }\n\n        for (var i = 0; i < this.info.slices; i++) {\n            this.cslice++;\n            this.ycodec.decodeSlice(zp, header);\n            if (this.crcodec && this.cbcodec && this.cslice > this.info.delayInit) {\n                this.cbcodec.decodeSlice(zp, header);\n                this.crcodec.decodeSlice(zp, header);\n            }\n        }\n    }\n\n    createPixelmap() {\n        var time = performance.now();\n\n        var ybitmap = this.ycodec.getBytemap();\n        var cbbitmap = this.cbcodec ? this.cbcodec.getBytemap() : null;\n        var crbitmap = this.crcodec ? this.crcodec.getBytemap() : null;\n\n        var pixelMapTime = performance.now();\n\n        this.pixelmap = new LazyPixelmap(ybitmap, cbbitmap, crbitmap);\n\n        DjVu.IS_DEBUG && console.log('Pixelmap constructor time = ', performance.now() - pixelMapTime);\n        DjVu.IS_DEBUG && console.log('IWImage.createPixelmap time = ', performance.now() - time);\n\n        // do it just to release RAM retained by IWBlocks\n        // In practice, this function is never called twice without full reset of the page.\n        // So technically we could just remove codecs (without create new objects)\n        this.resetCodecs();\n    }\n\n    /**\n     * @returns {ImageData}\n     */\n    getImage() {\n        const time = performance.now();\n        if (!this.pixelmap) this.createPixelmap();\n\n        const width = this.info.width;\n        const height = this.info.height;\n        const image = new ImageData(width, height);\n\n        const processRow = (i) => {\n            const rowOffset = i * this.pixelmap.width;\n            let pixelIndex = ((height - i - 1) * width) << 2;\n            for (let j = 0; j < width; j++) {\n                this.pixelmap.writePixel(rowOffset + j, image.data, pixelIndex);\n                image.data[pixelIndex | 3] = 255;\n                pixelIndex += 4;\n            }\n        }\n\n        //const imageConstructTime = performance.now();\n        for (let i = 0; i < height; i++) {\n            // Optimization for Chrome\n            // When the loop body is a function, it can be easily optimized\n            // In case of the last page of assets/slow.djvu, the whole loop takes\n            // 69ms now, but when the loop body wasn't wrapped into a function it took \n            // 21063ms in case of async mode, and usually in the viewer. Such a big difference\n            // didn't reproduce always, but most frequently. It was a strange case of deoptimization of code by Chrome.\n            // Without that arbitrary deoptimization, the time was the same - about 70ms, but it was unstable.\n            // It seems that now it works more stable.\n            processRow(i);\n        }\n        //DjVu.IS_DEBUG && console.log('***imageConstructTime = ', performance.now() - imageConstructTime);\n\n        DjVu.IS_DEBUG && console.log('IWImage.getImage time = ', performance.now() - time);\n        return image;\n    }\n}"
  },
  {
    "path": "library/src/iw44/IWImageWriter.js",
    "content": "import DjVuWriter from '../DjVuWriter';\nimport DjVuDocument from '../DjVuDocument';\nimport ByteStreamWriter from '../ByteStreamWriter';\nimport IWEncoder from './IWEncoder';\nimport { ZPEncoder } from '../ZPCodec';\nimport { Bytemap } from './IWStructures';\n\nexport default class IWImageWriter {\n\n    constructor(slicenumber, delayInit, grayscale) {\n        // число кусочков кодируемых\n        this.slicenumber = slicenumber || 100;\n        // серые ли изображения\n        this.grayscale = grayscale || 0;\n        // задержка кодирования цветовой информации\n        this.delayInit = (delayInit & 127) || 0;\n        this.onprocess = undefined; // обработчик события записи страницы\n    }\n\n    get width() {\n        return this.imageData.width;\n    }\n    get height() {\n        return this.imageData.height;\n    }\n\n    startMultiPageDocument() {\n        this.dw = new DjVuWriter();\n        this.dw.startDJVM();\n        this.pageBuffers = [];\n        var dirm = {};\n        this.dirm = dirm;\n        dirm.offsets = [];\n        dirm.dflags = 129; // 1000 0001\n        dirm.flags = [];\n        dirm.ids = [];\n        dirm.sizes = [];\n        // titles and names are not used by this encoder\n    }\n\n    addPageToDocument(imageData) {\n        var tbsw = new ByteStreamWriter(); // временный буфер для записи\n        this.writeImagePage(tbsw, imageData);\n        var buffer = tbsw.getBuffer();\n        this.pageBuffers.push(buffer);\n        this.dirm.flags.push(1); // страница без имени и заголовка\n        this.dirm.ids.push('p' + this.dirm.ids.length); // просто уникальный id\n        this.dirm.sizes.push(buffer.byteLength); // размеры\n    }\n\n    endMultiPageDocument() {\n        this.dw.writeDirmChunk(this.dirm);\n        var len = this.pageBuffers.length;\n        for (var i = 0; i < len; i++) {\n            this.dw.writeFormChunkBuffer(this.pageBuffers.shift());\n        }\n        var buffer = this.dw.getBuffer();\n        delete this.dw;\n        delete this.pageBuffers;\n        delete this.dirm;\n        return buffer;\n    }\n\n    createMultiPageDocument(imageArray) {\n        var dw = new DjVuWriter();\n        dw.startDJVM();\n        var length = imageArray.length;\n        var pageBuffers = new Array(imageArray.length);\n        var dirm = {};\n        this.dirm = dirm;\n        dirm.offsets = [];\n        dirm.dflags = 129; // 1000 0001\n        dirm.flags = new Array(imageArray.length);\n        dirm.ids = new Array(imageArray.length);\n        dirm.sizes = new Array(imageArray.length);\n        var tbsw = new ByteStreamWriter(); // временный буфер для записи\n        // генерируем все необходимые данные\n        for (var i = 0; i < imageArray.length; i++) {\n            this.writeImagePage(tbsw, imageArray[i]);\n            var buffer = tbsw.getBuffer();\n            pageBuffers[i] = buffer;\n            tbsw.reset();\n            dirm.flags[i] = 1; // страница без имени и заголовка\n            dirm.ids[i] = 'p' + i; // просто уникальный id\n            dirm.sizes[i] = buffer.byteLength; // размеры\n            this.onprocess ? this.onprocess((i + 1) / length) : 0; // событие обработки очередной страницы\n        }\n        dw.writeDirmChunk(dirm);\n        for (var i = 0; i < imageArray.length; i++) {\n            dw.writeFormChunkBuffer(pageBuffers[i]);\n        }\n\n        return new DjVuDocument(dw.getBuffer());\n    }\n\n    /**\n     * Кодирует и записывает в поток 1 картинку\n     */\n    writeImagePage(bsw, imageData) {\n        // пропускаем 4 байта для длины файла\n        bsw.writeStr('FORM').saveOffsetMark('formSize').jump(4).writeStr('DJVU');\n        // записываем порцию информации\n        bsw.writeStr('INFO')\n            .writeInt32(10)\n            .writeInt16(imageData.width)\n            .writeInt16(imageData.height)\n            .writeByte(24).writeByte(0)\n            .writeByte(100 & 0xff)\n            .writeByte(100 >> 8)\n            .writeByte(22).writeByte(1);\n\n        //начинаем запись порции цветной\n        bsw.writeStr('BG44').saveOffsetMark('BG44Size').jump(4);\n        //пишем заголовок\n        bsw.writeByte(0)\n            .writeByte(this.slicenumber)\n            //majver\n            .writeByte((this.grayscale << 7) | 1) // это 129 или 1 в зависимости от this.grayscale\n            //minver\n            .writeByte(2)\n            .writeUint16(imageData.width)\n            .writeUint16(imageData.height)\n            .writeByte(this.delayInit);\n\n        var ycodec = new IWEncoder(this.RGBtoY(imageData));\n        var crcodec, cbcodec;\n        if (!this.grayscale) {\n            cbcodec = new IWEncoder(this.RGBtoCb(imageData));\n            crcodec = new IWEncoder(this.RGBtoCr(imageData));\n        }\n\n        var zp = new ZPEncoder(bsw);\n        for (var i = 0; i < this.slicenumber; i++) {\n            ycodec.encodeSlice(zp);\n            if (cbcodec && crcodec && i >= this.delayInit) {\n                cbcodec.encodeSlice(zp);\n                crcodec.encodeSlice(zp);\n            }\n        }\n        zp.eflush();\n        bsw.rewriteSize('formSize');\n        bsw.rewriteSize('BG44Size');\n    }\n\n    createOnePageDocument(imageData) {\n        var bsw = new ByteStreamWriter(10 * 1024);\n        bsw.writeStr('AT&T');\n        this.writeImagePage(bsw, imageData);\n        // возвращаем новый одностраничный документ\n        return new DjVuDocument(bsw.getBuffer());\n    }\n\n\n    /**\n     * Перевод RGB в Y откопировано из djvulibre\n     * @param {ImageData} imageData\n     * @returns {Bytemap} двумерный байтовый массив\n     */\n    RGBtoY(imageData) {\n        var rmul = new Int32Array(256);\n        var gmul = new Int32Array(256);\n        var bmul = new Int32Array(256);\n        var data = imageData.data;\n        var width = imageData.width;\n        var height = imageData.height;\n        var bytemap = new Bytemap(width, height);\n        //преобразование необходимое при кодировании серых изображений, чтобы усилить цвет. \n        if (this.grayscale) {\n            for (var i = 0; i < data.length; i++) {\n                data[i] = 255 - data[i];\n            }\n        }\n        for (var k = 0; k < 256; k++) {\n            rmul[k] = (k * 0x10000 * this.rgb_to_ycc[0][0]);\n            gmul[k] = (k * 0x10000 * this.rgb_to_ycc[0][1]);\n            bmul[k] = (k * 0x10000 * this.rgb_to_ycc[0][2]);\n        }\n        for (var i = 0; i < height; i++) {\n            for (var j = 0; j < width; j++) {\n                //сразу разворачиваем в прямые координаты\n                var index = ((height - i - 1) * width + j) << 2;\n                var y = rmul[data[index]] + gmul[data[index + 1]] + bmul[data[index + 2]] + 32768;\n                bytemap[i][j] = ((y >> 16) - 128) << this.iw_shift;\n            }\n        }\n        return bytemap;\n    }\n\n    /**\n     * Перевод RGB в Cb откопировано из djvulibre\n     * @param {ImageData} imageData\n     * @returns {Bytemap} двумерный байтовый массив\n     */\n    RGBtoCb(imageData) {\n        var rmul = new Int32Array(256);\n        var gmul = new Int32Array(256);\n        var bmul = new Int32Array(256);\n        var data = imageData.data;\n        var width = imageData.width;\n        var height = imageData.height;\n        var bytemap = new Bytemap(width, height);\n        for (var k = 0; k < 256; k++) {\n            rmul[k] = (k * 0x10000 * this.rgb_to_ycc[2][0]);\n            gmul[k] = (k * 0x10000 * this.rgb_to_ycc[2][1]);\n            bmul[k] = (k * 0x10000 * this.rgb_to_ycc[2][2]);\n        }\n        for (var i = 0; i < height; i++) {\n            for (var j = 0; j < width; j++) {\n                //сразу разворачиваем в прямые координаты\n                var index = ((height - i - 1) * width + j) << 2;\n                var y = rmul[data[index]] + gmul[data[index + 1]] + bmul[data[index + 2]] + 32768;\n                bytemap[i][j] = Math.max(-128, Math.min(127, y >> 16)) << this.iw_shift;\n            }\n        }\n        return bytemap;\n    }\n\n    /**\n     * Перевод RGB в Cr откопировано из djvulibre\n     * @param {ImageData} imageData\n     * @returns {Bytemap} двумерный байтовый массив\n     */\n    RGBtoCr(imageData) {\n        var rmul = new Int32Array(256);\n        var gmul = new Int32Array(256);\n        var bmul = new Int32Array(256);\n        var data = imageData.data;\n        var width = imageData.width;\n        var height = imageData.height;\n        var bytemap = new Bytemap(width, height);\n        for (var k = 0; k < 256; k++) {\n            rmul[k] = (k * 0x10000 * this.rgb_to_ycc[1][0]);\n            gmul[k] = (k * 0x10000 * this.rgb_to_ycc[1][1]);\n            bmul[k] = (k * 0x10000 * this.rgb_to_ycc[1][2]);\n        }\n        for (var i = 0; i < height; i++) {\n            for (var j = 0; j < width; j++) {\n                //сразу разворачиваем в прямые координаты\n                var index = ((height - i - 1) * width + j) << 2;\n                var y = rmul[data[index]] + gmul[data[index + 1]] + bmul[data[index + 2]] + 32768;\n                bytemap[i][j] = Math.max(-128, Math.min(127, y >> 16)) << this.iw_shift;\n            }\n        }\n        return bytemap;\n    }\n}\n\n//сдвиг для кодирования изображений\nIWImageWriter.prototype.iw_shift = 6;\nIWImageWriter.prototype.rgb_to_ycc = [\n    [0.304348, 0.608696, 0.086956],\n    [0.463768, -0.405797, -0.057971],\n    [-0.173913, -0.347826, 0.521739]];"
  },
  {
    "path": "library/src/iw44/IWStructures.js",
    "content": "function _normalize(val) {\n    val = (val + 32) >> 6;   // убираем 6 дробных бит в этом псевдо дробном числе\n    if (val < -128) {\n        return -128;\n    } else if (val >= 128) {\n        return 127;\n    }\n    return val;\n}\n\nexport class LazyPixelmap {\n    constructor(ybytemap, cbbytemap, crbytemap) {\n        this.width = ybytemap.width; // required as outer property\n        this.yArray = ybytemap.array;\n        this.cbArray = cbbytemap ? cbbytemap.array : null;\n        this.crArray = crbytemap ? crbytemap.array : null;\n        this.writePixel = cbbytemap ? this.writeColoredPixel : this.writeGrayScalePixel;\n    }\n\n    writeGrayScalePixel(index, pixelArray, pixelIndex) {\n        const value = 127 - _normalize(this.yArray[index]);\n        pixelArray[pixelIndex] = value;\n        pixelArray[pixelIndex | 1] = value;\n        pixelArray[pixelIndex | 2] = value;\n    }\n\n    writeColoredPixel(index, pixelArray, pixelIndex) {\n        const y = _normalize(this.yArray[index]);\n        const b = _normalize(this.cbArray[index]);\n        const r = _normalize(this.crArray[index]);\n\n        const t2 = r + (r >> 1);\n        const t3 = y + 128 - (b >> 2);\n\n        pixelArray[pixelIndex] = y + 128 + t2;\n        pixelArray[pixelIndex | 1] = t3 - (t2 >> 1);\n        pixelArray[pixelIndex | 2] = t3 + (b << 1);\n    }\n}\n\nexport class Pixelmap {\n    constructor(ybytemap, cbbytemap, crbytemap) {\n        this.width = ybytemap.width; // required as outer property\n\n        var length = ybytemap.array.length;\n        this.r = new Uint8ClampedArray(length);\n        this.g = new Uint8ClampedArray(length);\n        this.b = new Uint8ClampedArray(length);\n\n        if (cbbytemap) {\n            this._constructColorfulPixelMap(ybytemap.array, cbbytemap.array, crbytemap.array);\n        } else {\n            this._constructGrayScalePixelMap(ybytemap.array);\n        }\n    }\n\n    _constructGrayScalePixelMap(yArray) {\n        yArray.forEach((v, i) => {\n            this.r[i] = this.g[i] = this.b[i] = 127 - _normalize(v);\n        });\n    }\n\n    _constructColorfulPixelMap(yArray, cbArray, crArray) {\n        // using forEach instead of for loop to make the loop body a function - \n        // it helps Chrome not to deoptimize code. It was added for slow.djvu (the last page). \n        yArray.forEach((val, i) => {\n            const y = _normalize(val);\n            const b = _normalize(cbArray[i]);\n            const r = _normalize(crArray[i]);\n\n            const t2 = r + (r >> 1);\n            const t3 = y + 128 - (b >> 2);\n\n            this.r[i] = y + 128 + t2;\n            this.g[i] = t3 - (t2 >> 1);\n            this.b[i] = t3 + (b << 1);\n        });\n    }\n\n    writePixel(index, pixelArray, pixelIndex) {\n        //var index = this.width * i + j;\n        pixelArray[pixelIndex] = this.r[index];\n        pixelArray[pixelIndex | 1] = this.g[index];\n        pixelArray[pixelIndex | 2] = this.b[index];\n    }\n\n    // writeLayer(maskPixelArray, scaleFactor, imageWidth, imageHeight, checker) {\n\n    //     var maskRowOffset = (imageHeight - 1) * imageWidth << 2;\n    //     var width4 = imageWidth << 2;\n    //     var widthStep = width4 * scaleFactor;\n\n    //     var layerRowOffset = 0;\n    //     for (var i = 0, li = 0; i < imageHeight; i += scaleFactor, li++) {\n\n    //         for (var j = 0, lj = 0; j < imageWidth; j += scaleFactor, lj++) {\n    //             var layerIndex = layerRowOffset + lj;\n    //             var intermediateMaskRowOffset = maskRowOffset;\n    //             for (var k = 0; k < scaleFactor; k++) {\n\n    //                 for (var m = 0; m < scaleFactor; m++) {\n    //                     var index = intermediateMaskRowOffset + ((m + j) << 2);\n    //                     if (maskPixelArray[index] === checker) {\n    //                         maskPixelArray[index] = this.r[layerIndex];\n    //                         maskPixelArray[index | 1] = this.g[layerIndex];\n    //                         maskPixelArray[index | 2] = this.b[layerIndex];\n    //                     }\n    //                 }\n\n    //                 intermediateMaskRowOffset -= width4;\n    //             }\n    //         }\n\n    //         layerRowOffset += this.width;\n    //         maskRowOffset -= widthStep;\n    //     }\n    // }\n}\n\n\n/**\n * A square variant (extends Array and keep an array of rows)\n * works about 15-20% slower (in case of inverse transform)\n */\nexport class LinearBytemap {\n    constructor(width, height) {\n        this.width = width;\n        this.array = new Int16Array(width * height);\n    }\n\n    get(i, j) {\n        return this.array[i * this.width + j];\n    }\n\n    set(i, j, val) {\n        this.array[i * this.width + j] = val;\n    }\n\n    sub(i, j, val) {\n        this.array[i * this.width + j] -= val;\n    }\n\n    add(i, j, val) {\n        this.array[i * this.width + j] += val;\n    }\n}\n\n/** Needed for IWImageWriter - should be replaced there with the LinearBytemap too */\nexport class Bytemap extends Array {\n    constructor(width, height) {\n        super(height);\n        this.height = height;\n        this.width = width;\n        for (var i = 0; i < height; i++) {\n            this[i] = new Int16Array(width);\n        }\n    }\n}\n\n//блок - структурная единица исходного изображения\nexport class Block {\n    constructor(buffer, offset, withBuckets = false) {\n        this.array = new Int16Array(buffer, offset, 1024);\n\n        if (withBuckets) { // just for IWEncoder, чтобы не переписывать код\n            this.buckets = new Array(64);\n            for (var i = 0; i < 64; i++) {\n                this.buckets[i] = new Int16Array(buffer, offset, 16);\n                offset += 32;\n            }\n        }\n    }\n\n    setBucketCoef(bucketNumber, index, value) {\n        this.array[(bucketNumber << 4) | index] = value; // index from 0 to 15\n    }\n\n    getBucketCoef(bucketNumber, index) {\n        return this.array[(bucketNumber << 4) | index]; // index from 0 to 15\n    }\n\n    getCoef(n) {\n        return this.array[n];\n    }\n\n    setCoef(n, val) {\n        this.array[n] = val;\n    }\n\n    /**\n     * Функция создания массива блоков на основе одного буфера, более быстрого выделения памяти\n     * @returns {Array<Block>}\n     */\n    static createBlockArray(length) {\n        var blocks = new Array(length);\n        var buffer = new ArrayBuffer(length << 11);  // выделяем память под все блоки\n        for (var i = 0; i < length; i++) {\n            blocks[i] = new Block(buffer, i << 11);\n        }\n        return blocks;\n    }\n}\n\nclass BlockMemoryManager {\n    constructor() {\n        this.buffer = null;\n        this.offset = 0;\n        this.retainedMemory = 0;\n        this.usedMemory = 0;\n    }\n\n    ensureBuffer() {\n        if (!this.buffer || this.offset >= this.buffer.byteLength) {\n            this.buffer = new ArrayBuffer(10 << 20); // 10MB\n            this.offset = 0;\n            this.retainedMemory += this.buffer.byteLength;\n        }\n        return this.buffer;\n    }\n\n    allocateBucket() {\n        this.ensureBuffer();\n        const array = new Int16Array(this.buffer, this.offset, 16);\n        this.offset += 32;\n        this.usedMemory += 32;\n        return array;\n    }\n}\n\nexport class LazyBlock {\n    constructor(memoryManager) {\n        this.buckets = new Array(64);\n        this.mm = memoryManager;\n    }\n\n    setBucketCoef(bucketNumber, index, value) {\n        if (!this.buckets[bucketNumber]) {\n            this.buckets[bucketNumber] = this.mm.allocateBucket();\n        }\n        this.buckets[bucketNumber][index] = value; // index from 0 to 15\n    }\n\n    getBucketCoef(bucketNumber, index) {\n        return this.buckets[bucketNumber] ? this.buckets[bucketNumber][index] : 0;\n    }\n\n    getCoef(n) {\n        return this.getBucketCoef(n >> 4, n & 15);\n    }\n\n    setCoef(n, val) {\n        return this.setBucketCoef(n >> 4, n & 15, val);\n    }\n\n    /**\n     * Функция создания массива блоков на основе одного буфера, более быстрого выделения памяти\n     * @returns {Array<LazyBlock>}\n     */\n    static createBlockArray(length) {\n        const mm = new BlockMemoryManager();\n        const blocks = new Array(length);\n        for (var i = 0; i < length; i++) {\n            blocks[i] = new LazyBlock(mm);\n        }\n        return blocks;\n    }\n}"
  },
  {
    "path": "library/src/jb2/JB2Codec.js",
    "content": "import { ZPDecoder } from '../ZPCodec';\nimport { Bitmap, NumContext } from './JB2Structures';\nimport { IFFChunk } from '../chunks/IFFChunks';\n\nexport default class JB2Codec extends IFFChunk {\n    constructor(bs) {\n        super(bs);\n        this.zp = new ZPDecoder(this.bs);\n        this.directBitmapCtx = new Uint8Array(1024);\n        this.refinementBitmapCtx = new Uint8Array(2048);\n        this.offsetTypeCtx = [0];\n        this.resetNumContexts();\n    }\n\n    resetNumContexts() {\n        this.recordTypeCtx = new NumContext();\n        this.imageSizeCtx = new NumContext();\n        this.symbolWidthCtx = new NumContext();\n        this.symbolHeightCtx = new NumContext();\n        this.inheritDictSizeCtx = new NumContext();\n        //гориз смещение\n        this.hoffCtx = new NumContext();\n        //вертикальное смещение\n        this.voffCtx = new NumContext();\n        //гориз смещение\n        this.shoffCtx = new NumContext();\n        //вертикальное смещение\n        this.svoffCtx = new NumContext();\n        this.symbolIndexCtx = new NumContext();\n        this.symbolHeightDiffCtx = new NumContext();\n        this.symbolWidthDiffCtx = new NumContext();\n        this.commentLengthCtx = new NumContext();\n        this.commentOctetCtx = new NumContext();\n\n        this.horizontalAbsLocationCtx = new NumContext();\n        this.verticalAbsLocationCtx = new NumContext();\n    }\n\n    /*decodeNumX(low, high, numctx) { // this is my own implementation\n        var v = 0;\n        var decision = 0;\n        var range = 0xffffffff;\n\n        if (low === high) {\n            return low;\n        }\n\n        //phase 1\n        decision = (low >= 0) || ((high >= 0) && this.zp.decode(numctx.ctx, 0));\n        // раскодировали знак\n        var negative = !decision;\n        numctx = negative ? numctx.left : numctx.right;\n\n        if (negative) { // переводим границы в положительную полуось\n            var temp = -low - 1;\n            low = -high - 1;\n            high = temp;\n        }\n\n        //phase 2\n        decision = (low > (v << 1) + 1) || ((high >= (v << 1) + 1) && this.zp.decode(numctx.ctx, 0));\n        while (decision) {\n            v += v + 1;\n            numctx = numctx.right;\n            decision = (low > (v << 1) + 1) || ((high >= (v << 1) + 1) && this.zp.decode(numctx.ctx, 0));\n        }\n        numctx = numctx.left;\n        //phase 3\n        range = (v + 1) >> 1;\n        while (range) {\n            decision = (low > v) || ((high >= (v + range)) && this.zp.decode(numctx.ctx, 0));\n            v += decision ? range : 0;\n            numctx = decision ? numctx.right : numctx.left;\n            range >>= 1;\n        }\n        //phase 4\n        return negative ? (-v - 1) : v;\n    }*/\n\n    decodeNum(low, high, numctx) { // this implementation was copied from DjVuLibre\n        var negative = false;\n        var cutoff;\n\n        // Start all phases\n        cutoff = 0;\n        for (var phase = 1, range = 0xffffffff; range != 1;) {\n            // encode\n            var decision = (low >= cutoff) || ((high >= cutoff) && this.zp.decode(numctx.ctx, 0));\n            // context for new bit\n            numctx = decision ? numctx.right : numctx.left;\n            // phase dependent part\n            switch (phase) {\n                case 1:\n                    negative = !decision;\n                    if (negative) {\n                        var temp = - low - 1;\n                        low = - high - 1;\n                        high = temp;\n                    }\n                    phase = 2; cutoff = 1;\n                    break;\n\n                case 2:\n                    if (!decision) {\n                        phase = 3;\n                        range = (cutoff + 1) / 2;\n                        if (range == 1)\n                            cutoff = 0;\n                        else\n                            cutoff -= range / 2;\n                    }\n                    else {\n                        cutoff += cutoff + 1;\n                    }\n                    break;\n\n                case 3:\n                    range /= 2;\n                    if (range != 1) {\n                        if (!decision)\n                            cutoff -= range / 2;\n                        else\n                            cutoff += range / 2;\n                    }\n                    else if (!decision) {\n                        cutoff--;\n                    }\n                    break;\n            }\n        }\n        return (negative) ? (- cutoff - 1) : cutoff;\n    }\n\n    decodeBitmap(width, height) {\n        var bitmap = new Bitmap(width, height);\n        for (var i = height - 1; i >= 0; i--) {\n            for (var j = 0; j < width; j++) {\n                var ind = this.getCtxIndex(bitmap, i, j);\n                if (this.zp.decode(this.directBitmapCtx, ind)) { bitmap.set(i, j) };\n            }\n        }\n        return bitmap;\n    }\n\n    getCtxIndex(bm, i, j) {\n        var index = 0;\n        var r = i + 2;\n        if (bm.hasRow(r)) {\n            //index = ((bm.get(r, j - 1)) << 9) | (bm.get(r, j) << 8) | ((bm.get(r, j + 1)) << 7);\n            index = (bm.getBits(r, j - 1, 3)) << 7;\n        }\n        r--;\n        if (bm.hasRow(r)) {\n            // index |= ((bm.get(r, j - 2)) << 6) | ((bm.get(r, j - 1)) << 5) |\n            //     (bm.get(r, j) << 4) | ((bm.get(r, j + 1)) << 3) | ((bm.get(r, j + 2)) << 2);\n\n            index |= bm.getBits(r, j - 2, 5) << 2;\n        }\n        //index |= ((bm.get(i, j - 2)) << 1) | (bm.get(i, j - 1));\n        index |= bm.getBits(i, j - 2, 2);\n        return index;\n    }\n\n    // don't forget to remove empty edges of the result bitmap before it is added to the dictionary\n    decodeBitmapRef(width, height, mbm) {\n        //current bitmap\n        var cbm = new Bitmap(width, height);\n        var alignInfo = this.alignBitmaps(cbm, mbm);\n        for (var i = height - 1; i >= 0; i--) {\n            for (var j = 0; j < width; j++) {\n                this.zp.decode(this.refinementBitmapCtx,\n                    this.getCtxIndexRef(cbm, mbm, alignInfo, i, j)) ? cbm.set(i, j) : 0;\n            }\n        }\n        return cbm;\n    }\n\n    getCtxIndexRef(cbm, mbm, alignInfo, i, j) {\n        var index = 0;\n        var r = i + 1;\n        if (cbm.hasRow(r)) {\n            //index = ((cbm.get(r, j - 1) || 0) << 10) | (cbm.get(r, j) << 9) | ((cbm.get(r, j + 1) || 0) << 8);\n            index = cbm.getBits(r, j - 1, 3) << 8;\n        }\n        index |= cbm.get(i, j - 1) << 7;\n\n        r = i + alignInfo.rowshift + 1;\n        var c = j + alignInfo.colshift;\n        index |= mbm.hasRow(r) ? mbm.get(r, c) << 6 : 0;\n        r--;\n        if (mbm.hasRow(r)) {\n            //index |= ((mbm.get(r, c - 1) || 0) << 5) | (mbm.get(r, c) << 4) | ((mbm.get(r, c + 1) || 0) << 3);\n            index |= mbm.getBits(r, c - 1, 3) << 3;\n        }\n        r--;\n        if (mbm.hasRow(r)) {\n            //index |= ((mbm.get(r, c - 1) || 0) << 2) | (mbm.get(r, c) << 1) | (mbm.get(r, c + 1) || 0);\n            index |= mbm.getBits(r, c - 1, 3);\n        }\n        return index;\n    }\n\n    alignBitmaps(cbm, mbm) {\n        var cwidth = cbm.width - 1;\n        var cheight = cbm.height - 1;\n        var crow, ccol, mrow, mcol;\n        crow = cheight >> 1;\n        ccol = cwidth >> 1;\n        mrow = (mbm.height - 1) >> 1;\n        mcol = (mbm.width - 1) >> 1;\n        return {\n            'rowshift': mrow - crow,\n            'colshift': mcol - ccol\n        };\n    }\n\n    decodeComment() {\n        var length = this.decodeNum(0, 262142, this.commentLengthCtx);\n        var comment = new Uint8Array(length);\n        for (var i = 0; i < length; comment[i++] = this.decodeNum(0, 255, this.commentOctetCtx)) { }\n        return comment;\n    }\n\n    /**\n     * Отладочная функция для просмотра символов.\n     * TODO: tranfer it to outside the library\n     */\n    drawBitmap(bm) {\n        var image = document.createElement('canvas')\n            .getContext('2d')\n            .createImageData(bm.width, bm.height);\n        for (var i = 0; i < bm.height; i++) {\n            for (var j = 0; j < bm.width; j++) {\n                var v = bm.get(i, j) ? 0 : 255;\n                var index = ((bm.height - i - 1) * bm.width + j) * 4;\n                image.data[index] = v;\n                image.data[index + 1] = v;\n                image.data[index + 2] = v;\n                image.data[index + 3] = 255;\n\n            }\n        }\n        // Globals.canvas.width = Globals.canvas.width;\n        //Globals.canvasCtx.putImageData(image, 0, 0);\n        Globals.drawImage(image);\n    }\n}\n"
  },
  {
    "path": "library/src/jb2/JB2Dict.js",
    "content": "import JB2Codec from './JB2Codec';\n\n export default class JB2Dict extends JB2Codec {\n    constructor(bs) {\n        super(bs);\n        this.dict = [];\n        this.isDecoded = false;\n    }\n\n    decode(djbz) {\n        if (this.isDecoded) {\n            return;\n        }\n        var type = this.decodeNum(0, 11, this.recordTypeCtx);\n        if (type == 9) {\n            // длина словаря\n            var size = this.decodeNum(0, 262142, this.inheritDictSizeCtx);\n            djbz.decode();\n            this.dict = djbz.dict.slice(0, size);\n            //тип следующей записи (должен быть 0)\n            type = this.decodeNum(0, 11, this.recordTypeCtx);\n            //console.log(size);\n        }\n\n        this.decodeNum(0, 262142, this.imageSizeCtx); // image width\n        this.decodeNum(0, 262142, this.imageSizeCtx); // image height\n        // флаг всегда должен быть = 0 \n        var flag = this.zp.decode([0], 0);\n        if (flag) {\n            throw new Error(\"Bad flag!!!\");\n        }\n        type = this.decodeNum(0, 11, this.recordTypeCtx);\n\n        var width, widthdiff, heightdiff, symbolIndex;\n        var height;\n        var bm;\n        while (type !== 11) {\n            switch (type) {\n                case 2:\n                    width = this.decodeNum(0, 262142, this.symbolWidthCtx);\n                    height = this.decodeNum(0, 262142, this.symbolHeightCtx);\n                    bm = this.decodeBitmap(width, height);\n                    this.dict.push(bm);\n                    //this.drawBitmap(bm);\n                    break;\n                case 5:\n                    symbolIndex = this.decodeNum(0, this.dict.length - 1, this.symbolIndexCtx);\n                    widthdiff = this.decodeNum(-262143, 262142, this.symbolWidthDiffCtx);\n                    heightdiff = this.decodeNum(-262143, 262142, this.symbolHeightDiffCtx);\n                    var mbm = this.dict[symbolIndex];\n                    var cbm = this.decodeBitmapRef(mbm.width + widthdiff, heightdiff + mbm.height, mbm);\n                    //this.drawBitmap(cbm);\n                    this.dict.push(cbm.removeEmptyEdges());\n                    break;\n\n                case 9: // Numcoder reset\n                    //this.decodeNum(0, 262142, this.inheritDictSizeCtx);\n                    console.log(\"RESET DICT\");\n                    this.resetNumContexts();\n                    break;\n\n                case 10:\n                    /*var comment = */this.decodeComment();\n                    /*var str = \"\"; // TODO: test comments\n                    for (var i = 0; i < comment.length; i++) {\n                        var byte = comment[i];\n                        str += String.fromCharCode(byte);\n                    }*/\n                    break;\n\n                default:\n                    throw new Error(\"Unsupported type in JB2Dict: \" + type);\n            }\n\n            type = this.decodeNum(0, 11, this.recordTypeCtx);\n            if (type > 11) {\n                console.error(\"TYPE ERROR \" + type);\n                break;\n            }\n        }\n\n        this.isDecoded = true;\n    }\n}"
  },
  {
    "path": "library/src/jb2/JB2Image.js",
    "content": "import JB2Codec from './JB2Codec';\nimport { Baseline, Bitmap } from './JB2Structures';\nimport DjVu from '../DjVu';\n\nexport default class JB2Image extends JB2Codec {\n    constructor(bs) {\n        super(bs);\n        this.dict = []; // dict of bitmaps\n        this.initialDictLength = 0; // a number of bitmaps from a shared dict (if required)     \n        this.blitList = []; // \"blit\" = \"block transfer\"\n        this.init();\n    }\n\n    /**\n     * Добавляет в список битмап и координаты левого нижнего угла в классической системе координат\n     * @param {Bitmap} bitmap \n     * @param {Number} x \n     * @param {Number} y \n     */\n    addBlit(bitmap, x, y) {\n        this.blitList.push({ bitmap, x, y });\n    }\n\n    //раскодируем первую запись в потоке\n    init() {\n        var type = this.decodeNum(0, 11, this.recordTypeCtx);\n        if (type == 9) {\n            // длина словаря\n            this.initialDictLength = this.decodeNum(0, 262142, this.inheritDictSizeCtx);\n            //тип следующей записи (должен быть 0)\n            type = this.decodeNum(0, 11, this.recordTypeCtx);\n            //console.log(\"Zero\", type);\n        }\n\n        this.width = this.decodeNum(0, 262142, this.imageSizeCtx) || 200;\n        this.height = this.decodeNum(0, 262142, this.imageSizeCtx) || 200;\n        // инициализация когда будет надо\n        this.bitmap = false;\n        //позиции первого и предыдущего символа на строке\n        this.lastLeft = 0;\n        this.lastBottom = this.height - 1;\n        this.firstLeft = -1; // получено экспериментально, чтобы не вычитать 1 каждый раз из x как это делается в javadjvu\n        this.firstBottom = this.height - 1;\n        // флаг всегда должен быть = 0 \n        var flag = this.zp.decode([0], 0);\n        if (flag) {\n            throw new Error(\"Bad flag!!!\");\n        }\n\n        this.baseline = new Baseline();\n    }\n\n    toString() {\n        var str = super.toString();\n        str += \"{width: \" + this.width + \", height: \" + this.height + '}\\n';\n        return str;\n    }\n\n    decode(djbz) {\n        // если затребован словарь \n        if (this.initialDictLength) {\n            //декодируем словарь (он может быть уже декодирован)\n            djbz.decode();\n            //копируем затребованное число символов\n            this.dict = djbz.dict.slice(0, this.initialDictLength);\n        }\n        var type = this.decodeNum(0, 11, this.recordTypeCtx);\n        var width, hoff, voff, flag;\n        var height, index;\n        var bm;\n        // var count = 0; // degug code\n        //var maxInterationNumber = 2000;\n        while (type !== 11 /*&& count < maxInterationNumber*/) { // 11 means \"End of data\"\n            //count++;\n            // DjVu.IS_DEBUG && console.log('count', count);\n            // DjVu.IS_DEBUG && console.log(type);\n            switch (type) {\n\n                case 1: // New symbol, add to image and library \n                    width = this.decodeNum(0, 262142, this.symbolWidthCtx);\n                    height = this.decodeNum(0, 262142, this.symbolHeightCtx);\n                    bm = this.decodeBitmap(width, height);\n                    //this.drawBitmap(bm);\n                    var coords = this.decodeSymbolCoords(bm.width, bm.height);\n                    this.addBlit(bm, coords.x, coords.y);\n                    //this.copyToBitmap(bm, coords.x, coords.y);\n                    this.dict.push(bm.removeEmptyEdges());\n                    //Globals.drawBitmapOnImageCanvas(bm, coords.x, coords.y, this);\n                    break;\n\n                case 2: // New symbol, add to library only\n                    width = this.decodeNum(0, 262142, this.symbolWidthCtx);\n                    height = this.decodeNum(0, 262142, this.symbolHeightCtx);\n                    bm = this.decodeBitmap(width, height);\n                    this.dict.push(bm.removeEmptyEdges());\n                    break;\n\n                case 3: // New symbol, add to image only \n                    width = this.decodeNum(0, 262142, this.symbolWidthCtx);\n                    height = this.decodeNum(0, 262142, this.symbolHeightCtx);\n                    bm = this.decodeBitmap(width, height);\n                    //this.drawBitmap(bm);\n                    var coords = this.decodeSymbolCoords(bm.width, bm.height);\n                    this.addBlit(bm, coords.x, coords.y);\n                    //this.copyToBitmap(bm, coords.x, coords.y);\n                    break;\n\n                case 4: // Matched symbol with refinement, add to image and library\n                    index = this.decodeNum(0, this.dict.length - 1, this.symbolIndexCtx);\n                    var widthdiff = this.decodeNum(-262143, 262142, this.symbolWidthDiffCtx);\n                    var heightdiff = this.decodeNum(-262143, 262142, this.symbolHeightDiffCtx);\n                    var mbm = this.dict[index];\n                    var cbm = this.decodeBitmapRef(mbm.width + widthdiff, heightdiff + mbm.height, mbm);\n                    var coords = this.decodeSymbolCoords(cbm.width, cbm.height);\n                    this.addBlit(cbm, coords.x, coords.y);\n                    //this.copyToBitmap(cbm, coords.x, coords.y);\n                    this.dict.push(cbm.removeEmptyEdges());\n                    break;\n\n                case 5: // Matched symbol with refinement, add to library only\n                    index = this.decodeNum(0, this.dict.length - 1, this.symbolIndexCtx);\n                    widthdiff = this.decodeNum(-262143, 262142, this.symbolWidthDiffCtx);\n                    heightdiff = this.decodeNum(-262143, 262142, this.symbolHeightDiffCtx);\n                    var mbm = this.dict[index];\n                    var cbm = this.decodeBitmapRef(mbm.width + widthdiff, heightdiff + mbm.height, mbm);\n                    this.dict.push(cbm.removeEmptyEdges());\n                    break;\n\n                case 6: // Matched symbol with refinement, add to image only\n                    index = this.decodeNum(0, this.dict.length - 1, this.symbolIndexCtx);\n                    var widthdiff = this.decodeNum(-262143, 262142, this.symbolWidthDiffCtx);\n                    var heightdiff = this.decodeNum(-262143, 262142, this.symbolHeightDiffCtx);\n                    var mbm = this.dict[index];\n                    var cbm = this.decodeBitmapRef(mbm.width + widthdiff, heightdiff + mbm.height, mbm);\n                    var coords = this.decodeSymbolCoords(cbm.width, cbm.height);\n                    this.addBlit(cbm, coords.x, coords.y);\n                    //this.copyToBitmap(cbm, coords.x, coords.y);\n                    break;\n\n                case 7: // Matched symbol, copy to image without refinement\n                    index = this.decodeNum(0, this.dict.length - 1, this.symbolIndexCtx);\n                    bm = this.dict[index];\n                    var coords = this.decodeSymbolCoords(bm.width, bm.height);\n                    this.addBlit(bm, coords.x, coords.y);\n                    //this.copyToBitmap(bm, coords.x, coords.y);\n                    //this.drawBitmap(bm);\n                    break;\n\n                case 8: // Non-symbol data \n                    width = this.decodeNum(0, 262142, this.symbolWidthCtx);\n                    height = this.decodeNum(0, 262142, this.symbolHeightCtx);\n                    bm = this.decodeBitmap(width, height);\n                    //this.drawBitmap(bm);\n                    var coords = this.decodeAbsoluteLocationCoords(bm.width, bm.height);\n                    this.addBlit(bm, coords.x, coords.y);\n                    //this.copyToBitmap(bm, coords.x, coords.y);\n                    break;\n\n                case 9: // Numcoder reset\n                    console.log(\"RESET NUM CONTEXTS\"); // it hasn't been checked, may work incorrectly\n                    this.resetNumContexts();\n                    break;\n\n                case 10:\n                    this.decodeComment(); // TODO: test comments\n                    break;\n\n                default:\n                    throw new Error(\"Unsupported type in JB2Image: \" + type);\n            }\n\n            type = this.decodeNum(0, 11, this.recordTypeCtx);\n\n            /*if (DjVu.IS_DEBUG && count > maxInterationNumber) {\n                 console.error(\"Too many iterations!\");\n                 break;\n             }*/\n            if (type > 11) {\n                console.error(\"TYPE ERROR \" + type);\n                break;\n            }\n        }\n    }\n\n    decodeAbsoluteLocationCoords(width, height) {\n        var left = this.decodeNum(1, this.width, this.horizontalAbsLocationCtx);\n        var top = this.decodeNum(1, this.height, this.verticalAbsLocationCtx);\n        return {\n            x: left,\n            y: top - height\n        }\n    }\n\n    decodeSymbolCoords(width, height) {\n        var flag = this.zp.decode(this.offsetTypeCtx, 0); // флаг новой строки\n        var horizontalOffsetCtx = flag ? this.hoffCtx : this.shoffCtx;\n        var verticalOffsetCtx = flag ? this.voffCtx : this.svoffCtx;\n        var horizontalOffset = this.decodeNum(-262143, 262142, horizontalOffsetCtx);\n        var verticalOffset = this.decodeNum(-262143, 262142, verticalOffsetCtx);\n        var x, y;\n        if (flag) {\n            x = this.firstLeft + horizontalOffset;\n            y = this.firstBottom + verticalOffset - height + 1;\n            this.firstLeft = x;\n            this.firstBottom = y;\n            this.baseline.fill(y);\n        }\n        else {\n            x = this.lastRight + horizontalOffset;\n            y = this.baseline.getVal() + verticalOffset;\n        }\n        this.baseline.add(y);\n        this.lastRight = x + width - 1;\n        return {\n            'x': x,  // не вычитаем 1, так как firstLeft инициализирован -1, а Baseline и так выдает верный результат\n            'y': y\n        };\n\n    }\n\n    // принимает битмап и координаты левого нижнего угла в обычной системе координат\n    copyToBitmap(bm, x, y) {\n        if (!this.bitmap) {\n            this.bitmap = new Bitmap(this.width, this.height);\n        }\n\n        for (var i = y, k = 0; k < bm.height; k++ , i++) {\n            for (var j = x, t = 0; t < bm.width; t++ , j++) {\n                if (bm.get(k, t)) {\n                    this.bitmap.set(i, j);\n                }\n            }\n        }\n    }\n\n    getBitmap() {\n        if (!this.bitmap) {\n            this.blitList.forEach(blit => this.copyToBitmap(blit.bitmap, blit.x, blit.y));\n        }\n        return this.bitmap;\n    }\n\n    getMaskImage() {\n        var imageData = new ImageData(this.width, this.height);\n        var pixelArray = imageData.data;\n        var time = performance.now();\n        pixelArray.fill(255); // все белым непрозрачным\n\n        for (var blitIndex = 0; blitIndex < this.blitList.length; blitIndex++) {\n            var blit = this.blitList[blitIndex];\n            var bm = blit.bitmap;\n            for (var i = blit.y, k = 0; k < bm.height; k++ , i++) {\n                for (var j = blit.x, t = 0; t < bm.width; t++ , j++) {\n                    if (bm.get(k, t)) {\n                        var pixelIndex = ((this.height - i - 1) * this.width + j) * 4;\n                        pixelArray[pixelIndex] = 0;\n                    }\n                }\n            }\n        }\n\n        DjVu.IS_DEBUG && console.log(\"JB2Image mask image creating time = \", performance.now() - time);\n        return imageData;\n    }\n\n    /**\n     * Создаем изображение из маски и палитры, если таковая имеется\n     * @param {DjVuPalette} palette \n     * @param {boolean} isMarkMaskPixels - чтобы понять какой пиксель брать из фона, а какой не трогать. \n     * Нужно только при составлении изображения из двух слоев\n     */\n    getImage(palette = null, isMarkMaskPixels = false) {\n\n        if (palette && palette.getDataSize() !== this.blitList.length) {\n            palette = null; // отбрасываем цвета если что-то не так.\n        }\n\n        var pixelArray = new Uint8ClampedArray(this.width * this.height * 4);\n        var time = performance.now();\n        pixelArray.fill(255); // все белым непрозрачным\n\n        var blackPixel = { r: 0, g: 0, b: 0 };\n        var alpha = isMarkMaskPixels ? 0 : 255;\n\n        for (var blitIndex = 0; blitIndex < this.blitList.length; blitIndex++) {\n            var blit = this.blitList[blitIndex];\n            var pixel = palette ? palette.getPixelByBlitIndex(blitIndex) : blackPixel;\n            var bm = blit.bitmap;\n            for (var i = blit.y, k = 0; k < bm.height; k++ , i++) {\n                for (var j = blit.x, t = 0; t < bm.width; t++ , j++) {\n                    if (bm.get(k, t)) {\n                        var pixelIndex = ((this.height - i - 1) * this.width + j) << 2;\n                        pixelArray[pixelIndex] = pixel.r;\n                        pixelArray[pixelIndex | 1] = pixel.g;\n                        pixelArray[pixelIndex | 2] = pixel.b;\n                        pixelArray[pixelIndex | 3] = alpha;\n                    }\n                }\n            }\n        }\n\n        DjVu.IS_DEBUG && console.log(\"JB2Image creating time = \", performance.now() - time);\n        return new ImageData(pixelArray, this.width, this.height);\n    }\n\n    getImageFromBitmap() { // debug function mostly\n        this.getBitmap();\n        var time = performance.now();\n        var image = new ImageData(this.width, this.height);\n        for (var i = 0; i < this.height; i++) {\n            for (var j = 0; j < this.width; j++) {\n                var v = this.bitmap.get(i, j) ? 0 : 255;\n                var index = ((this.height - i - 1) * this.width + j) * 4;\n                image.data[index] = v;\n                image.data[index + 1] = v;\n                image.data[index + 2] = v;\n                image.data[index + 3] = 255;\n            }\n        }\n        DjVu.IS_DEBUG && console.log(\"JB2Image creating time = \", performance.now() - time);\n        return image;\n    }\n}\n"
  },
  {
    "path": "library/src/jb2/JB2Structures.js",
    "content": "export class Bitmap {\n    constructor(width, height) {\n        var length = Math.ceil(width * height / 8); // число байт необходимых для кодировки черно-белого изображения\n        this.height = height;\n        this.width = width;\n        this.innerArray = new Uint8Array(length);\n    }\n\n    getBits(i, j, bitNumber) {\n        if (!this.hasRow(i) || j >= this.width) {\n            return 0;\n        }\n        if (j < 0) {\n            bitNumber += j;\n            j = 0;\n        }\n        var tmp = i * this.width + j;\n        var index = tmp >> 3;\n        var bitIndex = tmp & 7;\n        var mask = 32768 >>> bitIndex;\n        var twoBytes = ((this.innerArray[index] << 8) | (this.innerArray[index + 1] || 0));\n        var existingBits = this.width - j;\n        var border = bitNumber < existingBits ? bitNumber : existingBits;\n        for (var k = 1; k < border; k++) {\n            mask |= 32768 >>> (bitIndex + k)\n        }\n        return (twoBytes & mask) >>> (16 - bitIndex - bitNumber);\n    }\n\n    get(i, j) {\n        if (!this.hasRow(i) || j < 0 || j >= this.width) {\n            return 0;\n        }\n        var tmp = i * this.width + j;\n        var index = tmp >> 3;\n        var bitIndex = tmp & 7;\n        var mask = 128 >> bitIndex;\n        return (this.innerArray[index] & mask) ? 1 : 0;\n    }\n\n    set(i, j) { // сделать \"пиксель\" черным\n        var tmp = i * this.width + j;\n        var index = tmp >> 3;\n        var bitIndex = tmp & 7;\n        var mask = 128 >> bitIndex;\n        this.innerArray[index] |= mask;\n        return;\n    }\n\n    hasRow(r) {\n        return r >= 0 && r < this.height;\n    }\n\n    removeEmptyEdges() {\n        var bottomShift = 0;\n        var topShift = 0;\n        var leftShift = 0;\n        var rightShift = 0;\n\n        main_cycle: for (var i = 0; i < this.height; i++) {\n            for (var j = 0; j < this.width; j++) {\n                if (this.get(i, j)) {\n                    break main_cycle;\n                }\n            }\n            bottomShift++;\n        }\n\n        main_cycle: for (var i = this.height - 1; i >= 0; i--) {\n            for (var j = 0; j < this.width; j++) {\n                if (this.get(i, j)) {\n                    break main_cycle;\n                }\n            }\n            topShift++;\n        }\n\n        main_cycle: for (var j = 0; j < this.width; j++) {\n            for (var i = 0; i < this.height; i++) {\n                if (this.get(i, j)) {\n                    break main_cycle;\n                }\n            }\n            leftShift++;\n        }\n\n        main_cycle: for (var j = this.width - 1; j >= 0; j--) {\n            for (var i = 0; i < this.height; i++) {\n                if (this.get(i, j)) {\n                    break main_cycle;\n                }\n            }\n            rightShift++;\n        }\n\n        if (topShift || bottomShift || leftShift || rightShift) {\n            var newWidth = this.width - leftShift - rightShift;\n            var newHeight = this.height - topShift - bottomShift;\n            var newBitMap = new Bitmap(newWidth, newHeight);\n            for (var i = bottomShift, p = 0; p < newHeight; p++ , i++) {\n                for (var j = leftShift, q = 0; q < newWidth; q++ , j++) {\n                    if (this.get(i, j)) {\n                        newBitMap.set(p, q);\n                    }\n                }\n            }\n            return newBitMap;\n        }\n\n        return this;\n    }\n}\n\nexport class NumContext {\n    constructor() {\n        this.ctx = [0];\n        this._left = null;\n        this._right = null;\n    }\n\n    get left() {\n        if (!this._left) {\n            this._left = new NumContext();\n        }\n        return this._left;\n    }\n\n    get right() {\n        if (!this._right) {\n            this._right = new NumContext();\n        }\n        return this._right;\n    }\n}\n\n// структура для вычисления позиции символов на картинке\nexport class Baseline {\n    constructor() {\n        this.arr = new Array(3);\n        this.fill(0); // на всякий случай заполняем нулями, хотя вообще это не должно быть нужно\n        this.index = -1;\n    }\n\n    add(val) {\n        if (++this.index === 3) {\n            this.index = 0;\n        }\n        this.arr[this.index] = val;\n    }\n\n    getVal() { // возвращает медианное значение\n        if (this.arr[0] >= this.arr[1] && this.arr[0] <= this.arr[2]\n            || this.arr[0] <= this.arr[1] && this.arr[0] >= this.arr[2]) {\n            return this.arr[0];\n        }\n        else if (this.arr[1] >= this.arr[0] && this.arr[1] <= this.arr[2]\n            || this.arr[1] <= this.arr[0] && this.arr[1] >= this.arr[2]) {\n            return this.arr[1];\n        } else {\n            return this.arr[2];\n        }\n    }\n\n    fill(val) { // инициализируем все 3 значения положением 1 символа на строке (и пока не будет добавлено еще 2, это значение и будет медианным)\n        this.arr[0] = this.arr[1] = this.arr[2] = val;\n    }\n}"
  },
  {
    "path": "library/src/methods/bundle.js",
    "content": "import { loadPage, loadPageDependency, loadThumbnail } from './load';\nimport DjVuWriter from '../DjVuWriter';\nimport { pLimit } from '../DjVu';\n\n/**\n * A method to download and bundle an indirect djvu document\n * @this import('../DjVuDocument').DjVuDocument\n */\nexport default async function bundle(progressCallback = () => { }) {\n    const djvuWriter = new DjVuWriter();\n    djvuWriter.startDJVM();\n    const dirm = {\n        dflags: this.dirm.dflags | 128,\n        flags: [],\n        names: [],\n        titles: [],\n        sizes: [],\n        ids: [],\n    };\n    const chunkByteStreams = [];\n    const filesQuantity = this.dirm.getFilesQuantity();\n\n    const totalOperations = filesQuantity + 3;\n\n    let pageNumber = 0;\n\n    const limit = pLimit(4);\n    let downloadedNumber = 0;\n    const promises = [];\n\n    for (let i = 0; i < filesQuantity; i++) {\n        promises.push(limit(async () => {\n            let bs;\n            if (this.dirm.isPageIndex(i)) {\n                pageNumber++;\n                bs = await loadPage(pageNumber, this._getUrlByPageNumber(pageNumber));\n            } else if (this.dirm.isThumbnailIndex(i)) {\n                bs = await loadThumbnail(\n                    this.baseUrl + this.dirm.getComponentNameByItsId(this.dirm.ids[i]),\n                    this.dirm.ids[i]\n                );\n            } else {\n                bs = await loadPageDependency(\n                    this.dirm.ids[i],\n                    this.dirm.getComponentNameByItsId(this.dirm.ids[i]),\n                    this.baseUrl,\n                );\n            }\n\n            downloadedNumber++;\n            progressCallback(downloadedNumber / totalOperations);\n\n            //await new Promise(resolve => setTimeout(resolve, 1000));\n            return {\n                flags: this.dirm.flags[i],\n                id: this.dirm.ids[i],\n                name: this.dirm.names[i],\n                title: this.dirm.titles[i],\n                bs: bs,\n            };\n        }));\n    }\n\n    for (const data of await Promise.all(promises)) {\n        dirm.flags.push(data.flags);\n        dirm.ids.push(data.id);\n        dirm.names.push(data.names);\n        dirm.titles.push(data.title);\n        dirm.sizes.push(data.bs.length);\n        chunkByteStreams.push(data.bs);\n    }\n\n    djvuWriter.writeDirmChunk(dirm);\n    if (this.navm) {\n        djvuWriter.writeChunk(this.navm);\n    }\n\n    progressCallback((totalOperations - 2) / totalOperations);\n\n    for (let i = 0; i < chunkByteStreams.length; i++) {\n        djvuWriter.writeFormChunkBS(chunkByteStreams[i]);\n        chunkByteStreams[i] = null; // release memory\n    }\n\n    progressCallback((totalOperations - 1) / totalOperations);\n\n    const newBuffer = djvuWriter.getBuffer();\n\n    progressCallback(1);\n\n    return new this.constructor(newBuffer);\n}\n"
  },
  {
    "path": "library/src/methods/load.js",
    "content": "/** \n * Logic related to loading pages and dictionaries\n * for indirect djvu documents.\n */\n\nimport { loadFileViaXHR } from \"../DjVu\";\nimport ByteStream from '../ByteStream';\nimport {\n    NetworkDjVuError,\n    UnsuccessfulRequestDjVuError,\n    CorruptedFileDjVuError\n} from \"../DjVuErrors\";\n\n/** @returns {ByteStream} */\nasync function loadByteStream(url, errorData = {}) {\n    let xhr;\n\n    try {\n        xhr = await loadFileViaXHR(url);\n    } catch (e) {\n        throw new NetworkDjVuError({ url: url, ...errorData });\n    }\n\n    if (xhr.status && xhr.status !== 200) {\n        throw new UnsuccessfulRequestDjVuError(xhr, { ...errorData });\n    }\n\n    return new ByteStream(xhr.response);\n}\n\nfunction checkAndCropByteStream(bs, compositeChunkId = null, errorData = null) {\n    if (bs.readStr4() !== 'AT&T') {\n        throw new CorruptedFileDjVuError(`The byte stream isn't a djvu file.`, errorData);\n    }\n\n    if (!compositeChunkId) {\n        return bs.fork(); // we should skip format id in the page byte stream\n    }\n\n    let chunkId = bs.readStr4();\n    const length = bs.getInt32();\n    chunkId += bs.readStr4();\n    if (chunkId !== compositeChunkId) {\n        throw new CorruptedFileDjVuError(\n            `Unexpected chunk id. Expected \"${compositeChunkId}\", but got \"${chunkId}\"`,\n            errorData\n        );\n    }\n\n    return bs.jump(-12).fork(length + 8);\n}\n\n/** @returns {ByteStream} */\nexport async function loadPage(number, url) {\n    const errorData = { pageNumber: number };\n    return checkAndCropByteStream(await loadByteStream(url, errorData), null, errorData);\n}\n\n/** @returns {ByteStream} */\nexport async function loadPageDependency(id, name, baseUrl, pageNumber = null) {\n    const errorData = { pageNumber: pageNumber, dependencyId: id };\n    return checkAndCropByteStream(await loadByteStream(baseUrl + name, errorData), 'FORMDJVI', errorData);\n}\n\n/** @returns {ByteStream} */\nexport async function loadThumbnail(url, id = null) {\n    const errorData = { thumbnailId: id };\n    return checkAndCropByteStream(await loadByteStream(url, errorData), 'FORMTHUM', errorData);\n}"
  },
  {
    "path": "library/tests/embed.html",
    "content": "<!-- A page to test the browser extension, namely how the viewer builds into a third party page-->\n\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Embed test page</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style>\n        body {\n            height: 100vh;\n        }\n\n        body > * {\n            margin: 1em 0;\n        }\n\n        iframe {\n            width: 600px;\n            height: 400px;\n        }\n    </style>\n</head>\n\n<body>\n    <p><a href=\"/assets/boy.djvu\">boy.djvu</a></p>\n    <p><a href=\"/assets/boy.DJVU\">boy.DJVU</a></p>\n\n    <!-- It should be processed via request interception. This feature is needed for old websites where the main part is an iframe -->\n    <iframe src=\"/assets/boy.djvu\"></iframe>\n    <!-- This iframe should be processed by content script, despite that its src ends with \".djvu\" -->\n    <iframe src=\"/get_embed_djvu_html?file=/assets/boy.djvu\"></iframe>\n    <object type=\"image/x.djvu\" data=\"/assets/boy.djvu\" width=\"10\" height=\"10\">\n        <param name=\"src\" value=\"/assets/boy.djvu\">\n    </object>\n    <embed type=\"image/x-djvu\" src=\"/assets/DjVu3Spec.djvu\" width=\"600\">\n    <embed type=\"image/vnd.djvu\" src=\"/assets/malliavin.djvu\" width=\"600\" height=\"82%\">\n    <object classid=\"clsid:0e8d0700-75df-11d3-8b4a-0008c7450c4a\" width=\"100%\" height=\"100%\">\n        <param name=\"src\" value=\"/assets/czech.djvu\">\n    </object>\n</body>\n\n</html>"
  },
  {
    "path": "library/tests/tests.css",
    "content": "html,\nbody {\n    height: 100%;\n}\n\n#test_results_wrapper {\n    font-family: monospace;\n    box-shadow: 0 0 1px lightgray;\n    padding: 0.2em;\n    margin: 0.2em;\n    display: flex;\n    flex-wrap: wrap;\n    flex-direction: column;\n    align-content: flex-start;\n    box-sizing: border-box;\n    justify-content: flex-start;\n    height: 95%;\n    overflow: auto;\n}\n\n.test_block {\n    box-shadow: 0 0 1px gray;\n    padding: 0.5em;\n    flex: 0 0 auto;\n    width: 25em;\n    white-space: normal;\n    word-wrap: break-word;\n    margin: 0.2em;\n}"
  },
  {
    "path": "library/tests/tests.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n    <title>DjVu.js Tests</title>\n    <script type=\"text/javascript\" src=\"/js/reloader.js\"></script>\n    <script type=\"text/javascript\" src=\"/app/jquery-3.2.1.min.js\"></script>\n    <script type=\"text/javascript\" src=\"/dist/djvu.js\"></script>\n    <script type=\"text/javascript\" src=\"tests.js\" defer></script>\n    <link rel=\"stylesheet\" href=\"tests.css\">\n</head>\n\n<body>\n    <div id=\"test_results_wrapper\"></div>\n</body>\n\n</html>"
  },
  {
    "path": "library/tests/tests.js",
    "content": "'use strict';\n\nvar djvuWorker = new DjVu.Worker();\n\nvar outputBlock = $('#test_results_wrapper');\n\nfunction createBaseUrl(url) {\n    const a = document.createElement('a');\n    a.href = url;\n    const absoluteUrl = a.href;\n    return new URL('./', absoluteUrl).href\n}\n\n// test invocations \n\nasync function runAllTests() {\n    var testNames = Object.keys(Tests);\n    var inPrior = testNames.filter(name => name[0] === '$');\n    var usual = testNames.filter(name => !/^[$,_]/.test(name));\n    testNames = [...inPrior, ...usual];\n\n    var totalTime = 0;\n    var total = testNames.length;\n    var failed = 0;\n\n    while (testNames.length) {\n        var testName = testNames.shift();\n        TestHelper.writeLog(`${testName} started...`);\n        var startTime = performance.now();\n\n        try {\n            var result = await Tests[testName]();\n        } catch (e) {\n            console.error(e);\n            result = e;\n        }\n\n        var testTime = performance.now() - startTime;\n        totalTime += testTime;\n        if (!result) {\n            TestHelper.writeLog(`${testName} succeeded!`, \"green\");\n        } else if (result.isSuccess) {\n            TestHelper.writeLog(`${testName} succeeded!`, \"green\");\n            if (result.messages) {\n                result.messages.forEach(message => {\n                    TestHelper.writeLog(message, \"orange\");\n                });\n            }\n        } else {\n            failed++;\n            TestHelper.writeLog(`Error: ${JSON.stringify(result)}`, \"red\");\n            TestHelper.writeLog(`${testName} failed!`, \"red\");\n        }\n        TestHelper.writeLog(`It has taken ${Math.round(testTime)} milliseconds`, \"blue\");\n        TestHelper.endTestBlock();\n    }\n\n    TestHelper.writeLog(`Total time = ${Math.round(totalTime)} milliseconds`, \"blue\");\n    TestHelper.writeLog(`Total number of test = ${total}`, \"blue\");\n    if (failed) {\n        TestHelper.writeLog(`Number of failed tests = ${failed}`, \"red\");\n    } else {\n        TestHelper.writeLog('All tests succeeded!', \"green\");\n    }\n}\n\nvar TestHelper = {\n\n    testBlock: null,\n\n    renderImageData(imageData) {\n        var canvas = document.createElement('canvas');\n        canvas.width = imageData.width;\n        canvas.height = imageData.height;\n        canvas.getContext('2d').putImageData(imageData, 0, 0);\n        document.body.appendChild(canvas);\n    },\n\n    writeLog(message, color = \"black\") {\n        if (!this.testBlock) {\n            this.testBlock = $('<div class=\"test_block\"/>');\n            outputBlock.append(this.testBlock);\n        }\n        this.testBlock.append(`<div style=\"color:${color}\">${message}</div>`);\n    },\n\n    endTestBlock() {\n        this.testBlock = null;\n    },\n\n    getHashOfArray(array) {\n        var hash = 0, i, chr;\n        if (array.length === 0) return hash;\n        for (i = 0; i < array.length; i++) {\n            chr = array[i];\n            hash = ((hash << 5) - hash) + chr;\n            hash |= 0; // Convert to 32bit integer\n        }\n        return hash;\n    },\n\n    getImageDataByImageURI(imageURI, rotate = 0) {\n        var image = new Image();\n        image.src = imageURI;\n        return new Promise(resolve => {\n            image.onload = () => {\n                var canvas = document.createElement('canvas');\n                if (rotate === 0 || rotate === 180) {\n                    canvas.width = image.width;\n                    canvas.height = image.height;\n                } else {\n                    canvas.width = image.height;\n                    canvas.height = image.width;\n                }\n                var ctx = canvas.getContext('2d');\n                if (rotate) {\n                    ctx.translate(canvas.width / 2, canvas.height / 2)\n                    ctx.rotate(rotate * Math.PI / 180);\n                    ctx.translate(-canvas.width / 2, -canvas.height / 2);\n\n                    // canvas.style.border = \"1px solid black\";\n                    // document.body.appendChild(canvas);\n                }\n                ctx.drawImage(image, (canvas.width - image.width) / 2, (canvas.height - image.height) / 2);\n                var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n                resolve(imageData);\n            };\n        });\n    },\n\n    compareArrayBuffers(canonicBuffer, resultBuffer) {\n        var canonicArray = new Uint8Array(canonicBuffer);\n        var resultArray = new Uint8Array(resultBuffer);\n\n        if (canonicArray.length !== resultArray.length) {\n            return `Несовпадение длины байтовых массивов! ${canonicArray.length} и ${resultArray.length}`\n        }\n\n        for (var i = 0; i < canonicArray.length; i++) {\n            if (canonicArray[i] !== resultArray[i]) {\n                return `Расхождение в байте номер ${i} !`;\n            }\n        }\n    },\n\n    compareImageData(canonicImageData, resultImageData) {\n        if (canonicImageData.width !== resultImageData.width) {\n            return `Несовпадение ширины! ${canonicImageData.width} и ${resultImageData.width}`;\n        }\n\n        if (canonicImageData.height !== resultImageData.height) {\n            return `Несовпадение высоты! ${canonicImageData.height} и ${resultImageData.height}`;\n        }\n\n        var strictCheck = () => {\n            for (var i = 0; i < resultImageData.data.length; i++) {\n                if (\n                    canonicImageData.data[i] !== resultImageData.data[i]\n                ) {\n                    return i;\n                }\n            }\n            return null;\n        };\n\n        var height = canonicImageData.height * 4;\n        var width = canonicImageData.width * 4;\n        var byteStep = 4;\n\n        var luft1Check = () => {\n            var luftCheck = (luft) => {\n                for (var i = 0; i < resultImageData.data.length; i++) {\n                    if (\n                        canonicImageData.data[i + luft] !== resultImageData.data[i]\n                        && canonicImageData.data[i] !== resultImageData.data[i]\n                    ) {\n                        return i;\n                    }\n                }\n                return null;\n            };\n            var successLuft = null;\n            [byteStep, -byteStep, width, width + byteStep, width - byteStep, -width, -width + byteStep, -width - byteStep].some(luft => {\n                var index = luftCheck(luft);\n                if (index === null) {\n                    successLuft = luft;\n                    return true;\n                }\n            });\n            return successLuft;\n        };\n\n        var strictResult = strictCheck();\n        if (strictResult === null) {\n            return null;\n        } else {\n            var luft1Result = luft1Check();\n            if (luft1Result !== null) {\n                return `Нестрогая проверка пройдена luft = ${luft1Result}, однако имеется расхождение пикселей! Строгая проверка: ${strictResult}`;\n            } else {\n                return `Pасхождение пикселей! Строгая проверка: ${strictResult}`;\n            }\n        }\n    }\n\n};\n\nvar Tests = {\n\n    async _imageTest(djvuName, pageNumber, imageName = null, hash = null, rotate = 0) {\n        return await this._imageTestX({\n            djvuUrl: '/assets/' + djvuName,\n            pageNumber,\n            imageUrl: imageName ? '/assets/' + imageName : imageName,\n            hash,\n            rotate\n        });\n    },\n\n    async _imageTestX({ djvuUrl, baseUrl = null, pageNumber, imageUrl = null, hash = null, rotate = 0 }) {\n        function checkByHash(data, message) {\n            const calculatedHash = TestHelper.getHashOfArray(data);\n            const isHashTheSame = calculatedHash === hash;\n            return {\n                isSuccess: isHashTheSame,\n                messages: [\n                    isHashTheSame ? \"Hash is the same! Good\" :\n                        `Hash is different! Calculated: ${calculatedHash}, required: ${hash}`,\n                    message\n                ]\n            };\n        }\n\n        var buffer = await (await fetch(djvuUrl)).arrayBuffer();\n        await djvuWorker.createDocument(buffer, baseUrl ? { baseUrl } : undefined);\n        const resultImageData = await djvuWorker.doc.getPage(pageNumber).getImageData().run();\n        if (imageUrl === null) {\n            var result = checkByHash(resultImageData.data);\n            return result.isSuccess ? null : result.messages[0];\n        }\n        var canonicImageData = await TestHelper.getImageDataByImageURI(imageUrl, rotate);\n        var result = TestHelper.compareImageData(canonicImageData, resultImageData);\n        if (result !== null && hash) {\n            result = checkByHash(resultImageData.data, result);\n        } else if (!hash) {\n            result += \"... Hash is \" + TestHelper.getHashOfArray(resultImageData.data);\n        }\n        return result;\n    },\n\n    async _sliceTest(source, from, to, result) {\n        const buffer = await (await fetch(source)).arrayBuffer();\n        await djvuWorker.createDocument(buffer);\n        const resultBuffer = await djvuWorker.doc.slice(from, to).run();\n        const canonicBuffer = await (await fetch(result)).arrayBuffer();\n        return TestHelper.compareArrayBuffers(canonicBuffer, resultBuffer);\n    },\n\n    /*test3LayerSiglePageDocument() { // отключен так как не ясен алгоритм масштабирования слоев\n        return this._imageTest(\"happy_birthday.djvu\", 0, \"happy_birthday.png\");\n    },*/\n\n    async _testText(djvuUrl, pageNumber, txtUrl) {\n        const buffer = await (await fetch(djvuUrl)).arrayBuffer();\n        await djvuWorker.createDocument(buffer, { baseUrl: createBaseUrl(djvuUrl) });\n\n        const [resultString, binText] = await Promise.all([\n            pageNumber ? djvuWorker.doc.getPage(pageNumber).getText().run() : djvuWorker.doc.toString().run(),\n            (await fetch(txtUrl)).arrayBuffer()\n        ]);\n\n        const canonicCharCodesArray = new Uint16Array(binText);\n        for (var i = 0; i < canonicCharCodesArray.length; i++) {\n            if (resultString.charCodeAt(i) !== canonicCharCodesArray[i]) {\n                return \"Text is incorrect!\";\n            }\n        }\n        return canonicCharCodesArray.length ? null : \"No canonic text!\";\n    },\n\n    async _testTextUtf8(djvuUrl, pageNumber, txtUrl) {\n        const buffer = await (await fetch(djvuUrl)).arrayBuffer();\n        await djvuWorker.createDocument(buffer, { baseUrl: createBaseUrl(djvuUrl) });\n\n        const [resultString, binText] = await Promise.all([\n            pageNumber ? djvuWorker.doc.getPage(pageNumber).getText().run() : djvuWorker.doc.toString().run(),\n            (await fetch(txtUrl)).arrayBuffer()\n        ]);\n\n        if (resultString !== new TextDecoder().decode(binText)) {\n            return 'Text is incorrect!';\n        }\n\n        return null;\n    },\n\n    async _testTextZones(djvuUrl, pageNumber, txtUrl, isNormalized = false) {\n        const buffer = await (await fetch(djvuUrl)).arrayBuffer();\n        await djvuWorker.createDocument(buffer);\n\n        const page = djvuWorker.doc.getPage(pageNumber);\n        const [textZones, binText] = await Promise.all([\n            isNormalized ? page.getNormalizedTextZones().run() : page.getPageTextZone().run(),\n            (await fetch(txtUrl)).arrayBuffer()\n        ]);\n\n        const resultString = JSON.stringify(textZones);\n\n        const canonicCharCodesArray = new Uint16Array(binText);\n        for (var i = 0; i < canonicCharCodesArray.length; i++) {\n            if (resultString.charCodeAt(i) !== canonicCharCodesArray[i]) {\n                return \"Text Zones are incorrect!\";\n            }\n        }\n        return canonicCharCodesArray.length ? null : \"No canonic text zones!\";\n    },\n\n    testIncorrectFileFormatError() {\n        return fetch(`/assets/boy.png`).then(res => res.arrayBuffer())\n            .then(buffer => {\n                return djvuWorker.createDocument(buffer);\n            }).then(() => {\n                return \"No error! But there must be one!\";\n            }).catch(e => {\n                if (e.code === DjVu.ErrorCodes.INCORRECT_FILE_FORMAT) {\n                    return null;\n                } else {\n                    return e;\n                }\n            });\n    },\n\n    async testNoSuchPageError() {\n        const buffer = await (await fetch(`/assets/boy.djvu`)).arrayBuffer();\n        await djvuWorker.createDocument(buffer);\n        try {\n            var pageNumber = 100;\n            await djvuWorker.doc.getPage(pageNumber).getImageData().run();\n        } catch (e) {\n            if (e.code === DjVu.ErrorCodes.NO_SUCH_PAGE && e.pageNumber === pageNumber) {\n                return null;\n            } else {\n                return e;\n            }\n        }\n        return \"No error! But there must be one!\";\n    },\n\n    async testMetaDataOfDocWithShortINFOChunk() {\n        return this._testTextUtf8('/assets/carte.djvu', null, '/assets/carte_metadata.bin');\n    },\n\n    testPageTextZone() {\n        return this._testTextZones('/assets/DjVu3Spec.djvu', 1, '/assets/DjVu3Spec_1_page_text_zone.bin');\n    },\n\n    testNormalizedTextZones() {\n        return this._testTextZones('/assets/DjVu3Spec.djvu', 1, '/assets/DjVu3Spec_1_normalized_text_zones.bin', true);\n    },\n\n    async testContents() {\n        const buffer = await (await fetch(`/assets/DjVu3Spec.djvu`)).arrayBuffer();\n        await djvuWorker.createDocument(buffer);\n        const contents = await djvuWorker.doc.getContents().run();\n        var res = await fetch('/assets/DjVu3Spec_contents.json');\n        var canonicContents = await res.json();\n\n        if (JSON.stringify(canonicContents) === JSON.stringify(contents)) {\n            return null;\n        } else {\n            console.log(canonicContents, contents);\n            return \"Contents are different!\";\n        }\n    },\n\n    async testPageUrlWithLeadingZero() {\n        const buffer = await (await fetch(`/assets/djvu3spec+.djvu`)).arrayBuffer();\n        await djvuWorker.createDocument(buffer);\n        const contents = await await djvuWorker.doc.getContents().run();\n        const url = contents[2].url;\n        if (url !== '#002') {\n            return `Incorrect url of a page! Got ${url}, while expected #002`;\n        }\n        const pageNumber = await djvuWorker.doc.getPageNumberByUrl(url).run();\n        if (pageNumber !== 2) {\n            return `Incorrect page number was returned! Got ${pageNumber} for url ${url}`;\n        }\n        return null;\n    },\n\n    async testGetPageNumberByUrl() {\n        const buffer = await (await fetch(`/assets/DjVu3Spec.djvu`)).arrayBuffer();\n        await djvuWorker.createDocument(buffer);\n        var pageNum = await djvuWorker.doc.getPageNumberByUrl('#p0069.djvu').run();\n        if (pageNum !== 69) {\n            return `The url #p0069.djvu is targeted at 69 page but we got ${pageNum} !`;\n        }\n        pageNum = await djvuWorker.doc.getPageNumberByUrl('#57').run();\n        if (pageNum !== 57) {\n            return `The url #57 is targeted at 57 page but we got ${pageNum} !`;\n        }\n        pageNum = await djvuWorker.doc.getPageNumberByUrl('#900').run();\n        if (pageNum !== null) {\n            return `There is no page with the url #900, but we got ${pageNum} !`;\n        }\n        return null;\n    },\n\n    async testCancelAllWorkerTasks() {\n        const buffer = await (await fetch(`/assets/boy.djvu`)).arrayBuffer();\n        await djvuWorker.createDocument(buffer);\n        try {\n            var promises = [];\n            for (var i = 2; i < 4; i++) {\n                promises.push(djvuWorker.doc.getPage(i).getImageData().run());\n            }\n            djvuWorker.cancelAllTasks();\n            promises.push(djvuWorker.doc.getPage(i).getImageData().run());\n            await Promise.race(promises);\n        } catch (e) {\n            if (e.code === DjVu.ErrorCodes.NO_SUCH_PAGE && e.pageNumber === i) {\n                return null;\n            } else {\n                return e;\n            }\n        }\n        return \"No error! But there must be one!\";\n    },\n\n    async testCancelOneWorkerTask() {\n        const buffer = await (await fetch(`/assets/boy.djvu`)).arrayBuffer();\n        await djvuWorker.createDocument(buffer);\n        try {\n            var promises = [];\n            for (var i = 2; i < 4; i++) {\n                promises.push(djvuWorker.doc.getPage(i).getImageData().run());\n            }\n            djvuWorker.cancelTask(promises[0]);\n            promises.push(djvuWorker.doc.getPage(i).getImageData().run());\n            await Promise.race(promises);\n        } catch (e) {\n            if (e.code === DjVu.ErrorCodes.NO_SUCH_PAGE && e.pageNumber === 3) {\n                return null;\n            } else {\n                return e;\n            }\n        }\n        return \"No error! But there must be one!\";\n    },\n\n    testGetEnglishText() {\n        return this._testText('/assets/DjVu3Spec.djvu', 1, '/assets/DjVu3Spec_1_text.bin');\n    },\n\n    testGetCzechText() {\n        return this._testText('/assets/czech.djvu', 6, '/assets/czech_6_text.bin');\n    },\n\n    testGetIncorrectlyEncodedUtf8Text() {\n        return this._testTextUtf8('/assets/century_dict/index08.djvu', 475, '/assets/century_dict/page475_text.bin');\n    },\n\n    testCreateDocumentFromPictures() {\n        djvuWorker.startMultiPageDocument(90, 0, 0);\n        return Promise.all([\n            TestHelper.getImageDataByImageURI(`/assets/boy.png`),\n            TestHelper.getImageDataByImageURI(`/assets/chicken.png`)\n        ]).then(imageDatas => {\n            return Promise.all(imageDatas.map(imageData => djvuWorker.addPageToDocument(imageData)));\n        }).then(() => {\n            return Promise.all([\n                fetch(`/assets/boy_and_chicken.djvu`).then(res => res.arrayBuffer()),\n                djvuWorker.endMultiPageDocument()\n            ]);\n        }).then(arrayBuffers => {\n            return TestHelper.compareArrayBuffers(...arrayBuffers);\n        });\n    },\n\n    async testBundleDocument() {\n        const buffer = await (await fetch('/assets/DjVu3Spec_indirect/index.djvu')).arrayBuffer();\n        await djvuWorker.createDocument(buffer, { baseUrl: '/assets/DjVu3Spec_indirect' });\n        let counter = 88;\n        let progress = 0;\n        const resultBuffer = await djvuWorker.doc.bundle(p => {\n            progress = p;\n            counter--;\n        }).run();\n        const canonicBuffer = await (await fetch('/assets/DjVu3Spec_bundled.djvu')).arrayBuffer();\n        const progressCheck = counter === 0 && progress === 1 ? null : \"Проблемы с отслеживанием прогресса\";\n        return progressCheck || TestHelper.compareArrayBuffers(canonicBuffer, resultBuffer);\n    },\n\n    testSliceDocument() {\n        return this._sliceTest(`/assets/DjVu3Spec.djvu`, 5, 10, `/assets/DjVu3Spec_5-10.djvu`);\n    },\n\n    testSliceDocumentWithAnnotations() {\n        return this._sliceTest(`/assets/czech.djvu`, 1, 3, `/assets/czech_1-3.djvu`);\n    },\n\n    testSliceDocumentWithCyrillicIds() {\n        return this._sliceTest(`/assets/history.djvu`, 2, 2, `/assets/history_2.djvu`);\n    },\n\n    async testIndirectDjVu() {\n        var buffer = await (await fetch('/assets/czech_indirect/index.djvu')).arrayBuffer();\n        djvuWorker.createDocument(buffer, { baseUrl: '/assets/czech_indirect/', memoryLimit: 0 });\n\n        async function checkPage(number, canonicHash) {\n            var imageData = await djvuWorker.doc.getPage(number).getImageData().run();\n            //TestHelper.renderImageData(imageData);\n            var hash = TestHelper.getHashOfArray(imageData.data);\n            if (hash !== canonicHash) {\n                throw \"Hash of isn't the same!\";\n            }\n        }\n\n        await checkPage(3, 400840825);\n        var memoryUsage1 = await djvuWorker.doc.getMemoryUsage().run();\n        await checkPage(1, -769561152);\n        var memoryUsage2 = await djvuWorker.doc.getMemoryUsage().run();\n        await checkPage(3, 400840825);\n        var memoryUsage3 = await djvuWorker.doc.getMemoryUsage().run();\n\n        if (memoryUsage2 >= memoryUsage1) {\n            throw \"The memory wasn't released!\";\n        }\n        if (memoryUsage1 !== memoryUsage3) {\n            throw \"There is a memory leakage!\";\n        }\n        await djvuWorker.doc.setMemoryLimit(1000000).run();\n        await checkPage(1, -769561152);\n        var memoryUsage4 = await djvuWorker.doc.getMemoryUsage().run();\n        if (memoryUsage4 <= memoryUsage3) {\n            throw \"The memory limit is ignored!\";\n        }\n\n        try {\n            await djvuWorker.doc.getPage(2).getImageData().run();\n            throw \"There is no error, but there must be one!\";\n        } catch (e) {\n            if (!(\n                e.code === DjVu.ErrorCodes.UNSUCCESSFUL_REQUEST\n                && e.status === 404\n                && e.pageNumber === 2\n                && !e.dependencyId\n            )) {\n                throw { message: \"Different Error!\", error: e };\n            }\n        }\n\n        try {\n            await djvuWorker.doc.getPage(4).getImageData().run();\n            throw \"There is no error, but there must be one!\";\n        } catch (e) {\n            if (!(\n                e.code === DjVu.ErrorCodes.UNSUCCESSFUL_REQUEST\n                && e.status === 404\n                && e.pageNumber === 4\n                && e.dependencyId === 'dict1085.iff' // the dependency was spoiled manually in the file\n            )) {\n                throw { message: \"Different Error!\", error: e };\n            }\n        }\n    },\n\n    testOpenIndirectDjVuPageDirectly() {\n        return this._imageTestX({\n            djvuUrl: '/assets/czech_indirect/p0001.djvu',\n            baseUrl: '/assets/czech_indirect/',\n            imageUrl: '/assets/czech_indirect/p0001.png',\n            pageNumber: 1,\n            hash: 400840825,\n        });\n    },\n\n    testOpenIndirectDjVuWithEmptyDjVi() {\n        return this._imageTestX({\n            djvuUrl: '/assets/polish_indirect/index.djvu',\n            baseUrl: '/assets/polish_indirect/',\n            imageUrl: '/assets/polish_indirect/sw1-0002.png',\n            pageNumber: 1,\n            hash: -177861879,\n        });\n    },\n\n    testPageWithEmptyLastChunk() {\n        return this._imageTestX({\n            djvuUrl: '/assets/ccitt_2.djvu',\n            imageUrl: '/assets/ccitt_2.png',\n            pageNumber: 1,\n            hash: -1646655329,\n        });\n    },\n\n    testGrayscaleBG44() {\n        return this._imageTest(\"boy.djvu\", 1, \"boy.png\", -1560338846);\n    },\n\n    testColorBG44() {\n        return this._imageTest(\"chicken.djvu\", 1, \"chicken.png\", 1973539465);\n    },\n\n    testJB2Pure() {\n        return this._imageTest(\"boy_jb2.djvu\", 1, \"boy_jb2.png\", -650210314);\n    },\n\n    testRotate90() {\n        return this._imageTest(\"boy_jb2_rotate90.djvu\", 1, \"boy_jb2.png\", -76276490, 90);\n    },\n\n    testRotate180() {\n        return this._imageTest(\"boy_jb2_rotate180.djvu\", 1, \"boy_jb2.png\", -76276490, 180);\n    },\n\n    testRotate270() {\n        return this._imageTest(\"boy_jb2_rotate270.djvu\", 1, \"boy_jb2.png\", -80336394, 270);\n    },\n\n    testJB2WithBitOfBackground() {\n        return this._imageTest(\"DjVu3Spec.djvu\", 48, \"DjVu3Spec_48.png\", 1367724765);\n    },\n\n    testJB2WhereRemovingOfEmptyEdgesOfBitmapsBeforeAddingToDictRequired() {\n        return this._imageTest(\"problem_page.djvu\", 1, \"problem_page.png\", 826528816);\n    },\n\n    testFGbzColoredMask() {\n        return this._imageTest(\"navm_fgbz.djvu\", 3, \"navm_fgbz_3.png\", 1017482741);\n    },\n\n    testPageWithCyrillicId() {\n        return this._imageTest(\"history.djvu\", 2, null, 1203480221);\n    },\n\n    async testEmptyPage() {\n        var buffer = await (await fetch(`/assets/malliavin.djvu`)).arrayBuffer();\n        await djvuWorker.createDocument(buffer);\n        const imageData = await djvuWorker.doc.getPage(6).getImageData().run();\n        if (!imageData.data.every(byte => byte === 255)) {\n            return \"The page must be empty, but it isn't!\";\n        }\n    },\n\n    testDeutschBaseline() { // документ в котором Baseline считался неправильно и символы были не на своих местах\n        return this._imageTestX({\n            djvuUrl: '/assets/deutsch.djvu',\n            imageUrl: '/assets/deutsch_1.png',\n            pageNumber: 1,\n            hash: 2018317133,\n        });\n    },\n\n    testNewJB2SymbolWithEmptyEdges() {\n        return this._imageTestX({\n            djvuUrl: '/assets/vega.djvu',\n            imageUrl: '/assets/vega_1.png',\n            pageNumber: 1,\n            hash: 1675742877,\n        });\n    },\n\n    testFileWith2BZZEncodedBlocks() {\n        return this._imageTestX({\n            djvuUrl: '/assets/irish.djvu',\n            imageUrl: '/assets/irish_1.png',\n            pageNumber: 1,\n            hash: -371412505,\n        });\n    },\n\n    /*test3LayerColorImage() { // отключен так как не ясен алгоритм масштабирования слоев\n        return this._imageTest(\"colorbook.djvu\", 3, \"colorbook_4.png\");\n    }*/\n};\n\nrunAllTests();"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"DjVu.js_Project\",\n  \"scripts\": {\n    \"clean\": \"git clean -fdX --exclude=!/.*/\",\n    \"install\": \"cd library && npm install && cd .. && cd viewer && npm install && cd ..\",\n    \"build\": \"cd library && npm run build && cd .. && cd viewer && npm run build && cd .. && npm run copy\",\n    \"copy\": \"node .js copy\",\n    \"make\": \"npm run install && npm run build\",\n    \"remake\": \"npm run clean && npm run install && npm run build\",\n    \"_ext\": \"cd extension && npx web-ext build -n {name}-v{manifest_version}-{version}.zip -o\",\n    \"ext2\": \"node .js v2 && npm run _ext\",\n    \"ext3\": \"node .js v3 && npm run _ext\",\n    \"ext\": \"npm run ext2 && npm run ext3\"\n  }\n}\n"
  },
  {
    "path": "viewer/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n/public/tmp\n/src/css"
  },
  {
    "path": "viewer/CHANGELOG.md",
    "content": "# DjVu.js Viewer's Changelog\n\n## v.0.10.1 (30.05.2024)\n\n- Hide the \"Analyze headers\" option in the manifest v3 extension, because it's\n  not supported there.\n\n## v.0.10.0 (26.12.2023)\n\n- Fix: reset global styles provided by some CSS frameworks.\n- New translation: Ukrainian.\n- Chinese translation update.\n- Inner changes: dependency updates, E2E test fixes, new build and dev tools.\n\n## v.0.9.2 (09.04.2023)\n\n- Support for \"data:\" URLs\n\n## v.0.9.1 (01.02.2023)\n\n- Spanish translation update.\n- DjVu.js v.0.5.4: error messages are displayed again.\n\n## v.0.9.0 (24.05.2022)\n\n- Feature: several pages in a row in the continuous scroll mode.\n- Fix: pinch-zoom works on mobile devices in the continuous scroll mode. \n- Style improvements. \n\n## v.0.8.3 (14.11.2021)\n\n- French translation update.\n- Minor style fixes.\n\n## v.0.8.2 (23.09.2021)\n\n- Support for big images (up to 20K * 20K pixels) in the single page view mode.\n- Dynamic letter spacing in the text layer to make text fully fill its zone.\n- API to change view mode programmatically.\n- Fixed: a previous page was shown for a short while when one switched from\n  continuous scroll to single page view mode.\n- Getters, constants and action types are exposed as an escape hatch API for\n  temporary solutions.\n\n## v.0.8.1 (11.09.2021)\n\n- Fixed a bug from the previous release (the text layer couldn't be selected).\n- Updated Italian and Chinese translations.\n\n## v.0.8.0 (06.09.2021)\n\n- Mobile version.\n- Fullscreen mode.\n- Image scaling via pinch zoom.\n\n## v.0.7.1 (30.08.2021)\n\n- Toolbar can be pinned and unpinned.\n- Minor fixes and improvements.\n\n## v.0.7.0 (20.08.2021)\n\n- Removed footer, added menu in order to use less space for controls.\n- Contents panel is animated and can be closed completely.\n- Continuous scroll mode performance improvement.\n\n## v.0.6.2 (06.04.2021)\n\n- Spanish and Portuguese translations.\n- French translation update.\n\n## v.0.6.1 (14.03.2021)\n\n- Fixed print CSS to avoid printing empty pages in Safari.\n\n## v.0.6.0 (10.03.2021)\n\n- Print function.\n- New UI options to hide open, close, print and save buttons.\n\n## v.0.5.6 (21.02.2021)\n\n- Simplified Chinese translation.\n- UI options: a notification/agreement can be shown when a user tries to save a\n  document.\n- Save an indirect document after it's bundled with its original name.\n- UI improvement: a handle to resize the left panel.\n\n## v.0.5.5 (18.02.2021)\n\n- Translation of error messages.\n- New UI options: `showContentsAutomatically` and `changePageOnScroll`.\n- Options window.\n- Support of absolute and relative links in the table of contents.\n- Italian translation.\n- Minor improvements and corrections.\n\n## v.0.5.4 (16.01.2021)\n\n- French translation.\n- An option to hide the full-page mode switch.\n\n## v.0.5.3 (08.12.2020)\n\n- Fix: styles in the help window.\n- Update of the Swedish translation.\n\n## v.0.5.2 (06.12.2020)\n\n- Feature: bundle indirect djvu documents (suggestion in the save dialog).\n- Fix: hide the page's scrollbars in the full page mode.\n\n## v.0.5.1 (19.11.2020)\n- Separate image and text errors for a page in order to show the part of data which is available.\n  (Former behavior: if the text couldn't be decoded, an error page was shown, even if the image had been gotten)\n  It's related only to the single-page view mode.\n\n## v.0.5.0 (09.11.2020)\n- Dark and light color themes.\n- All CSS is built into the JS file.\n- Preference for the continuous-scroll view mode is saved in the options now.\n- Bug fixes and style improvements.\n\n## v.0.4.1 (23.08.2020)\n- Swedish translation.\n- File name is extracted from the Content-Disposition header, if it's present.\n- Russian translation was corrected.\n\n## v.0.4.0 (19.08.2020)\n- Multi-Language support.\n- Russian and English languages.\n\n## v.0.3.6 (27.07.2020)\n- An ability to load a file by URL manually in the extension. \n- An extension option to analyze http headers to detect djvu files.\n\n## v.0.3.5 (24.04.2020)\n- New DOCUMENT_CLOSED and DOCUMENT_CHANGED events to change the page title dynamically. \n- decodeURIComponent applied to a file's name derived from a URL.\n- Fixed a bug due to which there was no file names, but only ***. \n\n## v.0.3.4 (30.03.2020)\n- Viewer's programmatic API enhancement: page number can be set via configure(), getPageNumber() and PAGE_NUMBER_CHANGED event were added.\n\n## v.0.3.3 (03.11.2019)\n- 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.\n\n## v.0.3.2 (26.10.2019)\n- Fixed a bug due to which it was impossible to create many instance of the viewer on the same page.\n\n## v.0.3.1 (11.08.2019)\n- A page number can be set in the URL, e.g. some.djvu#page=10.\n\n## v.0.3.0 (12.05.2019)\n- Continuous scroll mode.\n\n## v.0.2.5 (30.03.2019)\n- Page scale can be set programmatically.\n\n## v.0.2.4 (15.11.2018)\n\n- Loading layer with a short delay. \n- Improvement in pages caching logic. \n- Minor fixes. \n\n## v.0.2.3 (12.10.2018)\n\n- Some errors are shown instead of pages rather than in a pop-up window.\n- Loading placeholder is shown when there is no image yet.\n- Minor changes to support indirect djvu.\n\n## v.0.2.2 (27.08.2018)\n\n- Rotate pages 0, 90, 180, 270 degrees clockwise.\n- New API allowing to set the initial page rotation programmatically. \n- Page positioning improvement.\n\n## v.0.2.1 (20.08.2018)\n\n- Turn over pages via scrolling.\n- Bug fixes. \n\n## v.0.2.0 (05.08.2018)\n\n- Now it's possible to create many instances of the viewer.\n- Ctrl+S works even when the keyboard layout isn't English.\n\n## v.0.1.7 (18.06.2018)\n\n- The height of the containing element (which the viewer renders into) isn't changed anymore.\n- DjVu global variable is encapsulated in a separate module.\n- Minor styles update.\n\n## v.0.1.6 (02.06.2018)\n\n- Layout update: no tools panel on the initial screen.\n- Drag&Drop file zone on the initial screen.\n- A possibility to close a document and return to the initial screen.\n\n## v.0.1.5 (25.05.2018)\n\n- A page text layer and two cursor modes. \n\n## v.0.1.4 (16.05.2018)\n\n- Pages are cached now, so better user experience is provided, since pages are switched faster. \n\n## v.0.1.3 (10.05.2018)\n\n- Help button and help window.\n- Layout update.\n- Save button near the file block.\n- Minor style improvements. \n\n## v.0.1.2 (01.05.2018)\n\n- Program API: loadDocument and loadDocumentByUrl.\n- File loading screen with a progress bar.\n- Hotkeys: Ctrl+S to save the document, right/left arrows to go to next/previous pages. \n\n## v.0.1.1 (20.04.2018)\n\n- Better error handling.\n- Table of contents styles improvements.\n- Now the page vertical scroll bar returns to the top, when a page is changed.\n\n## v.0.1.0 (10.04.2018)\n\n- Open single-page and multi-page .djvu documents.\n- Show text of pages if it is provided.\n- Scale images of pages. \n- Drag images of pages.\n- Turn pages of documents, go to arbitrary page by its number.\n- 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.\n- Full page mode.\n- Status bar shows when a task is being executed."
  },
  {
    "path": "viewer/cypress/e2e/fullscreen_mode.cy.js",
    "content": "import { customId, customClass, renderViewer, loadDocument } from \"../utils\";\n\ndescribe('Full page mode', () => {\n    beforeEach(() => {\n        cy.visit('/');\n        renderViewer();\n    });\n\n    it('Full page mode button works', () => {\n        cy.window().then(win => {\n            const check = (chainer) => {\n                cy.get(customId('root')).then($el => $el.get(0).getBoundingClientRect()).as('boundingRect');\n                cy.get(\"@boundingRect\").its(\"width\").should(chainer, win.innerWidth);\n                cy.get(\"@boundingRect\").its(\"height\").should(chainer, win.innerHeight);\n            };\n\n            check('be.lessThan');\n            cy.get(customClass('full_page_button')).click();\n            check('eq');\n            cy.get(customClass('full_page_button')).click();\n            check('be.lessThan');\n        })\n    });\n});\n\n// Doesn't work in Cypress's Electron, only in Firefox\ndescribe.skip('Fullscreen mode unavailable', () => {\n    beforeEach(() => {\n        cy.visit('/');\n        renderViewer();\n    });\n\n    it('Fullscreen button on initial screen', () => {\n        cy.get(customClass('fullscreen_button')).should('not.exist');\n    });\n\n    it('Fullscreen button in menu', () => {\n        loadDocument();\n        cy.get(customId('menu_button')).click();\n        cy.get(customId('menu')).within(() => {\n            cy.contains('Fullscreen mode').should('not.exist');\n            cy.get(customClass('fullscreen_button')).should('not.exist');\n        });\n    });\n});\n\ndescribe('Fullscreen mode', () => {\n    beforeEach(() => {\n        cy.visit('/');\n        cy.window().then(win => {\n            win.parent.document\n                .querySelector('.aut-iframe')\n                .setAttribute('allow', 'fullscreen');\n            win.location.reload();\n            cy.document().its('fullscreenEnabled').should('be.true');\n        });\n        renderViewer();\n    });\n\n    // Browser doesn't allow to toggle fullscreen programmatically without a user gesture.\n    it.skip('Fullscreen mode', () => {\n        cy.window().then(win => {\n            const check = (chainer) => {\n                cy.get(customId('root')).then($el => $el.get(0).getBoundingClientRect()).as('boundingRect');\n                cy.get(\"@boundingRect\").its(\"width\").should(chainer, win.screen.width);\n                cy.get(\"@boundingRect\").its(\"height\").should(chainer, win.screen.width);\n            };\n\n            check('be.lessThan');\n            cy.get(customClass('fullscreen_button')).click();\n            cy.document().its('fullscreenElement').should('not.equal', null);\n            cy.get(customClass('fullscreen_button')).click().wait(2000);\n            check('eq');\n            cy.get(customClass('fullscreen_button')).click();\n            check('be.lessThan');\n        })\n    });\n\n    it('Fullscreen button on initial screen', () => {\n        cy.get(customClass('fullscreen_button')).should('be.visible');\n    });\n\n    it('Fullscreen button in menu', () => {\n        loadDocument();\n        cy.get(customId('menu_button')).click();\n        cy.get(customId('menu')).within(() => {\n            cy.contains('Fullscreen mode').should('be.visible');\n            cy.get(customClass('fullscreen_button')).should('be.visible');\n        });\n    });\n});"
  },
  {
    "path": "viewer/cypress/e2e/initial_screen.cy.js",
    "content": "import { getByCustomId, haveCustomClass, hexToRGB, notHaveCustomClass, renderViewer } from \"../utils\";\nimport { initialScreenShouldBeVisible } from \"../shared\";\n\ndescribe.only('Initial screen', () => {\n    beforeEach(() => {\n        cy.visit('/');\n        renderViewer();\n    });\n\n    it('Initial screen is visible', () => {\n        initialScreenShouldBeVisible();\n    });\n\n    it('Dark and white theme', () => {\n        getByCustomId('root').should('have.css', 'background-color', hexToRGB('#fcfcfc'));\n        getByCustomId('light_theme_button').should(haveCustomClass('active'));\n        getByCustomId('dark_theme_button').click();\n        getByCustomId('root').should('have.css', 'background-color', hexToRGB('#1e1e1e'));\n        getByCustomId('light_theme_button').click();\n        getByCustomId('root').should('have.css', 'background-color', hexToRGB('#fcfcfc'));\n    });\n\n    it('Language switch', () => {\n        cy.contains('English').should(haveCustomClass('selected'));\n        cy.contains('Русский').click().should(haveCustomClass('selected'));\n        cy.contains('изменение настроек').should('be.visible');\n        cy.contains('English').should(notHaveCustomClass('selected'));\n    });\n});"
  },
  {
    "path": "viewer/cypress/e2e/menu.cy.js",
    "content": "import { customClass, customId, loadDocument, renderViewer } from \"../utils\";\nimport { helpWindowShouldBeOpen, initialScreenShouldBeVisible, optionsWindowShouldBeOpen } from \"../shared\";\n\nconst menuShouldNotBeVisible = () => cy.get(customId('menu')).should('not.be.visible');\n\ndescribe('Document menu opens and closes', () => {\n    beforeEach(() => {\n        cy.visit('/');\n        renderViewer();\n        loadDocument();\n    });\n\n    it('Menu opens and closes via menu button', () => {\n        cy.contains(\"Menu\").should('not.be.visible');\n        cy.get(customId('menu_button')).click();\n        cy.contains(\"Menu\").should('be.visible');\n        cy.get(customId('menu_button')).click();\n        menuShouldNotBeVisible();\n    });\n\n    it('Menu can be closed with the close button', () => {\n        cy.get(customId('menu_button')).click().wait(500);\n        cy.get(customId('menu')).should('be.visible')\n            .find(customClass('close_button')).first().click();\n        menuShouldNotBeVisible();\n    });\n});\n\ndescribe('Document menu controls', () => {\n    beforeEach(() => {\n        cy.visit('/');\n        renderViewer();\n        loadDocument();\n        cy.get(customId('menu_button')).click().wait(500);\n    });\n\n    it('Options inside menu and menu closes', () => {\n        cy.contains('Options').click();\n        optionsWindowShouldBeOpen();\n        menuShouldNotBeVisible();\n    });\n\n    it('Help button inside menu', () => {\n        cy.contains('About').click();\n        helpWindowShouldBeOpen();\n        menuShouldNotBeVisible();\n    });\n\n    it('Print document', () => {\n        cy.contains('Print').click();\n        menuShouldNotBeVisible();\n        cy.get(customClass('modal_window')).within(() => {\n            cy.contains('Pages must be rendered before printing').should('be.visible');\n            cy.contains('From').should('be.visible');\n            cy.contains('to').should('be.visible');\n            cy.contains('Prepare pages for printing').click();\n        });\n\n        cy.contains('Prepare pages for printing').should('not.exist');\n        cy.get(customClass('modal_window'))\n            .contains('Preparing pages for printing...').should('be.visible');\n    });\n\n    it('Close document', () => {\n        cy.contains('test_document').should('be.visible');\n        cy.contains('Close').click();\n        cy.get(customId('menu')).should('not.exist');\n        initialScreenShouldBeVisible();\n    });\n});"
  },
  {
    "path": "viewer/cypress/e2e/mobile_version.cy.js",
    "content": "import { customClass, customId, loadDocument, renderViewer } from \"../utils\";\n\ndescribe('Adaptive layout', () => {\n    beforeEach(() => {\n        cy.visit('/');\n        renderViewer();\n        loadDocument();\n    });\n\n    it('Dynamic layout change', () => {\n\n        const checkVisibility = (hidden = false) => {\n            cy.get(customId('toolbar')).within(() => {\n                cy.get(customId('view_mode_buttons')).should(`be.${hidden ? 'not.' : ''}visible`);\n                cy.get(customId('cursor_mode_buttons')).should(`be.${hidden ? 'not.' : ''}visible`);\n                cy.get(customId('scale_gizmo')).should(`be.${hidden ? 'not.' : ''}visible`);\n                cy.get(customId('rotation_control')).should(`be.${hidden ? 'not.' : ''}visible`);\n                cy.get(customId('pin_button')).should(hidden ? 'not.exist' : `be.visible`);\n                cy.get(customClass('right_panel') + '>' + customClass('full_page_button'))\n                    .should(hidden ? 'not.exist' : `be.visible`);\n            });\n            cy.contains('Contents').should(`be.${hidden ? 'not.' : ''}visible`);\n        }\n\n        checkVisibility();\n        cy.viewport(700, 800);\n        checkVisibility(true);\n\n        cy.get(customId('contents_button')).should('be.visible');\n        cy.get(customId('page_number_block')).should('be.visible');\n        cy.get(customId('page_number_block')).should('be.visible');\n        cy.get(customId('hide_button')).should('be.visible');\n        cy.get(customId('menu_button')).should('be.visible');\n    });\n});\n\ndescribe('Mobile version', {\n    viewportWidth: 700,\n    viewportHeight: 800,\n}, () => {\n    beforeEach(() => {\n        cy.visit('/');\n        renderViewer();\n        loadDocument();\n    });\n\n    it('Hide button', () => {\n        cy.get(customId('toolbar')).should('be.visible');\n        cy.get(customId('hide_button')).should('be.visible').click();\n        cy.get(customId('toolbar')).should('not.be.visible');\n        cy.get(customId('hide_button')).should('be.visible').click();\n        cy.get(customId('toolbar')).should('be.visible');\n    });\n\n    it('Contents button', () => {\n        cy.contains('Contents').should('not.be.visible');\n        cy.get(customId('contents_button')).click();\n        cy.contains('Contents').should('be.visible');\n        cy.get(customId('contents_button')).click();\n        cy.contains('Contents').should('not.be.visible');\n    });\n\n    it('Mobile menu', () => {\n        cy.get(customId('menu_button')).click();\n        cy.get(customId('menu')).should('be.visible').within(() => {\n            cy.contains('View mode').should('be.visible');\n            cy.contains('Scale').should('be.visible');\n            cy.contains('Rotation').should('be.visible');\n            cy.contains('Cursor mode').should('be.visible');\n            cy.contains('Full page mode').should('be.visible');\n        });\n    });\n});\n\n\n"
  },
  {
    "path": "viewer/cypress/e2e/modal_windows.cy.js",
    "content": "import { customClass, customId, renderViewer } from \"../utils\";\nimport { closeModalWindow, helpWindowShouldBeOpen, optionsWindowShouldBeOpen } from \"../shared\";\n\ndescribe('Modal windows', () => {\n    beforeEach(() => {\n        cy.visit('/');\n        renderViewer();\n    });\n\n    it('A click on the dark layer closes the modal window', () => {\n        cy.get(customClass('help_button')).click();\n        cy.get(customClass('modal_window')).as('modal_window').should('be.visible');\n        cy.get(customId('root')).click(5, 5);\n        cy.get('@modal_window').should('not.exist');\n    });\n\n    it('The close button closes the modal window', () => {\n        cy.get(customClass('help_button')).click();\n        closeModalWindow();\n        cy.get(\"@modal_window\").should('not.exist');\n    });\n\n    it('Options window', () => {\n        cy.get(customClass('options_button')).click();\n        optionsWindowShouldBeOpen();\n    });\n\n    it('Help window', () => {\n        cy.get(customClass('help_button')).click();\n        helpWindowShouldBeOpen();\n    });\n});"
  },
  {
    "path": "viewer/cypress/e2e/toolbar.cy.js",
    "content": "import {  customId, loadDocument, renderViewer } from \"../utils\";\n\ndescribe('Toolbar controls', () => {\n    beforeEach(() => {\n        cy.visit('/');\n        renderViewer();\n        loadDocument();\n    });\n\n    it('Pin/Unpin toolbar', () => {\n        cy.get(customId('toolbar')).should('be.visible');\n        cy.get(customId('pin_button')).click();\n        cy.get(customId('toolbar')).trigger('mouseout').wait(500);\n        cy.get(customId('toolbar')).should('not.be.visible');\n        cy.get(customId('root')).trigger('mouseover', 'bottom');\n        cy.get(customId('toolbar')).should('be.visible');\n        cy.get(customId('pin_button')).click();\n        cy.get(customId('toolbar')).trigger('mouseout').wait(500);\n        cy.get(customId('toolbar')).should('be.visible');\n    });\n\n    it('Contents button works', () => {\n        cy.contains(\"Contents\").should('be.visible');\n        cy.get(customId('contents_button')).click().wait(500);\n        cy.contains(\"Contents\").should('not.be.visible');\n        cy.get(customId('contents_button')).click();\n        cy.contains(\"Contents\").should('be.visible');\n    });\n\n    it('Go to the next/previous page', () => {\n        cy.get(customId('page_number_block')).find('svg:first-of-type').as('prev');\n        cy.get(customId('page_number_block')).find('svg:last-of-type').as('next');\n        cy.contains('1 / 71').should('be.visible');\n        cy.get('@next').click();\n        cy.contains('2 / 71').should('be.visible');\n        cy.get('@prev').click();\n        cy.contains('1 / 71').should('be.visible');\n        cy.get('@prev').click();\n        cy.contains('71 / 71').should('be.visible');\n        cy.get('@next').click();\n        cy.contains('1 / 71').should('be.visible');\n    });\n});\n"
  },
  {
    "path": "viewer/cypress/shared.js",
    "content": "import { customClass } from \"./utils\";\n\nexport function helpWindowShouldBeOpen() {\n    cy.get(customClass('modal_window')).should('be.visible').within(() => {\n        cy.contains('DjVu.js Viewer');\n        cy.contains('Hotkeys');\n        cy.contains('Controls');\n    });\n}\n\nexport function optionsWindowShouldBeOpen() {\n    cy.get(customClass('modal_window')).should('be.visible').within(() => {\n        cy.contains('Options');\n        cy.contains('Language');\n        cy.contains('Color theme');\n    });\n}\n\nexport function closeModalWindow() {\n    cy.get(customClass('modal_window')).as('modal_window').should('be.visible')\n        .find(customClass('close_button')).click();\n}\n\nexport function initialScreenShouldBeVisible() {\n    cy.contains(\"DjVu.js Viewer\").should('be.visible');\n    cy.contains(\"powered with DjVu.js\").should('be.visible');\n    cy.get(customClass('help_button')).should('be.visible');\n    cy.get(customClass('options_button')).should('be.visible');\n}"
  },
  {
    "path": "viewer/cypress/utils.js",
    "content": "export const hexToRGB = (string) => {\n    if ((string.length !== 4 && string.length !== 7) || string[0] !== '#') {\n        throw new Error('Incorrect hex color string: ' + string);\n    }\n    const componentLength = string.length === 4 ? 1 : 2;\n\n    const arr = [];\n    for (let i = 0; i < 3; i++) {\n        arr.push(string.slice(i * componentLength + 1, componentLength * (i + 1) + 1));\n    }\n    const result = arr.map(color => Number.parseInt(color.length === 2 ? color : (color + color), 16)).join(', ');\n\n    return `rgb(${result})`;\n}\n\nexport const customId = id => `[data-djvujs-id=\"${id}\"]`;\nexport const customClass = className => `[data-djvujs-class~=\"${className}\"]`;\n\nexport const haveCustomId = id => $el => $el.is(customId(id));\nexport const haveCustomClass = className => $el => expect($el).to.match(customClass(className));\nexport const notHaveCustomClass = className => $el => expect($el).not.to.match(customClass(className));\n\nexport const getByCustomId = id => cy.get(customId(id));\nexport const getByCustomClass = className => cy.get(customClass(className));\n\nexport const renderViewer = () => {\n    cy.window().then(win => {\n        win.viewer && win.viewer.destroy();\n        win.viewer = new win.DjVu.Viewer({ language: 'en', theme: 'light' });\n        win.viewer.render(win.document.getElementById('root'));\n    });\n};\n\nexport function loadDocument() {\n    cy.clearLocalStorage();\n    cy.window().then(win => {\n        win.viewer.loadDocumentByUrl('DjVu3Spec.djvu', {\n            name: \"test_document\",\n            locale: 'en',\n        });\n    });\n}"
  },
  {
    "path": "viewer/cypress.config.js",
    "content": "import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n    viewportWidth: 1200,\n    viewportHeight: 900,\n    video: false,\n    fixturesFolder: false,\n    e2e: {\n        setupNodeEvents(on, config) {},\n        baseUrl: 'http://localhost:8000/?tests=1',\n        supportFile: false,\n    },\n})\n"
  },
  {
    "path": "viewer/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <meta name=\"theme-color\" content=\"#000000\">\n    <link rel=\"shortcut icon\" href=\"/djvu.png\">\n    <title>DjVu Viewer</title>\n    <style>\n\n        html, body {\n            height: 100%;\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n            /*writing-mode: vertical-lr;*/\n        }\n\n        body {\n            margin: 0;\n            padding: 0;\n            display: flex; /* in order to prevent child's margin:auto collapse (become) parent's margins */\n            font-family: sans-serif;\n            background: repeating-linear-gradient(135deg, #5aa0fa, #ffffff 50%);\n        }\n\n        #root, #root2 {\n            box-sizing: border-box;\n            width: 80%;\n            padding: 2px;\n            height: 90%;\n            margin: 5vh auto;\n            box-shadow: 0 0 1px gray;\n            background: #b0c093;\n            /*writing-mode: horizontal-tb;*/\n        }\n\n        /* used to check that such \"default\" styles are overridden for the viewer */\n        :where(#root) *, :where(#root) ::before, :where(#root) ::after {\n            background: red;\n            padding: 100em;\n            margin: 100em;\n            border: 100px solid red;\n            display: block;\n            position: absolute;\n        }\n\n    </style>\n    <script id=\"djvu_js_lib\" src=\"/tmp/djvu.js\"></script>\n    <script src=\"./src/index.js\" type=\"module\"></script>\n    <script>\n        window.onload = function () {\n            // window.DjVuViewerInstance2 = new window.DjVu.Viewer();\n            // window.DjVuViewerInstance2.render(document.getElementById('root2'));\n            // window.DjVuViewerInstance2.loadDocumentByUrl(\"/tmp/colorbook.djvu\");\n        };\n    </script>\n</head>\n\n<body>\n    <noscript>\n        You need to enable JavaScript to run this app.\n    </noscript>\n    <div id=\"root\"></div>\n</body>\n\n</html>"
  },
  {
    "path": "viewer/jsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"ES6\"\n    },\n    \"exclude\": [\n        \"node_modules\",\n        \"**/node_modules/*\"\n    ]\n}"
  },
  {
    "path": "viewer/package.json",
    "content": "{\n    \"name\": \"DjVu.js_Viewer\",\n    \"private\": true,\n    \"type\": \"module\",\n    \"devDependencies\": {\n        \"@vitejs/plugin-react\": \"^4.0.4\",\n        \"babel-plugin-styled-components\": \"^2.1.4\",\n        \"cypress\": \"^12.17.4\",\n        \"npm-run-all\": \"^4.1.5\",\n        \"serve-static\": \"^1.15.0\",\n        \"vite\": \"^4.4.9\"\n    },\n    \"dependencies\": {\n        \"content-disposition-header\": \"0.6.0\",\n        \"eventemitter3\": \"^5.0.1\",\n        \"memoize-one\": \"^6.0.0\",\n        \"prop-types\": \"^15.8.1\",\n        \"react\": \"^18.2.0\",\n        \"react-dom\": \"^18.2.0\",\n        \"react-icons\": \"^4.10.1\",\n        \"react-redux\": \"^8.1.2\",\n        \"redux\": \"^4.2.1\",\n        \"redux-saga\": \"^1.2.3\",\n        \"redux-thunk\": \"^2.4.2\",\n        \"reselect\": \"^4.1.8\",\n        \"styled-components\": \"^6.0.7\"\n    },\n    \"scripts\": {\n        \"start\": \"vite --open\",\n        \"build\": \"vite build\",\n        \"test:open\": \"cypress open\",\n        \"test\": \"cypress run\",\n        \"test:visual\": \"cypress run --headed\",\n        \"syncLocales\": \"node syncLocales.js\"\n    }\n}\n"
  },
  {
    "path": "viewer/public/manifest.json",
    "content": "{\n  \"short_name\": \"DjVu Viewer\",\n  \"name\": \"DjVu Viewer\",\n  \"icons\": [\n    {\n      \"src\": \"djvu.src\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    }\n  ],\n  \"start_url\": \"./index.html\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "viewer/src/App.test.js",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport App from './App';\n\nit('renders without crashing', () => {\n  const div = document.createElement('div');\n  ReactDOM.render(<App />, div);\n});\n"
  },
  {
    "path": "viewer/src/DjVu.js",
    "content": "/**\n * The module is required due to the bug https://bugzilla.mozilla.org/show_bug.cgi?id=1408996\n * Because of which I can't address to the global object directly via window.DjVu\n * Also it encapsulates the logic of getting the global DjVu object.\n */\nif (typeof DjVu !== 'object') {\n    throw new Error(\"There is no DjVu object! You have to include the DjVu.js library first!\");\n}\n\nconst djvu = DjVu; // eslint-disable-line\n\nexport default djvu;"
  },
  {
    "path": "viewer/src/DjVuViewer.jsx",
    "content": "import React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { Provider } from 'react-redux'\nimport App from './components/App.jsx';\nimport Actions from './actions/actions';\nimport configureStore from './store';\nimport EventEmitter from 'eventemitter3';\nimport Constants, { constant, ActionTypes } from './constants';\nimport { get } from './reducers';\nimport dictionaries from './locales';\n\nconst Events = constant({\n    PAGE_NUMBER_CHANGED: null,\n    DOCUMENT_CHANGED: null,\n    DOCUMENT_CLOSED: null,\n});\n\nexport default class DjVuViewer extends EventEmitter {\n\n    static VERSION = '0.10.1';\n\n    static Events = Events;\n    static Constants = Constants;\n    static ActionTypes = ActionTypes;\n    static get = get;\n\n    static getAvailableLanguages() {\n        return Object.keys(dictionaries);\n    };\n\n    /**\n     * Technically, we can pass the same config as to the configure() method.\n     * But some options are reset when a new document is loaded.\n     */\n    constructor(config = null) {\n        super();\n        this.store = configureStore(this.eventMiddleware);\n        config && this.configure(config);\n    }\n\n    eventMiddleware = store => next => action => {\n        let result;\n        switch (action.type) {\n            case Constants.SET_NEW_PAGE_NUMBER_ACTION:\n                const oldPageNumber = this.getPageNumber();\n                result = next(action);\n                const newPageNumber = this.getPageNumber();\n                if (oldPageNumber !== newPageNumber) {\n                    this.emit(Events.PAGE_NUMBER_CHANGED);\n                }\n                break;\n\n            case Constants.DOCUMENT_CREATED_ACTION:\n                result = next(action);\n                this.emit(Events.DOCUMENT_CHANGED);\n                break;\n\n            case Constants.CLOSE_DOCUMENT_ACTION:\n                result = next(action);\n                this.emit(Events.DOCUMENT_CLOSED);\n                break;\n\n            case Constants.END_FILE_LOADING_ACTION: // use in this.loadDocumentByUrl only\n                result = next(action);\n                this.emit(Constants.END_FILE_LOADING_ACTION);\n                break;\n\n            default:\n                result = next(action);\n                break;\n        }\n\n        return result;\n    };\n\n    getPageNumber() {\n        return get.currentPageNumber(this.store.getState());\n    }\n\n    getDocumentName() {\n        return get.fileName(this.store.getState());\n    }\n\n    _render() {\n        this.reactRoot.render(\n            <Provider store={this.store}>\n                <App shadowRoot={this.htmlElement} />\n            </Provider>\n        );\n    }\n\n    render(element) {\n        this.unmount();\n        this.htmlElement = element;\n        this.reactRoot = createRoot(element);\n        //this.shadow = element.attachShadow({ mode: 'open' });\n\n        this._render();\n    }\n\n    unmount() {\n        this.reactRoot && this.reactRoot.unmount();\n        this.reactRoot = null;\n        this.htmlElement = null;\n    }\n\n    destroy() {\n        this.unmount();\n        this.store.dispatch({ type: ActionTypes.DESTROY });\n    }\n\n    /**\n     * The config object is destructed merely for the purpose of documentation\n     * @param {number} pageNumber\n     * @param {0|90|180|270} pageRotation\n     * @param {'continuous'|'single'|'text'} viewMode\n     * @param {number} pageScale\n     * @param {string} language\n     * @param {'dark'|'light'} theme\n     * @param {{\n          hideFullPageSwitch: boolean,\n          changePageOnScroll: boolean,\n          showContentsAutomatically: boolean,\n          hideOpenAndCloseButtons: boolean,\n          hidePrintButton: boolean,\n          hideSaveButton: boolean,\n       }} uiOptions\n     * @returns {DjVuViewer}\n     */\n    configure({\n        pageNumber,\n        pageRotation,\n        viewMode,\n        pageScale,\n        language,\n        theme,\n        uiOptions,\n    } = {}) {\n        this.store.dispatch({\n            type: ActionTypes.CONFIGURE,\n            pageNumber, pageRotation, viewMode, pageScale, language, theme, uiOptions,\n        });\n\n        return this;\n    }\n\n    loadDocument(buffer, name = \"***\", config = {}) {\n        return new Promise(resolve => {\n            this.once(Events.DOCUMENT_CHANGED, () => resolve());\n            // the buffer is transferred to the worker, so we copy it\n            this.store.dispatch(Actions.createDocumentFromArrayBufferAction(buffer.slice(0), name, config));\n        });\n    }\n\n    loadDocumentByUrl(url, config = null) {\n        return new Promise(resolve => {\n            this.once(Constants.END_FILE_LOADING_ACTION, () => resolve());\n            this.store.dispatch({\n                type: ActionTypes.LOAD_DOCUMENT_BY_URL,\n                url: url,\n                config: config\n            });\n        });\n    }\n}\n"
  },
  {
    "path": "viewer/src/actions/actions.js",
    "content": "import Constants, { ActionTypes } from '../constants';\nimport { get } from '../reducers';\nimport DjVu from '../DjVu';\n\nconst Actions = {\n\n    dropPageAction: pageNumber => ({ type: Constants.DROP_PAGE_ACTION, pageNumber: pageNumber }),\n\n    pagesSizesAreGottenAction: (pagesSizes) => ({\n        type: Constants.PAGES_SIZES_ARE_GOTTEN,\n        sizes: pagesSizes,\n    }),\n\n    pageIsLoadedAction: (pageData, pageNumber) => ({\n        type: Constants.PAGE_IS_LOADED_ACTION,\n        pageNumber: pageNumber,\n        pageData: pageData,\n    }),\n\n    setPageRotationAction: rotation => dispatch => {\n        if (rotation === 0 || rotation === 90 || rotation === 180 || rotation === 270) {\n            dispatch({\n                type: Constants.SET_PAGE_ROTATION_ACTION,\n                pageRotation: rotation\n            });\n        }\n    },\n\n    closeDocumentAction: () => ({ type: Constants.CLOSE_DOCUMENT_ACTION }),\n\n    setCursorModeAction: cursorMode => ({ type: Constants.SET_CURSOR_MODE_ACTION, cursorMode: cursorMode }),\n\n    closeHelpWindowAction: () => ({ type: Constants.CLOSE_HELP_WINDOW_ACTION }),\n\n    showHelpWindowAction: () => ({ type: Constants.SHOW_HELP_WINDOW_ACTION }),\n\n    tryToSaveDocument: () => (dispatch, getState) => {\n        if (get.isIndirect(getState())) {\n            dispatch({ type: ActionTypes.OPEN_SAVE_DIALOG });\n        } else {\n            dispatch({ type: ActionTypes.SAVE_DOCUMENT });\n        }\n    },\n\n    startFileLoadingAction: () => ({ type: Constants.START_FILE_LOADING_ACTION }),\n\n    endFileLoadingAction: () => ({ type: Constants.END_FILE_LOADING_ACTION }),\n\n    goToNextPageAction: () => (dispatch, getState) => {\n        const state = getState();\n        if (get.currentPageNumber(state) < get.pagesQuantity(state)) {\n            dispatch(Actions.setNewPageNumberAction(get.currentPageNumber(state) + 1, true));\n        }\n    },\n\n    goToPreviousPageAction: () => (dispatch, getState) => {\n        const state = getState();\n        if (get.currentPageNumber(state) > 1) {\n            dispatch(Actions.setNewPageNumberAction(get.currentPageNumber(state) - 1, true));\n        }\n    },\n\n    fileLoadingProgressAction: (loaded, total) => ({\n        type: Constants.FILE_LOADING_PROGRESS_ACTION,\n        loaded: loaded,\n        total: total\n    }),\n\n    errorAction: error => {\n        console.error(error);\n\n        return {\n            type: ActionTypes.ERROR,\n            payload: error,\n        }\n    },\n\n    createDocumentFromArrayBufferAction: (arrayBuffer, fileName = \"***\", config = {}) => ({\n        type: Constants.CREATE_DOCUMENT_FROM_ARRAY_BUFFER_ACTION,\n        arrayBuffer: arrayBuffer,\n        fileName: fileName,\n        config: config,\n    }),\n\n    setNewPageNumberAction: (pageNumber, shouldScrollToPage = false) => ({\n        type: Constants.SET_NEW_PAGE_NUMBER_ACTION,\n        pageNumber: pageNumber,\n        shouldScrollToPage: shouldScrollToPage,\n    }),\n\n    setPageByUrlAction(url, closeContentsOnSuccess = false) {\n        return {\n            type: Constants.SET_PAGE_BY_URL_ACTION,\n            url: url,\n            closeContentsOnSuccess: closeContentsOnSuccess,\n        };\n    },\n\n    setUserScaleAction: (scale) => ({\n        type: Constants.SET_USER_SCALE_ACTION,\n        scale: scale < 0.1 ? 0.1 : scale > 6 ? 6 : scale\n    }),\n\n    toggleFullPageViewAction: (isFullPageView) => (dispatch) => {\n        const disableScrollClass = 'disable_scroll_djvujs';\n        if (isFullPageView) {\n            document.querySelector('html').classList.add(disableScrollClass);\n            document.body.classList.add(disableScrollClass);\n        } else {\n            document.querySelector('html').classList.remove(disableScrollClass);\n            document.body.classList.remove(disableScrollClass);\n        }\n\n        dispatch({\n            type: Constants.TOGGLE_FULL_PAGE_VIEW_ACTION,\n            isFullPageView: isFullPageView\n        });\n    },\n};\n\nexport default Actions;"
  },
  {
    "path": "viewer/src/components/App.jsx",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { createGlobalStyle, css, /* StyleSheetManager */ } from 'styled-components';\n\nimport { get } from '../reducers';\nimport Toolbar from \"./Toolbar/Toolbar\";\nimport InitialScreen from './InitialScreen/InitialScreen';\nimport FileLoadingScreen from './FileLoadingScreen';\nimport ErrorWindow from './ModalWindows/ErrorWindow';\nimport HelpWindow from './ModalWindows/HelpWindow';\nimport { TranslationProvider } from './Translation';\nimport Main from './Main';\nimport SaveDialog from \"./ModalWindows/SaveDialog\";\nimport OptionsWindow from \"./ModalWindows/OptionsWindow\";\nimport PrintDialog from \"./ModalWindows/PrintDialog\";\nimport AppContextProvider from \"./AppContext\";\n\nconst GlobalStyle = createGlobalStyle`\n    html.disable_scroll_djvujs,\n    body.disable_scroll_djvujs {\n        width: 100% !important;\n        height: 100% !important;\n        overflow: hidden !important;\n    }\n\n    /*\n     Reset styles to get rid of default global styles provided by some frameworks, \n     e.g. https://tailwindcss.com/docs/preflight that adds \"svg {display: block}\".\n     The specificity is (0, 0, 2) for tags and (0, 1, 1) for pseudo elements to both override the default styles,\n     but not override class-based styles from styled-components. \n     :not(span) and :not(html) are added to increased the specificity.\n     \n     We cannot use \"all: revert\" for svg and its children, because it will override all svg attributes, \n     including \"d\" prop of <path>, which will make all icons invisible. \n     */\n    :where(.djvujs-viewer-root) *:not(svg *):not(svg),\n    div:not(span):where(.djvujs-viewer-root),\n    :where(.djvujs-viewer-root, .djvujs-viewer-root *):not(html)::before,\n    :where(.djvujs-viewer-root, .djvujs-viewer-root *):not(html)::after {\n        all: revert;\n    }\n\n    :where(.djvujs-viewer-root) :is(svg:not(span), svg *) {\n        display: revert;\n        position: revert;\n        vertical-align: revert;\n        border: revert;\n        box-sizing: revert;\n        background: revert;\n        margin: revert;\n        padding: revert;\n    }\n\n    // -------------------------- end of styles reset --------------------------\n`;\n\nconst lightTheme = css`\n    --background-color: #fcfcfc;\n    --alternative-background-color: #eee;\n    --modal-window-background-color: var(--background-color);\n    --color: #000;\n    --border-color: #555;\n    --highlight-color: #084475;\n    --scrollbar-track-color: var(--alternative-background-color);\n    --scrollbar-thumb-color: #cccccc;\n`;\n\nconst darkTheme = css`\n    --background-color: #1e1e1e;\n    --alternative-background-color: #333333;\n    --modal-window-background-color: var(--background-color);\n    --color: #CCCCCC;\n    --border-color: #999999;\n    --highlight-color: #d89416;\n    --scrollbar-track-color: var(--alternative-background-color);\n    --scrollbar-thumb-color: #858585;\n`;\n\nconst style = css`\n    font-family: Arial, Helvetica, sans-serif;\n    font-size: ${p => p.theme.isMobile ? 10 : 14}px;\n    overflow: hidden;\n    position: relative;\n    box-sizing: border-box;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    line-height: initial;\n    writing-mode: horizontal-tb;\n    \n    --app-padding: 5px;\n    padding: var(--app-padding);\n\n    background: var(--background-color);\n    color: var(--color);\n\n    a {\n        color: var(--highlight-color);\n    }\n\n    *::-webkit-scrollbar {\n        background-color: var(--scrollbar-track-color);\n    }\n\n    *::-webkit-scrollbar-thumb {\n        background-color: var(--scrollbar-thumb-color);\n    }\n    \n    *::-webkit-scrollbar-corner {\n        background-color: var(--background-color);\n    }\n`;\n\nconst fullPageStyle = css`\n    top: 0;\n    left: 0;\n    position: fixed;\n    width: 100%;\n    height: 100%;\n    z-index: 100;\n`;\n\nconst AppRoot = React.forwardRef(({ shadowRoot }, ref) => {\n    const isFileLoaded = useSelector(get.isDocumentLoaded);\n    const isFileLoading = useSelector(get.isFileLoading);\n    const isFullPageView = useSelector(get.isFullPageView);\n    const theme = useSelector(get.options).theme;\n    const isPrintDialogOpened = useSelector(get.isPrintDialogOpened);\n\n    return (\n        <TranslationProvider>\n            <GlobalStyle />\n            <div\n                css={`\n                    ${theme === 'dark' ? darkTheme : lightTheme};\n                    ${style};\n                    ${isFullPageView ? fullPageStyle : ''};\n                `}\n                data-djvujs-id=\"root\" // used in E2E tests\n                className=\"djvujs-viewer-root\" // used to reset styles\n                ref={ref}\n            >\n                {isFileLoading ?\n                    <FileLoadingScreen /> :\n\n                    !isFileLoaded ? <InitialScreen /> :\n                        <React.Fragment>\n                            <Main />\n                            <Toolbar />\n                        </React.Fragment>\n                }\n                {/*{isFileLoading ? null : <Footer />}*/}\n\n                <ErrorWindow />\n                <HelpWindow />\n                <SaveDialog />\n                <OptionsWindow />\n                {isPrintDialogOpened ? <PrintDialog /> : null}\n                <div id=\"djvujs-modal-windows-container\" />\n            </div>\n        </TranslationProvider>\n    );\n});\n\nexport default ({ shadowRoot }) => {\n    return (\n        //<StyleSheetManager target={shadowRoot}>\n        <AppContextProvider AppRoot={AppRoot} shadowRoot={shadowRoot} />\n        //</StyleSheetManager>\n    );\n}"
  },
  {
    "path": "viewer/src/components/AppContext.jsx",
    "content": "import React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport { ThemeContext, ThemeProvider } from \"styled-components\";\nimport { useDispatch } from \"react-redux\";\nimport { ActionTypes } from \"../constants\";\n\nconst widthThreshold = 890;\nconst heightThreshold = 569;\nconst defaultValue = { appWidth: widthThreshold, appHeight: heightThreshold, isMobile: false };\nexport const AppContext = ThemeContext;\nexport const useAppContext = () => React.useContext(AppContext);\n\nexport const withAppContext = Component => props => (\n    <AppContext.Consumer>\n        {appContext => <Component {...props} appContext={appContext} />}\n    </AppContext.Consumer>\n);\n\nfunction useAppSize(ref) {\n    const [appSize, setAppSize] = useState(defaultValue);\n\n    useEffect(() => {\n        if (!ref.current) return;\n\n        const observer = new ResizeObserver(([entry]) => {\n            // maybe it's better to observe borderBox, but in mobile browsers only contentRect is supported now\n            const boxSize = entry.contentBoxSize ? (entry.contentBoxSize[0] || entry.contentBoxSize) : {\n                inlineSize: entry.contentRect.width,\n                blockSize: entry.contentRect.height,\n            };\n            setAppSize({\n                appWidth: boxSize.inlineSize,\n                appHeight: boxSize.blockSize,\n                isMobile: boxSize.blockSize < heightThreshold || boxSize.inlineSize < widthThreshold,\n            });\n        });\n\n        observer.observe(ref.current);\n\n        return () => observer.disconnect();\n    }, [ref.current, setAppSize]);\n\n    return appSize;\n}\n\nfunction useFullscreen(ref) {\n    const [isFullscreen, setIsFullscreen] = useState(false);\n    useEffect(() => {\n        if (!ref.current) return;\n\n        const handler = () => {\n            setIsFullscreen(document.fullscreenElement === ref.current || document.webkitFullscreenElement === ref.current);\n        };\n\n        ref.current.addEventListener('fullscreenchange', handler);\n        ref.current.addEventListener('webkitfullscreenchange', handler);\n\n    }, [ref.current]);\n\n    const toggleFullscreen = async () => {\n        if (!ref.current || !(document.fullscreenEnabled || document.webkitFullscreenEnabled)) return;\n\n        let promise = null;\n        if (!isFullscreen) {\n            if (ref.current.requestFullscreen) {\n                promise = ref.current.requestFullscreen();\n            } else if (ref.current.webkitRequestFullScreen) {\n                promise = ref.current.webkitRequestFullScreen();\n            }\n        } else if (document.fullscreenElement || document.webkitFullscreenElement) {\n            if (document.exitFullscreen) {\n                promise = document.exitFullscreen();\n            } else if (document.webkitExitFullscreen) {\n                promise = ref.current.webkitExitFullscreen();\n            }\n        }\n\n        try {\n            await promise;\n        } catch (e) {\n            console.warn('Cannot change fullscreen mode. Error: \\n', e);\n        }\n    };\n\n    return { isFullscreen, toggleFullscreen };\n}\n\nexport default ({ AppRoot }) => {\n    const rootRef = useRef(null);\n    const appSize = useAppSize(rootRef);\n    const fullscreen = useFullscreen(rootRef);\n    const dispatch = useDispatch();\n    const appContext = { ...appSize, ...fullscreen };\n\n    useEffect(() => {\n        dispatch({ type: ActionTypes.UPDATE_APP_CONTEXT, payload: appContext })\n    }, [dispatch, appContext]);\n\n    const app = useMemo(() => <AppRoot ref={rootRef} />, [rootRef]);\n\n    return (\n        <ThemeProvider theme={appContext}>\n            {app}\n        </ThemeProvider>\n    );\n}"
  },
  {
    "path": "viewer/src/components/ErrorPage.jsx",
    "content": "import React from 'react';\nimport { useTranslation } from \"./Translation\";\nimport styled from 'styled-components';\nimport { getHeaderAndErrorMessage } from \"./helpers\";\n\nconst Root = styled.div`\n    background: pink;\n    color: black;\n    padding: 1em;\n    font-family: monospace;\n    border: 1px solid gray;\n    overflow: auto;\n    height: 100%;\n    box-sizing: border-box;\n`;\n\nconst Header = styled.div`\n    font-weight: 600;\n    font-size: 1.5em;\n    margin-bottom: 0.5em;\n`;\nexport default ({ pageNumber, error }) => {\n    const t = useTranslation();\n    const { header, message } = getHeaderAndErrorMessage(t, error);\n\n    return (\n        <Root>\n            <Header>{`${t(\"Error on page\")} №${pageNumber}`}</Header>\n            <div>\n                <div css={`font-size: 1.2em; margin-bottom: 0.5em;`}>{header}</div>\n                <div css={`white-space: pre; font-size: 1.2em;`}>{message}</div>\n            </div>\n        </Root>\n    );\n}"
  },
  {
    "path": "viewer/src/components/FileBlock.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\nimport { FaUpload } from \"react-icons/fa\";\n\nimport Actions from '../actions/actions';\nimport { TranslationContext } from './Translation';\nimport styled from 'styled-components';\n\nconst FileIcon = styled(FaUpload)`\n    flex: 0 0 auto;\n    //font-size: var(--button-basic-size, 1.5em);\n`;\n\nconst FileName = styled.span`\n    overflow: hidden;\n    flex: 0 1 auto;\n    max-width: 20em;\n    text-align: left;\n    text-overflow: ellipsis;\n    margin: 0 0.5em;\n`;\n\nconst Root = styled.div`\n    flex: 0 1 auto;\n    cursor: pointer;\n    display: flex;\n    flex-wrap: nowrap;\n    align-items: center;\n    justify-content: flex-start;\n    white-space: nowrap;\n    overflow: hidden;\n\n    &:hover {\n        ${FileIcon} {\n            transform: scale(1.1)\n        }\n    }\n`;\n\nclass FileBlock extends React.Component {\n\n    static propTypes = {\n        fileName: PropTypes.string,\n        createNewDocument: PropTypes.func.isRequired\n    };\n\n    static contextType = TranslationContext;\n\n    onChange = (e) => {\n        if (!e.target.files.length) {\n            return;\n        }\n        const file = e.target.files[0];\n\n        var fr = new FileReader();\n        fr.readAsArrayBuffer(file);\n        fr.onload = () => {\n            this.props.createNewDocument(fr.result, file.name);\n        }\n    };\n\n    onClick = (e) => {\n        this.input && this.input.click();\n    };\n\n    render() {\n        const t = this.context;\n\n        return (\n            <Root\n                className=\"file_block\"\n                onClick={this.onClick}\n                title={t(\"Open another .djvu file\")}\n            >\n                <FileIcon />\n                <FileName>{this.props.fileName == null ? t(\"Choose a file\") : (this.props.fileName || '')}</FileName>\n                <input\n                    style={{ display: 'none' }}\n                    type=\"file\"\n                    onChange={this.onChange}\n                    accept=\".djvu, .djv\"\n                    ref={node => this.input = node}\n                />\n            </Root>\n        );\n    }\n}\n\nexport default connect(null, {\n    createNewDocument: Actions.createDocumentFromArrayBufferAction,\n})(FileBlock);"
  },
  {
    "path": "viewer/src/components/FileLoadingScreen.jsx",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\n\nimport { get } from '../reducers';\nimport styled from 'styled-components';\nimport ProgressBar from \"./misc/ProgressBar\";\nimport LoadingPhrase from \"./misc/LoadingPhrase\";\n\nconst Root = styled.div`\n    flex-direction: column;\n    width: 100%;\n    height: 100%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    white-space: nowrap;\n`;\n\nconst FileLoadingScreen = () => {\n    const loaded = useSelector(get.loadedBytes);\n    const total = useSelector(get.totalBytes);\n    const percentage = (loaded && total) ? Math.round(loaded / total * 100) : 0;\n\n    return (\n        <Root>\n            <LoadingPhrase css={`font-size: 3em; margin-bottom: 0.5em`} />\n            <div css={`font-size: 1.5em`} style={(loaded || total) ? null : { visibility: \"hidden\" }}>\n                {Math.round(loaded / 1024).toLocaleString('ru-RU')} KB {total ? `/ ${Math.round(total / 1024).toLocaleString('ru-RU')} KB` : ''}\n            </div>\n            <ProgressBar percentage={percentage} css={total ? null : `visibility: hidden`} />\n        </Root>\n    );\n};\n\nexport default FileLoadingScreen;"
  },
  {
    "path": "viewer/src/components/ImageBlock/CanvasImage.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport Constants from '../../constants';\n\n/**\n * A component containing logic of rendering ImageData on canvas element.\n * Scales itself via css and via logarithmic scale method.\n *\n * Must be used with a unique key for each page.\n */\nexport default class CanvasImage extends React.Component {\n\n    static propTypes = {\n        imageData: PropTypes.object.isRequired,\n        imageDpi: PropTypes.number.isRequired,\n        userScale: PropTypes.number.isRequired\n    };\n\n    constructor(props) {\n        super(props);\n        this.tmpCanvas = document.createElement('canvas');\n        this.tmpCanvasCtx = this.tmpCanvas.getContext('2d');\n        this.lastUserScale = null;\n        this.redrawImageTimeout = -1;\n    }\n\n    componentWillUnmount() {\n        clearTimeout(this.redrawImageTimeout);\n    }\n\n    componentDidUpdate() {\n        this.updateImageIfRequired();\n    }\n\n    componentDidMount() {\n        this.updateImageIfRequired();\n    }\n\n    getScaleFactor() {\n        return (this.props.imageDpi ? this.props.imageDpi / Constants.DEFAULT_DPI : 1) / this.props.userScale;\n    }\n\n    getScaledImageWidth() {\n        return Math.floor(this.props.imageData.width / this.getScaleFactor());\n    }\n\n    getScaledImageHeight() {\n        return Math.floor(this.props.imageData.height / this.getScaleFactor());\n    }\n\n    updateImageIfRequired() {\n        if (!this.canvas) {\n            return;\n        }\n        if (this.lastUserScale !== this.props.userScale) {\n            if (this.lastUserScale === null) { // if there is no image at all\n                return this.drawImageOnCanvas();\n            }\n            clearTimeout(this.redrawImageTimeout);\n            this.redrawImageTimeout = setTimeout(() => {\n                this.drawImageOnCanvas();\n            }, 300);\n        }\n    }\n\n    logarithmicScale() {\n        const image = this.props.imageData;\n        var tmpH, tmpW, tmpH2, tmpW2;\n\n        let scale = this.getScaleFactor();\n\n        if (scale <= 1) {\n            return image; // when it's scaled up, it will be just scaled with css\n        }\n\n        this.tmpCanvas.width = tmpW = tmpW2 = image.width;\n        this.tmpCanvas.height = tmpH = tmpH2 = image.height;\n        this.tmpCanvasCtx.putImageData(image, 0, 0);\n        while (Math.abs(scale - 1) >= 0.001 && tmpW > 1 && tmpH > 1) {\n            var divisor = scale > 2 ? 2 : scale;\n            scale /= divisor;\n            tmpH2 /= divisor;\n            tmpW2 /= divisor;\n            this.tmpCanvasCtx.drawImage(this.tmpCanvas, 0, 0, tmpW, tmpH, 0, 0, tmpW2, tmpH2);\n            tmpH = tmpH2;\n            tmpW = tmpW2;\n        }\n\n        const newImageData = this.tmpCanvasCtx.getImageData(0, 0, Math.max(tmpW, 1), Math.max(tmpH, 1));\n        this.tmpCanvas.width = this.tmpCanvas.height = 0;\n        return newImageData;\n    }\n\n    drawImageOnCanvas() {\n        this.putImageData(this.logarithmicScale());\n        this.lastUserScale = this.props.userScale;\n    }\n\n    putImageData(imageData) {\n        if (!this.canvas) {\n            return;\n        }\n        this.canvas.width = imageData.width;\n        this.canvas.height = imageData.height;\n        this.canvasCtx.putImageData(imageData, 0, 0);\n\n        if (this.getScaleFactor() >= 1) { // if it's not scaled only with css\n            this.canvas.style.width = imageData.width + 'px'; // just in case, since there may be a rounding error\n            this.canvas.style.height = imageData.height + 'px';\n        }\n    }\n\n    canvasRef = (node) => {\n        this.canvas = node;\n        if (this.canvas) {\n            this.canvasCtx = this.canvas.getContext('2d');\n        }\n    };\n\n    render() {\n        return (\n            <canvas\n                style={{ width: this.getScaledImageWidth(), height: this.getScaledImageHeight() }}\n                ref={this.canvasRef}\n            />\n        );\n    }\n}\n"
  },
  {
    "path": "viewer/src/components/ImageBlock/ComplexImage.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport CanvasImage from './CanvasImage';\nimport TextLayer from './TextLayer';\nimport Constants from '../../constants';\nimport styled from 'styled-components';\nimport LoadingPhrase from '../misc/LoadingPhrase';\nimport memoize from \"memoize-one\";\n\nconst Root = styled.div`\n    position: relative;\n    border: 1px solid darkgray;\n    overflow: hidden;\n    \n    &:first-child {\n        margin-left: auto;\n    }\n    \n    &:last-child {\n        margin-right: auto;\n    }\n\n    & > div:first-child {\n        position: absolute;\n        left: 50%;\n        top: 50%;\n        transform: translateX(-50%) translateY(-50%);\n\n        img {\n            display: block;\n        }\n\n        & > canvas {\n            display: block;\n        }\n\n        ${p => p.$rotation ? `transform: translateX(-50%) translateY(-50%) rotate(${p.$rotation}deg)` : ''};\n    }\n`;\n\n/**\n * A component encapsulating the text layer, the canvas image, and adding additional wrapper to fix the size of the block,\n * when the element is rotated.\n */\nclass ComplexImage extends React.PureComponent {\n\n    static propTypes = {\n        imageData: PropTypes.object,\n        imageUrl: PropTypes.string,\n        imageWidth: PropTypes.number,\n        imageHeight: PropTypes.number,\n        imageDpi: PropTypes.number,\n        userScale: PropTypes.number,\n        textZones: PropTypes.array,\n        rotation: PropTypes.oneOf([0, 90, 180, 270]),\n        outerRef: PropTypes.func,\n        currentPageNumber: PropTypes.number,\n    };\n\n    /**\n     * Firefox cannot putImageData() bigger than about 12_000 * 12_000 pixels.\n     * createImageBitmap() fails too.\n     * Chrome fails on 25_000 * 22_000.\n     * So the solution is just to scale down an image once, as if it were of a smaller size.\n     * The current solution should be replaced with createImageBitmap() scaling\n     * once it's supported by Firefox and Safari. Now images up to 20K * 20K pixels are supported:\n     * the image is downscaled to half being divided into 4 quarters, using 4 canvases.\n     * Theoretically, the image can be split into more than 4 equal blocks, so that even bigger ones are processed.\n     * But there is a doubt the library will be able to decode bigger images,\n     * since it sometimes fails with an out of memory error on a 16K * 12K image.\n     */\n    resizeImageIfRequired = memoize((image) => {\n        const dimensionThreshold = 10000;\n        const { width, height } = image;\n        const maxDimension = Math.max(width, height);\n\n        if (maxDimension <= dimensionThreshold) return image;\n\n        const createTempCanvas = (image, x, y, width, height) => {\n            const canvas = document.createElement('canvas');\n            canvas.width = width;\n            canvas.height = height;\n            const ctx = canvas.getContext('2d', { alpha: false });\n            ctx.putImageData(image, -x, -y, x, y, width, height);\n            return canvas;\n        };\n\n        const outputCanvas = document.createElement('canvas');\n        const outputCtx = outputCanvas.getContext('2d', { alpha: false });\n\n        const halfWidth = Math.floor(width / 2);\n        const halfHeight = Math.floor(height / 2);\n\n        const width1 = Math.floor(halfWidth / 2);\n        const width2 = Math.floor((width - halfWidth) / 2);\n        const height1 = Math.floor(halfHeight / 2);\n        const height2 = Math.floor((height - halfHeight) / 2);\n\n        outputCanvas.width = width1 + width2;\n        outputCanvas.height = height1 + height2;\n\n        const drawImage = (x, y, width, height, destX, destY, destWidth, destHeight) => outputCtx.drawImage(\n            createTempCanvas(image, x, y, width, height),\n            0, 0, width, height,\n            destX, destY, destWidth, destHeight,\n        );\n\n        drawImage(\n            0, 0, halfWidth, halfHeight,\n            0, 0, width1, height1,\n        );\n        drawImage(\n            halfWidth, 0, width - halfWidth, halfHeight,\n            width1, 0, width2, height1,\n        );\n        drawImage(\n            0, halfHeight, halfWidth, height - halfHeight,\n            0, height1, width1, height2,\n        );\n        drawImage(\n            halfWidth, halfHeight, width - halfWidth, height - halfHeight,\n            width1, height1, width2, height2,\n        );\n\n        return outputCtx.getImageData(0, 0, outputCanvas.width, outputCanvas.height);\n    });\n\n    render() {\n        const imageData = this.props.imageData && this.resizeImageIfRequired(this.props.imageData);\n\n        const initialWidth = this.props.imageWidth || imageData.width;\n        const initialHeight = this.props.imageHeight || imageData.height;\n\n        const scaleFactor = Constants.DEFAULT_DPI / this.props.imageDpi * this.props.userScale;\n\n        let width, height;\n        let scaledWidth = width = Math.floor(initialWidth * scaleFactor);\n        let scaledHeight = height = Math.floor(initialHeight * scaleFactor);\n\n        if (this.props.rotation === 90 || this.props.rotation === 270) {\n            [width, height] = [height, width];\n        }\n\n        return (\n            <Root\n                style={{\n                    width: width + \"px\",\n                    height: height + \"px\"\n                }}\n                $rotation={this.props.rotation}\n                ref={this.props.outerRef}\n            >\n                <div>\n                    {imageData ?\n                        <CanvasImage\n                            imageData={imageData}\n                            imageDpi={this.props.imageDpi}\n                            userScale={this.props.userScale}\n                            key={this.props.currentPageNumber}\n                        /> :\n                        this.props.imageUrl ?\n                            <img\n                                src={this.props.imageUrl}\n                                style={{\n                                    width: scaledWidth + \"px\",\n                                    height: scaledHeight + \"px\"\n                                }}\n                                alt=\"djvu_page\"\n                            />\n                            :\n                            <LoadingPhrase\n                                style={{\n                                    fontSize: Math.min(scaledWidth * 0.1, scaledHeight * 0.1) + 'px',\n                                    whiteSpace: 'nowrap',\n                                }}\n                            />\n                    }\n                    {this.props.textZones ?\n                        <TextLayer\n                            textZones={this.props.textZones}\n                            imageHeight={initialHeight}\n                            imageWidth={initialWidth}\n                            imageDpi={this.props.imageDpi}\n                            userScale={this.props.userScale}\n                        /> : null}\n                </div>\n            </Root>\n        );\n    }\n}\n\nexport default ComplexImage;"
  },
  {
    "path": "viewer/src/components/ImageBlock/ImageBlock.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect, useSelector } from 'react-redux';\nimport memoize from 'memoize-one';\n\nimport Actions from '../../actions/actions';\nimport { get } from '../../reducers';\nimport Constants from '../../constants';\nimport ComplexImage from './ComplexImage';\nimport VirtualList from './VirtualList';\nimport { createDeferredHandler } from '../helpers';\nimport styled, { css } from 'styled-components';\n\nconst grabbingCursor = css`\n    &.djvujs_grabbing {\n        cursor: grabbing;\n    }\n`;\n\nconst grabCursor = css`\n    cursor: grab;\n\n    * {\n        user-select: none;\n    }\n`;\n\nconst sharedStyle = css`\n    touch-action: pan-x pan-y;\n    ${p => p.$grab ? grabCursor : ''};\n    ${grabbingCursor};\n`;\n\nconst SinglePageModeRoot = styled.div`\n    flex: 1 1 auto;\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n    height: 100%;\n    overflow: auto;\n    box-sizing: border-box;\n    padding-bottom: 30px;\n    ${sharedStyle};\n`;\n\nconst ContinuousScrollItem = styled.div`\n    box-sizing: border-box;\n    min-width: 100%;\n    padding: 2px 0;\n    transform: translate3d(0, 0, 0); // just for performance optimization when continuous mode is enabled\n    display: flex;\n    justify-content: center;\n`;\n\nfunction resetEventListener(node, event, handler, options = undefined) {\n    node.removeEventListener(event, handler, options);\n    node.addEventListener(event, handler, options);\n}\n\n/**\n * CanvasImage wrapper. Handles user scaling of the image and grabbing.\n */\nclass ImageBlock extends React.Component {\n\n    static propTypes = {\n        imageData: PropTypes.object,\n        imageDpi: PropTypes.number,\n        userScale: PropTypes.number\n    };\n\n    initialGrabbingState = null;\n    pointerEventCache = {};\n    lastPointerDiff = -1;\n\n    getSnapshotBeforeUpdate() {\n        if (!this.wrapper) {\n            return null;\n        }\n        let horizontalRatio = null;\n        if (this.wrapper.scrollWidth > this.wrapper.clientWidth) {\n            horizontalRatio = (this.wrapper.scrollLeft + this.wrapper.clientWidth / 2) / this.wrapper.scrollWidth;\n        }\n        let verticalRatio = null;\n        if (this.wrapper.scrollHeight > this.wrapper.clientHeight && this.wrapper.scrollTop) {\n            // the position of the central point of a scroll bar\n            verticalRatio = (this.wrapper.scrollTop + this.wrapper.clientHeight / 2) / this.wrapper.scrollHeight;\n        }\n\n        return { horizontalRatio, verticalRatio };\n    }\n\n    scrollCurrentPageIntoViewIfRequired(prevProps) {\n        if (\n            this.props.viewMode === Constants.CONTINUOUS_SCROLL_MODE\n            && this.props.shouldScrollToPage\n            && (prevProps.currentPageNumber !== this.props.currentPageNumber || prevProps.viewMode !== Constants.CONTINUOUS_SCROLL_MODE)\n            && this.virtualList\n        ) {\n\n            const { pageCountInRow, firstRowPageCount, currentPageNumber } = this.props;\n            const index = Math.max(0,\n                Math.ceil((currentPageNumber - firstRowPageCount) / pageCountInRow)\n                + (currentPageNumber > firstRowPageCount ? 1 : 0) - 1,\n            );\n\n            if (this.virtualList.isItemVisible(index)) return;\n            this.virtualList.scrollToItem(index);\n        }\n    }\n\n    componentDidUpdate(prevProps, prevState, snapshot) {\n        if (!this.wrapper) {\n            return;\n        }\n        var widthDiff = this.wrapper.scrollWidth - this.wrapper.clientWidth;\n        if (widthDiff > 0) {\n            this.wrapper.scrollLeft = snapshot.horizontalRatio ? (snapshot.horizontalRatio * this.wrapper.scrollWidth - this.wrapper.clientWidth / 2) : (widthDiff / 2);\n        }\n        if (prevProps.imageData !== this.props.imageData) { // when a page was changed\n            if (this.scrollToBottomOnUpdate) {\n                this.wrapper.scrollTop = this.wrapper.scrollHeight;\n                this.scrollToBottomOnUpdate = false;\n            } else {\n                this.wrapper.scrollTop = 0;\n            }\n        } else {\n            var heightDiff = this.wrapper.scrollHeight - this.wrapper.clientHeight;\n            if (heightDiff > 0 && this.wrapper.scrollTop) {\n                this.wrapper.scrollTop = snapshot.verticalRatio ? (snapshot.verticalRatio * this.wrapper.scrollHeight - this.wrapper.clientHeight / 2) : (heightDiff / 2);\n            }\n        }\n\n        this.scrollCurrentPageIntoViewIfRequired(prevProps);\n\n        this.complexImage && (this.complexImage.style.opacity = 1); // show the content after the scroll bars were adjusted\n    }\n\n    enableScaleHandler = e => {\n        if (this.isScaleHandlerEnabled || e.key !== 'Control' || !this.wrapper) return;\n        this.wrapper.addEventListener('wheel', this.wheelScaleHandler);\n        this.isScaleHandlerEnabled = true;\n    }\n\n    disableScaleHandler = e => {\n        if (!this.isScaleHandlerEnabled || e.key !== 'Control') return;\n        this.wrapper.removeEventListener('wheel', this.wheelScaleHandler);\n        this.isScaleHandlerEnabled = false;\n    }\n\n    componentDidMount() {\n        window.addEventListener('keydown', this.enableScaleHandler);\n        window.addEventListener('keyup', this.disableScaleHandler);\n\n        this.componentDidUpdate({}, {}, {});\n    }\n\n    componentWillUnmount() {\n        window.removeEventListener('keydown', this.enableScaleHandler);\n        window.removeEventListener('keyup', this.disableScaleHandler);\n    }\n\n    wheelScaleHandler = e => {\n        if (!e.ctrlKey) return;\n        e.preventDefault();\n        const scaleDelta = 0.05 * (-Math.sign(e.deltaY));\n        const newScale = this.props.userScale + scaleDelta;\n        this.props.dispatch(Actions.setUserScaleAction(newScale));\n    }\n\n    singlePageWheelHandler = (e) => {\n        if (e.ctrlKey) return;\n\n        if (!this.props.changePageOnScroll) return;\n\n        // scrollTimeStamp is needed to ignore scroll events following immediately after the event which\n        // caused the page change.\n        if (this.scrollTimeStamp) {\n            if (e.timeStamp - this.scrollTimeStamp < 100) {\n                e.preventDefault();\n                this.scrollTimeStamp = e.timeStamp;\n                return;\n            } else {\n                this.scrollTimeStamp = null;\n            }\n        }\n        if (!e.ctrlKey && e.cancelable) {\n            if ((this.wrapper.scrollHeight === this.wrapper.scrollTop + this.wrapper.clientHeight) && e.deltaY > 0) {\n                e.preventDefault();\n                this.scrollTimeStamp = e.timeStamp;\n                this.props.dispatch(Actions.goToNextPageAction());\n            } else if (this.wrapper.scrollTop === 0 && e.deltaY < 0) {\n                e.preventDefault();\n                this.scrollTimeStamp = e.timeStamp;\n                this.scrollToBottomOnUpdate = true;\n                this.props.dispatch(Actions.goToPreviousPageAction());\n            }\n        }\n    };\n\n    handleMoving = (e) => {\n        e.preventDefault();\n        if (!this.initialGrabbingState) {\n            return;\n        }\n        const { clientX, clientY, scrollLeft, scrollTop } = this.initialGrabbingState\n        const deltaX = clientX - e.clientX;\n        const deltaY = clientY - e.clientY;\n\n        this.wrapper.scrollLeft = scrollLeft + deltaX;\n        this.wrapper.scrollTop = scrollTop + deltaY;\n    };\n\n    startMoving = (e) => {\n        if (!this.isGrabMode()) {\n            return;\n        }\n        e.preventDefault();\n        this.initialGrabbingState = {\n            clientX: e.clientX,\n            clientY: e.clientY,\n            scrollLeft: this.wrapper.scrollLeft,\n            scrollTop: this.wrapper.scrollTop\n        };\n        this.wrapper.classList.add('djvujs_grabbing');\n    };\n\n    finishMoving = (e) => {\n        if (!this.isGrabMode()) {\n            return;\n        }\n        e.preventDefault();\n        this.initialGrabbingState = null;\n        this.wrapper.classList.remove('djvujs_grabbing');\n    };\n\n    onPointerDown = (e) => {\n        if (e.pointerType === 'mouse') {\n            if (this.isGrabMode()) {\n                this.wrapper.addEventListener('pointermove', this.onPointerMove);\n                this.startMoving(e);\n            }\n        } else {\n            this.wrapper.addEventListener('pointermove', this.onPointerMove)\n            this.pointerEventCache[e.pointerId] = e;\n        }\n    };\n\n    onPointerMove = (e) => {\n        if (e.pointerType === 'mouse') {\n            return this.handleMoving(e);\n        }\n\n        this.pointerEventCache[e.pointerId] = e;\n\n        const events = Object.values(this.pointerEventCache);\n        if (events.length === 2) {\n            e.preventDefault();\n            e.stopPropagation(); // isn't needed for mobile chrome, but maybe for other browsers\n\n            const pointerDiff = Math.hypot(events[0].clientX - events[1].clientX, events[0].clientY - events[1].clientY);\n            if (this.lastPointerDiff > 0) {\n                const blockSize = Math.hypot(this.wrapper.offsetWidth, this.wrapper.offsetHeight);\n                this.props.dispatch(Actions.setUserScaleAction(\n                    this.props.userScale + (pointerDiff - this.lastPointerDiff) / blockSize\n                ));\n            }\n\n            this.lastPointerDiff = pointerDiff;\n        }\n    }\n\n    onPointerUp = (e) => {\n        if (e.pointerType === 'mouse') {\n            this.finishMoving(e);\n        }\n\n        delete this.pointerEventCache[e.pointerId];\n        const events = Object.values(this.pointerEventCache);\n        if (events.length < 2) {\n            this.lastPointerDiff = -1;\n        }\n\n        if (events.length === 0) {\n            this.wrapper.removeEventListener('pointermove', this.onPointerMove);\n        }\n    }\n\n\n    wrapperRef = (node) => {\n        this.wrapper = node;\n        if (!node) return;\n\n        resetEventListener(node, 'pointerdown', this.onPointerDown);\n        resetEventListener(node, 'pointerup', this.onPointerUp);\n        resetEventListener(node, 'pointerleave', this.onPointerUp);\n        resetEventListener(node, 'pointercancel', this.onPointerUp);\n\n        if (this.props.viewMode === Constants.CONTINUOUS_SCROLL_MODE) {\n            resetEventListener(node, 'scroll', this.onScroll, { passive: true });\n        } else {\n            resetEventListener(node, 'wheel', this.singlePageWheelHandler);\n        }\n    }\n\n    isGrabMode() {\n        return this.props.cursorMode === Constants.GRAB_CURSOR_MODE;\n    }\n\n    complexImageRef = node => this.complexImage = node;\n\n    setNewPageNumber(pageNumber) {\n        if (pageNumber && pageNumber !== this.props.currentPageNumber) {\n            this.props.dispatch(Actions.setNewPageNumberAction(pageNumber));\n        }\n    }\n\n    onScroll = createDeferredHandler(() => {\n        const { pageCountInRow, firstRowPageCount, currentPageNumber } = this.props;\n        const index = this.virtualList.getCurrentVisibleItemIndex();\n        const pageNumber = index * pageCountInRow + (index ? -pageCountInRow + firstRowPageCount : 0) + 1;\n        if (!(\n            currentPageNumber >= pageNumber\n            && currentPageNumber < pageNumber + (pageNumber === 1 ? firstRowPageCount : pageCountInRow)\n        )) {\n            this.setNewPageNumber(pageNumber);\n        }\n    });\n\n    getItemSizes = memoize((pageList, userScale, rotation, pageCountInRow, firstRowPageCount) => {\n        const sizes = [];\n        const isRotated = rotation === 90 || rotation === 270;\n\n        const processPageRow = (from, to) => {\n            let max = 0;\n            for (let i = from; i < to; i++) {\n                const page = pageList[i];\n                const scaleFactor = Constants.DEFAULT_DPI / page.dpi * userScale;\n                const size = Math.floor((isRotated ? page.width : page.height) * scaleFactor) + 6;\n                if (size > max) max = size;\n            }\n            return max;\n        };\n\n        sizes.push(processPageRow(0, Math.min(pageCountInRow, pageList.length)));\n        for (let i = Math.min(firstRowPageCount, pageList.length); i < pageList.length; i += pageCountInRow) {\n            sizes.push(processPageRow(i, Math.min(i + pageCountInRow, pageList.length)));\n        }\n\n        return sizes;\n    });\n\n    virtualListRef = component => this.virtualList = component;\n\n    itemRenderer = React.memo(({ index, style }) => {\n        const pageCountInRow = useSelector(get.pageCountInRow);\n        const firstRowPageCount = useSelector(get.firstRowPageCount);\n        const from = index * pageCountInRow + (index ? -pageCountInRow + firstRowPageCount : 0);\n        const to = from + (index === 0 ? firstRowPageCount : pageCountInRow);\n        const allPages = useSelector(get.pageList);\n        const pagesInCurrentRow = allPages.slice(from, to);\n\n        return (\n            <ContinuousScrollItem style={style} key={index}>\n                {pagesInCurrentRow.map((pageData, i) => (\n                    <ComplexImage\n                        key={i}\n                        imageUrl={pageData.url}\n                        imageDpi={pageData.dpi}\n                        imageWidth={pageData.width}\n                        imageHeight={pageData.height}\n                        userScale={this.props.userScale}\n                        rotation={this.props.rotation}\n                        textZones={pageData.textZones}\n                    />\n                ))}\n            </ContinuousScrollItem>\n        )\n    });\n\n    render() {\n        const isGrabMode = this.props.cursorMode === Constants.GRAB_CURSOR_MODE;\n        const {\n            documentId,\n            pageSizeList,\n            userScale,\n            rotation,\n            viewMode,\n            imageData,\n            pageCountInRow,\n            firstRowPageCount\n        } = this.props;\n\n        return (viewMode === Constants.CONTINUOUS_SCROLL_MODE && pageSizeList.length) ?\n            <VirtualList\n                ref={this.virtualListRef}\n                outerRef={this.wrapperRef}\n                $grab={isGrabMode}\n                css={sharedStyle}\n                itemSizes={this.getItemSizes(pageSizeList, userScale, rotation, pageCountInRow, firstRowPageCount)}\n                //data={pageList}\n                itemRenderer={this.itemRenderer}\n                key={documentId}\n            />\n            : imageData ?\n                <SinglePageModeRoot\n                    $grab={isGrabMode}\n                    ref={this.wrapperRef}\n                >\n                    <div\n                        ref={this.complexImageRef}\n                        css={`padding: 1em; margin: auto`}\n                        style={{ opacity: 0 }} // is changed in the ComponentDidUpdate\n                    >\n                        <ComplexImage {...this.props} />\n                    </div>\n                </SinglePageModeRoot> : null;\n    }\n}\n\nexport default connect(\n    state => ({\n        documentId: get.documentId(state),\n        currentPageNumber: get.currentPageNumber(state),\n        shouldScrollToPage: get.shouldScrollToPage(state),\n        viewMode: get.viewMode(state),\n        pageCountInRow: get.pageCountInRow(state),\n        firstRowPageCount: get.firstRowPageCount(state),\n        //pageList: get.pageList(state),\n        pageSizeList: get.pageSizeList(state),\n        imageData: get.imageData(state),\n        imageDpi: get.imageDpi(state),\n        userScale: get.userScale(state),\n        textZones: get.textZones(state),\n        cursorMode: get.cursorMode(state),\n        rotation: get.pageRotation(state),\n        changePageOnScroll: get.uiOptions(state).changePageOnScroll,\n    })\n)(ImageBlock);"
  },
  {
    "path": "viewer/src/components/ImageBlock/TextLayer.jsx",
    "content": "import React, { useEffect, useRef } from 'react';\nimport Constants from '../../constants';\nimport styled from 'styled-components';\n\nconst Root = styled.div`\n    overflow: hidden;\n    position: absolute;\n    top: 0;\n    left: 0;\n\n    & > div:first-child {\n        top: 0;\n        left: 0;\n        position: absolute;\n    }\n`;\n\nconst TextZone = styled.div`\n    line-height: initial;\n    color: rgba(0, 0, 0, 0);\n    text-align-last: justify;\n    text-align: justify;\n    position: absolute;\n    box-sizing: border-box;\n    font-family: 'Times New Roman', Garamond, Times, serif;\n\n    span {\n        white-space: pre;\n    }\n`;\n\nconst TextLayer = ({ textZones, imageHeight, imageWidth, userScale, imageDpi }) => {\n    const wrapper = useRef(null);\n\n    useEffect(() => {\n        if (!wrapper.current) return;\n\n        for (const textZone of wrapper.current.children) {\n            const span = textZone.firstChild;\n            if (span.offsetWidth < textZone.offsetWidth) {\n                const letterSpacing = (textZone.offsetWidth - span.offsetWidth) / span.innerText.length;\n                span.style.letterSpacing = letterSpacing + 'px';\n            }\n        }\n    }, [textZones, wrapper.current]);\n\n    if (!textZones) return null;\n\n    const scaleFactor = Constants.DEFAULT_DPI / imageDpi * userScale;\n    const scaledWidth = Math.floor(imageWidth * scaleFactor);\n    const scaledHeight = Math.floor(imageHeight * scaleFactor);\n\n    return (\n        <Root\n            style={{\n                width: scaledWidth + 'px',\n                height: scaledHeight + 'px'\n            }}\n        >\n            <div\n                style={{\n                    left: (-(imageWidth - scaledWidth) / 2) + 'px',\n                    top: (-(imageHeight - scaledHeight) / 2) + 'px',\n                    width: imageWidth + 'px',\n                    height: imageHeight + 'px',\n                    transform: `scale(${scaleFactor})`\n                }}\n                ref={wrapper}\n            >\n                {textZones.map((zone, i) => (\n                    <TextZone\n                        key={i}\n                        style={{\n                            left: zone.x + 'px',\n                            bottom: zone.y + 'px',\n                            width: zone.width + 'px',\n                            height: zone.height + 'px',\n                            fontSize: zone.height * 0.9 + 'px'\n                        }}\n                    >\n                        <span>{zone.text}</span>\n                    </TextZone>\n                ))}\n            </div>\n        </Root>\n    );\n}\n\nexport default TextLayer;"
  },
  {
    "path": "viewer/src/components/ImageBlock/VirtualList.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport memoize from 'memoize-one';\nimport { createDeferredHandler } from '../helpers';\nimport styled from 'styled-components';\n\nconst Root = styled.div`\n    overflow: auto;\n    padding-bottom: 30px;\n    width: 100%;\n    height: 100%;\n    box-sizing: border-box;\n    transform: translateZ(0); // removes lags when the page is changed while scrolling\n\n    & > div {\n        min-width: 100%;\n        position: relative;\n    }\n`;\n\n/**\n * This component doesn't reset its state when the document change, so it should be recreated\n * (a unique key must be provided for each new document in the parent component)\n */\nexport default class VirtualList extends React.PureComponent {\n    static propTypes = {\n        itemSizes: PropTypes.array.isRequired,\n        itemRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,\n        renderingRadius: PropTypes.number,\n        outerRef: PropTypes.func,\n        className: PropTypes.string,\n        //data: PropTypes.any,\n        resizeKey: PropTypes.any,\n    }\n\n    static defaultProps = {\n        renderingRadius: 3,\n        className: '',\n    }\n\n    state = {\n        startIndex: 0,\n        stopIndex: -1,\n    }\n\n    get viewportHeight() {\n        return this.topNode.getBoundingClientRect().height;\n    }\n\n    componentDidMount() {\n        this.topNode.addEventListener('scroll', this.onScroll, { passive: true });\n        this.updateRenderedItems();\n    }\n\n    componentWillUnmount() {\n        this.topNode.removeEventListener('scroll', this.onScroll, { passive: true });\n    }\n\n    _prepareSpacialDataAndStyles = memoize(itemSizes => {\n        const itemTops = new Array(itemSizes.length);\n        const itemStyles = new Array(itemSizes.length);\n        const contentHeight = itemSizes.reduce((sum, value, i) => {\n            itemTops[i] = sum;\n            itemStyles[i] = { position: 'absolute', top: sum + 'px' };\n            sum += value;\n            return sum;\n        }, 0);\n        return { itemTops, itemStyles, contentHeight };\n    });\n\n    get itemTops() {\n        return this._prepareSpacialDataAndStyles(this.props.itemSizes).itemTops;\n    }\n\n    get contentHeight() {\n        return this._prepareSpacialDataAndStyles(this.props.itemSizes).contentHeight;\n    }\n\n    get itemStyles() {\n        return this._prepareSpacialDataAndStyles(this.props.itemSizes).itemStyles;\n    }\n\n    findItemIndexByScrollTop(scrollTop) {\n        if (scrollTop <= 0) {\n            return 0;\n        }\n        let left = 0;\n        let right = this.itemTops.length - 1;\n        let limit = 100;\n        let count = 0;\n        while (true) {\n            if (++count > limit) {\n                console.warn(\"Error in binary search\");\n                return left;\n            }\n            if (right === left) {\n                return right;\n            }\n            const index = ((right - left) >> 1) + left;\n            if (this.itemTops[index] <= scrollTop && this.itemTops[index + 1] > scrollTop) {\n                return index;\n            } else if (this.itemTops[index] < scrollTop) {\n                left = index + 1;\n            } else {\n                right = index - 1;\n            }\n        }\n    }\n\n    updateRenderedItems = (viewportHeight = this.viewportHeight) => {\n        const scrollTop = this.topNode.scrollTop;\n        const startIndex = this.findItemIndexByScrollTop(scrollTop - this.props.renderingRadius * viewportHeight);\n\n        let stopIndex = startIndex;\n        const stopThreshold = scrollTop + (this.props.renderingRadius + 1) * viewportHeight;\n        const maxIndex = this.itemTops.length - 1;\n        for (; stopIndex < maxIndex; stopIndex++) {\n            if (this.itemTops[stopIndex] >= stopThreshold) {\n                break;\n            }\n        }\n\n        this.setState({ startIndex, stopIndex });\n    }\n\n    onScroll = createDeferredHandler(() => this.updateRenderedItems(), 300, 600);\n\n    renderItems() {\n        const { startIndex, stopIndex } = this.state;\n        const items = new Array(stopIndex - startIndex + 1);\n        const Item = this.props.itemRenderer;\n\n        for (let i = startIndex; i <= stopIndex; i++) {\n            items[i - startIndex] = <Item\n                index={i}\n                style={this.itemStyles[i]}\n                //data={this.props.data ? this.props.data[i] : null}\n                key={i}\n            />;\n        }\n        return items;\n    }\n\n    ref = node => {\n        this.topNode = node;\n        this.props.outerRef && this.props.outerRef(node);\n    }\n\n    /**\n     * A page is considered visible, if there is at least 25% of it is shown and it's at the top of the viewport (actual when there are many small pages, or a scale is small)\n     * or if it takes more than 50% if the viewport (actual when there are bigger pages, the most common situation)\n     */\n    isItemVisible(index) {\n        const scrollTop = this.topNode.scrollTop;\n        const bottom = this.itemTops[index] + this.props.itemSizes[index];\n        const viewportHeight = this.viewportHeight;\n        return (\n            (bottom - scrollTop >= 0.25 * this.props.itemSizes[index] && scrollTop >= this.itemTops[index])\n            || (scrollTop >= this.itemTops[index] && (bottom - scrollTop) >= viewportHeight * 0.5)\n            || (scrollTop < this.itemTops[index] && (this.itemTops[index] - scrollTop) < viewportHeight * 0.5)\n        );\n    }\n\n    getCurrentVisibleItemIndex() {\n        const index = this.findItemIndexByScrollTop(this.topNode.scrollTop);\n        if (!this.isItemVisible(index) && (index + 1 < this.itemTops.length)) {\n            return index + 1;\n        } else {\n            return index;\n        }\n    }\n\n    scrollToItem(index) {\n        this.topNode.scrollTop = this.itemTops[index];\n    }\n\n    getHeightStyle = memoize(contentHeight => ({ height: contentHeight + 'px' }));\n\n    render() {\n        const itemSizes = this.props.itemSizes;\n\n        return (\n            <Root\n                ref={this.ref}\n                className={this.props.className}\n            >\n                {itemSizes && itemSizes.length ?\n                    <div style={this.getHeightStyle(this.contentHeight)}>\n                        {this.renderItems()}\n                    </div>\n                    : null\n                }\n            </Root>\n        );\n    }\n}"
  },
  {
    "path": "viewer/src/components/InitialScreen/FileZone.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\nimport { FaUpload } from \"react-icons/fa\";\nimport { TranslationContext } from '../Translation';\nimport Actions from '../../actions/actions';\nimport styled, { css, keyframes } from 'styled-components';\n\nconst FileIcon = styled(FaUpload)`\n    font-size: 1.5em;\n`;\n\nconst shake = keyframes`\n    from {transform: rotateY(0deg)}\n    25% {transform: rotateY(5deg)}\n    75% {transform: rotateY(-5deg)}\n    to {transform: rotateY(0deg)}\n`;\n\nconst dragOverStyle = css`\n    animation: ${shake} 1s infinite linear;\n    opacity: 0.8;\n    border-color: var(--highlight-color);\n`;\n\nconst Root = styled.div`\n    border: 0.1em dashed var(--border-color);\n    background: var(--alternative-background-color);\n    padding: 0.5em;\n    max-width: 20em;\n    min-height: 5em;\n    margin: auto;\n    border-radius: 0.5em;\n    cursor: pointer;\n\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n\n    &:hover {\n        ${FileIcon} {\n            transform: scale(1.1);\n        }\n    }\n\n    ${p => p.$dragOver ? dragOverStyle : ''};\n`;\n\nclass FileZone extends React.Component {\n\n    static propTypes = {\n        createNewDocument: PropTypes.func.isRequired\n    };\n\n    static contextType = TranslationContext;\n\n    state = {\n        isDragOver: false\n    };\n\n    onChange = (e) => {\n        if (!e.target.files.length) {\n            return;\n        }\n        this.processFile(e.target.files[0]);\n    };\n\n    processFile(file) {\n        var fr = new FileReader();\n        fr.readAsArrayBuffer(file);\n        fr.onload = () => {\n            this.props.createNewDocument(fr.result, file.name);\n        }\n    }\n\n    onClick = (e) => {\n        this.input && this.input.click();\n    };\n\n    checkDrag = (e) => {\n        if (e.dataTransfer.items.length === 1 && e.dataTransfer.items[0].kind === 'file') {\n            e.preventDefault();\n            this.setState({ isDragOver: true })\n        }\n    };\n\n    onDragLeave = (e) => {\n        this.setState({ isDragOver: false });\n    };\n\n    onDrop = (e) => {\n        this.setState({ isDragOver: false });\n        if (e.dataTransfer.items[0].kind === 'file') {\n            e.preventDefault();\n            this.processFile(e.dataTransfer.items[0].getAsFile());\n        }\n    };\n\n    render() {\n        const t = this.context;\n\n        return (\n            <Root\n                $dragOver={this.state.isDragOver}\n                onClick={this.onClick}\n                title={t(\"Open another .djvu file\")}\n                onDragEnter={this.checkDrag}\n                onDragOver={this.checkDrag}\n                onDragLeave={this.onDragLeave}\n                onDrop={this.onDrop}\n            >\n                <FileIcon />\n                <span>{t('Drag & Drop a file here or click to choose manually')}</span>\n                <input\n                    style={{ display: 'none' }}\n                    type=\"file\"\n                    onChange={this.onChange}\n                    accept=\".djvu, .djv\"\n                    ref={node => this.input = node}\n                />\n            </Root>\n        );\n    }\n}\n\nexport default connect(null,\n    {\n        createNewDocument: Actions.createDocumentFromArrayBufferAction,\n    }\n)(FileZone);"
  },
  {
    "path": "viewer/src/components/InitialScreen/InitialScreen.jsx",
    "content": "import React from 'react';\n\nimport HelpButton from '../misc/HelpButton';\nimport FileZone from './FileZone';\nimport DjVu from '../../DjVu';\nimport { inExtension } from '../../utils';\nimport LinkBlock from './LinkBlock';\nimport { useTranslation } from '../Translation';\nimport { LanguagePanel } from \"../Language/LanguagePanel\";\nimport styled from 'styled-components';\nimport ThemeSwitcher from './ThemeSwitcher';\nimport OptionsButton from \"../misc/OptionsButton\";\nimport FullPageViewButton from \"../misc/FullPageViewButton\";\nimport { useAppContext } from \"../AppContext\";\nimport LanguageSelector from \"../Language/LanguageSelector\";\nimport FullscreenButton from \"../misc/FullscreenButton\";\n\nconst Root = styled.div`\n    font-size: ${p => p.theme.isMobile ? 1.5 : 2}em;\n    text-align: center;\n    flex: 1 1 auto;\n    width: 100%;\n    height: 100%;\n    overflow: auto;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n`;\n\nconst InfoBlock = styled.div`\n    width: max-content;\n    margin: 0 auto 1em auto;\n    text-align: left;\n    font-size: 0.8em;\n\n    svg {\n        font-size: 1.5em;\n    }\n\n    div {\n        display: flex;\n        align-items: center;\n        margin-bottom: 0.25em;\n    }\n`;\n\nconst Footer = styled.div`\n    width: 100%;\n    display: flex;\n    justify-content: flex-end;\n    contain: layout;\n\n    & > * {\n        margin-left: 0.5em;\n    }\n`;\n\nexport default () => {\n    const t = useTranslation();\n    const { isMobile } = useAppContext();\n\n    return (\n        <Root>\n            {isMobile ? <LanguageSelector /> : <LanguagePanel />}\n            <ThemeSwitcher />\n            <div css={`margin: auto;`}>\n\n                <div css={`text-align: center; font-size: 2em`}>\n                    {`DjVu.js Viewer v.${DjVu.Viewer.VERSION}`}\n                </div>\n                <div css={`font-style: italic; margin-top: 0.5em; margin-bottom: 1em; font-size: 0.8em`}>\n                    {`(${t('powered with')} DjVu.js v.${DjVu.VERSION})`}\n                </div>\n\n                <InfoBlock>\n                    <div>{t('#optionsButton - see the available options', {\n                        '#optionsButton': <OptionsButton />\n                    })}</div>\n                    <div>{t('#helpButton - learn more about the app', { '#helpButton': <HelpButton /> })}</div>\n                </InfoBlock>\n                {inExtension ? <LinkBlock /> : null}\n                <FileZone />\n            </div>\n            <Footer>\n                {(document.fullscreenEnabled || document.webkitFullscreenEnabled) ?\n                    <FullscreenButton css={`margin-right: 0.5em;`} /> : null}\n                <FullPageViewButton />\n            </Footer>\n        </Root>\n    );\n};"
  },
  {
    "path": "viewer/src/components/InitialScreen/LinkBlock.jsx",
    "content": "import React from 'react';\n\nimport styled from 'styled-components';\nimport { useDispatch } from 'react-redux';\nimport { ActionTypes } from '../../constants';\nimport { useTranslation } from \"../Translation\";\nimport { styledInput } from '../cssMixins';\n\nconst LinkBlockRoot = styled.form`\n    max-width: 20em;\n    display: flex;\n    justify-content: center;\n    margin: 1em auto;\n\n    input {\n        ${styledInput};\n        flex: 1 1 auto;\n        height: 2em;\n        font-style: italic;\n    }\n\n    button {\n        color: var(--color);\n        margin-left: 1em;\n        border-radius: 0.5em;\n        background: none;\n        border: 1px solid var(--border-color);\n        cursor: pointer;\n\n        &:hover {\n            background: var(--alternative-background-color);\n        }\n    }\n`;\n\nconst LinkBlock = () => {\n    const [url, setUrl] = React.useState('');\n    const dispatch = useDispatch();\n    const t = useTranslation();\n\n    return (\n        <LinkBlockRoot onSubmit={(e) => {\n            e.preventDefault();\n            const trimmedUrl = url.trim();\n            if (/((^https?:\\/\\/)|(^data:)).+/.test(trimmedUrl)) {\n                dispatch({\n                    type: ActionTypes.LOAD_DOCUMENT_BY_URL,\n                    url: trimmedUrl,\n                });\n            } else {\n                alert(t('Enter a valid URL (it should start with \"http(s)://\" | \"data:\")'));\n            }\n        }}>\n            <input\n                value={url}\n                placeholder={t(\"Paste a URL to a djvu file here\")}\n                onChange={e => setUrl(e.target.value)}\n            />\n            <button type=\"submit\">{t(\"Open URL\")}</button>\n        </LinkBlockRoot>\n    );\n};\n\nexport default LinkBlock;"
  },
  {
    "path": "viewer/src/components/InitialScreen/ThemeSwitcher.jsx",
    "content": "import React from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport styled, { css } from 'styled-components';\nimport { get } from '../../reducers';\nimport { FaRegSun, FaRegMoon } from \"react-icons/fa\";\nimport { ActionTypes } from '../../constants/index';\n\nconst Root = styled.span`\n    margin-top: 1.5em;\n\n    svg {\n        margin: 0 0.5em;\n        cursor: pointer;\n    }\n`;\n\nconst activeStyle = css`\n    transform: scale(1.5);\n    color: var(--highlight-color);\n`;\n\nexport default () => {\n    const { theme } = useSelector(get.options);\n    const dispatch = useDispatch();\n    const createClickHandler = theme => () => dispatch({ type: ActionTypes.UPDATE_OPTIONS, payload: { theme } });\n\n    return (\n        <Root>\n            <FaRegSun\n                css={theme === 'light' ? activeStyle : null}\n                onClick={createClickHandler('light')}\n                data-djvujs-id={'light_theme_button'}\n                data-djvujs-class={theme === 'light' ? 'active' : null}\n            />\n            <FaRegMoon\n                css={theme === 'dark' ? activeStyle : null}\n                onClick={createClickHandler('dark')}\n                data-djvujs-id={'dark_theme_button'}\n                data-djvujs-class={theme === 'dark' ? 'active' : null}\n            />\n        </Root>\n    )\n};"
  },
  {
    "path": "viewer/src/components/Language/AddLanguageButton.jsx",
    "content": "import React from \"react\";\nimport Constants from \"../../constants/Constants\";\nimport { IoAddCircleOutline } from \"react-icons/io5\";\nimport { useTranslation } from \"../Translation\";\n\nexport default ({ className }) => {\n    const t = useTranslation();\n\n    return (\n        <a\n            className={className}\n            href={Constants.TRANSLATION_PAGE_URL}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            title={t(\"Add more\")}\n            css={`\n                color: var(--color) !important;\n                display: inline-block;\n                height: 1em;\n                width: 1em;\n\n                :hover {\n                    transform: scale(1.2);\n                }\n            `}\n        >\n            <IoAddCircleOutline />\n        </a>\n    );\n}"
  },
  {
    "path": "viewer/src/components/Language/IncompleteTranslationWindow.jsx",
    "content": "import ModalWindow from \"../ModalWindows/ModalWindow\";\nimport React from \"react\";\nimport styled from \"styled-components\";\nimport { useTranslation } from \"../Translation\";\nimport Constants from \"../../constants\";\n\nconst Root = styled.div`\n    font-size: 18px;\n    min-width: 20em;\n    text-align: left;\n    padding: 1em;\n    color: var(--color);\n`;\n\nconst NotTranslatedList = styled.ul`\n    max-height: 15em;\n    overflow: auto;\n    padding: 1em 2em;\n    font-style: italic;\n`;\n\nexport default ({ onClose, missedPhrases }) => {\n    const t = useTranslation();\n\n    return (\n        <ModalWindow\n            onClose={(e) => {\n                e.stopPropagation();\n                onClose();\n            }}\n            usePortal={true}\n        >\n            <Root>\n                <div>\n                    <strong>{t(\"The translation isn't complete.\")} </strong>\n                    {t(\"The following phrases are not translated:\")}\n                </div>\n                <NotTranslatedList>\n                    {missedPhrases.map((phrase, i) => <li key={i}>{phrase}</li>)}\n                </NotTranslatedList>\n                <a target=\"_blank\" rel=\"noopener noreferrer\" href={Constants.TRANSLATION_PAGE_URL}>\n                    {t('You can improve the translation here')}\n                </a>\n            </Root>\n        </ModalWindow>\n    );\n};"
  },
  {
    "path": "viewer/src/components/Language/LanguagePanel.jsx",
    "content": "import React from 'react';\nimport styled, { css } from \"styled-components\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { get } from \"../../reducers\";\nimport { ActionTypes } from \"../../constants\";\nimport dictionaries from '../../locales';\nimport LanguageWarningSign from \"./LanguageWarningSign\";\nimport AddLanguageButton from \"./AddLanguageButton\";\n\nconst LanguagePanelRoot = styled.div`\n    display: flex;\n    font-size: 20px;\n    margin-top: 0.5em;\n    align-items: flex-end;\n    justify-content: center;\n    flex-wrap: wrap;\n    padding: 0 0.5em;\n`;\n\nconst selectedLanguageItem = css`\n    border-bottom: 3px solid var(--highlight-color);\n    color: var(--highlight-color);\n    cursor: default;\n    padding-top: 0;\n`;\n\nconst LanguageItem = styled.div`\n    margin-left: 0.5em;\n    margin-bottom: 0.2em;\n    cursor: pointer;\n    white-space: nowrap;\n    padding-top: 2px;\n    border-bottom: 1px solid transparent;\n    vertical-align: top;\n\n    ${p => p.$selected ? selectedLanguageItem : `\n        :hover {\n            border-color: var(--color);\n        }\n    `};\n`;\n\nexport const LanguagePanel = () => {\n    const { locale } = useSelector(get.options);\n    const dispatch = useDispatch();\n\n    return (\n        <LanguagePanelRoot>\n            {Object.entries(dictionaries).map(([code, dict]) => {\n                return (\n                    <LanguageItem\n                        key={code}\n                        $selected={locale === code}\n                        data-djvujs-class={'language_name ' + (locale === code ? 'selected' : '')}\n                        onClick={() => dispatch({\n                            type: ActionTypes.UPDATE_OPTIONS,\n                            payload: { locale: code },\n                        })}\n                    >\n                        {dict.nativeName}\n                        <LanguageWarningSign languageCode={code} />\n                    </LanguageItem>\n                );\n            })}\n            <AddLanguageButton css={`font-size: 1.5em; align-self: center;`} />\n        </LanguagePanelRoot>\n    );\n};"
  },
  {
    "path": "viewer/src/components/Language/LanguageSelector.jsx",
    "content": "import { ActionTypes } from \"../../constants\";\nimport dictionaries from \"../../locales\";\nimport LanguageWarningSign from \"./LanguageWarningSign\";\nimport AddLanguageButton from \"./AddLanguageButton\";\nimport React from \"react\";\nimport styled from \"styled-components\";\nimport { styledInput } from \"../cssMixins\";\nimport { useTranslation } from \"../Translation\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { get } from \"../../reducers\";\n\nconst Select = styled.select`\n    font-size: 1em;\n    margin-right: 0.5em;\n    padding-right: 0.5em;\n    ${styledInput};\n`;\n\nconst Root = styled.div`\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n`;\n\nexport default () => {\n    const { locale } = useSelector(get.options);\n    const dispatch = useDispatch();\n    const t = useTranslation();\n\n    return (\n        <Root>\n            <span style={{ marginRight: '0.5em' }}>{t('Language')}:</span>\n            <Select\n                value={locale}\n                onChange={(e) => dispatch({\n                    type: ActionTypes.UPDATE_OPTIONS,\n                    payload: { locale: e.target.value }\n                })}\n            >\n                {Object.entries(dictionaries).map(([code, dic]) => (\n                    <option value={code} key={code}>{dic.nativeName}</option>\n                ))}\n            </Select>\n            <LanguageWarningSign languageCode={locale} />\n            <AddLanguageButton css={`font-size: 1.2em`} />\n        </Root>\n    )\n};"
  },
  {
    "path": "viewer/src/components/Language/LanguageWarningSign.jsx",
    "content": "import React from \"react\";\nimport { FaExclamationTriangle } from \"react-icons/fa\";\nimport styled from \"styled-components\";\nimport IncompleteTranslationWindow from \"./IncompleteTranslationWindow\";\nimport dictionaries from '../../locales';\n\nconst Warning = styled.span`\n    color: var(--color);\n    cursor: pointer;\n    margin-right: 0.5em;\n    display: inline-flex;\n    align-items: center;\n\n    :hover {\n        color: var(--highlight-color);\n    }\n    \n    svg {\n        margin-left: 0.5em;\n        font-size: 0.8em;\n    }\n`;\n\nexport default ({ languageCode }) => {\n    const dict = dictionaries[languageCode];\n    const notTranslatedPhrases = Object.keys(dictionaries.en).filter(key => {\n        return dict[key] == null;\n    });\n    let [isWindowOpened, toggleWindow] = React.useState(false);\n\n    if (!notTranslatedPhrases.length) return null;\n\n    return (\n        <>\n            <Warning onClick={e => {\n                e.stopPropagation();\n                toggleWindow(true);\n            }}>\n                <FaExclamationTriangle />\n            </Warning>\n            {isWindowOpened ? <IncompleteTranslationWindow\n                missedPhrases={notTranslatedPhrases}\n                onClose={() => toggleWindow(false)}\n            /> : null}\n        </>\n    );\n}"
  },
  {
    "path": "viewer/src/components/LeftPanel/ContentsPanel.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\n\nimport Actions from '../../actions/actions';\nimport TreeItem from './TreeItem';\nimport { TranslationContext } from \"../Translation\";\nimport styled from 'styled-components';\nimport CloseButton from \"../misc/CloseButton\";\nimport { ActionTypes } from \"../../constants\";\nimport { withAppContext } from \"../AppContext\";\n\nconst Root = styled.div`\n    padding: 0.5em;\n    box-sizing: border-box;\n    height: 100%;\n    overflow: auto;\n`;\n\nconst Header = styled.div`\n    font-size: 1.5em;\n    border-bottom: 1px solid var(--border-color);\n    margin-bottom: 0.5em;\n    padding-bottom: 0.2em;\n    display: flex;\n    justify-content: space-between;\n\n    span:first-child {\n        margin-right: 0.5em;\n        overflow: hidden;\n        text-overflow: ellipsis;\n    }\n`;\n\nclass ContentsPanel extends React.Component {\n\n    static propTypes = {\n        contents: PropTypes.array\n    };\n\n    static contextType = TranslationContext;\n\n    onTreeItemClick = (url) => {\n        this.props.dispatch(Actions.setPageByUrlAction(url, this.props.appContext.isMobile));\n    };\n\n    convertBookmarkArrayToTreeItemDataArray(bookmarkArray) {\n        return bookmarkArray && bookmarkArray.map(bookmark => this.makeTreeItemDataByBookmark(bookmark));\n    }\n\n    makeTreeItemDataByBookmark(bookmark) {\n        return {\n            name: bookmark.description,\n            children: this.convertBookmarkArrayToTreeItemDataArray(bookmark.children),\n            callback: this.onTreeItemClick,\n            callbackData: bookmark.url\n        };\n    }\n\n    render() {\n        const { contents, dispatch } = this.props;\n        const t = this.context;\n\n        return (\n            <Root>\n                <Header>\n                    <span>{t(\"Contents\")}</span>\n                    <CloseButton\n                        onClick={() => dispatch({ type: ActionTypes.CLOSE_CONTENTS })}\n                    />\n                </Header>\n                {contents && contents.map((bookmark, i) => {\n                    return <TreeItem key={i} {...this.makeTreeItemDataByBookmark(bookmark)} />\n                })}\n                {contents ? null :\n                    <div css={`font-style: italic;`}>{t(\"No contents provided\")}</div>\n                }\n            </Root>\n        );\n    }\n}\n\nexport default connect()(withAppContext(ContentsPanel));"
  },
  {
    "path": "viewer/src/components/LeftPanel/LeftPanel.jsx",
    "content": "import React from 'react';\nimport { connect } from 'react-redux';\n\nimport ContentsPanel from './ContentsPanel';\nimport { get } from '../../reducers';\nimport styled, { css } from 'styled-components';\nimport { ActionTypes } from \"../../constants\";\nimport { AppContext } from \"../AppContext\";\nimport { DarkLayer } from \"../ModalWindows/ModalWindow\";\n\nconst closeWidth = 40;\n\nconst mobileStyle = css`\n    position: absolute;\n    z-index: 1;\n    height: 100%;\n    background: var(--background-color);\n    max-width: 90%;\n`;\n\nconst Root = styled.div`\n    flex: 0 0 auto;\n    border: 1px solid var(--border-color);\n    border-radius: 1em 0 1em 0;\n    box-sizing: border-box;\n    max-width: 80%;\n    transition: margin-left 0.5s, width 0.5s;\n    font-size: 14px;\n\n    ${p => p.theme.isMobile ? mobileStyle : ''};\n`;\n\nconst Border = styled.div`\n    box-sizing: border-box;\n    float: right;\n    height: 100%;\n    position: relative;\n    width: 7px;\n    left: 4px;\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    --point-size: 5px;\n\n    div {\n        width: var(--point-size);\n        height: var(--point-size);\n        transform: scaleX(0.75) scaleY(1.25) rotateZ(45deg);\n        background: var(--border-color);\n        margin-bottom: var(--point-size);\n    }\n\n    &:hover {\n        cursor: col-resize;\n    }\n`;\n\nclass LeftPanel extends React.Component {\n\n    static contextType = AppContext;\n\n    prevIsMobile = null;\n    lastContents = null;\n    contentsWasClosed = null;\n\n    onBeginResizing = (e) => {\n        e.preventDefault();\n        const width = this.topNode.getBoundingClientRect().width;\n        this.topNode.style.transition = 'none';\n        this.initialState = {\n            clientX: e.clientX,\n            width: width\n        };\n        window.addEventListener('mousemove', this.onResizing);\n        window.addEventListener('mouseup', this.onEndResizing);\n    };\n\n    onResizing = (e) => {\n        e.preventDefault();\n        if (!this.initialState) {\n            return;\n        }\n        const diff = e.clientX - this.initialState.clientX;\n        this.topNode.style.width = this.initialState.width + diff + 'px';\n    };\n\n    onEndResizing = (e) => {\n        e.preventDefault();\n        window.removeEventListener('mousemove', this.onResizing);\n        window.removeEventListener('mouseup', this.onEndResizing);\n        this.topNode.style.transition = null;\n        this.initialState = null;\n\n        if (this.topNode.getBoundingClientRect().width < closeWidth) {\n            this.closeContents();\n        }\n    };\n\n    closeContents = () => this.props.dispatch({ type: ActionTypes.CLOSE_CONTENTS });\n\n    ref = node => this.topNode = node;\n\n    // just to beautifully close contents when the window is resized and the app switches to the mobile version\n    componentDidUpdate(prevProps, prevState, snapshot) {\n        if (this.prevIsMobile !== this.context.isMobile) {\n            if (this.context.isMobile && this.props.isContentsOpened) {\n                this.closeContents()\n                this.contentsWasClosed = true;\n            } else if (!this.context.isMobile && this.contentsWasClosed) {\n                if (!this.props.isContentsOpened) {\n                    this.props.dispatch({ type: ActionTypes.TOGGLE_CONTENTS });\n                }\n                this.contentsWasClosed = false;\n            }\n        }\n\n        this.prevIsMobile = this.context.isMobile;\n    }\n\n    render() {\n        const { contents, isContentsOpened } = this.props;\n        const isMobile = this.context.isMobile;\n        const firstRender = contents && this.lastContents !== contents;\n        this.lastContents = contents;\n\n        const currentWidth = this.topNode ? this.topNode.getBoundingClientRect().width : 0;\n        const getCloseShift = (width) => `calc(-${width}px - var(--app-padding))`;\n\n        const initialWidth = isMobile ? '90%' : '20%';\n\n        return (\n            <>\n                {isMobile && isContentsOpened ? <DarkLayer onClick={this.closeContents} /> : null}\n                <Root\n                    ref={this.ref}\n                    style={isContentsOpened && !(isMobile && firstRender) ? {\n                        width: initialWidth,\n                        marginLeft: 0,\n                        transition: firstRender ? 'none' : null\n                    } : {\n                        width: currentWidth,\n                        marginLeft: getCloseShift(currentWidth),\n                    }}\n                    onTransitionEnd={e => {\n                        if (e.propertyName === 'margin-left' && !isContentsOpened) {\n                            this.topNode.style.width = initialWidth;\n                            this.topNode.style.marginLeft = `calc(-${initialWidth} - var(--app-padding))`;\n                            this.topNode.style.transition = `none`;\n                        }\n                    }}>\n                    <Border onMouseDown={this.onBeginResizing}>\n                        <div />\n                        <div />\n                        <div />\n                    </Border>\n                    <div style={{ height: '100%', overflow: \"hidden\" }}>\n                        <ContentsPanel contents={contents} />\n                    </div>\n                </Root>\n            </>\n        );\n    }\n}\n\nexport default connect(state => ({\n    contents: get.contents(state),\n    isContentsOpened: get.isContentsOpened(state),\n}))(LeftPanel);"
  },
  {
    "path": "viewer/src/components/LeftPanel/TreeItem.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { FaRegPlusSquare, FaRegMinusSquare, FaCircle } from \"react-icons/fa\";\nimport styled from 'styled-components';\n\nconst Name = styled.div`\n    cursor: pointer;\n    margin-left: 0.5em;\n    line-height: 20px;\n\n    &:hover {\n        text-decoration: underline;\n    }\n`;\n\nconst Root = styled.div`\n    display: flex;\n    flex-wrap: nowrap;\n    \n    & > svg {\n        flex: 0 0 auto;\n        font-size: 20px;\n    }\n`;\n\nexport default class TreeItem extends React.Component {\n\n    static propTypes = {\n        name: PropTypes.string.isRequired,\n        children: PropTypes.array,\n        callback: PropTypes.func,\n        callbackData: PropTypes.any\n    };\n\n    constructor(props) {\n        super(props);\n        this.state = { isCollapsed: true };\n    }\n\n    onClick = () => {\n        this.props.callback && this.props.callback(this.props.callbackData);\n    };\n\n    renderChildren() {\n        if (!this.props.children) {\n            return null;\n        }\n        return (\n            <div css={`padding-left: 0.5em;`}>\n                {this.props.children.map((treeItem, i) => {\n                    return <TreeItem key={i} {...treeItem} />\n                })}\n            </div>\n        );\n    }\n\n    toggleItem = () => {\n        this.setState({ isCollapsed: !this.state.isCollapsed });\n    };\n\n    render() {\n        const Icon = this.state.isCollapsed ? FaRegPlusSquare : FaRegMinusSquare\n        return (\n            <Root>\n                {this.props.children ?\n                    <Icon\n                        onClick={this.toggleItem}\n                    /> : <FaCircle css={`transform: scale(0.5)`} />\n                }\n                <div>\n                    <Name className=\"name\" onClick={this.onClick}>{this.props.name}</Name>\n                    {this.state.isCollapsed ? null : this.renderChildren()}\n                </div>\n            </Root>\n        );\n    }\n}"
  },
  {
    "path": "viewer/src/components/LoadingLayer.jsx",
    "content": "import React from 'react';\nimport styled from 'styled-components';\nimport LoadingPhrase from './misc/LoadingPhrase';\n\nconst DarkLayer = styled.div`\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: var(--alternative-background-color);\n    opacity: 0.7;\n`;\n\nconst MessageWrapper = styled.div`\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    opacity: 0.8;\n    font-size: 3em;\n    flex: 1 1 auto;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    white-space: nowrap;\n`;\n\nexport default class LoadingLayer extends React.Component {\n    constructor(props) {\n        super(props);\n        this.showTimeout = null;\n        this.rootRef = React.createRef();\n    }\n\n    componentDidMount() {\n        this.showTimeout = setTimeout(() => {\n            if (this.rootRef.current) this.rootRef.current.style.display = null;\n            this.showTimeout = null;\n        }, 500);\n    }\n\n    componentWillUnmount() {\n        this.showTimeout && clearTimeout(this.showTimeout);\n    }\n\n    render() {\n        return (\n            <div\n                style={{ display: 'none' }}\n                ref={this.rootRef}\n            >\n                <DarkLayer />\n                <MessageWrapper><LoadingPhrase /></MessageWrapper>\n            </div>\n        );\n    }\n}"
  },
  {
    "path": "viewer/src/components/Main.jsx",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport styled from 'styled-components';\nimport { get } from '../reducers';\nimport Constants from '../constants';\nimport LeftPanel from './LeftPanel/LeftPanel';\nimport LoadingLayer from './LoadingLayer';\nimport ImageBlock from './ImageBlock/ImageBlock';\nimport TextBlock from './TextBlock';\nimport ErrorPage from './ErrorPage';\n\nconst Root = styled.div`\n    position: relative;\n    flex: 1 1 auto;\n    width: 100%;\n    height: 100%;\n    display: flex;\n    box-sizing: border-box;\n    overflow: hidden;\n`;\n\nconst PageZone = styled.div`\n    flex: 1 1 auto;\n    overflow: hidden;\n    position: relative;\n    padding: 0.5em;  \n`;\n\nexport default () => {\n    const viewMode = useSelector(get.viewMode);\n    const pageNumber = useSelector(get.currentPageNumber);\n    const isLoading = useSelector(get.isLoading);\n    const pageText = useSelector(get.pageText);\n    const imageData = useSelector(get.imageData);\n    const imagePageError = useSelector(get.imagePageError);\n    const textPageError = useSelector(get.textPageError);\n\n    const renderMainElement = () => {\n        if (imagePageError && viewMode === Constants.SINGLE_PAGE_MODE) {\n            return <ErrorPage pageNumber={pageNumber} error={imagePageError} />;\n        }\n        if (viewMode === Constants.TEXT_MODE) {\n            if (textPageError) {\n                return <ErrorPage pageNumber={pageNumber} error={textPageError} />;\n            }\n            return <TextBlock text={pageText} />\n        }\n        if (viewMode === Constants.CONTINUOUS_SCROLL_MODE || imageData) {\n            return <ImageBlock />;\n        }\n    };\n\n    return (\n        <Root>\n            <LeftPanel />\n            <PageZone>\n                {renderMainElement()}\n                {(isLoading && viewMode === Constants.SINGLE_PAGE_MODE) ? <LoadingLayer /> : null}\n            </PageZone>\n        </Root>\n    );\n}"
  },
  {
    "path": "viewer/src/components/Menu.jsx",
    "content": "import React from \"react\";\nimport styled, { css } from \"styled-components\";\nimport CloseButton from \"./misc/CloseButton\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { get } from \"../reducers\";\nimport FileBlock from \"./FileBlock\";\nimport OptionsButton from \"./misc/OptionsButton\";\nimport HelpButton from \"./misc/HelpButton\";\nimport { ControlButton } from \"./StyledPrimitives\";\nimport { FaPrint } from \"react-icons/fa\";\nimport { ActionTypes } from \"../constants\";\nimport { useTranslation } from \"./Translation\";\nimport SaveButton from \"./misc/SaveButton\";\nimport Actions from \"../actions/actions\";\nimport { useAppContext } from \"./AppContext\";\nimport ScaleGizmo from \"./Toolbar/ScaleGizmo\";\nimport RotationControl from \"./Toolbar/RotationControl\";\nimport ViewModeButtons from \"./Toolbar/ViewModeButtons\";\nimport CursorModeButtonGroup from \"./Toolbar/CursorModeButtonGroup\";\nimport FullPageViewButton from \"./misc/FullPageViewButton\";\nimport FullscreenButton from \"./misc/FullscreenButton\";\n\nconst Root = styled.div`\n    font-size: 16px;\n    --button-basic-size: 1em;\n    position: absolute;\n    bottom: calc(100% + var(--app-padding));\n    right: 0;\n    z-index: 1;\n    min-height: min(15em, ${p => p.theme.appHeight * 0.7}px);\n    max-height: ${p => p.theme.appHeight * 0.7}px;\n\n    width: fit-content;\n    max-width: 90%;\n    background: var(--background-color);\n    border: 1px solid var(--border-color);\n    border-radius: 5px 0 5px 0;\n    padding: 0.5em;\n    overflow: hidden;\n\n    display: flex;\n    flex-direction: column;\n\n    ${p => p.$opened ? 'transform: translateX(0);' : 'transform: translateX(calc(100% + var(--app-padding) * 2));'};\n\n    transition: transform 0.5s;\n`;\n\nconst MenuWrapper = styled.div`\n    display: flex;\n    flex-direction: column;\n\n    & > * {\n        margin-bottom: 1em;\n    }\n`;\n\nconst Header = styled.div`\n    display: flex;\n    align-items: center;\n    border-bottom: 1px solid var(--border-color);\n    padding-bottom: 0.5em;\n    margin-bottom: 0.5em;\n    font-size: 1.5em;\n\n    svg {\n        margin-left: auto;\n    }\n\n    span {\n        margin-right: 1em;\n    }\n`;\n\nconst DocumentWrapper = styled.div`\n    border-bottom: 1px solid var(--border-color);\n    margin-bottom: 1em;\n    padding-bottom: 0.5em;\n    padding-left: 0.5em;\n\n    & > div:first-child {\n        margin-bottom: 1em;\n    }\n`;\n\nconst documentControlsMobileStyle = css`\n    flex-direction: column;\n    padding-left: 1em;\n    align-items: flex-start;\n    border-bottom: 1px dashed var(--border-color);\n    margin-bottom: 1em;\n\n    & > * {\n        margin-bottom: 0.5em;\n    }\n`;\n\nconst DocumentControls = styled.div`\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n    margin-top: 1em;\n\n    ${p => p.theme.isMobile ? documentControlsMobileStyle : ''};\n`;\n\nconst MenuItemStyle = css`\n    cursor: pointer;\n\n    :hover {\n        & svg {\n            transform: scale(1.1);\n        }\n    }\n`;\n\nconst DocumentControl = styled.div`\n    ${MenuItemStyle};\n    margin-right: 1.5em;\n    white-space: nowrap;\n    display: flex;\n    align-items: center;\n\n    ${ControlButton} {\n        margin-left: 0;\n    }\n`;\n\nconst MobileControl = styled.div`\n    display: flex;\n    align-items: center;\n    margin-bottom: 1em;\n\n    & > span:first-child {\n        margin-right: 1em;\n    }\n`;\n\nconst Content = styled.div`\n    overflow: auto;\n`;\n\nexport default ({ isOpened, onClose }) => {\n    const dispatch = useDispatch();\n    const t = useTranslation();\n    const fileName = useSelector(get.fileName);\n    const { hideOpenAndCloseButtons, hidePrintButton, hideSaveButton } = useSelector(get.uiOptions);\n    const { isMobile } = useAppContext();\n\n    const closeHandler = onClose;\n\n    return (\n        <Root $opened={isOpened} data-djvujs-id=\"menu\">\n            <Header>\n                <span>{t('Menu')}</span>\n                <CloseButton onClick={closeHandler} />\n            </Header>\n\n            <Content>\n                <DocumentWrapper>\n                    <div>{t('Document')}:</div>\n                    {hideOpenAndCloseButtons ? fileName ? <span>{fileName}</span> : null :\n                        <FileBlock fileName={fileName || ''} />}\n\n                    <DocumentControls>\n                        {hidePrintButton ? null :\n                            <DocumentControl\n                                onClick={() => {\n                                    dispatch({ type: ActionTypes.OPEN_PRINT_DIALOG });\n                                    closeHandler();\n                                }}\n                                title={t('Print document')}\n                            >\n                                <ControlButton as={FaPrint} />\n                                <span>{t('Print')}</span>\n                            </DocumentControl>}\n\n                        {hideSaveButton ? null : <DocumentControl onClick={closeHandler}>\n                            <SaveButton onClick={closeHandler} withLabel={true} />\n                        </DocumentControl>}\n\n                        {hideOpenAndCloseButtons ? null :\n                            <DocumentControl onClick={() => dispatch(Actions.closeDocumentAction())}>\n                                <ControlButton as={CloseButton} css={`font-size: 1em;`} />\n                                <span>{t('Close')}</span>\n                            </DocumentControl>}\n                    </DocumentControls>\n\n                    {isMobile ?\n                        <>\n                            <MobileControl>\n                                <span>{t('View mode')}:</span>\n                                <ViewModeButtons />\n                            </MobileControl>\n                            <MobileControl>\n                                <span>{t('Scale')}:</span>\n                                <ScaleGizmo />\n                            </MobileControl>\n                            <MobileControl>\n                                <span>{t('Rotation')}:</span>\n                                <RotationControl />\n                            </MobileControl>\n                            <MobileControl>\n                                <span>{t('Cursor mode')}:</span>\n                                <CursorModeButtonGroup />\n                            </MobileControl>\n                        </> : null}\n                </DocumentWrapper>\n\n                <MenuWrapper>\n                    <MobileControl>\n                        <span>{t('Full page mode')}:</span>\n                        <FullPageViewButton />\n                    </MobileControl>\n                    {(document.fullscreenEnabled || document.webkitFullscreenEnabled) ? <MobileControl>\n                        <span>{t('Fullscreen mode')}:</span>\n                        <FullscreenButton />\n                    </MobileControl> : null}\n                    <OptionsButton onClick={closeHandler} withLabel={true} />\n                    <HelpButton onClick={closeHandler} withLabel={true} />\n                </MenuWrapper>\n            </Content>\n        </Root>\n    );\n}"
  },
  {
    "path": "viewer/src/components/ModalWindows/ErrorWindow.jsx",
    "content": "import React from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport ModalWindow from './ModalWindow';\nimport { useTranslation } from \"../Translation\";\nimport styled from \"styled-components\";\nimport { ActionTypes } from \"../../constants\";\nimport { get } from \"../../reducers\";\nimport { getHeaderAndErrorMessage } from \"../helpers\";\n\nconst Header = styled.div`\n    border-bottom: 1px solid gray;\n    padding: 0 0.5em;\n`;\n\nconst Body = styled.div`\n    margin-top: 1em;\n    padding: 0 0.5em;\n\n    ${p => p.$json ? `\n        white-space: pre;\n        font-family: Consolas, monospace;\n    ` : ''};\n`;\n\nexport default () => {\n    const t = useTranslation();\n    const dispatch = useDispatch();\n    const error = useSelector(get.error);\n\n    if (!error) return null;\n\n    const { header, message, isJSON } = getHeaderAndErrorMessage(t, error);\n\n    return (\n        <ModalWindow isError={true} onClose={() => dispatch({ type: ActionTypes.CLOSE_ERROR_WINDOW })}>\n            <div>\n                <Header>{header}</Header>\n                <Body $json={isJSON}>{message}</Body>\n            </div>\n        </ModalWindow>\n    );\n};"
  },
  {
    "path": "viewer/src/components/ModalWindows/HelpWindow.jsx",
    "content": "import React from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { FaExpand, FaCompress } from \"react-icons/fa\";\n\nimport ModalWindow from './ModalWindow';\nimport Actions from '../../actions/actions';\nimport { get } from '../../reducers';\nimport DjVu from '../../DjVu';\nimport { useTranslation } from '../Translation';\nimport styled from \"styled-components\";\n\nconst Root = styled.div`\n    padding: 0.5em;\n    font-size: ${p => p.theme.isMobile ? 12 : 20}px;\n`;\n\nconst Header = styled.div`\n    font-size: 1.2em;\n    width: 100%;\n    font-weight: 600;\n    border-bottom: 1px solid var(--border-color);\n    margin: 0.5em 0;\n`;\n\nconst HotkeyGrid = styled.div`\n    display: grid;\n    grid-template-columns: auto 1fr;\n    column-gap: 0.5em;\n\n    & > :nth-child(2n+1) {\n        text-align: center;\n    }\n`;\n\nexport default () => {\n    const isShown = useSelector(get.isHelpWindowShown);\n    const dispatch = useDispatch();\n    const t = useTranslation();\n    const { hideFullPageSwitch } = useSelector(get.uiOptions);\n\n    if (!isShown) {\n        return null;\n    }\n\n    return (\n        <ModalWindow onClose={() => dispatch(Actions.closeHelpWindowAction())} isFixedSize={true}>\n            <Root>\n                <Header>{`DjVu.js Viewer v.${DjVu.Viewer.VERSION} (DjVu.js v.${DjVu.VERSION})`}</Header>\n                <div>\n                    {t('The application for viewing .djvu files in the browser.')}<br />\n                    {t(\"If something doesn't work properly, feel free to write about the problem at #email.\", {\n                        '#email': <a target=\"_blank\" rel=\"noopener noreferrer\"\n                                     href=\"mailto:djvujs@yandex.ru\">djvujs@yandex.ru</a>\n                    })}\n                    <br />\n                    {t(\"The official website is #website.\", {\n                        \"#website\": <a target=\"_blank\" rel=\"noopener noreferrer\"\n                                       href=\"https://djvu.js.org/\">djvu.js.org</a>\n                    })}<br />\n                    {t(\"The source code is available on #link.\", {\n                        \"#link\": <a target=\"_blank\" rel=\"noopener noreferrer\"\n                                    href=\"https://github.com/RussCoder/djvujs\">GitHub</a>\n                    })}<br />\n                </div>\n\n                <Header>{t('Hotkeys')}</Header>\n                <HotkeyGrid>\n                    <em>Ctrl+S</em><span>- {t('save the document')}</span>\n                    <em>{'\\u2190'}</em><span>- {t('go to the previous page')}</span>\n                    <em>{'\\u2192'}</em><span>- {t('go to the next page')}</span>\n                </HotkeyGrid>\n\n                {hideFullPageSwitch ? null :\n                    <>\n                        <Header>{t('Controls')}</Header>\n                        <div>\n                            {t(\"#expandIcon and #collapseIcon are to switch the viewer to the full page mode and back.\", {\n                                \"#expandIcon\": <FaExpand />,\n                                \"#collapseIcon\": <FaCompress />,\n                            })}\n                            {' ' + t(\"If you work with the browser extension, these buttons will cause no effect, since the viewer takes the whole page by default.\")}\n                        </div>\n                    </>\n                }\n            </Root>\n        </ModalWindow>\n    );\n}"
  },
  {
    "path": "viewer/src/components/ModalWindows/ModalWindow.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport PropTypes from 'prop-types';\nimport styled, { css } from 'styled-components';\nimport CloseButton from \"../misc/CloseButton\";\n\nconst style = css`\n    z-index: 4; // 0 was used to just make windows with their dark layers lie one on top of another when they are created in sequence \n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n`;\n\nconst ModalWindowRoot = styled.div`\n    color: var(--color);\n    box-shadow: 0 0 2px var(--color);\n    background: var(--modal-window-background-color);\n    border-radius: 0.5em;\n    font-size: 1.5em;\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translateX(-50%) translateY(-50%);\n    max-width: 90%;\n    max-height: 90%;\n    width: max-content;\n    height: max-content;\n    z-index: 2;\n    padding: 0;\n    overflow: hidden;\n    display: flex; // to enable overflow: auto in the content wrapper\n    flex-direction: column;\n    --closeButtonBlockHeight: 28px;\n\n    ${p => p.$fixedSize ? `\n        height: ${p.theme.isMobile ? 90 : 80}%;\n        width: ${p.theme.isMobile ? 90 : 80}%;\n    ` : ''};\n\n    ${p => p.$error ? `\n       background: rgb(255, 209, 212);\n       color: black;\n    ` : ''};\n`;\n\nconst ContentWrapper = styled.div`\n    overflow: auto;\n    padding-bottom: var(--closeButtonBlockHeight);\n`\n\nexport const DarkLayer = styled.div`\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: var(--alternative-background-color);\n    backdrop-filter: blur(2px);\n    opacity: 0.8;\n    z-index: 1;\n`;\n\nexport default class ModalWindow extends React.Component {\n\n    static propTypes = {\n        isError: PropTypes.bool,\n        isFixedSize: PropTypes.bool,\n        usePortal: PropTypes.bool,\n        onClose: PropTypes.func.isRequired\n    };\n\n    render() {\n        const { onClose, isError, isFixedSize, className = '', usePortal = false } = this.props;\n\n        const component = (\n            <div css={style} data-djvujs-class=\"modal_window\">\n                <DarkLayer onClick={onClose} data-djvujs-class=\"dark_layer\" />\n                <ModalWindowRoot\n                    className={className}\n                    $error={isError}\n                    $fixedSize={isFixedSize}\n                >\n                    <CloseButton\n                        onClick={onClose}\n                        css={`\n                            height: var(--closeButtonBlockHeight);\n                            margin-left: auto;\n                            margin-right: 0.25em;\n                        `}\n                    />\n                    <ContentWrapper>\n                        {this.props.children}\n                    </ContentWrapper>\n                </ModalWindowRoot>\n            </div>\n        );\n\n        if (usePortal) { // portal is needed to a modal window from another modal window.\n            // In the first render, when the app is mounted, there is no container element,\n            // but in normal case a modal window should be shown before the app is mounted.\n            const container = document.getElementById('djvujs-modal-windows-container');\n            return container ? ReactDOM.createPortal(component, container) : component;\n        } else {\n            return component;\n        }\n    }\n}"
  },
  {
    "path": "viewer/src/components/ModalWindows/OptionsWindow.jsx",
    "content": "import React from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { get } from '../../reducers';\nimport { useTranslation } from \"../Translation\";\nimport { ActionTypes } from \"../../constants\";\nimport styled from 'styled-components';\nimport ModalWindow from \"./ModalWindow\";\nimport ThemeSwitcher from \"../InitialScreen/ThemeSwitcher\";\nimport { inExtension, isManifestV3 } from \"../../utils\";\nimport LanguageSelector from \"../Language/LanguageSelector\";\n\nconst Root = styled.div`\n    box-sizing: border-box;\n    padding: 0 0.5em;\n`;\n\nconst MainHeader = styled.div`\n    font-size: 1.5em;\n    font-weight: bold;\n    margin-bottom: 0.5em;\n    text-align: center;\n`\n\nconst ExtensionOption = styled.label`\n    display: flex;\n    align-items: center;\n    cursor: pointer;\n\n    input[type=checkbox] {\n        transform: scale(1.5);\n        flex: 0 0 auto;\n        cursor: pointer;\n        display: inline-block;\n        margin-right: 1em;\n        outline: none;\n    }\n`;\n\nconst Option = styled.label`\n    display: block;\n    margin-bottom: 1em;\n`;\n\nexport default () => {\n    const options = useSelector(get.options);\n    const dispatch = useDispatch();\n    const t = useTranslation();\n    const isShown = useSelector(get.isOptionsWindowOpened);\n\n    if (!isShown) return null;\n\n    return (\n        <ModalWindow\n            onClose={() => dispatch({ type: ActionTypes.TOGGLE_OPTIONS_WINDOW, payload: false })}\n            css={`min-width: 15em;`}\n            data-djvujs-id=\"options_window\"\n        >\n            <Root>\n                <MainHeader>{t('Options')}</MainHeader>\n                <Option as=\"div\">\n                    <LanguageSelector />\n                </Option>\n                <Option>\n                    <span style={{ marginRight: '0.5em' }}>{t('Color theme')}:</span>\n                    <ThemeSwitcher />\n                </Option>\n                {inExtension ? <div>\n                    <div css={`margin-bottom: 1em;`}>{t('Extension options')}:</div>\n                    <ExtensionOption\n                        title={t(\"All links to .djvu files will be opened by the viewer via a simple click on a link\")}\n                    >\n                        <input\n                            type=\"checkbox\"\n                            checked={options.interceptHttpRequests}\n                            onChange={e => dispatch({\n                                type: ActionTypes.UPDATE_OPTIONS,\n                                payload: { interceptHttpRequests: e.target.checked, analyzeHeaders: false }\n                            })}\n                        />{t(\"Open all links with .djvu at the end via the viewer\")}\n                    </ExtensionOption>\n                    {isManifestV3 ? null : <ExtensionOption\n                        title={t('Analyze headers of every new tab in order to process even links which do not end with the .djvu extension')}\n                        style={{ marginLeft: '1em' }}\n                    >\n                        <input\n                            type=\"checkbox\"\n                            checked={options.analyzeHeaders}\n                            onChange={e => dispatch({\n                                type: ActionTypes.UPDATE_OPTIONS,\n                                payload: { analyzeHeaders: e.target.checked, interceptHttpRequests: true }\n                            })}\n                        />{t('Detect .djvu files by means of http headers')}\n                    </ExtensionOption>}\n                </div> : null}\n            </Root>\n        </ModalWindow>\n    )\n};"
  },
  {
    "path": "viewer/src/components/ModalWindows/PrintDialog.jsx",
    "content": "import React from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport ModalWindow from './ModalWindow';\nimport { get } from '../../reducers';\nimport { useTranslation } from '../Translation';\nimport styled from \"styled-components\";\nimport { ActionTypes } from \"../../constants\";\nimport { styledInput } from \"../cssMixins\";\nimport { TextButton } from \"../StyledPrimitives\";\nimport ProgressBar from \"../misc/ProgressBar\";\nimport { isFirefox } from \"../../utils\";\n\nconst Root = styled.div`\n    padding: 0.5em;\n`;\n\nconst Select = styled.select`\n    min-width: 4em;\n    ${styledInput};\n`;\n\nfunction renderPageNumberOptions(pagesQuantity) {\n    const pages = new Array(pagesQuantity);\n    for (let i = 1; i <= pagesQuantity; i++) {\n        pages[i - 1] = <option value={i} key={i}>{i}</option>;\n    }\n    return pages;\n}\n\nexport default () => {\n    const isPreparing = useSelector(get.isPreparingForPrinting);\n    const printProgress = useSelector(get.printProgress);\n    const pages = useSelector(get.pagesForPrinting);\n    const dispatch = useDispatch();\n    const t = useTranslation();\n    const pagesQuantity = useSelector(get.pagesQuantity);\n\n    const [from, setFrom] = React.useState(1);\n    let [to, setTo] = React.useState(null);\n    to = to || pagesQuantity;\n\n    const print = (elem) => {\n        const win = elem.contentWindow;\n        win.onafterprint = () => {\n            // If we do it synchronously, Firefox ceases to react to mouse movements (e.g. no hover animations).\n            // and even after the page is reloaded, the main thread doesn't receive messages from the worker (although they are sent)\n            // So it can be cured only via closing the tab and opening a new one.\n            // In Chrome everything is OK.\n            // Actually, 0 timeout works for Firefox too, but to make it more robust we use 100 ms.\n            setTimeout(() => {\n                dispatch({ type: ActionTypes.CLOSE_PRINT_DIALOG });\n            }, isFirefox ? 100 : 0);\n        };\n        const styleSheet = document.createElement('style');\n        // language=css\n        styleSheet.innerHTML = `\n            html, body {\n                margin: 0;\n                padding: 0;\n                height: 100%;\n                width: 100%;\n            }\n\n            img {\n                display: block;\n                margin: 0 auto;\n                /*\n                Firefox ignores \"break-inside: avoid\" (while Chrome seems to apply it by default)\n                so we have to use break-after.\n                */\n                break-after: ${isFirefox ? 'page' : 'auto'};\n                break-inside: avoid;\n                /* \n                When the print scale is bigger than 100%, there can be a situation when height can be increased, but \n                width is limited with max-width, so the proportions are distorted. To prevent this we use object-fit.                \n                */\n                object-fit: contain;\n                box-sizing: border-box;\n                /* \n                It seems like 100vw and 100vh can be used as width and height of the paper sheet in Chrome and Firefox.\n                But in Safari they seem to correspond to the size of the iframe, which is 0, so empty pages are printed.\n                So we use 100% width and height here (and for html and body too) to fit big images to the paper size. \n                */\n                max-width: 100%;\n                max-height: 100%;\n            }\n        `;\n        win.document.head.appendChild(styleSheet);\n\n        const promises = [];\n\n        for (const page of pages) {\n            const img = win.document.createElement('img');\n            promises.push(new Promise(resolve => img.onload = resolve));\n            img.src = page.url;\n            img.width = page.width;\n            img.height = page.height;\n            img.style.width = (page.width / page.dpi) + 'in';\n            img.style.height = (page.height / page.dpi) + 'in';\n\n            win.document.body.appendChild(img);\n        }\n\n        if (isFirefox) {\n            // Firefox shows blank pages if we wait for images (although prints correctly)\n            // Also, it seems to not fire \"load\" event if the images have been already shown before\n            // (as pages in the continuous scroll mode) in the browser extension.\n            win.print();\n        } else {\n            // Chrome shows empty images on pages if we do not wait\n            Promise.all(promises).then(() => win.print());\n        }\n    };\n\n    return (\n        <ModalWindow onClose={() => dispatch({ type: ActionTypes.CLOSE_PRINT_DIALOG })}>\n            <Root>\n                {isPreparing ?\n                    <>\n                        <div css=\"text-align: center; margin-bottom: 1em;\">\n                            {t('Preparing pages for printing')}...\n                            <span css=\"min-width: 3em; display: inline-block\">{printProgress}%</span>\n                        </div>\n                        <ProgressBar percentage={printProgress} />\n                        {pages ?\n                            <iframe\n                                css={`\n                                    width: 0;\n                                    height: 0;\n                                    position: absolute;\n                                    left: 0;\n                                    top: 0;\n                                    opacity: 0;\n                                `}\n                                src=\"about:blank\"\n                                ref={elem => elem && print(elem)}\n                            /> : null}\n                    </>\n                    : <>\n                        <div>\n                            {t('Pages must be rendered before printing.') + ' ' + t('It may take a while.')}\n                        </div>\n                        <div>{t('Select the pages you want to print.')}</div>\n\n                        <div css=\"margin: 1em 0; text-align: center\">\n                            <span css=\"margin-right: 1em;\">{t('From')}</span>\n                            <Select value={from} onChange={e => setFrom(e.target.value)}>\n                                {renderPageNumberOptions([pagesQuantity])}\n                            </Select>\n                            <span css=\"margin: 0 1em\">{t('to')}</span>\n                            <Select value={to} onChange={e => setTo(e.target.value)}>\n                                {renderPageNumberOptions([pagesQuantity])}\n                            </Select>\n                        </div>\n                        <TextButton\n                            css=\"font-size: 0.8em; margin: 0 auto; display: block\"\n                            onClick={() => dispatch({\n                                type: ActionTypes.PREPARE_PAGES_FOR_PRINTING,\n                                payload: { from, to }\n                            })}\n                        >\n                            {t('Prepare pages for printing')}\n                        </TextButton>\n                    </>}\n            </Root>\n        </ModalWindow>\n    );\n};"
  },
  {
    "path": "viewer/src/components/ModalWindows/SaveDialog.jsx",
    "content": "import React from 'react';\nimport styled from \"styled-components\";\nimport ModalWindow from \"./ModalWindow\";\nimport { useTranslation } from \"../Translation\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { TextButton } from \"../StyledPrimitives\";\nimport { get } from \"../../reducers\";\nimport { ActionTypes } from \"../../constants\";\nimport ProgressBar from \"../misc/ProgressBar\";\nimport { normalizeFileName } from \"../../utils\";\n\nconst Root = styled.div`\n    padding: 1em;\n`;\n\nconst OptionButton = styled(TextButton)`\n    width: 10em;\n`;\n\nconst OptionsWrapper = styled.div`\n    display: flex;\n    justify-content: space-around;\n    margin-top: 2em;\n`;\n\nconst ProcessingBlock = styled.div`\n    margin-top: 2em;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n`;\n\nconst getBundledFileName = fileName => {\n    const normalized = normalizeFileName(fileName);\n    return /(?:[^a-z]|\\b)index(?:[^a-z]|\\b)/.test(normalized) ? \"bundled_\" + normalized : normalized;\n}\n\nexport default () => {\n    const t = useTranslation();\n    const dispatch = useDispatch();\n    const isShown = useSelector(get.isSaveDialogShown);\n    const buffer = useSelector(get.resultBuffer);\n    const progress = useSelector(get.fileProcessingProgress);\n    const isBundling = useSelector(get.isBundling);\n    const fileName = useSelector(get.fileName);\n    const [url, setUrl] = React.useState(null);\n\n    const closeDialog = () => {\n        dispatch({ type: ActionTypes.CLOSE_SAVE_DIALOG });\n    };\n\n    React.useEffect(() => {\n        if (buffer) {\n            const url = URL.createObjectURL(new Blob([buffer], { type: 'image/vnd.djvu' }));\n            setUrl(url);\n            return () => URL.revokeObjectURL(url);\n        } else {\n            setUrl(null);\n        }\n    }, [buffer]);\n\n    if (!isShown) return null;\n\n    const percentage = Math.round(progress * 100);\n\n    return (\n        <ModalWindow\n            onClose={closeDialog}\n            isFixedSize={false}\n            css={`width: 25em`}\n        >\n            <Root>\n                {!isBundling ? <>\n                    <div css={'margin-bottom: 2em;'}>\n                        {t(\"You are trying to save an indirect (multi-file) document.\") + ' '}\n                        {t(\"What exactly do you want to do?\")}\n                    </div>\n                    <OptionsWrapper>\n                        <OptionButton\n                            onClick={() => {\n                                closeDialog();\n                                dispatch({ type: ActionTypes.SAVE_DOCUMENT });\n                            }}\n                        >\n                            {t('Save only index file')}\n                        </OptionButton>\n                        <OptionButton onClick={() => dispatch({ type: ActionTypes.START_TO_BUNDLE })}>\n                            {t('Download, bundle and save the whole document as one file')}\n                        </OptionButton>\n                    </OptionsWrapper>\n                </> : null}\n\n                {isBundling ?\n                    <ProcessingBlock>\n                        {!url ?\n                            <>\n                                <div css={`text-align: center; margin-bottom: 1em;`}>\n                                    {t(\"Downloading and bundling the document\")}... {percentage}%\n                                </div>\n                                <ProgressBar percentage={Math.round(progress * 100)} />\n                            </> :\n                            <>\n                                <div css={`text-align: center; margin-bottom: 1em;`}>\n                                    {t(\"The document has been downloaded and bundled into one file successfully\")}\n                                </div>\n                                <TextButton\n                                    as=\"a\"\n                                    href={url}\n                                    download={getBundledFileName(fileName)}\n                                    css={`text-decoration: none; margin: 0.5em`}\n                                >\n                                    {t('Save')}\n                                </TextButton>\n                            </>}\n                    </ProcessingBlock> : null}\n            </Root>\n        </ModalWindow>\n    );\n}"
  },
  {
    "path": "viewer/src/components/StyledPrimitives.jsx",
    "content": "import styled, { keyframes } from 'styled-components';\nimport { controlButton } from './cssMixins';\nimport { FaSpinner } from \"react-icons/fa\";\n\nexport const ControlButton = styled.span`\n    ${controlButton};\n`;\n\nexport const ControlButtonWrapper = styled.span`\n    cursor: pointer;\n\n    :hover {\n        ${ControlButton} {\n            transform: scale(1.1);\n        }\n    }\n`;\n\nexport const TextButton = styled.button`\n    background: inherit;\n    color: var(--color);\n    border: 1px solid var(--color);\n    border-radius: 3px;\n    padding: 0.2em;\n    cursor: pointer;\n\n    &:hover {\n        background: var(--alternative-background-color);\n    }\n\n    &:focus {\n        outline: none;\n    }\n`;\n\nconst rotateDiscrete = keyframes`\n    0% { transform: rotate(0turn); }\n    100% { transform: rotate(1turn); }\n`;\n\nexport const Spinner = styled(FaSpinner)`\n    animation: ${rotateDiscrete} 1s infinite steps(9, end);\n`;"
  },
  {
    "path": "viewer/src/components/TextBlock.jsx",
    "content": "import React from 'react';\nimport styled from 'styled-components';\nimport LoadingPhrase from './misc/LoadingPhrase';\nimport { useTranslation } from './Translation';\n\nconst Root = styled.div`\n    overflow: auto;\n    max-height: 100%;\n    padding: 0.5em;\n    box-sizing: border-box;\n\n    pre {\n        width: fit-content;\n        margin: auto;\n        background: inherit;\n        border: 1px solid var(--border-color);\n        padding: 0.5em;\n        white-space: pre-wrap;\n    } \n`;\n\nexport default ({ text }) => {\n    const t = useTranslation();\n\n    return (\n        <Root>\n            <pre>\n                {text === null ?\n                    <LoadingPhrase /> :\n                    text || <em>{t(\"No text on this page\")}</em>}\n            </pre>\n        </Root>\n    );\n};\n"
  },
  {
    "path": "viewer/src/components/Toolbar/ContentsButton.jsx",
    "content": "import { IoListCircleSharp, IoListCircleOutline } from \"react-icons/io5\";\nimport React from \"react\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { ActionTypes } from \"../../constants\";\nimport { iconButton } from \"../cssMixins\";\nimport { get } from \"../../reducers\";\nimport styled from \"styled-components\";\nimport { useTranslation } from \"../Translation\";\n\nconst Root = styled.svg`\n    ${iconButton};\n    font-size: 2em;\n    flex: 0 0 auto;\n`;\n\nexport default () => {\n    const dispatch = useDispatch();\n    const isOpened = useSelector(get.isContentsOpened);\n    const t = useTranslation();\n\n    return (\n        <Root\n            as={isOpened ? IoListCircleSharp : IoListCircleOutline}\n            onClick={() => dispatch({ type: ActionTypes.TOGGLE_CONTENTS })}\n            data-djvujs-id=\"contents_button\"\n            title={t(\"Table of contents\")}\n        />\n    );\n}"
  },
  {
    "path": "viewer/src/components/Toolbar/CursorModeButtonGroup.jsx",
    "content": "import React from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { FaRegHandPaper, FaICursor } from \"react-icons/fa\";\n\nimport { get } from '../../reducers';\nimport Constants from '../../constants';\nimport Actions from '../../actions/actions';\nimport { useTranslation } from \"../Translation\";\nimport styled from \"styled-components\";\nimport { controlButton } from \"../cssMixins\";\n\nconst Root = styled.div`\n    white-space: nowrap;\n    padding: 0 0.1em;\n\n    span {\n        opacity: 0.5;\n        display: inline-block;\n\n        &.active {\n            opacity: 1;\n        }\n    }\n`;\n\nconst CursorModeButtonGroup = () => {\n    const cursorMode = useSelector(get.cursorMode);\n    const dispatch = useDispatch();\n    const t = useTranslation();\n\n    return (\n        <Root data-djvujs-id=\"cursor_mode_buttons\">\n            <span title={t(\"Text cursor mode\")} className={cursorMode === Constants.TEXT_CURSOR_MODE ? \"active\" : null}>\n                <FaICursor\n                    css={controlButton}\n                    onClick={() => dispatch(Actions.setCursorModeAction(Constants.TEXT_CURSOR_MODE))}\n                />\n            </span>\n            <span title={t(\"Grab cursor mode\")} className={cursorMode === Constants.GRAB_CURSOR_MODE ? \"active\" : null}>\n                <FaRegHandPaper\n                    css={controlButton}\n                    onClick={() => dispatch(Actions.setCursorModeAction(Constants.GRAB_CURSOR_MODE))}\n                />\n            </span>\n        </Root>\n    );\n};\n\nexport default CursorModeButtonGroup;"
  },
  {
    "path": "viewer/src/components/Toolbar/HideButton.jsx",
    "content": "import styled, { css } from \"styled-components\";\nimport { FiChevronsDown } from \"react-icons/fi\";\n\nconst hiddenStyle = css`\n    bottom: calc(100% + var(--app-padding));\n    right: 0;\n    transform: rotate(180deg);\n    transition: transform 1s, bottom 0.5s, right 0.5s 0.5s;\n`;\n\nconst Root = styled.div`\n    --size: 28px;\n    width: var(--size);\n    height: var(--size);\n    font-size: calc(var(--size) * 0.7);\n    position: absolute;\n    z-index: 1;\n    background: var(--background-color);\n    border-radius: 100px;\n    border: 1px solid var(--color);\n    cursor: pointer;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    transition: transform 1s, bottom 0.5s 0.5s, right 0.5s;\n\n    right: 0;\n    bottom: calc(100% + var(--app-padding));\n    ${p => p.theme.appWidth > 400 ? `\n        right: 25%;\n        bottom: 50%;\n        transform: translateX(50%) translateY(50%);\n    ` : ''};\n\n    ${p => p.$hidden ? hiddenStyle : ''};\n`;\n\nexport default ({ isToolbarHidden, onClick }) => {\n    return (\n        <Root\n            $hidden={isToolbarHidden}\n            onClick={onClick}\n            data-djvujs-id=\"hide_button\"\n        >\n            <FiChevronsDown />\n        </Root>\n    );\n}"
  },
  {
    "path": "viewer/src/components/Toolbar/MenuButton.jsx",
    "content": "import { FiCommand } from \"react-icons/fi\";\nimport React from \"react\";\nimport { iconButton } from \"../cssMixins\";\nimport styled from \"styled-components\";\n\nconst Root = styled(FiCommand)`\n    ${iconButton};\n    font-size: 2em;\n    color: var(--highlight-color);\n    margin-left: ${p => p.theme.isMobile ? 0 : '1em'};\n`;\n\nexport default ({ onClick }) => {\n    return (\n        <Root onClick={onClick} data-djvujs-id=\"menu_button\" />\n    );\n}"
  },
  {
    "path": "viewer/src/components/Toolbar/PageNumber.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport styled from 'styled-components';\nimport { styledInput } from '../cssMixins';\n\nconst Root = styled.span`\n    flex: 0 0 auto;\n    min-width: 4em;\n    max-width: 8em;\n    height: 90%;\n    line-height: normal;\n    box-sizing: border-box;\n    white-space: nowrap;\n    position: relative;\n    text-align: center;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n\n    & > * {\n        text-align: center;\n        box-sizing: border-box;\n    }\n\n    & > input {\n        ${styledInput};\n        position: absolute;\n        left: 0;\n        top: 0;\n        width: 100%;\n        height: 100%;\n        z-index: 1;\n    }\n`;\n\nexport default class PageNumber extends React.Component {\n\n    static propTypes = {\n        pageNumber: PropTypes.number.isRequired,\n        pagesQuantity: PropTypes.number,\n        setNewPageNumber: PropTypes.func.isRequired\n    };\n\n    constructor(props) {\n        super(props);\n        this.state = {\n            isEditing: false,\n            tempValue: null\n        };\n    }\n\n    componentDidUpdate() { // тупо костыль, так как Firefox на autoFocus кидает Blur сразу же\n        if (this.input) {\n            setTimeout(() => {\n                try {\n                    if (this.input) {\n                        this.input.focus();\n                        this.input.select();\n                        this.input = null;\n                    }\n                } catch (e) { }\n            }, 10);\n        }\n    }\n\n    setNewPageNumber(number) {\n        if (!this.props.pagesQuantity) {\n            return;\n        }\n        if (number < 1) {\n            number = 1;\n        } else if (number > this.props.pagesQuantity) {\n            number = this.props.pagesQuantity;\n        }\n        if (number !== this.props.pageNumber) {\n            this.props.setNewPageNumber(number, true);\n        }\n    }\n\n    startPageNumberEditing = () => {\n        this.setState({ isEditing: true })\n    };\n\n    finishPageNumberEditing = (e) => {\n        this.setState({\n            isEditing: false,\n            tempValue: null\n        });\n        var value = +e.target.value;\n        this.setNewPageNumber(value);\n\n    };\n\n    onKeyDown = (e) => {\n        if (e.key === 'Enter') {\n            this.finishPageNumberEditing(e);\n        }\n    };\n\n    onChange = (e) => {\n        this.setState({ tempValue: e.target.value })\n    };\n\n    inputRef = node => {\n        this.input = node;\n    }\n\n    render() {\n        return (\n            <Root>\n                {this.state.isEditing ?\n                    <input\n                        onKeyDown={this.onKeyDown}\n                        onBlur={this.finishPageNumberEditing}\n                        type=\"number\"\n                        min=\"1\"\n                        onChange={this.onChange}\n                        value={this.state.tempValue === null ? this.props.pageNumber : this.state.tempValue}\n                        ref={this.inputRef}\n                    /> : null}\n                <span\n                    onClick={this.startPageNumberEditing}\n                    style={this.state.isEditing ? { visibility: 'hidden', zIndex: -1 } : null}\n                >\n                    {this.props.pageNumber + (this.props.pagesQuantity ? \" / \" + this.props.pagesQuantity : \"\")}\n                </span>\n            </Root>\n        )\n    }\n}"
  },
  {
    "path": "viewer/src/components/Toolbar/PageNumberBlock.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\nimport { FaRegArrowAltCircleLeft, FaRegArrowAltCircleRight } from \"react-icons/fa\";\n\nimport Actions from '../../actions/actions';\nimport PageNumberElement from './PageNumber';\nimport { get } from '../../reducers';\nimport { TranslationContext } from \"../Translation\";\nimport styled, { css } from 'styled-components';\nimport { controlButton } from \"../cssMixins\";\n\nconst Root = styled.div`\n    margin: 0 0.5em;\n    flex: 0 0 auto;\n    display: flex;\n    flex-wrap: nowrap;\n    justify-content: center;\n    align-items: center;\n    height: 100%;\n`;\n\nconst navButtonStyle = css`\n    ${controlButton};\n    margin: 0 0.1em;\n    border-radius: 100%;\n    cursor: pointer;\n\n    &:hover {\n        transform: scale(1.1);\n        box-shadow: 0 0 1px gray;\n    }\n\n    &:active {\n        background: #555;\n        color: white;\n    }\n`;\n\nclass PageNumberBlock extends React.Component {\n\n    static propTypes = {\n        pageNumber: PropTypes.number,\n        pagesQuantity: PropTypes.number\n    };\n\n    static contextType = TranslationContext;\n\n    setNewPageNumber(number, isNext = true) {\n        if (number >= 1 && number <= this.props.pagesQuantity) {\n            this.props.setNewPageNumber(number, true);\n        } else {\n            this.props.setNewPageNumber(isNext ? 1 : this.props.pagesQuantity, true);\n        }\n    }\n\n    onInputChange = (e) => {\n        this.setNewPageNumber(+e.target.value);\n    };\n\n    goToNextPage = () => {\n        this.setNewPageNumber(this.props.pageNumber + 1, true);\n    };\n\n    goToPrevPage = () => {\n        this.setNewPageNumber(this.props.pageNumber - 1, false);\n    };\n\n    render() {\n        const t = this.context;\n\n        return (\n            <Root\n                title={t(\"Click on the number to enter it manually\")}\n                data-djvujs-id=\"page_number_block\"\n            >\n                <FaRegArrowAltCircleLeft\n                    onClick={this.goToPrevPage}\n                    css={navButtonStyle}\n                />\n\n                <PageNumberElement {...this.props} />\n\n                <FaRegArrowAltCircleRight\n                    onClick={this.goToNextPage}\n                    css={navButtonStyle}\n                />\n            </Root>\n        );\n    }\n}\n\nexport default connect(state => {\n    return {\n        pageNumber: get.currentPageNumber(state),\n        pagesQuantity: get.pagesQuantity(state)\n    };\n},\n    {\n        setNewPageNumber: Actions.setNewPageNumberAction\n    }\n)(PageNumberBlock);"
  },
  {
    "path": "viewer/src/components/Toolbar/PinButton.jsx",
    "content": "import { TiPin } from 'react-icons/ti';\nimport styled from \"styled-components\";\nimport { useTranslation } from \"../Translation\";\n\nconst Root = styled(TiPin)`\n    font-size: calc(var(--button-basic-size) * 1.2);\n    margin-right: 1em;\n    cursor: pointer;\n    ${p => !p.$pinned ? 'transform: rotate(45deg)' : ''};\n\n    :hover {\n        transform: scale(1.1) ${p => !p.$pinned ? 'rotate(45deg)' : ''};\n    }\n`;\n\nexport default ({ isPinned, onClick }) => {\n    const t = useTranslation();\n\n    return (\n        <Root\n            $pinned={isPinned}\n            onClick={onClick}\n            data-djvujs-id=\"pin_button\"\n            title={t(isPinned ? 'Toolbar is always shown' : 'Toolbar automatically hides')}\n        />\n    );\n};"
  },
  {
    "path": "viewer/src/components/Toolbar/RotationControl.jsx",
    "content": "import React from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { FaUndo } from \"react-icons/fa\";\n\nimport Actions from '../../actions/actions';\nimport { get } from '../../reducers';\nimport { useTranslation } from \"../Translation\";\nimport styled from 'styled-components';\n\nconst Root = styled.span`\n    display: inline-flex;\n    align-items: center;\n    cursor: pointer;\n    margin: 0 0.5em;\n    text-align: center;\n\n    svg {\n        font-size: calc(var(--button-basic-size) * 0.7);\n    }\n\n    svg:first-child {\n        &:hover {\n            transform: scale(1.1);\n        }\n    }\n\n    svg:last-child {\n        transform: scale(-1, 1);\n\n        &:hover {\n            transform: scale(-1.1, 1.1);\n        }\n    }\n`;\n\nconst RotationControl = () => {\n    const dispatch = useDispatch();\n    const rotation = useSelector(get.pageRotation);\n    const t = useTranslation();\n\n    const rotateLeft = () => {\n        dispatch(Actions.setPageRotationAction(rotation ? rotation - 90 : 270));\n    };\n\n    const rotateRight = () => {\n        dispatch(Actions.setPageRotationAction(rotation === 270 ? 0 : rotation + 90));\n    };\n\n    return (\n        <Root data-djvujs-id=\"rotation_control\" title={t(\"Rotate the page\")}>\n            <FaUndo onClick={rotateLeft} />\n            <span css={`width: 2.5em;`}>{rotation}&deg;</span>\n            <FaUndo onClick={rotateRight} />\n        </Root>\n    );\n};\n\nexport default RotationControl;"
  },
  {
    "path": "viewer/src/components/Toolbar/ScaleGizmo.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\nimport Actions from '../../actions/actions';\nimport { FaPlus, FaMinus } from \"react-icons/fa\";\nimport { get } from '../../reducers';\nimport { TranslationContext } from \"../Translation\";\nimport styled from 'styled-components';\nimport { iconButton, styledInput } from '../cssMixins';\n\nconst Root = styled.span`\n    display: inline-flex;\n    flex-wrap: nowrap;\n    align-items: center;\n    justify-content: center;\n\n    svg {\n        ${iconButton};\n        font-size: calc(var(--button-basic-size) * 0.8);\n    }\n\n    input {\n        ${styledInput};\n        display: inline-block;\n        width: 3em;\n        margin: 0 0.5em;\n    }\n`;\n\nclass ScaleGizmo extends React.Component {\n\n    constructor(props) {\n        super(props);\n        this.state = { tempValue: null };\n    }\n\n    static propTypes = {\n        scale: PropTypes.number.isRequired,\n        setUserScale: PropTypes.func.isRequired\n    };\n\n    static contextType = TranslationContext;\n\n    increaseScale = (e) => {\n        e.preventDefault();\n        var newScale = Math.floor((Math.round(this.props.scale * 100) + 10) / 10) / 10;\n        this.props.setUserScale(newScale);\n    };\n\n    decreaseScale = (e) => {\n        e.preventDefault();\n        var newScale = Math.floor((Math.round(this.props.scale * 100) - 10) / 10) / 10;\n        this.props.setUserScale(newScale);\n    };\n\n    startEditing = (e) => {\n        e.target.select();\n    };\n\n    finishEditing = (e) => {\n        var res = /\\d+/.exec(e.target.value);\n        var number = res ? +res[0] : 1;\n        var newScale = Math.round(number) / 100;\n        this.props.setUserScale(newScale);\n        e.target.blur();\n        this.setState({ tempValue: null });\n    };\n\n    onKeyPress = (e) => {\n        if (e.key === 'Enter') {\n            this.finishEditing(e);\n        }\n    };\n\n    onChange = (e) => {\n        this.setState({ tempValue: e.target.value })\n    };\n\n    render() {\n        const currentValue = Math.round(this.props.scale * 100);\n        const t = this.context;\n\n        return (\n            <Root\n                title={t(\"You also can scale the page via Ctrl+MouseWheel\")}\n                data-djvujs-id=\"scale_gizmo\"\n            >\n                <FaMinus onClick={this.decreaseScale} />\n                <input\n                    onFocus={this.startEditing}\n                    onKeyPress={this.onKeyPress}\n                    onBlur={this.finishEditing}\n                    type=\"text\"\n                    value={this.state.tempValue === null ? currentValue + '%' : this.state.tempValue}\n                    onChange={this.onChange}\n                />\n                <FaPlus onClick={this.increaseScale} />\n            </Root>\n        );\n    }\n}\n\nexport default connect(state => {\n    return {\n        scale: get.userScale(state),\n    };\n}, {\n    setUserScale: Actions.setUserScaleAction\n})(ScaleGizmo);"
  },
  {
    "path": "viewer/src/components/Toolbar/Toolbar.jsx",
    "content": "import React from 'react';\nimport PageNumberBlock from './PageNumberBlock'\nimport ScaleGizmo from './ScaleGizmo';\nimport ViewModeButtons from './ViewModeButtons';\nimport CursorModeButtonGroup from './CursorModeButtonGroup';\nimport RotationControl from './RotationControl';\nimport styled, { css } from 'styled-components';\nimport ContentsButton from \"./ContentsButton\";\nimport FullPageViewButton from \"../misc/FullPageViewButton\";\nimport MenuButton from \"./MenuButton\";\nimport PinButton from \"./PinButton\";\nimport Menu from \"../Menu\";\nimport { useAppContext } from \"../AppContext\";\nimport HideButton from \"./HideButton\";\n\nconst toolbarHeight = '42px';\n\nconst mobileStyle = css`\n    font-size: 16px;\n    \n    & > * {\n        margin-right: 0;\n        margin-left: 0;\n    }\n`\n\nconst Root = styled.div`\n    position: relative;\n    flex: 0 0 auto;\n    border: 1px solid var(--border-color);\n    border-radius: 0 7px 0 7px;\n    padding: 7px 4px;\n    display: flex;\n    flex-wrap: nowrap;\n    justify-content: space-between;\n    align-items: center;\n    height: ${toolbarHeight};\n    box-sizing: border-box;\n    align-self: stretch;\n    margin-top: var(--app-padding);\n    z-index: 2;\n\n    font-size: 14px;\n    --button-basic-size: 1.5em;\n\n    margin-bottom: 0;\n    transition: margin-bottom 0.5s;\n    ${p => p.$hidden ? `margin-bottom: calc(-${toolbarHeight} - var(--app-padding) - 1px)` : ''}; // -1px just for cypress\n    \n    ${p => p.$mobile ? mobileStyle : ''};\n`;\n\nconst CentralPanel = styled.div`\n    height: 100%;\n    max-width: 45em;\n    margin: 0 auto;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    \n    & > * {\n        margin: 0 0.8em;\n    }\n`;\n\nconst RightPanel = styled.div`\n    height: 100%;\n    display: flex;\n    align-items: center;\n`;\n\nconst InvisibleLayer = styled.div`\n    position: absolute;\n    bottom: 0;\n    height: calc(${toolbarHeight} + var(--app-padding) * 2);\n    width: 100%;\n    z-index: 1;\n`;\n\nexport default () => {\n    const [pinned, setPinned] = React.useState(true);\n    const [autoHidden, setAutoHidden] = React.useState(false);\n    const [manuallyHidden, setManuallyHidden] = React.useState(false);\n    const [isMenuOpen, setIsMenuOpen] = React.useState(false);\n\n    const onMouseEnter = React.useCallback(() => setAutoHidden(false), [setAutoHidden]);\n    const onMouseLeave = React.useCallback(() => setAutoHidden(true), [setAutoHidden]);\n    const handlePin = React.useCallback(() => {\n        setPinned(!pinned);\n    }, [pinned, setPinned]);\n\n    const { isMobile } = useAppContext();\n    const reallyPinned = isMobile || pinned;\n    const reallyHidden = manuallyHidden && isMobile || autoHidden && !reallyPinned;\n\n    return (\n        <>\n            {reallyPinned ? null : <InvisibleLayer\n                onMouseEnter={onMouseEnter}\n                onMouseLeave={onMouseLeave}\n            />}\n            <Root\n                $hidden={reallyHidden}\n                onMouseEnter={reallyPinned ? null : onMouseEnter}\n                onMouseLeave={reallyPinned ? null : onMouseLeave}\n                data-djvujs-id=\"toolbar\"\n                $mobile={isMobile}\n            >\n                <ContentsButton />\n                <CentralPanel>\n                    {isMobile ? null : <ViewModeButtons />}\n                    {isMobile ? null : <CursorModeButtonGroup />}\n                    <PageNumberBlock />\n                    {isMobile ? null : <ScaleGizmo />}\n                    {isMobile ? null : <RotationControl />}\n                </CentralPanel>\n                <RightPanel data-djvujs-class=\"right_panel\">\n                    {isMobile ? null : <PinButton isPinned={pinned} onClick={handlePin} />}\n                    {isMobile ? null : <FullPageViewButton />}\n                    {isMobile ? <HideButton\n                        onClick={() => setManuallyHidden(!manuallyHidden)}\n                        isToolbarHidden={reallyHidden}\n                    /> : null}\n                    <MenuButton onClick={() => setIsMenuOpen(!isMenuOpen)} />\n                </RightPanel>\n                <Menu isOpened={isMenuOpen && !reallyHidden} onClose={() => setIsMenuOpen(false)} />\n            </Root>\n        </>\n    );\n}"
  },
  {
    "path": "viewer/src/components/Toolbar/ViewModeButtons.jsx",
    "content": "import React from 'react';\nimport { connect, useDispatch, useSelector } from 'react-redux';\nimport { FaRegFileAlt, FaRegFileImage, FaCaretLeft, FaCaretRight } from \"react-icons/fa\";\n\nimport { get } from '../../reducers';\nimport Constants, { ActionTypes } from '../../constants';\nimport { useTranslation } from '../Translation';\nimport styled from 'styled-components';\nimport { controlButton } from \"../cssMixins\";\nimport { ControlButton } from \"../StyledPrimitives\";\nimport { useAppContext } from '../AppContext.jsx';\n\nconst ContinuousScrollButton = styled.span`\n    ${controlButton};\n    display: inline-flex;\n    flex-direction: column;\n    flex-wrap: nowrap;\n    justify-content: center;\n    overflow: hidden;\n    max-height: 100%;\n\n    svg {\n        flex: 0 0 auto;\n    }\n`;\n\nconst ContinuousScrollButtonWrapper = styled.span`\n    height: 100%;\n    box-sizing: border-box;\n    display: inline-flex;\n    align-items: center;\n    opacity: 1;\n`;\n\nconst Root = styled.span`\n    display: inline-flex;\n    align-items: center;\n    height: calc(var(--button-basic-size) * 1.2);\n\n    & > span:not(${ContinuousScrollButtonWrapper}), ${ContinuousScrollButton} {\n        opacity: 0.5;\n    }\n`;\n\nconst PageCount = ({ value, max, onChange, title, style }) => {\n    return (\n        <span\n            title={title}\n            style={style}\n            css={`\n                display: inline-flex;\n                align-items: center;\n\n                svg {\n                    font-size: 2em;\n                    cursor: pointer;\n\n                    &[djvujs-disabled] {\n                        opacity: 0.5;\n                        cursor: not-allowed;\n                        pointer-events: none;\n                    }\n\n                    &:hover {\n                        transform: scale(1.1);\n                    }\n                }\n            `}>\n            <FaCaretLeft djvujs-disabled={value < 2 ? 1 : null} onClick={() => onChange(value - 1)} />\n            <span>{value}</span>\n            <FaCaretRight djvujs-disabled={value >= max ? 1 : null} onClick={() => onChange(value + 1)} />\n        </span>\n    );\n}\n\nconst ViewModeButtons = () => {\n    const dispatch = useDispatch();\n    const viewMode = useSelector(get.viewMode);\n    const isIndirect = useSelector(get.isIndirect);\n    const pageCountInRow = useSelector(get.pageCountInRow);\n    const firstRowPageCount = useSelector(get.firstRowPageCount);\n    const isContScroll = viewMode === Constants.CONTINUOUS_SCROLL_MODE;\n    const t = useTranslation();\n    const { isMobile } = useAppContext();\n\n    const enableContinuousScrollMode = () => {\n        dispatch({ type: ActionTypes.SET_VIEW_MODE, payload: Constants.CONTINUOUS_SCROLL_MODE });\n    };\n\n    const enableSinglePageMode = () => {\n        dispatch({ type: ActionTypes.SET_VIEW_MODE, payload: Constants.SINGLE_PAGE_MODE });\n    };\n\n    const enableTextMode = () => {\n        dispatch({ type: ActionTypes.SET_VIEW_MODE, payload: Constants.TEXT_MODE });\n    };\n\n    return (\n        <Root data-djvujs-id=\"view_mode_buttons\">\n                <span\n                    title={t(\"Text view mode\")}\n                    style={viewMode === Constants.TEXT_MODE ? { opacity: 1 } : null}\n                >\n                    <ControlButton\n                        as={FaRegFileAlt}\n                        onClick={enableTextMode}\n                    />\n                </span>\n            <span\n                title={t(\"Single page view mode\")}\n                style={viewMode === Constants.SINGLE_PAGE_MODE ? { opacity: 1 } : null}\n            >\n                    <ControlButton\n                        as={FaRegFileImage}\n                        onClick={enableSinglePageMode}\n                    />\n                </span>\n            {isIndirect ? null :\n                <ContinuousScrollButtonWrapper>\n                    <ContinuousScrollButton\n                        style={isContScroll ? { opacity: 1 } : null}\n                        title={t(\"Continuous scroll view mode\")}\n                        onClick={enableContinuousScrollMode}\n                    >\n                        <FaRegFileImage />\n                        <FaRegFileImage />\n                    </ContinuousScrollButton>\n                    {isMobile ? null : <>\n                        <PageCount\n                            style={!isContScroll ? { visibility: 'hidden' } : null}\n                            title={t(\"Number of pages in a row\")}\n                            max={Constants.MAX_PAGE_COUNT_IN_ROW}\n                            value={pageCountInRow}\n                            onChange={(value) => dispatch({\n                                type: ActionTypes.UPDATE_OPTIONS,\n                                payload: {\n                                    pageCountInRow: value,\n                                    firstRowPageCount: Math.min(\n                                        firstRowPageCount === pageCountInRow ? value : firstRowPageCount,\n                                        value\n                                    ),\n                                }\n                            })}\n                        />\n                        <PageCount\n                            style={!isContScroll ? { visibility: 'hidden' } : null}\n                            title={t(\"Number of pages in the first row\")}\n                            max={Math.min(Constants.MAX_PAGE_COUNT_IN_ROW, pageCountInRow)}\n                            value={firstRowPageCount}\n                            onChange={(value) => dispatch({\n                                type: ActionTypes.UPDATE_OPTIONS,\n                                payload: {\n                                    firstRowPageCount: value,\n                                }\n                            })}\n                        />\n                    </>}\n                </ContinuousScrollButtonWrapper>\n            }\n        </Root>\n    );\n};\n\nexport default connect(state => ({\n    viewMode: get.viewMode(state),\n    isIndirect: get.isIndirect(state),\n    pageCountInRow: get.pageCountInRow(state),\n    firstRowPageCount: get.firstRowPageCount(state),\n}))(ViewModeButtons);"
  },
  {
    "path": "viewer/src/components/Translation.jsx",
    "content": "import React from 'react';\nimport { useSelector } from \"react-redux\";\nimport { get } from \"../reducers\";\nimport dictionaries from '../locales';\n\nexport const TranslationContext = React.createContext((text, insertions = null) => text);\n\nexport const TranslationProvider = ({ children }) => {\n    const dict = useSelector(get.dictionary);\n\n    return (\n        <TranslationContext.Provider value={createTranslator(dict)}>\n            {children}\n        </TranslationContext.Provider>\n    );\n};\n\nexport const useTranslation = () => {\n    return React.useContext(TranslationContext);\n};\n\nconst escapingRegex = /[.*+\\-?^${}()|[\\]\\\\]/g;\nconst escapeRegExp = (string) => string.replace(escapingRegex, '\\\\$&');\nconst untranslatedPhrases = {};\nlet warningTimeout = 0;\n\nexport function createTranslator(dict) {\n    return (text, insertions = null) => {\n        const translatedText = dict[text] || dictionaries.en[text] || text;\n\n        if (!dictionaries.en[text]) {\n            untranslatedPhrases[text] = \"\";\n            clearTimeout(warningTimeout);\n            warningTimeout = setTimeout(() => {\n                console.warn(`\\nThere are untranslated phrases (missing from the English dictionary):`);\n                console.warn('\\n' + JSON.stringify(untranslatedPhrases, null, 2)\n                    .replaceAll('\"\"', '\\n      \"\"'));\n            }, 1000); // timeout to collect many phrases to show them as JSON\n        }\n\n        if (!insertions) return translatedText;\n\n        const st = Object.keys(insertions).map(escapeRegExp).join('|');\n        const regex = new RegExp(`(${st})`, 'g');\n\n        const textParts = translatedText.split(regex);\n\n        const textWithInsertions = textParts.map(entry => {\n            if (entry in insertions) {\n                return <React.Fragment key={entry}>{insertions[entry]}</React.Fragment>;\n            } else {\n                return entry;\n            }\n        });\n\n        return textWithInsertions;\n    };\n}"
  },
  {
    "path": "viewer/src/components/cssMixins.js",
    "content": "import { css } from \"styled-components\";\n\nexport const iconButton = css`\n    cursor: pointer;\n    flex: 0 0 auto;\n\n    &:hover {\n        transform: scale(1.1);\n    }\n`;\n\nexport const controlButton = css`\n    ${iconButton};\n    font-size: var(--button-basic-size);\n    margin: 0 0.5em;\n`;\n\nexport const styledInput = css`\n    background: var(--background-color);\n    border: 1px solid var(--border-color);\n    border-radius: 2px;\n    color: var(--color);\n`;"
  },
  {
    "path": "viewer/src/components/helpers.js",
    "content": "import DjVu from \"../DjVu\";\n\n/**\n * Delays execution of a callback for a while in order to wait whether there will be one more such an event,\n * and if there is one more event, the only last event is processed. However, a handler is invoked not less frequently than\n * once within maxDelayTime. Good optimization for scroll events, since there are many of them.\n */\nexport const createDeferredHandler = (callback, delayTime = 100, maxDelayTime = 500) => {\n    let firstTimestamp = null;\n    let timeout = null;\n    const wrappedCallback = e => {\n        firstTimestamp = null;\n        timeout = null;\n        callback(e);\n    }\n\n    return e => {\n        if (firstTimestamp && (e.timeStamp - firstTimestamp) > maxDelayTime) {\n            timeout && clearTimeout(timeout);\n            wrappedCallback(e);\n        } else {\n            if (!firstTimestamp) {\n                firstTimestamp = e.timeStamp;\n            }\n            timeout && clearTimeout(timeout);\n            timeout = setTimeout(wrappedCallback, delayTime, e);\n        }\n    };\n};\n\nexport function getHeaderAndErrorMessage(t, error) {\n    let header, message, isJSON = false;\n    switch (error.code) {\n        case DjVu.ErrorCodes.NETWORK_ERROR:\n            header = t('Network error');\n            message = t('Check your network connection');\n            break;\n\n        case DjVu.ErrorCodes.UNSUCCESSFUL_REQUEST:\n            header = t('Web request error');\n            switch (error.status) {\n                case 404:\n                    message = t('404 Document not found');\n                    break;\n                case 403:\n                    message = t('403 Access forbidden');\n                    break;\n                case 500:\n                    message = t('500 Internal server error');\n                    break;\n\n                default:\n                    message = t('The request failed with HTTP status #status', { '#status': error.status });\n            }\n            break;\n\n        case DjVu.ErrorCodes.FILE_IS_CORRUPTED:\n            header = t('DjVu file is corrupted');\n            message = t(\"The file doesn't comply with the DjVu format specification or it's not a whole DjVu document\");\n            break;\n\n        case DjVu.ErrorCodes.INCORRECT_FILE_FORMAT:\n            header = t('Incorrect file format');\n            message = t('The provided file is not a DjVu document');\n            break;\n\n        case DjVu.ErrorCodes.NO_SUCH_PAGE:\n            header = t('Incorrect page number');\n            message = t('There is no page with the number #pageNumber', { '#pageNumber': error.pageNumber });\n            break;\n\n        case DjVu.ErrorCodes.NO_BASE_URL:\n            header = t('No base URL for an indirect DjVu document');\n            message = t('You probably opened an indirect (multi-file) DjVu document manually.') + '\\n' +\n                t('But such multi-file documents can be only loaded by URL.');\n            break;\n\n        default: {\n            header = t('Unexpected error');\n            try {\n                message = JSON.stringify(error, null, 2);\n                isJSON = true;\n            } catch {\n                message = t('Cannot print the error, look in the console');\n            }\n        }\n    }\n\n    return { header, message, isJSON };\n}"
  },
  {
    "path": "viewer/src/components/misc/CloseButton.jsx",
    "content": "import React from \"react\";\nimport { iconButton } from \"../cssMixins\";\nimport { FaRegTimesCircle } from \"react-icons/fa\";\n\nexport default ({ onClick, className = null }) => {\n    return (\n        <FaRegTimesCircle\n            className={className}\n            css={iconButton}\n            onClick={onClick}\n            data-djvujs-class=\"close_button\"\n        />\n    );\n};"
  },
  {
    "path": "viewer/src/components/misc/FullPageViewButton.jsx",
    "content": "import React from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { FaExpand, FaCompress } from \"react-icons/fa\";\n\nimport Actions from '../../actions/actions';\nimport { get } from '../../reducers';\nimport { useTranslation } from '../Translation';\nimport { ControlButton } from \"../StyledPrimitives\";\n\nconst FullPageViewButton = () => {\n    const { hideFullPageSwitch } = useSelector(get.uiOptions);\n    const isFullPageView = useSelector(get.isFullPageView);\n    const dispatch = useDispatch();\n    const t = useTranslation();\n\n    if (hideFullPageSwitch) return null;\n\n    return (\n        <div title={t(\"Switch full page mode\")} data-djvujs-class=\"full_page_button\">\n            <ControlButton\n                as={isFullPageView ? FaCompress : FaExpand}\n                onClick={() => dispatch(Actions.toggleFullPageViewAction(!isFullPageView))}\n            />\n        </div>\n    );\n};\n\nexport default FullPageViewButton;"
  },
  {
    "path": "viewer/src/components/misc/FullscreenButton.jsx",
    "content": "import React from \"react\";\nimport styled from \"styled-components\";\nimport { IoDesktopOutline } from \"react-icons/io5\";\nimport { iconButton } from \"../cssMixins\";\nimport { useAppContext } from \"../AppContext\";\nimport { useTranslation } from \"../Translation\";\n\nconst FullscreenButton = styled(IoDesktopOutline)`\n    ${iconButton};\n    font-size: 1.1em;\n\n    color: ${p => p.$active ? 'var(--highlight-color)' : 'inherit'};\n`;\n\nexport default ({ className = null }) => {\n    const { isFullscreen, toggleFullscreen } = useAppContext();\n    const t = useTranslation();\n\n    return (\n        <FullscreenButton\n            className={className}\n            data-djvujs-class=\"fullscreen_button\"\n            title={t('Fullscreen mode')}\n            $active={isFullscreen}\n            onClick={toggleFullscreen}\n        />\n    );\n};"
  },
  {
    "path": "viewer/src/components/misc/HelpButton.jsx",
    "content": "import React from 'react';\nimport { useDispatch } from 'react-redux';\nimport { FaRegQuestionCircle } from \"react-icons/fa\";\n\nimport Actions from '../../actions/actions';\nimport { useTranslation } from \"../Translation\";\nimport { controlButton } from \"../cssMixins\";\nimport { ControlButtonWrapper } from '../StyledPrimitives';\n\nconst HelpButton = ({ withLabel = null, onClick = () => {} }) => {\n    const dispatch = useDispatch();\n    const t = useTranslation();\n\n    return (\n        <ControlButtonWrapper\n            title={t(\"Show help window\")}\n            data-djvujs-class=\"help_button\"\n            onClick={() => {\n                dispatch(Actions.showHelpWindowAction());\n                onClick();\n            }}\n        >\n            <FaRegQuestionCircle css={controlButton} />\n            {withLabel ? <span>{t('About')}</span> : null}\n        </ControlButtonWrapper>\n    );\n};\n\nexport default HelpButton;"
  },
  {
    "path": "viewer/src/components/misc/LoadingPhrase.jsx",
    "content": "import React from 'react';\nimport { useTranslation } from '../Translation';\nimport { Spinner } from \"../StyledPrimitives\";\nimport styled from \"styled-components\";\n\nconst Root = styled.div`\n    display: flex;\n    align-items: center;\n\n    span {\n        margin-left: 0.5em;\n    }\n`;\n\nexport default ({ style, className }) => {\n    const t = useTranslation();\n\n    return (\n        <Root style={style} className={className}>\n            <Spinner />\n            <span>{t('Loading')}...</span>\n        </Root>\n    );\n};"
  },
  {
    "path": "viewer/src/components/misc/OptionsButton.jsx",
    "content": "import React from 'react';\nimport { useDispatch } from 'react-redux';\nimport { FaCog } from \"react-icons/fa\";\n\nimport { useTranslation } from \"../Translation\";\nimport { ControlButton, ControlButtonWrapper } from '../StyledPrimitives';\nimport { ActionTypes } from \"../../constants\";\n\nconst OptionsButton = ({ withLabel = false, onClick = () => {} }) => {\n    const dispatch = useDispatch();\n    const t = useTranslation();\n\n    return (\n        <ControlButtonWrapper\n            title={t(\"Show options window\")}\n            data-djvujs-class=\"options_button\"\n            onClick={() => {\n                dispatch({ type: ActionTypes.TOGGLE_OPTIONS_WINDOW, payload: true })\n                onClick();\n            }}\n        >\n            <ControlButton as={FaCog} />\n            {withLabel ? <span>{t('Options')}</span> : null}\n        </ControlButtonWrapper>\n    );\n};\n\nexport default OptionsButton;"
  },
  {
    "path": "viewer/src/components/misc/ProgressBar.jsx",
    "content": "import React from \"react\";\nimport styled from \"styled-components\";\n\nconst ProgressBar = styled.div`\n    border: 1px solid var(--color);\n    width: 25em;\n    max-width: 90%;\n    height: 3px;\n    margin-top: 0.5em;\n\n    div:first-child {\n        background: var(--color);\n        height: 100%;\n    }\n`;\n\nexport default ({ percentage, className }) => (\n    <ProgressBar className={className}>\n        <div style={{ width: percentage + \"%\" }} />\n    </ProgressBar>\n);"
  },
  {
    "path": "viewer/src/components/misc/SaveButton.jsx",
    "content": "import React from \"react\";\nimport { ControlButton, ControlButtonWrapper } from \"../StyledPrimitives\";\nimport { FaDownload } from \"react-icons/fa\";\nimport SaveNotification from \"./SaveNotification\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { get } from \"../../reducers\";\nimport Actions from \"../../actions/actions\";\nimport { useTranslation } from \"../Translation\";\n\nexport default ({ withLabel = false, onClick = () => {} }) => {\n    const t = useTranslation();\n    const dispatch = useDispatch();\n    const [isNotificationShown, showNotification] = React.useState(false);\n    const { onSaveNotification } = useSelector(get.uiOptions);\n    const saveHandler = () => dispatch(Actions.tryToSaveDocument());\n\n    return (\n        <>\n            <ControlButtonWrapper\n                title={t(\"Save document\")}\n                onClick={() => {\n                    if (onSaveNotification && onSaveNotification.text) {\n                        showNotification(true);\n                    } else {\n                        saveHandler();\n                    }\n                    onClick();\n                }}\n            >\n                <ControlButton as={FaDownload} />\n                {withLabel ? <span>{t('Save')}</span> : null}\n            </ControlButtonWrapper>\n            {isNotificationShown ?\n                <SaveNotification onSave={saveHandler} onClose={() => showNotification(false)} /> : null}\n        </>\n    );\n};"
  },
  {
    "path": "viewer/src/components/misc/SaveNotification.jsx",
    "content": "import React from \"react\";\nimport ModalWindow from \"../ModalWindows/ModalWindow\";\nimport { TextButton } from \"../StyledPrimitives\";\nimport { useSelector } from \"react-redux\";\nimport { get } from \"../../reducers\";\nimport styled from \"styled-components\";\n\nconst SaveNotification = styled.div`\n    padding: 1em;\n`;\n\nconst ButtonBlock = styled.div`\n    margin-top: 1em;\n    display: flex;\n    justify-content: space-around;\n\n    ${TextButton} {\n        font-size: 0.8em;\n    }\n`;\n\nexport default ({ onSave = () => {}, onClose = () => {} }) => {\n    const { onSaveNotification } = useSelector(get.uiOptions);\n\n    return (\n        <ModalWindow\n            onClose={() => {\n                if (!onSaveNotification.yesButton && !onSaveNotification.noButton) {\n                    onSave();\n                }\n                onClose();\n            }}\n            usePortal={true}\n        >\n            <SaveNotification>\n                <div>{onSaveNotification.text}</div>\n                <ButtonBlock>\n                    {onSaveNotification.yesButton ?\n                        <TextButton\n                            onClick={() => {\n                                onClose();\n                                onSave();\n                            }}\n                        >\n                            {onSaveNotification.yesButton}\n                        </TextButton> : null}\n                    {onSaveNotification.noButton ?\n                        <TextButton onClick={onClose}>\n                            {onSaveNotification.noButton}\n                        </TextButton> : null}\n                </ButtonBlock>\n            </SaveNotification>\n        </ModalWindow>\n    )\n};"
  },
  {
    "path": "viewer/src/constants/Constants.js",
    "content": "const Constants = {\n    TRANSLATION_PAGE_URL: \"https://github.com/RussCoder/djvujs/blob/master/TRANSLATION.md\",\n    DEFAULT_DPI: 100,\n    TEXT_CURSOR_MODE: null,\n    GRAB_CURSOR_MODE: null,\n    MAX_PAGE_COUNT_IN_ROW: 5,\n\n    TEXT_MODE: 'text',\n    CONTINUOUS_SCROLL_MODE: 'continuous',\n    SINGLE_PAGE_MODE: 'single',\n\n    SET_CURSOR_MODE_ACTION: null,\n    ERROR_ACTION: null,\n    CREATE_DOCUMENT_FROM_ARRAY_BUFFER_ACTION: null,\n    CONTENTS_IS_GOTTEN_ACTION: null,\n    ARRAY_BUFFER_LOADED_ACTION: null,\n    IMAGE_DATA_RECEIVED_ACTION: null,\n    DOCUMENT_CREATED_ACTION: null,\n    SET_NEW_PAGE_NUMBER_ACTION: null,\n    SET_PAGE_BY_URL_ACTION: null,\n    TOGGLE_FULL_PAGE_VIEW_ACTION: null,\n    PAGE_TEXT_FETCHED_ACTION: null,\n    SET_USER_SCALE_ACTION: null,\n    START_FILE_LOADING_ACTION: null,\n    END_FILE_LOADING_ACTION: null,\n    FILE_LOADING_PROGRESS_ACTION: null,\n    SHOW_HELP_WINDOW_ACTION: null,\n    CLOSE_HELP_WINDOW_ACTION: null,\n    CLOSE_DOCUMENT_ACTION: null,\n    SET_PAGE_ROTATION_ACTION: null,\n    PAGE_ERROR_ACTION: null,\n    PAGE_IS_LOADED_ACTION: null,\n    PAGES_SIZES_ARE_GOTTEN: null,\n    DROP_PAGE_ACTION: null,\n    DROP_ALL_PAGES_ACTION: null,\n\n    SET_API_CALLBACK_ACTION: null, // A special action for interaction with sagas. Used for program API of the viewer, look at the DjVuViewer.js\n};\n\n/**\n * @template T\n * @param {T} obj\n * @returns {Readonly<{[prop in keyof T]: string|number}>}\n */\nexport function constant(obj) {\n    for (const key in obj) {\n        if (obj[key] === null) {\n            obj[key] = key;\n        }\n    }\n    return Object.freeze(obj);\n}\n\nexport default constant(Constants);"
  },
  {
    "path": "viewer/src/constants/actionTypes.js",
    "content": "import { constant } from './Constants';\n\nexport const ActionTypes = constant({\n    LOAD_DOCUMENT_BY_URL: null,\n    CONFIGURE: null,\n    LOAD_DOCUMENT: null,\n    UPDATE_OPTIONS: null,\n    SET_IMAGE_PAGE_ERROR: null,\n    SET_TEXT_PAGE_ERROR: null,\n    START_TO_BUNDLE: null,\n    FINISH_TO_BUNDLE: null,\n    CLOSE_SAVE_DIALOG: null,\n    OPEN_SAVE_DIALOG: null,\n    UPDATE_FILE_PROCESSING_PROGRESS: null,\n    ERROR: null,\n    SAVE_DOCUMENT: null,\n    SET_UI_OPTIONS: null,\n    TOGGLE_OPTIONS_WINDOW: null,\n    CLOSE_ERROR_WINDOW: null,\n    DESTROY: null,\n    OPEN_PRINT_DIALOG: null,\n    CLOSE_PRINT_DIALOG: null,\n    PREPARE_PAGES_FOR_PRINTING: null,\n    START_PRINTING: null,\n    UPDATE_PRINT_PROGRESS: null,\n    CLOSE_CONTENTS: null,\n    TOGGLE_CONTENTS: null,\n    PIN_TOOLBAR: null,\n    UNPIN_TOOLBAR: null,\n    UPDATE_APP_CONTEXT: null,\n    SET_VIEW_MODE: null,\n});"
  },
  {
    "path": "viewer/src/constants/index.js",
    "content": "export { default } from './Constants';\nexport * from './Constants';\nexport * from './actionTypes';"
  },
  {
    "path": "viewer/src/hotkeys.js",
    "content": "import Actions from './actions/actions';\nimport { ActionTypes } from './constants/index';\nimport { get } from './reducers';\n\nexport default function initHotkeys(store) {\n    document.addEventListener('keydown', (e) => {\n        if (!get.isDocumentLoaded(store.getState())) return;\n\n        if ((e.key === 's' || e.code === 'KeyS') && e.ctrlKey) { // code property isn't supported in Edge yet \n            e.preventDefault();\n            store.dispatch(Actions.tryToSaveDocument());\n            return;\n        }\n\n        if (e.key === 'ArrowRight') {\n            e.preventDefault();\n            store.dispatch(Actions.goToNextPageAction());\n            return;\n        }\n\n        if (e.key === 'ArrowLeft') {\n            e.preventDefault();\n            store.dispatch(Actions.goToPreviousPageAction());\n            return;\n        }\n\n        if (e.code === 'KeyT' && e.altKey) {\n            e.preventDefault();\n            const { theme } = get.options(store.getState());\n            store.dispatch({ type: ActionTypes.UPDATE_OPTIONS, payload: { theme: theme === 'light' ? 'dark' : 'light' } });\n            return;\n        }\n    });\n}"
  },
  {
    "path": "viewer/src/index.js",
    "content": "import DjVu from './DjVu';\nimport DjVuViewer from './DjVuViewer';\n\nDjVu.Viewer = DjVuViewer;\n\nif (process.env.NODE_ENV !== 'production') {\n    window.addEventListener('load', () => {\n        if (new URLSearchParams(location.search).get('tests')) return; // do nothing in case of end-to-end tests\n\n        window.viewer = window.DjVuViewerInstance = new window.DjVu.Viewer({\n            uiOptions: {\n                // showContentsAutomatically: false,\n                // changePageOnScroll: false,\n                // onSaveNotification: {\n                //     text: \"Doing this you agree with something else\",\n                //     yesButton: \"Maybe\",\n                //     noButton: \"Never!\",\n                // }\n                // hideSaveButton: true,\n                // hideOpenAndCloseButtons: true,\n                // hidePrintButton: true,\n            }\n        });\n        window.DjVuViewerInstance.render(document.getElementById('root'));\n\n        window.DjVuViewerInstance.loadDocumentByUrl(\"/DjVu3Spec.djvu\");\n        //window.DjVuViewerInstance.loadDocumentByUrl(\"/DjVu3Spec_indirect/index.djvu\", { viewMode: 'continuous' });\n\n        //window.DjVuViewerInstance.loadDocumentByUrl(\"/czech_indirect/index.djvu\", { pageRotation: 0, djvuOptions: {baseUrl: '/czech_indirect/'} });\n        //window.DjVuViewerInstance.loadDocumentByUrl(\"/tmp/DjVu3Spec.djvu\").then(() => window.DjVuViewerInstance.configure({pageRotation: 270}));\n\n        // Tests for file names from Content-Disposition header\n        //window.DjVuViewerInstance.loadDocumentByUrl(\"/djvufile?fname=%E5%9C%B0%E5%9B%BE.djvu\");\n        //window.DjVuViewerInstance.loadDocumentByUrl(\"/djvufile?fname=%E5%9C%B0%E5%9B%BE.djvu&cd=attachment\");\n\n        //window.DjVuViewerInstance.loadDocumentByUrl(\"http://localhost/djvuMap/obs-thats-an-error\");\n    });\n}"
  },
  {
    "path": "viewer/src/locales/ChineseSimplified.js",
    "content": "/**\n * Some phrases contain insertions, e.g. icons and buttons, which are inserted in the code.\n * Here instead of visual components we use placeholders, e.g. #helpButton, which start with #.\n * Your translated phrase MUST also contain the same placeholder, but you can change its position.\n *\n * Some phrases are tooltips, that is, they are visible only when you hover the cursor over controls.\n *\n * Preserve the order of phrases and put the translation on a new line.\n * (for convenience of further additions and corrections).\n *\n * All null values mean that the corresponding strings need to be translated.\n * Such values are added automatically for convenience as placeholders.\n */\n\nexport default {\n    // language info\n    englishName:\n        \"Simplified Chinese\",\n    nativeName:\n        \"简体中文\",\n\n    \"Language\":\n        \"语言\", // not used now, but will be used in options afterwards\n\n    // Translation: tooltips and notification\n    // (to see the notification window, remove several phrases from any dictionary, except for the English one)\n    \"Add more\":\n        \"添加更多\",\n    \"The translation isn't complete.\":\n        \"翻译尚未完成。\",\n    \"The following phrases are not translated:\":\n        \"以下短语尚未被翻译\",\n    \"You can improve the translation here\":\n        \"你可以在这里完善翻译\",\n\n    // Initial screen\n    \"#helpButton - learn more about the app\":\n        \"#helpButton - 了解更多\",\n    \"#optionsButton - see the available options\":\n        \"#optionsButton - 查看所有选项\",\n    \"powered with\":\n        \"基于\",\n    \"Drag & Drop a file here or click to choose manually\":\n        \"将文件拖拽至此处或者单击手动选择\",\n    \"Paste a URL to a djvu file here\":\n        \"在此处粘贴djvu文件的地址\",\n    \"Open URL\":\n        \"打开地址\",\n    'Enter a valid URL (it should start with \"http(s)://\" | \"data:\")': // an alert shown when you try to open an empty URL\n        '输入一个有效的地址 (应当以 \"http(s)://\" | \"data:\" 开头)',\n\n    // Errors. Usually there is a header and a message for each error type.\n    // For the web request error there are different types of messages depending on the HTTP status.\n    // The ways to see the errors in the viewer are described in comments below.\n    // In case of web requests you can load links via the browser extension (via the URL field on the initial screen)\n    \"Error\":\n        \"错误\",\n    \"Error on page\":\n        \"页面错误\", // Open 'library/assets/czech_indirect/index.djvu\n    \"Network error\":\n        \"网络错误\", // Disable internet connection and try to load something by URL\n    \"Check your network connection\":\n        \"检查你的网络连接\",\n    // Load any URL to a nonexistent page on the Internet,\n    // e.g. https://djvu.js.org/nonexistentpage\n    \"Web request error\":\n        \"网络请求错误\",\n    \"404 Document not found\":\n        \"404 文件未找到\",\n    \"403 Access forbidden\":\n        \"403 禁止访问\",\n    \"500 Internal server error\":\n        \"500 服务器内部错误\",\n    \"The request failed with HTTP status #status\":\n        \"请求失败，HTTP状态 #status\",\n    \"DjVu file is corrupted\": // Open \"/library/assets/czech_indirect/dict0085.iff\"\n        \"DjVu文件损坏\",\n    \"The file doesn't comply with the DjVu format specification or it's not a whole DjVu document\":\n        \"文件不满足DjVu格式要求或不完整\",\n    \"Incorrect file format\": // Open a not-djvu file.\n        \"文件格式错误\",\n    \"The provided file is not a DjVu document\":\n        \"提供的文件不是一个DjVu文件\",\n    // Load a URL to a DjVu file with \"#page=100500\" at the end (both in continuous scroll and single-page view modes)\n    // e.g. https://djvu.js.org/assets/djvu_examples/DjVu3Spec.djvu#page=100500\n    \"Incorrect page number\":\n        \"页码错误\",\n    \"There is no page with the number #pageNumber\":\n        \"没有页面为#pageNumber的页面\",\n    // \"baseURL\" is a URL to a document directory,\n    // all links inside the document index.djvu are considered relative to this URL.\n    // The term \"base URL\" can be translated as \"a URL to the document's folder\".\n    \"No base URL for an indirect DjVu document\":  // Open \"/library/assets/czech_indirect/index.djvu\"\n        \"相对路径的DjVu文档缺少根路径\",\n    \"You probably opened an indirect (multi-file) DjVu document manually.\":\n        \"你很有可能手动打开了一个（多文件）相对路径DjVu文档。\",\n    \"But such multi-file documents can be only loaded by URL.\":\n        \"但是这种多文件文档只能通过完整路径打开。\",\n    \"Unexpected error\": // Of course there is no standard way to produce this kind of error\n        \"未知错误\",\n    \"Cannot print the error, look in the console\":\n        \"无法打印错误，请查看控制台\",\n\n    // Options and its tooltips\n    \"Options\":\n        \"选项\",\n    \"Show options window\":\n        \"显示选项窗口\",\n    \"Color theme\":\n        \"颜色主题\",\n    \"Extension options\":\n        \"扩展选项\", // the options of the browser extension\n    \"Open all links with .djvu at the end via the viewer\":\n        \"通过阅读器打开所有以.djvu结尾的链接\",\n    \"All links to .djvu files will be opened by the viewer via a simple click on a link\":\n        \"所有.djvu文件链接将会被阅读器打开\",\n    \"Detect .djvu files by means of http headers\":\n        \"通过http头信息检测.djvu文件\",\n    \"Analyze headers of every new tab in order to process even links which do not end with the .djvu extension\":\n        \"分析每一个新选项卡以处理不是以.djvu结尾的链接\",\n\n    // Footer: status bar\n    \"Ready\":\n        \"就绪\",\n    \"Loading\":\n        \"加载中\",\n\n    // Footer: buttons' tooltips\n    \"Show help window\":\n        \"显示帮助窗口\",\n    \"Switch full page mode\":\n        \"切换全屏模式\",\n\n    // File Block tooltips\n    \"Choose a file\":\n        \"选择文件\",\n    \"Close document\":\n        \"关闭文档\",\n    \"Save document\":\n        \"保存文档\",\n    \"Save\":\n        \"保存\",\n    \"Open another .djvu file\":\n        \"打开另一个.djvu文件\",\n\n    // Help window\n    \"The application for viewing .djvu files in the browser.\":\n        \"这是一个在浏览器中浏览.djvu文件的应用。\",\n    \"If something doesn't work properly, feel free to write about the problem at #email.\":\n        \"如果运行有问题，请向#email反馈。\",\n    \"The official website is #website.\":\n        \"官方网站是#website。\",\n    \"The source code is available on #link.\":\n        \"源代码位于#link。\",\n    \"Hotkeys\":\n        \"热键\",\n    \"save the document\":\n        \"保存文档\",\n    \"go to the previous page\":\n        \"上一页\",\n    \"go to the next page\":\n        \"下一页\",\n    \"Controls\":\n        \"控件\",\n    \"#expandIcon and #collapseIcon are to switch the viewer to the full page mode and back.\":\n        \"#expandIcon 和 #collapseIcon 用于将阅读器在全屏模式和正常模式间切换。\",\n    \"If you work with the browser extension, these buttons will cause no effect, since the viewer takes the whole page by default.\":\n        \"如果你是在使用浏览器插件，则这些按钮将不起作用，因为阅读器会默认使用整个页面。\",\n\n    // Toolbar tooltips\n    \"Continuous scroll view mode\":\n        \"连续滚动模式\",\n    \"Number of pages in a row\":\n        \"一排的页数\",\n    \"Number of pages in the first row\":\n        \"第一排的页数\",\n    \"Single page view mode\":\n        \"单页模式\",\n    \"Text view mode\":\n        \"文本模式\",\n    \"Click on the number to enter it manually\":\n        \"点击数字以手动输入\",\n    \"Rotate the page\":\n        \"旋转页面\",\n    \"You also can scale the page via Ctrl+MouseWheel\":\n        \"你也可以通过Ctrl+鼠标滚轮来缩放页面\",\n    \"Text cursor mode\":\n        \"文本选择模式\",\n    \"Grab cursor mode\":\n        \"拖拽模式\",\n    \"Table of contents\":\n        \"目录\",\n    \"Toolbar is always shown\":\n        \"工具栏常显示\",\n    \"Toolbar automatically hides\":\n        \"工具栏自动隐藏\",\n\n    // Contents\n    \"Contents\":\n        \"目录\",\n    \"No contents provided\":\n        \"没有目录\",\n    // A rare case. Open /library/assets/links.djvu in the viewer on https://djvu.js.org/ (not in the extension!)\n    // and click the \"Absolute Link\" in the contents\n    \"The link points to another document. Do you want to proceed?\":\n        \"这个链接指向另一个文档，你确定要继续吗？\",\n\n    // Text Block (shown in the text view mode)\n    \"No text on this page\":\n        \"该页没有文本\",\n\n    // Save dialog (shows when you save an indirect djvu)\n    \"You are trying to save an indirect (multi-file) document.\":\n        \"你在试图保存相对路径的多文件文档。\",\n    \"What exactly do you want to do?\":\n        \"你具体是想做什么？\",\n    \"Save only index file\":\n        \"只保存索引文件\",\n    \"Download, bundle and save the whole document as one file\":\n        \"下载打包并保存成一个文件\",\n    \"Downloading and bundling the document\":\n        \"正在下载并打包文件\",\n    \"The document has been downloaded and bundled into one file successfully\":\n        \"文件已成功下载打包到一个文件\",\n\n    // Printing\n    \"Print document\":\n        \"打印文档\",\n    \"Pages must be rendered before printing.\":\n        \"打印前需渲染页面，\",\n    \"It may take a while.\":\n        \"可能需要一会儿。\",\n    \"Select the pages you want to print.\":\n        \"选择你想打印的页面。\",\n    \"From\":\n        \"从\",\n    \"to\":\n        \"到\",\n    \"Prepare pages for printing\":\n        \"准备页面以打印\",\n    \"Preparing pages for printing\":\n        \"准备页面中\",\n\n    // Menu\n    \"Menu\":\n        \"菜单\",\n    \"Document\":\n        \"文档\",\n    \"About\":\n        \"关于\",\n    \"Print\":\n        \"打印\",\n    \"Close\":\n        \"关闭\",\n    \"View mode\":\n        \"显示模式\",\n    \"Scale\":\n        \"缩放\",\n    \"Rotation\":\n        \"旋转\",\n    \"Cursor mode\":\n        \"光标模式\",\n    \"Full page mode\":\n        \"整页模式\",\n    \"Fullscreen mode\":\n        \"全屏模式\",\n};"
  },
  {
    "path": "viewer/src/locales/English.js",
    "content": "/**\n * Some phrases contain insertions, e.g. icons and buttons, which are inserted in the code.\n * Here instead of visual components we use placeholders, e.g. #helpButton, which start with #.\n * Your translated phrase MUST also contain the same placeholder, but you can change its position.\n *\n * Some phrases are tooltips, that is, they are visible only when you hover the cursor over controls.\n *\n * Preserve the order of phrases and put the translation on a new line.\n * (for convenience of further additions and corrections).\n *\n * All null values mean that the corresponding strings need to be translated.\n * Such values are added automatically for convenience as placeholders.\n */\n\nexport default {\n    // language info\n    englishName:\n        \"English\",\n    nativeName:\n        \"English\",\n\n    \"Language\":\n        \"Language\", // not used now, but will be used in options afterwards\n\n    // Translation: tooltips and notification\n    // (to see the notification window, remove several phrases from any dictionary, except for the English one)\n    \"Add more\":\n        \"Add more\",\n    \"The translation isn't complete.\":\n        \"The translation isn't complete.\",\n    \"The following phrases are not translated:\":\n        \"The following phrases are not translated:\",\n    \"You can improve the translation here\":\n        \"You can improve the translation here\",\n\n    // Initial screen\n    \"#helpButton - learn more about the app\":\n        \"#helpButton - learn more about the app\",\n    \"#optionsButton - see the available options\":\n        \"#optionsButton - see the available options\",\n    \"powered with\":\n        \"powered with\",\n    \"Drag & Drop a file here or click to choose manually\":\n        \"Drag & Drop a file here or click to choose manually\",\n    \"Paste a URL to a djvu file here\":\n        \"Paste a URL to a djvu file here\",\n    \"Open URL\":\n        \"Open URL\",\n    'Enter a valid URL (it should start with \"http(s)://\" | \"data:\")': // an alert shown when you try to open an empty URL\n        'Enter a valid URL (it should start with \"http(s)://\" | \"data:\")',\n\n    // Errors. Usually there is a header and a message for each error type.\n    // For the web request error there are different types of messages depending on the HTTP status.\n    // The ways to see the errors in the viewer are described in comments below.\n    // In case of web requests you can load links via the browser extension (via the URL field on the initial screen)\n    \"Error\":\n        \"Error\",\n    \"Error on page\":\n        \"Error on page\", // Open 'library/assets/czech_indirect/index.djvu\n    \"Network error\":\n        \"Network error\", // Disable internet connection and try to load something by URL\n    \"Check your network connection\":\n        \"Check your network connection\",\n    // Load any URL to a nonexistent page on the Internet,\n    // e.g. https://djvu.js.org/nonexistentpage\n    \"Web request error\":\n        \"Web request error\",\n    \"404 Document not found\":\n        \"404 Document not found\",\n    \"403 Access forbidden\":\n        \"403 Access forbidden\",\n    \"500 Internal server error\":\n        \"500 Internal server error\",\n    \"The request failed with HTTP status #status\":\n        \"The request failed with HTTP status #status\",\n    \"DjVu file is corrupted\": // Open \"/library/assets/czech_indirect/dict0085.iff\"\n        \"DjVu file is corrupted\",\n    \"The file doesn't comply with the DjVu format specification or it's not a whole DjVu document\":\n        \"The file doesn't comply with the DjVu format specification or it's not a whole DjVu document\",\n    \"Incorrect file format\": // Open a not-djvu file.\n        \"Incorrect file format\",\n    \"The provided file is not a DjVu document\":\n        \"The provided file is not a DjVu document\",\n    // Load a URL to a DjVu file with \"#page=100500\" at the end (both in continuous scroll and single-page view modes)\n    // e.g. https://djvu.js.org/assets/djvu_examples/DjVu3Spec.djvu#page=100500\n    \"Incorrect page number\":\n        \"Incorrect page number\",\n    \"There is no page with the number #pageNumber\":\n        \"There is no page with the number #pageNumber\",\n    // \"baseURL\" is a URL to a document directory,\n    // all links inside the document index.djvu are considered relative to this URL.\n    // The term \"base URL\" can be translated as \"a URL to the document's folder\".\n    \"No base URL for an indirect DjVu document\":  // Open \"/library/assets/czech_indirect/index.djvu\"\n        \"No base URL for an indirect DjVu document\",\n    \"You probably opened an indirect (multi-file) DjVu document manually.\":\n        \"You probably opened an indirect (multi-file) DjVu document manually.\",\n    \"But such multi-file documents can be only loaded by URL.\":\n        \"But such multi-file documents can be only loaded by URL.\",\n    \"Unexpected error\": // Of course there is no standard way to produce this kind of error\n        \"Unexpected error\",\n    \"Cannot print the error, look in the console\":\n        \"Cannot print the error, look in the console\",\n\n    // Options and its tooltips\n    \"Options\":\n        \"Options\",\n    \"Show options window\":\n        \"Show options window\",\n    \"Color theme\":\n        \"Color theme\",\n    \"Extension options\":\n        \"Extension options\", // the options of the browser extension\n    \"Open all links with .djvu at the end via the viewer\":\n        \"Open all links with .djvu at the end via the viewer\",\n    \"All links to .djvu files will be opened by the viewer via a simple click on a link\":\n        \"All links to .djvu files will be opened by the viewer via a simple click on a link\",\n    \"Detect .djvu files by means of http headers\":\n        \"Detect .djvu files by means of http headers\",\n    \"Analyze headers of every new tab in order to process even links which do not end with the .djvu extension\":\n        \"Analyze headers of every new tab in order to process even links which do not end with the .djvu extension\",\n\n    // Footer: status bar\n    \"Ready\":\n        \"Ready\",\n    \"Loading\":\n        \"Loading\",\n\n    // Footer: buttons' tooltips\n    \"Show help window\":\n        \"Show help window\",\n    \"Switch full page mode\":\n        \"Switch full page mode\",\n\n    // File Block tooltips\n    \"Choose a file\":\n        \"Choose a file\",\n    \"Close document\":\n        \"Close document\",\n    \"Save document\":\n        \"Save document\",\n    \"Save\":\n        \"Save\",\n    \"Open another .djvu file\":\n        \"Open another .djvu file\",\n\n    // Help window\n    \"The application for viewing .djvu files in the browser.\":\n        \"The application for viewing .djvu files in the browser.\",\n    \"If something doesn't work properly, feel free to write about the problem at #email.\":\n        \"If something doesn't work properly, feel free to write about the problem at #email.\",\n    \"The official website is #website.\":\n        \"The official website is #website.\",\n    \"The source code is available on #link.\":\n        \"The source code is available on #link.\",\n    \"Hotkeys\":\n        \"Hotkeys\",\n    \"save the document\":\n        \"save the document\",\n    \"go to the previous page\":\n        \"go to the previous page\",\n    \"go to the next page\":\n        \"go to the next page\",\n    \"Controls\":\n        \"Controls\",\n    \"#expandIcon and #collapseIcon are to switch the viewer to the full page mode and back.\":\n        \"#expandIcon and #collapseIcon are to switch the viewer to the full page mode and back.\",\n    \"If you work with the browser extension, these buttons will cause no effect, since the viewer takes the whole page by default.\":\n        \"If you work with the browser extension, these buttons will cause no effect, since the viewer takes the whole page by default.\",\n\n    // Toolbar tooltips\n    \"Continuous scroll view mode\":\n        \"Continuous scroll view mode\",\n    \"Number of pages in a row\":\n        \"Number of pages in a row\",\n    \"Number of pages in the first row\":\n        \"Number of pages in the first row\",\n    \"Single page view mode\":\n        \"Single page view mode\",\n    \"Text view mode\":\n        \"Text view mode\",\n    \"Click on the number to enter it manually\":\n        \"Click on the number to enter it manually\",\n    \"Rotate the page\":\n        \"Rotate the page\",\n    \"You also can scale the page via Ctrl+MouseWheel\":\n        \"You also can scale the page via Ctrl+MouseWheel\",\n    \"Text cursor mode\":\n        \"Text cursor mode\",\n    \"Grab cursor mode\":\n        \"Grab cursor mode\",\n    \"Table of contents\":\n        \"Table of contents\",\n    \"Toolbar is always shown\":\n        \"Toolbar is always shown\",\n    \"Toolbar automatically hides\":\n        \"Toolbar automatically hides\",\n\n    // Contents\n    \"Contents\":\n        \"Contents\",\n    \"No contents provided\":\n        \"No contents provided\",\n    // A rare case. Open /library/assets/links.djvu in the viewer on https://djvu.js.org/ (not in the extension!)\n    // and click the \"Absolute Link\" in the contents\n    \"The link points to another document. Do you want to proceed?\":\n        \"The link points to another document. Do you want to proceed?\",\n\n    // Text Block (shown in the text view mode)\n    \"No text on this page\":\n        \"No text on this page\",\n\n    // Save dialog (shows when you save an indirect djvu)\n    \"You are trying to save an indirect (multi-file) document.\":\n        \"You are trying to save an indirect (multi-file) document.\",\n    \"What exactly do you want to do?\":\n        \"What exactly do you want to do?\",\n    \"Save only index file\":\n        \"Save only index file\",\n    \"Download, bundle and save the whole document as one file\":\n        \"Download, bundle and save the whole document as one file\",\n    \"Downloading and bundling the document\":\n        \"Downloading and bundling the document\",\n    \"The document has been downloaded and bundled into one file successfully\":\n        \"The document has been downloaded and bundled into one file successfully\",\n\n    // Printing\n    \"Print document\":\n        \"Print document\",\n    \"Pages must be rendered before printing.\":\n        \"Pages must be rendered before printing.\",\n    \"It may take a while.\":\n        \"It may take a while.\",\n    \"Select the pages you want to print.\":\n        \"Select the pages you want to print.\",\n    \"From\":\n        \"From\",\n    \"to\":\n        \"to\",\n    \"Prepare pages for printing\":\n        \"Prepare pages for printing\",\n    \"Preparing pages for printing\":\n        \"Preparing pages for printing\",\n\n    // Menu\n    \"Menu\":\n        \"Menu\",\n    \"Document\":\n        \"Document\",\n    \"About\":\n        \"About\",\n    \"Print\":\n        \"Print\",\n    \"Close\":\n        \"Close\",\n    \"View mode\":\n        \"View mode\",\n    \"Scale\":\n        \"Scale\",\n    \"Rotation\":\n        \"Rotation\",\n    \"Cursor mode\":\n        \"Cursor mode\",\n    \"Full page mode\":\n        \"Full page mode\",\n    \"Fullscreen mode\":\n        \"Fullscreen mode\",\n};"
  },
  {
    "path": "viewer/src/locales/French.js",
    "content": "/**\n * Some phrases contain insertions, e.g. icons and buttons, which are inserted in the code.\n * Here instead of visual components we use placeholders, e.g. #helpButton, which start with #.\n * Your translated phrase MUST also contain the same placeholder, but you can change its position.\n *\n * Some phrases are tooltips, that is, they are visible only when you hover the cursor over controls.\n *\n * Preserve the order of phrases and put the translation on a new line.\n * (for convenience of further additions and corrections).\n *\n * All null values mean that the corresponding strings need to be translated.\n * Such values are added automatically for convenience as placeholders.\n */\n\nexport default {\n    // language info\n    englishName:\n        \"French\",\n    nativeName:\n        \"Français\",\n\n    \"Language\":\n        \"Langue\", // not used now, but will be used in options afterwards\n\n    // Translation: tooltips and notification\n    // (to see the notification window, remove several phrases from any dictionary, except for the English one)\n    \"Add more\":\n        \"Ajouter une traduction\",\n    \"The translation isn't complete.\":\n        \"La traduction n'est pas terminée.\",\n    \"The following phrases are not translated:\":\n        \"Les assertions suivantes n'ont pas été traduites:\",\n    \"You can improve the translation here\":\n        \"Vous pouvez améliorer la traduction ici\",\n\n    // Initial screen\n    \"#helpButton - learn more about the app\":\n        \"#helpButton - à propos\",\n    \"#optionsButton - see the available options\":\n        \"#optionsButton - options disponibles\",\n    \"powered with\":\n        \"basé sur\",\n    \"Drag & Drop a file here or click to choose manually\":\n        \"Glisser-Déposer ici un fichier, ou cliquer pour le slectionner manuellement\",\n    \"Paste a URL to a djvu file here\":\n        \"Copier un lien vers un fichier .djvu\",\n    \"Open URL\":\n        \"Ouvrir le lien\",\n    'Enter a valid URL (it should start with \"http(s)://\" | \"data:\")': // an alert shown when you try to open an empty URL\n        'Insérer une URL valide (doit commencer par \"http(s)://\" | \"data:\")',\n\n    // Errors. Usually there is a header and a message for each error type.\n    // For the web request error there are different types of messages depending on the HTTP status.\n    // The ways to see the errors in the viewer are described in comments below.\n    // In case of web requests you can load links via the browser extension (via the URL field on the initial screen)\n    \"Error\":\n        \"Erreur\",\n    \"Error on page\":\n        \"Erreur dans la page\", // Open 'library/assets/czech_indirect/index.djvu\n    \"Network error\":\n        \"Erreur de réseau\", // Disable internet connection and try to load something by URL\n    \"Check your network connection\":\n        \"Vérifier votre connexion internet\",\n    // Load any URL to a nonexistent page on the Internet,\n    // e.g. https://djvu.js.org/nonexistentpage\n    \"Web request error\":\n        \"Erreur de requête web\",\n    \"404 Document not found\":\n        \"404 Document non trouvé\",\n    \"403 Access forbidden\":\n        \"403 Accès interdit\",\n    \"500 Internal server error\":\n        \"500 Erreur interne du serveur\",\n    \"The request failed with HTTP status #status\":\n        \"La requête a échouée avec le statut HTTP #status\",\n    \"DjVu file is corrupted\": // Open \"/library/assets/czech_indirect/dict0085.iff\"\n        \"Le fichier DjVu est corrompu\",\n    \"The file doesn't comply with the DjVu format specification or it's not a whole DjVu document\":\n        \"Le fichier ne respecte pas la spécification du format DjVu ou il s'agit d'un document DjVu incomplet\",\n    \"Incorrect file format\": // Open a not-djvu file.\n        \"Format de fichier incorrect\",\n    \"The provided file is not a DjVu document\":\n        \"Le fichier fourni n'est pas un document DjVu\",\n    // Load a URL to a DjVu file with \"#page=100500\" at the end (both in continuous scroll and single-page view modes)\n    // e.g. https://djvu.js.org/assets/djvu_examples/DjVu3Spec.djvu#page=100500\n    \"Incorrect page number\":\n        \"Numéro de page incorrect\",\n    \"There is no page with the number #pageNumber\":\n        \"Il n'existe pas de page avec le numéro #pageNumber\",\n    // \"baseURL\" is a URL to a document directory,\n    // all links inside the document index.djvu are considered relative to this URL.\n    // The term \"base URL\" can be translated as \"a URL to the document's folder\".\n    \"No base URL for an indirect DjVu document\":  // Open \"/library/assets/czech_indirect/index.djvu\"\n        \"URL de base manquante du document DjVu indirect (multi-fichier)\",\n    \"You probably opened an indirect (multi-file) DjVu document manually.\":\n        \"Vous avez probablement ouvert manuellement un document DjVu indirect (multi-fichier).\",\n    \"But such multi-file documents can be only loaded by URL.\":\n        \"Mais de tels documents multi-fichier ne peuvent être ouvert que par URL\",\n    \"Unexpected error\": // Of course there is no standard way to produce this kind of error\n        \"Erreur inconnue\",\n    \"Cannot print the error, look in the console\":\n        \"Impossible de rapporter l'erreur, regarder dans la console.\",\n\n    // Options and its tooltips\n    \"Options\":\n        \"Options\",\n    \"Show options window\":\n        \"Afficher la fenêtre d'option\",\n    \"Color theme\":\n        \"Changer de thème\",\n    \"Extension options\":\n        \"Options de l'extension\", // the options of the browser extension\n    \"Open all links with .djvu at the end via the viewer\":\n        \"Ouvrir tous les liеns .djvu avec l'extension\",\n    \"All links to .djvu files will be opened by the viewer via a simple click on a link\":\n        \"Tous les liens .djvu seront ouverts avec l'extension en cliquant simplement sur le lien\",\n    \"Detect .djvu files by means of http headers\":\n        \"Identifier les fichiers .djvu par leur en-tête http\",\n    \"Analyze headers of every new tab in order to process even links which do not end with the .djvu extension\":\n        \"Analyser les en-têtes de chaque nouvel onglet pour identifier les fichiers même sans l'extension .djvu en fin de lien\",\n\n    // Footer: status bar\n    \"Ready\":\n        \"Prêt\",\n    \"Loading\":\n        \"Chargement\",\n\n    // Footer: buttons' tooltips\n    \"Show help window\":\n        \"Afficher la fenêtre d'aide\",\n    \"Switch full page mode\":\n        \"Passer en mode pleine page\",\n\n    // File Block tooltips\n    \"Choose a file\":\n        \"Choisir un fichier\",\n    \"Close document\":\n        \"Fermer le document\",\n    \"Save document\":\n        \"Enregistrer le document\",\n    \"Save\":\n        \"Enregistrer\",\n    \"Open another .djvu file\":\n        \"Ouvrir un autre fichier .djvu\",\n\n    // Help window\n    \"The application for viewing .djvu files in the browser.\":\n        \"Une application pour afficher des fichiers .djvu dans le naviguateur.\",\n    \"If something doesn't work properly, feel free to write about the problem at #email.\":\n        \"Si quelque chose ne fonctionne pas correctement, vous pouvez écrire à #email.\",\n    \"The official website is #website.\":\n        \"Le site officiel est #website.\",\n    \"The source code is available on #link.\":\n        \"Le code source est disponible à l'adresse: #link.\",\n    \"Hotkeys\":\n        \"Raccourcis Clavier\",\n    \"save the document\":\n        \"enregistrer le document\",\n    \"go to the previous page\":\n        \"page précédente\",\n    \"go to the next page\":\n        \"page suivante\",\n    \"Controls\":\n        \"Boutons\",\n    \"#expandIcon and #collapseIcon are to switch the viewer to the full page mode and back.\":\n        \"#expandIcon & #collapseIcon permettent de passer en mode pleine page et inversement.\",\n    \"If you work with the browser extension, these buttons will cause no effect, since the viewer takes the whole page by default.\":\n        \"Si vous utilisez l'extension pour navigateur, ces boutons n'auront aucun effet, car l'application occupe déjà toute la page.\",\n\n    // Toolbar tooltips\n    \"Continuous scroll view mode\":\n        \"Mode défilement continu\",\n    \"Number of pages in a row\":\n        null,\n    \"Number of pages in the first row\":\n        null,\n    \"Single page view mode\":\n        \"Mode page unique\",\n    \"Text view mode\":\n        \"Mode texte\",\n    \"Click on the number to enter it manually\":\n        \"Cliquer sur le nombre pour en saisir un manuellement\",\n    \"Rotate the page\":\n        \"Faire pivoter la page\",\n    \"You also can scale the page via Ctrl+MouseWheel\":\n        \"Vous pouvez également ajuster la page avec Ctrl+Molette\",\n    \"Text cursor mode\":\n        \"Curseur pour mettre le texte en surbrillance\",\n    \"Grab cursor mode\":\n        \"Défilement glisser-déposer\",\n    \"Table of contents\":\n        \"Table des matières\",\n    \"Toolbar is always shown\":\n        \"Les contrôles sont toujours affichés\",\n    \"Toolbar automatically hides\":\n        \"Les contrôles se masquent automatiquement\",\n\n    // Contents\n    \"Contents\":\n        \"Table des matières\",\n    \"No contents provided\":\n        \"Pas de sommaire\",\n    // A rare case. Open /library/assets/links.djvu in the viewer on https://djvu.js.org/ (not in the extension!)\n    // and click the \"Absolute Link\" in the contents\n    \"The link points to another document. Do you want to proceed?\":\n        \"Le lien redirige vers un autre document. Voulez-vous continuer ?\",\n\n    // Text Block (shown in the text view mode)\n    \"No text on this page\":\n        \"Pas de texte dans cette page\",\n\n    // Save dialog (shows when you save an indirect djvu)\n    \"You are trying to save an indirect (multi-file) document.\":\n        \"Vous essayez d'enregistrer un document multi-fichier \",\n    \"What exactly do you want to do?\":\n        \"Que voulez-vous faire ?\",\n    \"Save only index file\":\n        \"Enregistrer uniquement le fichier racine\",\n    \"Download, bundle and save the whole document as one file\":\n        \"Télécharger, rassembler & sauveguarder l'ensemble du document dans un seul fichier\",\n    \"Downloading and bundling the document\":\n        \"Téléchargement & assemblage du document\",\n    \"The document has been downloaded and bundled into one file successfully\":\n        \"Le document a été téléchargé, rassemblé & sauveguardé dans un seul fichier avec succès\",\n\n    // Printing\n    \"Print document\":\n        \"Imprimer le document\",\n    \"Pages must be rendered before printing.\":\n        \"Les pages doivent être rendues avant impression\",\n    \"It may take a while.\":\n        \"Cela peut prendre un moment\",\n    \"Select the pages you want to print.\":\n        \"Sélectionner la page à imprimer\",\n    \"From\":\n        \"De\",\n    \"to\":\n        \"à\",\n    \"Prepare pages for printing\":\n        \"Préparer les pages pour l'impression\",\n    \"Preparing pages for printing\":\n        \"Préparation des pages pour l'impression\",\n\n    // Menu\n    \"Menu\":\n        \"Menu\",\n    \"Document\":\n        \"Document\",\n    \"About\":\n        \"À propos\",\n    \"Print\":\n        \"Imprimer\",\n    \"Close\":\n        \"Fermer\",\n    \"View mode\":\n        \"Mode de vue\",\n    \"Scale\":\n        \"Taille\",\n    \"Rotation\":\n        \"Rotation\",\n    \"Cursor mode\":\n        \"Mode de curseur\",\n    \"Full page mode\":\n        \"Page entier\",\n    \"Fullscreen mode\":\n        \"Plein écran\",\n};"
  },
  {
    "path": "viewer/src/locales/Italian.js",
    "content": "/**\n * Some phrases contain insertions, e.g. icons and buttons, which are inserted in the code.\n * Here instead of visual components we use placeholders, e.g. #helpButton, which start with #.\n * Your translated phrase MUST also contain the same placeholder, but you can change its position.\n *\n * Some phrases are tooltips, that is, they are visible only when you hover the cursor over controls.\n *\n * Preserve the order of phrases and put the translation on a new line.\n * (for convenience of further additions and corrections).\n *\n * All null values mean that the corresponding strings need to be translated.\n * Such values are added automatically for convenience as placeholders.\n */\n\nexport default {\n    // language info\n    englishName:\n        \"Italian\",\n    nativeName:\n        \"Italiano\",\n\n    \"Language\":\n        \"Lingua\", // not used now, but will be used in options afterwards\n\n    // Translation: tooltips and notification\n    // (to see the notification window, remove several phrases from any dictionary, except for the English one)\n    \"Add more\":\n        \"Aggiungi traduzione\",\n    \"The translation isn't complete.\":\n        \"La traduzione non è completa\",\n    \"The following phrases are not translated:\":\n        \"Le seguenti frasi non sono tradotte:\",\n    \"You can improve the translation here\":\n        \"Migliora la traduzione qui\",\n\n    // Initial screen\n    \"#helpButton - learn more about the app\":\n        \"#helpButton - Istruzioni all'uso dell'app\",\n    \"#optionsButton - see the available options\":\n        \"#optionsButton - Opzioni disponibili\",\n    \"powered with\":\n        \"Powered with\",\n    \"Drag & Drop a file here or click to choose manually\":\n        \"Trascina e rilascia qui il file DjVu o fai clic per selezionarlo manualmente\",\n    \"Paste a URL to a djvu file here\":\n        \"Incolla qui l'URL al file DjVu\",\n    \"Open URL\":\n        \"Apri URL\",\n    'Enter a valid URL (it should start with \"http(s)://\" | \"data:\")': // an alert shown when you try to open an empty URL\n        'Inserire un URL valido (deve iniziare con \"http(s)://\" | \"data:\")',\n\n    // Errors. Usually there is a header and a message for each error type.\n    // For the web request error there are different types of messages depending on the HTTP status.\n    // The ways to see the errors in the viewer are described in comments below.\n    // In case of web requests you can load links via the browser extension (via the URL field on the initial screen)\n    \"Error\":\n        \"Errore\",\n    \"Error on page\":\n        \"Errore nella pagina\", // Open 'library/assets/czech_indirect/index.djvu\n    \"Network error\":\n        \"Errore di rete\", // Disable internet connection and try to load something by URL\n    \"Check your network connection\":\n        \"Controlla la connessione di rete\",\n    // Load any URL to a nonexistent page on the Internet,\n    // e.g. https://djvu.js.org/nonexistentpage\n    \"Web request error\":\n        \"Errore richiesta web\",\n    \"404 Document not found\":\n        \"404 Documento non trovato\",\n    \"403 Access forbidden\":\n        \"403 Accesso negato\",\n    \"500 Internal server error\":\n        \"500 Errore interno del server\",\n    \"The request failed with HTTP status #status\":\n        \"La richiesta web è fallita con stato HTTP #status\",\n    \"DjVu file is corrupted\": // Open \"/library/assets/czech_indirect/dict0085.iff\"\n        \"Il file DjVu è corrotto\",\n    \"The file doesn't comply with the DjVu format specification or it's not a whole DjVu document\":\n        \"Il file non è conforme alle specifiche del formato DjVu oppure è incompleto\",\n    \"Incorrect file format\": // Open a not-djvu file.\n        \"Formato file non corretto\",\n    \"The provided file is not a DjVu document\":\n        \"Il file non è in formato DjVu\",\n    // Load a URL to a DjVu file with \"#page=100500\" at the end (both in continuous scroll and single-page view modes)\n    // e.g. https://djvu.js.org/assets/djvu_examples/DjVu3Spec.djvu#page=100500\n    \"Incorrect page number\":\n        \"Numero pagina non corretto\",\n    \"There is no page with the number #pageNumber\":\n        \"Non esiste una pagina con il numero #pageNumber\",\n    // \"baseURL\" is a URL to a document directory,\n    // all links inside the document index.djvu are considered relative to this URL.\n    // The term \"base URL\" can be translated as \"a URL to the document's folder\".\n    \"No base URL for an indirect DjVu document\":  // Open \"/library/assets/czech_indirect/index.djvu\"\n        \"Manca l'URL di base del documento DjVu multi-file (formato indirect)\",\n    \"You probably opened an indirect (multi-file) DjVu document manually.\":\n        \"Si è cercato di aprire manualmente un documento DjVu multi-file (formato indirect).\",\n    \"But such multi-file documents can be only loaded by URL.\":\n        \"Un documento DjVu multi-file (formato indirect) può essere aperto solo tramite URL.\",\n    \"Unexpected error\": // Of course there is no standard way to produce this kind of error\n        \"Errore sconosciuto\",\n    \"Cannot print the error, look in the console\":\n        \"Non è possibile riportare l'errore, cercare nella console\",\n\n    // Options and its tooltips\n    \"Options\":\n        \"Opzioni\",\n    \"Show options window\":\n        \"Mostra opzioni\",\n    \"Color theme\":\n        \"Colore tema\",\n    \"Extension options\":\n        \"Opzioni estensioni\", // the options of the browser extension\n    \"Open all links with .djvu at the end via the viewer\":\n        \"Aprire tutti i link con estensione .djvu tramite viewer\",\n    \"All links to .djvu files will be opened by the viewer via a simple click on a link\":\n        \"Tutti i link con estensione .djvu saranno aperti nel viewer con un solo clic\",\n    \"Detect .djvu files by means of http headers\":\n        \"Identifica un file .djvu tramite gli header http\",\n    \"Analyze headers of every new tab in order to process even links which do not end with the .djvu extension\":\n        \"Аnalizza gli header http di ogni nuovo tab del browser al fine di processare i link che non terminano con estensione .djvu\",\n\n    // Footer: status bar\n    \"Ready\":\n        \"Pronto\",\n    \"Loading\":\n        \"In caricamento\",\n\n    // Footer: buttons' tooltips\n    \"Show help window\":\n        \"Mostra guida\",\n    \"Switch full page mode\":\n        \"Attiva/disattiva modalità a piena pagina\",\n\n    // File Block tooltips\n    \"Choose a file\":\n        \"Scegli un file\",\n    \"Close document\":\n        \"Chiudi documento\",\n    \"Save document\":\n        \"Salva documento\",\n    \"Save\":\n        \"Salva\",\n    \"Open another .djvu file\":\n        \"Apri un altro file .djvu\",\n\n    // Help window\n    \"The application for viewing .djvu files in the browser.\":\n        \"Applicazione per visualizzare i file .djvu nel browser.\",\n    \"If something doesn't work properly, feel free to write about the problem at #email.\":\n        \"In caso di malfunzionamento scrivere a #email.\",\n    \"The official website is #website.\":\n        \"Sito web ufficiale #website.\",\n    \"The source code is available on #link.\":\n        \"Il codice sorgente è disponibile all'indirizzo #link.\",\n    \"Hotkeys\":\n        \"Scorciatoie da tastiera\",\n    \"save the document\":\n        \"Salva il documento\",\n    \"go to the previous page\":\n        \"Vai alla pagina precedente\",\n    \"go to the next page\":\n        \"Vai alla pagina successiva\",\n    \"Controls\":\n        \"Controlli\",\n    \"#expandIcon and #collapseIcon are to switch the viewer to the full page mode and back.\":\n        \"#expandIcon e #collapseIcon servono per attivare/disattivare la modalità a piena pagina.\",\n    \"If you work with the browser extension, these buttons will cause no effect, since the viewer takes the whole page by default.\":\n        \"Se si usa l'estensione per il browser questi pulsanti non hanno effetto perché la modalità di default è a piena pagina.\",\n\n    // Toolbar tooltips\n    \"Continuous scroll view mode\":\n        \"Vista a pagina continua\",\n    \"Number of pages in a row\":\n        null,\n    \"Number of pages in the first row\":\n        null,\n    \"Single page view mode\":\n        \"Vista a pagina singola\",\n    \"Text view mode\":\n        \"Vista testo\",\n    \"Click on the number to enter it manually\":\n        \"Fai clic sul numero per andare alla pagina\",\n    \"Rotate the page\":\n        \"Ruota pagina\",\n    \"You also can scale the page via Ctrl+MouseWheel\":\n        \"Puoi ingrandire la pagina tramite ctrl + rotella mouse\",\n    \"Text cursor mode\":\n        \"Modalità selezione testo\",\n    \"Grab cursor mode\":\n        \"Modalità trascinamento pagina\",\n    \"Table of contents\":\n        \"Indice dei contenuti\",\n    \"Toolbar is always shown\":\n        \"Toolbar sempre visibile\",\n    \"Toolbar automatically hides\":\n        \"Toolbar nascosta automaticamente\",\n\n    // Contents\n    \"Contents\":\n        \"Contenuti\",\n    \"No contents provided\":\n        \"Nessun contenuto disponibile\",\n    // A rare case. Open /library/assets/links.djvu in the viewer on https://djvu.js.org/ (not in the extension!)\n    // and click the \"Absolute Link\" in the contents\n    \"The link points to another document. Do you want to proceed?\":\n        \"Il link punta ad un altro documento. Vuoi procedere?\",\n\n    // Text Block (shown in the text view mode)\n    \"No text on this page\":\n        \"Nessun contenuto testuale nel documento\",\n\n    // Save dialog (shows when you save an indirect djvu)\n    \"You are trying to save an indirect (multi-file) document.\":\n        \"Stai per salvare un documento DjVu multi-file (formato indirect).\",\n    \"What exactly do you want to do?\":\n        \"Cosa vuoi fare?\",\n    \"Save only index file\":\n        \"Salva solo indice\",\n    \"Download, bundle and save the whole document as one file\":\n        \"Scarica, impacchetta e salva documento completo in un unico file\",\n    \"Downloading and bundling the document\":\n        \"Scaricamento e impacchettamento in corso\",\n    \"The document has been downloaded and bundled into one file successfully\":\n        \"Il documento è stato scaricato e impacchettato correttamente\",\n\n    // Printing\n    \"Print document\":\n        \"Stampa documento\",\n    \"Pages must be rendered before printing.\":\n        \"Le pagine devono essere processate prima della stampa.\",\n    \"It may take a while.\":\n        \"Potrebbe richiedere tempo\",\n    \"Select the pages you want to print.\":\n        \"Seleziona le pagine da stampare\",\n    \"From\":\n        \"Da\",\n    \"to\":\n        \"a\",\n    \"Prepare pages for printing\":\n        \"Avvia processo di stampa\",\n    \"Preparing pages for printing\":\n        \"Sto preparando le pagine per la stampa\",\n\n    // Menu\n    \"Menu\":\n        \"Menu\",\n    \"Document\":\n        \"Documento\",\n    \"About\":\n        \"Info\",\n    \"Print\":\n        \"Stampa\",\n    \"Close\":\n        \"Chiudi\",\n    \"View mode\":\n        \"Modalità vista\",\n    \"Scale\":\n        \"Scala\",\n    \"Rotation\":\n        \"Ruota\",\n    \"Cursor mode\":\n        \"Moldalità cursore\",\n    \"Full page mode\":\n        \"Modalità a piena pagina\",\n    \"Fullscreen mode\":\n        \"Modalità a pieno schermo\",\n};"
  },
  {
    "path": "viewer/src/locales/Portuguese.js",
    "content": "/**\n * Some phrases contain insertions, e.g. icons and buttons, which are inserted in the code.\n * Here instead of visual components we use placeholders, e.g. #helpButton, which start with #.\n * Your translated phrase MUST also contain the same placeholder, but you can change its position.\n *\n * Some phrases are tooltips, that is, they are visible only when you hover the cursor over controls.\n *\n * Preserve the order of phrases and put the translation on a new line.\n * (for convenience of further additions and corrections).\n *\n * All null values mean that the corresponding strings need to be translated.\n * Such values are added automatically for convenience as placeholders.\n */\n\nexport default {\n    // language info\n    englishName:\n        \"Portuguese\",\n    nativeName:\n        \"Português\",\n\n    \"Language\":\n        \"Idioma\", // not used now, but will be used in options afterwards\n\n    // Translation: tooltips and notification\n    // (to see the notification window, remove several phrases from any dictionary, except for the English one)\n    \"Add more\":\n        \"Adicionar mais\",\n    \"The translation isn't complete.\":\n        \"A tradução não está completa.\",\n    \"The following phrases are not translated:\":\n        \"As seguintes frases não são traduzidas:\",\n    \"You can improve the translation here\":\n        \"Pode melhorar a tradução aqui\",\n\n    // Initial screen\n    \"#helpButton - learn more about the app\":\n        \"#helpButton - saber mais sobre a aplicação\",\n    \"#optionsButton - see the available options\":\n        \"#optionsButton - ver as opções disponíveis\",\n    \"powered with\":\n        \"powered by\",\n    \"Drag & Drop a file here or click to choose manually\":\n        \"Arrastar e largar um ficheiro aqui ou clicar para escolher manualmente\",\n    \"Paste a URL to a djvu file here\":\n        \"Colar aqui um URL a um ficheiro djvu\",\n    \"Open URL\":\n        \"Abrir URL\",\n    'Enter a valid URL (it should start with \"http(s)://\" | \"data:\")': // an alert shown when you try to open an empty URL\n        'Introduza um URL válido (deve começar com \"http(s)://\" | \"data:\")',\n\n    // Errors. Usually there is a header and a message for each error type.\n    // For the web request error there are different types of messages depending on the HTTP status.\n    // The ways to see the errors in the viewer are described in comments below.\n    // In case of web requests you can load links via the browser extension (via the URL field on the initial screen)\n    \"Error\":\n        \"Erro\",\n    \"Error on page\":\n        \"Erro na página\", // Open 'library/assets/czech_indirect/index.djvu\n    \"Network error\":\n        \"Erro de rede\", // Disable internet connection and try to load something by URL\n    \"Check your network connection\":\n        \"Verifique a sua ligação à rede\",\n    // Load any URL to a nonexistent page on the Internet,\n    // e.g. https://djvu.js.org/nonexistentpage\n    \"Web request error\":\n        \"Pedido de erro na Web\",\n    \"404 Document not found\":\n        \"404 Documento não encontrado\",\n    \"403 Access forbidden\":\n        \"403 Acesso proibido\",\n    \"500 Internal server error\":\n        \"500 Erro interno do servidor\",\n    \"The request failed with HTTP status #status\":\n        \"O pedido falhou com o status HTTP #status\",\n    \"DjVu file is corrupted\": // Open \"/library/assets/czech_indirect/dict0085.iff\"\n        \"O ficheiro DjVu está corrompido\",\n    \"The file doesn't comply with the DjVu format specification or it's not a whole DjVu document\":\n        \"O ficheiro não cumpre as especificações do formato DjVu ou não é um documento DjVu completo\",\n    \"Incorrect file format\": // Open a not-djvu file.\n        \"Formato de ficheiro incorrecto\",\n    \"The provided file is not a DjVu document\":\n        \"O ficheiro fornecido não é um documento DjVu\",\n    // Load a URL to a DjVu file with \"#page=100500\" at the end (both in continuous scroll and single-page view modes)\n    // e.g. https://djvu.js.org/assets/djvu_examples/DjVu3Spec.djvu#page=100500\n    \"Incorrect page number\":\n        \"Número de página incorrecto\",\n    \"There is no page with the number #pageNumber\":\n        \"Não há página com o número #pageNumber\",\n    // \"baseURL\" is a URL to a document directory,\n    // all links inside the document index.djvu are considered relative to this URL.\n    // The term \"base URL\" can be translated as \"a URL to the document's folder\".\n    \"No base URL for an indirect DjVu document\":  // Open \"/library/assets/czech_indirect/index.djvu\"\n        \"Sem URL base para um documento indirecto DjVu\",\n    \"You probably opened an indirect (multi-file) DjVu document manually.\":\n        \"Provavelmente abriu manualmente um documento DjVu indirecto (multi-arquivos).\",\n    \"But such multi-file documents can be only loaded by URL.\":\n        \"Mas tais documentos multi-arquivos só podem ser carregados por URL.\",\n    \"Unexpected error\": // Of course there is no standard way to produce this kind of error\n        \"Erro inesperado\",\n    \"Cannot print the error, look in the console\":\n        \"Não é possível imprimir o erro, procurar na consola\",\n\n    // Options and its tooltips\n    \"Options\":\n        \"Opções\",\n    \"Show options window\":\n        \"Mostrar janela de opções\",\n    \"Color theme\":\n        \"Tema de cor\",\n    \"Extension options\":\n        \"Opções de extensão\", // the options of the browser extension\n    \"Open all links with .djvu at the end via the viewer\":\n        \"Abrir todas as ligações com .djvu no final através do visualizador\",\n    \"All links to .djvu files will be opened by the viewer via a simple click on a link\":\n        \"Todos os links para ficheiros .djvu serão abertos pelo espectador através de um simples clique num link\",\n    \"Detect .djvu files by means of http headers\":\n        \"Detectar ficheiros .djvu por meio de cabeçalhos http\",\n    \"Analyze headers of every new tab in order to process even links which do not end with the .djvu extension\":\n        \"Analisar os cabeçalhos de cada novo separador a fim de processar até ligações que não terminam com a extensão .djvu\",\n\n    // Footer: status bar\n    \"Ready\":\n        \"Preparado\",\n    \"Loading\":\n        \"Carregando\",\n\n    // Footer: buttons' tooltips\n    \"Show help window\":\n        \"Mostrar janela de ajuda\",\n    \"Switch full page mode\":\n        \"Mudar o modo de página inteira\",\n\n    // File Block tooltips\n    \"Choose a file\":\n        \"Escolha um ficheiro\",\n    \"Close document\":\n        \"Fechar documento\",\n    \"Save document\":\n        \"Guardar documentação\",\n    \"Save\":\n        \"Guardar\",\n    \"Open another .djvu file\":\n        \"Abrir outro ficheiro .djvu\",\n\n    // Help window\n    \"The application for viewing .djvu files in the browser.\":\n        \"A aplicação para visualizar ficheiros .djvu no browser.\",\n    \"If something doesn't work properly, feel free to write about the problem at #email.\":\n        \"Se algo não funcionar correctamente, sinta-se à vontade para escrever sobre o problema em #email.\",\n    \"The official website is #website.\":\n        \"O site oficial é #website.\",\n    \"The source code is available on #link.\":\n        \"O código fonte está disponível em #link.\",\n    \"Hotkeys\":\n        \"Teclas de atalho\",\n    \"save the document\":\n        \"guardar o documento\",\n    \"go to the previous page\":\n        \"ir para a página anterior\",\n    \"go to the next page\":\n        \"ir para a página seguinte\",\n    \"Controls\":\n        \"Controles\",\n    \"#expandIcon and #collapseIcon are to switch the viewer to the full page mode and back.\":\n        \"#expandIcon e #collapseIcon devem mudar o visualizador para o modo página inteira e voltar.\",\n    \"If you work with the browser extension, these buttons will cause no effect, since the viewer takes the whole page by default.\":\n        \"Se trabalhar com a extensão do navegador, estes botões não causarão qualquer efeito, uma vez que o visualizador leva a página inteira por defeito.\",\n\n    // Toolbar tooltips\n    \"Continuous scroll view mode\":\n        \"Modo de visualização scroll contínuo\",\n    \"Number of pages in a row\":\n        null,\n    \"Number of pages in the first row\":\n        null,\n    \"Single page view mode\":\n        \"Modo de visualização de uma página\",\n    \"Text view mode\":\n        \"Modo de visualização de texto\",\n    \"Click on the number to enter it manually\":\n        \"Clique no número para o introduzir manualmente\",\n    \"Rotate the page\":\n        \"Rodar a página\",\n    \"You also can scale the page via Ctrl+MouseWheel\":\n        \"Também pode escalar a página através de Ctrl+MouseWheel\",\n    \"Text cursor mode\":\n        \"Modo cursor de texto\",\n    \"Grab cursor mode\":\n        \"Modo de agarrar o cursor\",\n    \"Table of contents\":\n        null,\n    \"Toolbar is always shown\":\n        null,\n    \"Toolbar automatically hides\":\n        null,\n\n    // Contents\n    \"Contents\":\n        \"Conteúdos\",\n    \"No contents provided\":\n        \"Nenhum conteúdo fornecido\",\n    // A rare case. Open /library/assets/links.djvu in the viewer on https://djvu.js.org/ (not in the extension!)\n    // and click the \"Absolute Link\" in the contents\n    \"The link points to another document. Do you want to proceed?\":\n        \"A ligação aponta para outro documento. Quer prosseguir?\",\n\n    // Text Block (shown in the text view mode)\n    \"No text on this page\":\n        \"Nenhum texto nesta página\",\n\n    // Save dialog (shows when you save an indirect djvu)\n    \"You are trying to save an indirect (multi-file) document.\":\n        \"Está a tentar salvar um documento indirecto (multi-arquivo).\",\n    \"What exactly do you want to do?\":\n        \"Que queres fazer exactamente?\",\n    \"Save only index file\":\n        \"Guardar só ficheiro de índice\",\n    \"Download, bundle and save the whole document as one file\":\n        \"Descarregar, agrupar e guardar o documento inteiro como um só ficheiro\",\n    \"Downloading and bundling the document\":\n        \"Descarregar e empacotar o documento\",\n    \"The document has been downloaded and bundled into one file successfully\":\n        \"O documento foi descarregado e agrupado num único ficheiro com sucesso\",\n\n    // Printing\n    \"Print document\":\n        \"Imprimir documento\",\n    \"Pages must be rendered before printing.\":\n        \"As páginas devem ser entregues antes da impressão.\",\n    \"It may take a while.\":\n        \"Pode demorar algum tempo.\",\n    \"Select the pages you want to print.\":\n        \"Seleccione as páginas que pretende imprimir.\",\n    \"From\":\n        \"De\",\n    \"to\":\n        \"a\",\n    \"Prepare pages for printing\":\n        \"Preparar páginas para impressão\",\n    \"Preparing pages for printing\":\n        \"Preparar páginas para impressão\",\n\n    // Menu\n    \"Menu\":\n        null,\n    \"Document\":\n        null,\n    \"About\":\n        null,\n    \"Print\":\n        null,\n    \"Close\":\n        null,\n    \"View mode\":\n        null,\n    \"Scale\":\n        null,\n    \"Rotation\":\n        null,\n    \"Cursor mode\":\n        null,\n    \"Full page mode\":\n        null,\n    \"Fullscreen mode\":\n        null,\n};"
  },
  {
    "path": "viewer/src/locales/Russian.js",
    "content": "/**\n * The exemplary dictionary which should be used for the creation of other localizations.\n * Copy this file and change all Russian strings to your own.\n * Remove this (the topmost) comment, but leave other comments in place.\n *\n * Another way to create a template file with nulls instead of translated strings is to\n * run the following command inside the `viewer` directory:\n *\n * npm run syncLocales EnglishNameOfNewLanguage\n *\n * It will generate the EnglishNameOfNewLanguage.js file in the locales folder.\n */\n\n/**\n * Some phrases contain insertions, e.g. icons and buttons, which are inserted in the code.\n * Here instead of visual components we use placeholders, e.g. #helpButton, which start with #.\n * Your translated phrase MUST also contain the same placeholder, but you can change its position.\n *\n * Some phrases are tooltips, that is, they are visible only when you hover the cursor over controls.\n *\n * Preserve the order of phrases and put the translation on a new line.\n * (for convenience of further additions and corrections).\n *\n * All null values mean that the corresponding strings need to be translated.\n * Such values are added automatically for convenience as placeholders.\n */\n\nexport default {\n    // language info\n    englishName:\n        \"Russian\",\n    nativeName:\n        \"Русский\",\n\n    \"Language\":\n        \"Язык\", // not used now, but will be used in options afterwards\n\n    // Translation: tooltips and notification\n    // (to see the notification window, remove several phrases from any dictionary, except for the English one)\n    \"Add more\":\n        \"Добавить еще\",\n    \"The translation isn't complete.\":\n        \"Перевод неполный.\",\n    \"The following phrases are not translated:\":\n        \"Следующие фразы не переведены:\",\n    \"You can improve the translation here\":\n        \"Вы можете улучшить перевод тут\",\n\n    // Initial screen\n    \"#helpButton - learn more about the app\":\n        \"#helpButton - узнать больше о программе\",\n    \"#optionsButton - see the available options\":\n        \"#optionsButton - изменение настроек\",\n    \"powered with\":\n        \"основано на\",\n    \"Drag & Drop a file here or click to choose manually\":\n        \"Перетащите сюда файл или кликните, чтобы выбрать его вручную\",\n    \"Paste a URL to a djvu file here\":\n        \"Вставьте ссылку на .djvu файл\",\n    \"Open URL\":\n        \"Открыть ссылку\",\n    'Enter a valid URL (it should start with \"http(s)://\" | \"data:\")': // an alert shown when you try to open an empty URL\n        'Введите корректную ссылку (она должна начинаться с \"http(s)://\" | \"data:\")',\n\n    // Errors. Usually there is a header and a message for each error type.\n    // For the web request error there are different types of messages depending on the HTTP status.\n    // The ways to see the errors in the viewer are described in comments below.\n    // In case of web requests you can load links via the browser extension (via the URL field on the initial screen)\n    \"Error\":\n        \"Ошибка\",\n    \"Error on page\":\n        \"Ошибка на странице\", // Open 'library/assets/czech_indirect/index.djvu\n    \"Network error\":\n        \"Ошибка сети\", // Disable internet connection and try to load something by URL\n    \"Check your network connection\":\n        \"Проверьте свое интернет-соединение\",\n    // Load any URL to a nonexistent page on the Internet,\n    // e.g. https://djvu.js.org/nonexistentpage\n    \"Web request error\":\n        \"Ошибка веб-запроса\",\n    \"404 Document not found\":\n        \"404 Документ не найден\",\n    \"403 Access forbidden\":\n        \"403 Доступ запрещен\",\n    \"500 Internal server error\":\n        \"500 Внутренняя ошибка сервера\",\n    \"The request failed with HTTP status #status\":\n        \"Запрос завершился с HTTP-статусом #status\",\n    \"DjVu file is corrupted\": // Open \"/library/assets/czech_indirect/dict0085.iff\"\n        \"DjVu-файл поврежден\",\n    \"The file doesn't comply with the DjVu format specification or it's not a whole DjVu document\":\n        \"Файл не соответствует спецификации формата DjVu, или же это не весь DjVu-документ\",\n    \"Incorrect file format\": // Open a not-djvu file.\n        \"Неверный формат файла\",\n    \"The provided file is not a DjVu document\":\n        \"Загруженный файл не является DjVu-документом\",\n    // Load a URL to a DjVu file with \"#page=100500\" at the end (both in continuous scroll and single-page view modes)\n    // e.g. https://djvu.js.org/assets/djvu_examples/DjVu3Spec.djvu#page=100500\n    \"Incorrect page number\":\n        \"Некорректный номер страницы\",\n    \"There is no page with the number #pageNumber\":\n        \"Страницы с номером #pageNumber не существует\",\n    // \"baseURL\" is a URL to a document directory,\n    // all links inside the document index.djvu are considered relative to this URL.\n    // The term \"base URL\" can be translated as \"a URL to the document's folder\".\n    \"No base URL for an indirect DjVu document\":  // Open \"/library/assets/czech_indirect/index.djvu\"\n        \"Нет ссылки на директорию документа\",\n    \"You probably opened an indirect (multi-file) DjVu document manually.\":\n        \"Вероятно, вы открыли многофайловый (indirect) DjVu-документ вручную.\",\n    \"But such multi-file documents can be only loaded by URL.\":\n        \"Однако, такие документы могут быть загружены только по ссылке.\",\n    \"Unexpected error\": // Of course there is no standard way to produce this kind of error\n        \"Непредвиденная ошибка\",\n    \"Cannot print the error, look in the console\":\n        \"Невозможно вывести ошибку в текстовом виде, посмотрите в консоль\",\n\n    // Options and its tooltips\n    \"Options\":\n        \"Настройки\",\n    \"Show options window\":\n        \"Открыть окно настроек\",\n    \"Color theme\":\n        \"Цветовая схема\",\n    \"Extension options\":\n        \"Настройки расширения\", // the options of the browser extension\n    \"Open all links with .djvu at the end via the viewer\":\n        \"Открывать все ссылки с .djvu на конце через расширение\",\n    \"All links to .djvu files will be opened by the viewer via a simple click on a link\":\n        \"Все ссылки на .djvu файлы будут открываться расширением по клику на ссылке\",\n    \"Detect .djvu files by means of http headers\":\n        \"Определять .djvu файлы по http заголовкам\",\n    \"Analyze headers of every new tab in order to process even links which do not end with the .djvu extension\":\n        \"Анализировать заголовки каждой новой вкладки, чтобы определять файлы даже без расширения \\\".djvu\\\" в ссылке\",\n\n    // Footer: status bar\n    \"Ready\":\n        \"Готово\",\n    \"Loading\":\n        \"Загрузка\",\n\n    // Footer: buttons' tooltips\n    \"Show help window\":\n        \"Показать окно справки\",\n    \"Switch full page mode\":\n        \"Переключить полностраничный режим\",\n\n    // File Block tooltips\n    \"Choose a file\":\n        \"Выберите файл\",\n    \"Close document\":\n        \"Закрыть документ\",\n    \"Save document\":\n        \"Сохранить документ\",\n    \"Save\":\n        \"Сохранить\",\n    \"Open another .djvu file\":\n        \"Открыть другой .djvu файл\",\n\n    // Help window\n    \"The application for viewing .djvu files in the browser.\":\n        \"Приложение для просмотра .djvu файлов в браузере.\",\n    \"If something doesn't work properly, feel free to write about the problem at #email.\":\n        \"Если что-то не работает, пишите на #email.\",\n    \"The official website is #website.\":\n        \"Официальный веб-сайт #website.\",\n    \"The source code is available on #link.\":\n        \"Исходный код находится на #link.\",\n    \"Hotkeys\":\n        \"Горячие клавиши\",\n    \"save the document\":\n        \"сохранить документ\",\n    \"go to the previous page\":\n        \"прейти к предыдущей странице\",\n    \"go to the next page\":\n        \"перейти к следующей странице\",\n    \"Controls\":\n        \"Кнопки\",\n    \"#expandIcon and #collapseIcon are to switch the viewer to the full page mode and back.\":\n        \"#expandIcon и #collapseIcon нужны, чтобы переключать программу в полностраничный режим и обратно.\",\n    \"If you work with the browser extension, these buttons will cause no effect, since the viewer takes the whole page by default.\":\n        \"Если вы используете расширение для браузера, то эти кнопки не работают, так как приложение по умолчанию занимает всю страницу.\",\n\n    // Toolbar tooltips\n    \"Continuous scroll view mode\":\n        \"Режим непрерывной прокрутки\",\n    \"Number of pages in a row\":\n        \"Число страниц в строке\",\n    \"Number of pages in the first row\":\n        \"Число страниц в первой строке\",\n    \"Single page view mode\":\n        \"Одностраничный режим\",\n    \"Text view mode\":\n        \"Текстовый режим\",\n    \"Click on the number to enter it manually\":\n        \"Кликните по номеру, чтобы ввести его вручную\",\n    \"Rotate the page\":\n        \"Повернуть страницу\",\n    \"You also can scale the page via Ctrl+MouseWheel\":\n        \"Вы также можете масштабировать страницу через Ctrl+Колесо мыши\",\n    \"Text cursor mode\":\n        \"Курсор для выделения текста\",\n    \"Grab cursor mode\":\n        \"Режим перетаскивания\",\n    \"Table of contents\":\n        \"Оглавление\",\n    \"Toolbar is always shown\":\n        \"Панель инструментов всегда отображается\",\n    \"Toolbar automatically hides\":\n        \"Панель инструментов автоматически скрывается\",\n\n    // Contents\n    \"Contents\":\n        \"Содержание\",\n    \"No contents provided\":\n        \"Нет содержания\",\n    // A rare case. Open /library/assets/links.djvu in the viewer on https://djvu.js.org/ (not in the extension!)\n    // and click the \"Absolute Link\" in the contents\n    \"The link points to another document. Do you want to proceed?\":\n        \"Ссылка ведет на другой документ. Вы хотите продолжить?\",\n\n    // Text Block (shown in the text view mode)\n    \"No text on this page\":\n        \"Нет текста на этой странице\",\n\n    // Save dialog (shows when you save an indirect djvu)\n    \"You are trying to save an indirect (multi-file) document.\":\n        \"Вы пытаетесь сохранить многофайловый документ.\",\n    \"What exactly do you want to do?\":\n        \"Что именно вы хотите сделать?\",\n    \"Save only index file\":\n        \"Сохранить только корневой файл\",\n    \"Download, bundle and save the whole document as one file\":\n        \"Скачать, собрать и сохранить весь документ одним файлом\",\n    \"Downloading and bundling the document\":\n        \"Скачиваем и собираем документ\",\n    \"The document has been downloaded and bundled into one file successfully\":\n        \"Документ был успешно скачан и собран в единый файл\",\n\n    // Printing\n    \"Print document\":\n        \"Распечатать документ\",\n    \"Pages must be rendered before printing.\":\n        \"Страницы должны быть отрисованы перед печатью.\",\n    \"It may take a while.\":\n        \"Это может занять некоторое время.\",\n    \"Select the pages you want to print.\":\n        \"Выберите те страницы, которые вы хотите распечатать.\",\n    \"From\":\n        \"Начиная с\",\n    \"to\":\n        \"по\",\n    \"Prepare pages for printing\":\n        \"Подготовить страницы к печати\",\n    \"Preparing pages for printing\":\n        \"Подготавливаем страницы к печати\",\n\n    // Menu\n    \"Menu\":\n        \"Меню\",\n    \"Document\":\n        \"Документ\",\n    \"About\":\n        \"О приложении\",\n    \"Print\":\n        \"Печать\",\n    \"Close\":\n        \"Закрыть\",\n    \"View mode\":\n        \"Режим просмотра\",\n    \"Scale\":\n        \"Масштаб\",\n    \"Rotation\":\n        \"Поворот\",\n    \"Cursor mode\":\n        \"Курсор\",\n    \"Full page mode\":\n        \"Полностраничный режим\",\n    \"Fullscreen mode\":\n        \"Полноэкранный режим\",\n};"
  },
  {
    "path": "viewer/src/locales/Spanish.js",
    "content": "/**\n * Some phrases contain insertions, e.g. icons and buttons, which are inserted in the code.\n * Here instead of visual components we use placeholders, e.g. #helpButton, which start with #.\n * Your translated phrase MUST also contain the same placeholder, but you can change its position.\n *\n * Some phrases are tooltips, that is, they are visible only when you hover the cursor over controls.\n *\n * Preserve the order of phrases and put the translation on a new line.\n * (for convenience of further additions and corrections).\n *\n * All null values mean that the corresponding strings need to be translated.\n * Such values are added automatically for convenience as placeholders.\n */\n\nexport default {\n    // language info\n    englishName:\n        \"Spanish\",\n    nativeName:\n        \"Castellano\",\n\n    \"Language\":\n        \"Idioma\", // not used now, but will be used in options afterwards\n\n    // Translation: tooltips and notification\n    // (to see the notification window, remove several phrases from any dictionary, except for the English one)\n    \"Add more\":\n        \"Añadir mas\",\n    \"The translation isn't complete.\":\n        \"La traducción está incompleta.\",\n    \"The following phrases are not translated:\":\n        \"Las siguientes frases no estan traduccidas:\",\n    \"You can improve the translation here\":\n        \"Puedes mejorar la traducción aquí\",\n\n    // Initial screen\n    \"#helpButton - learn more about the app\":\n        \"#helpButton - saber más sobre la aplicación\",\n    \"#optionsButton - see the available options\":\n        \"#optionsButton - ver las opciones disponibles\",\n    \"powered with\":\n        \"Powered  with\",\n    \"Drag & Drop a file here or click to choose manually\":\n        \"Arrastrar y soltar un archivo o click para elegirlo manualmente\",\n    \"Paste a URL to a djvu file here\":\n        \"Pegar una URL al archivo djvu aquí\",\n    \"Open URL\":\n        \"Abrir URL\",\n    'Enter a valid URL (it should start with \"http(s)://\" | \"data:\")': // an alert shown when you try to open an empty URL\n        'Introducir una URL válida (debe comenzar con \"http(s)://\" | \"data:\")',\n\n    // Errors. Usually there is a header and a message for each error type.\n    // For the web request error there are different types of messages depending on the HTTP status.\n    // The ways to see the errors in the viewer are described in comments below.\n    // In case of web requests you can load links via the browser extension (via the URL field on the initial screen)\n    \"Error\":\n        \"Error\",\n    \"Error on page\":\n        \"Error en la página\", // Open 'library/assets/czech_indirect/index.djvu\n    \"Network error\":\n        \"Error de red\", // Disable internet connection and try to load something by URL\n    \"Check your network connection\":\n        \"Compruebe su conexión a la red\",\n    // Load any URL to a nonexistent page on the Internet,\n    // e.g. https://djvu.js.org/nonexistentpage\n    \"Web request error\":\n        \"Error en la solicitud de la web\",\n    \"404 Document not found\":\n        \"404 Documento no encontrado\",\n    \"403 Access forbidden\":\n        \"403 Acceso prohibido\",\n    \"500 Internal server error\":\n        \"500 Error interno del servidor\",\n    \"The request failed with HTTP status #status\":\n        \"La solicitud ha fallado con el estado HTTP #status\",\n    \"DjVu file is corrupted\": // Open \"/library/assets/czech_indirect/dict0085.iff\"\n        \"El archivo DjVu esta corrupto\",\n    \"The file doesn't comply with the DjVu format specification or it's not a whole DjVu document\":\n        \"El archivo no cumple con la especificación del formato DjVu o no es un documento DjVu completo\",\n    \"Incorrect file format\": // Open a not-djvu file.\n        \"Formato de archivo incorrecto\",\n    \"The provided file is not a DjVu document\":\n        \"El archivo proporcionado no es un documento DjVu\",\n    // Load a URL to a DjVu file with \"#page=100500\" at the end (both in continuous scroll and single-page view modes)\n    // e.g. https://djvu.js.org/assets/djvu_examples/DjVu3Spec.djvu#page=100500\n    \"Incorrect page number\":\n        \"Número de página incorrecto\",\n    \"There is no page with the number #pageNumber\":\n        \"No hay ninguna página con el número #pageNumber\",\n    // \"baseURL\" is a URL to a document directory,\n    // all links inside the document index.djvu are considered relative to this URL.\n    // The term \"base URL\" can be translated as \"a URL to the document's folder\".\n    \"No base URL for an indirect DjVu document\":  // Open \"/library/assets/czech_indirect/index.djvu\"\n        \"No hay URL base para un documento DjVu indirecto\",\n    \"You probably opened an indirect (multi-file) DjVu document manually.\":\n        \"Probablemente haya abierto manualmente un documento DjVu indirecto (de varios archivos).\",\n    \"But such multi-file documents can be only loaded by URL.\":\n        \"Pero estos documentos de varios archivos sólo pueden cargarse por URL.\",\n    \"Unexpected error\": // Of course there is no standard way to produce this kind of error\n        \"Error inesperado\",\n    \"Cannot print the error, look in the console\":\n        \"No se puede imprimir el error, mira en la consola\",\n\n    // Options and its tooltips\n    \"Options\":\n        \"Opciones\",\n    \"Show options window\":\n        \"Mostrar ventana de opciones\",\n    \"Color theme\":\n        \"Tema de color\",\n    \"Extension options\":\n        \"Opciones de extensión\", // the options of the browser extension\n    \"Open all links with .djvu at the end via the viewer\":\n        \"Abrir todos los enlaces con .djvu al final a través del visor\",\n    \"All links to .djvu files will be opened by the viewer via a simple click on a link\":\n        \"Todos los enlaces a archivos .djvu serán abiertos por el visor mediante un clic simple en un enlace\",\n    \"Detect .djvu files by means of http headers\":\n        \"Detectar archivos .djvu mediante cabeceras http\",\n    \"Analyze headers of every new tab in order to process even links which do not end with the .djvu extension\":\n        \"Analizar las cabeceras de cada nueva pestaña para procesar incluso los enlaces que no terminan con la extensión .djvu\",\n\n    // Footer: status bar\n    \"Ready\":\n        \"Listo\",\n    \"Loading\":\n        \"Cargando\",\n\n    // Footer: buttons' tooltips\n    \"Show help window\":\n        \"Mostrar ventana de ayuda\",\n    \"Switch full page mode\":\n        \"Cambiar el modo de página completa\",\n\n    // File Block tooltips\n    \"Choose a file\":\n        \"Seleccionar archivo\",\n    \"Close document\":\n        \"Cerrar documento\",\n    \"Save document\":\n        \"Guardar documento\",\n    \"Save\":\n        \"Guardar\",\n    \"Open another .djvu file\":\n        \"Abrir otro archivo .djvu\",\n\n    // Help window\n    \"The application for viewing .djvu files in the browser.\":\n        \"La aplicación para ver archivos .djvu en el navegador.\",\n    \"If something doesn't work properly, feel free to write about the problem at #email.\":\n        \"Si algo no funciona correctamente, no dudes en escribir sobre el problema en #email.\",\n    \"The official website is #website.\":\n        \"El sitio web oficial es #website.\",\n    \"The source code is available on #link.\":\n        \"El código fuente está disponible en #link.\",\n    \"Hotkeys\":\n        \"Atajos de teclado\",\n    \"save the document\":\n        \"Guardar el documento\",\n    \"go to the previous page\":\n        \"ir a la página anterior\",\n    \"go to the next page\":\n        \"ir a la página siguiente\",\n    \"Controls\":\n        \"Controles\",\n    \"#expandIcon and #collapseIcon are to switch the viewer to the full page mode and back.\":\n        \"#expandIcon y #collapseIcon son para cambiar el visor al modo de página completa y viceversa.\",\n    \"If you work with the browser extension, these buttons will cause no effect, since the viewer takes the whole page by default.\":\n        \"Si trabaja con la extensión del navegador, estos botones no causarán ningún efecto, ya que el visor toma toda la página por defecto.\",\n\n    // Toolbar tooltips\n    \"Continuous scroll view mode\":\n        \"Modo de scroll continuo\",\n    \"Number of pages in a row\":\n        \"Número de páginas por fila\",\n    \"Number of pages in the first row\":\n        \"Número de páginas en la primer fila\",\n    \"Single page view mode\":\n        \"Modo de vista de una sola página\",\n    \"Text view mode\":\n        \"Modo de vista de texto\",\n    \"Click on the number to enter it manually\":\n        \"Haga clic en el número para introducirlo manualmente\",\n    \"Rotate the page\":\n        \"Rotar página\",\n    \"You also can scale the page via Ctrl+MouseWheel\":\n        \"También puede escalar la página mediante Ctrl+Rueda del ratón\",\n    \"Text cursor mode\":\n        \"Modo cursor de texto\",\n    \"Grab cursor mode\":\n        \"Modo cursor de agarre\",\n    \"Table of contents\":\n        \"Tabla de contenidos\",\n    \"Toolbar is always shown\":\n        \"La barra de herramientas siempre se muestra\",\n    \"Toolbar automatically hides\":\n        \"La barra de herramientas si oculta automáticamente\",\n\n    // Contents\n    \"Contents\":\n        \"Contenido\",\n    \"No contents provided\":\n        \"No se proporciona ningún contenido\",\n    // A rare case. Open /library/assets/links.djvu in the viewer on https://djvu.js.org/ (not in the extension!)\n    // and click the \"Absolute Link\" in the contents\n    \"The link points to another document. Do you want to proceed?\":\n        \"El enlace apunta a otro documento. ¿Desea continuar?\",\n\n    // Text Block (shown in the text view mode)\n    \"No text on this page\":\n        \"No hay texto en esta página\",\n\n    // Save dialog (shows when you save an indirect djvu)\n    \"You are trying to save an indirect (multi-file) document.\":\n        \"Está intentando guardar un documento indirecto (de varios archivos)\",\n    \"What exactly do you want to do?\":\n        \"¿Qué quiere hacer exactamente?\",\n    \"Save only index file\":\n        \"Guardar sólo el archivo de índice\",\n    \"Download, bundle and save the whole document as one file\":\n        \"Descargue, agrupe y guarde todo el documento como un solo archivo\",\n    \"Downloading and bundling the document\":\n        \"Descargando y agrupando el documento\",\n    \"The document has been downloaded and bundled into one file successfully\":\n        \"El documento ha sido descargado y agrupado en un archivo con éxito\",\n\n    // Printing\n    \"Print document\":\n        \"Imprimir documento\",\n    \"Pages must be rendered before printing.\":\n        \"Las páginas deben ser renderizadas antes de la impresión.\",\n    \"It may take a while.\":\n        \"Puede llevar un tiempo.\",\n    \"Select the pages you want to print.\":\n        \"Seleccione las páginas que desea imprimir.\",\n    \"From\":\n        \"Desde\",\n    \"to\":\n        \"hasta\",\n    \"Prepare pages for printing\":\n        \"Preparar las páginas para la impresión\",\n    \"Preparing pages for printing\":\n        \"Preparando páginas para imprimir\",\n\n    // Menu\n    \"Menu\":\n        \"Menú\",\n    \"Document\":\n        \"Documento\",\n    \"About\":\n        \"Acerca\",\n    \"Print\":\n        \"Imprimir\",\n    \"Close\":\n        \"Cerrar\",\n    \"View mode\":\n        \"Modo de visualización\",\n    \"Scale\":\n        \"Escala\",\n    \"Rotation\":\n        \"Rotación\",\n    \"Cursor mode\":\n        \"Modo del cursor\",\n    \"Full page mode\":\n        \"Modo de página completa\",\n    \"Fullscreen mode\":\n        \"Modo de pantalla completa\",\n};"
  },
  {
    "path": "viewer/src/locales/Swedish.js",
    "content": "/**\n * Some phrases contain insertions, e.g. icons and buttons, which are inserted in the code.\n * Here instead of visual components we use placeholders, e.g. #helpButton, which start with #.\n * Your translated phrase MUST also contain the same placeholder, but you can change its position.\n *\n * Some phrases are tooltips, that is, they are visible only when you hover the cursor over controls.\n *\n * Preserve the order of phrases and put the translation on a new line.\n * (for convenience of further additions and corrections).\n *\n * All null values mean that the corresponding strings need to be translated.\n * Such values are added automatically for convenience as placeholders.\n */\n\nexport default {\n    // language info\n    englishName:\n        \"Swedish\",\n    nativeName:\n        \"Svenska\",\n\n    \"Language\":\n        \"Språk\", // not used now, but will be used in options afterwards\n\n    // Translation: tooltips and notification\n    // (to see the notification window, remove several phrases from any dictionary, except for the English one)\n    \"Add more\":\n        \"Lägg till mer\",\n    \"The translation isn't complete.\":\n        \"Översättningen är inte klar.\",\n    \"The following phrases are not translated:\":\n        \"Följande meningar är inte översatta:\",\n    \"You can improve the translation here\":\n        \"Du kan förtydliga översättningen här\",\n\n    // Initial screen\n    \"#helpButton - learn more about the app\":\n        \"#helpButton - lär dig mer om applikationen\",\n    \"#optionsButton - see the available options\":\n        \"#optionsButton - Se tillgängliga alternativ\",\n    \"powered with\":\n        \"baserat på\",\n    \"Drag & Drop a file here or click to choose manually\":\n        \"Dra och släpp en fil här eller klicka för att välja en manuellt\",\n    \"Paste a URL to a djvu file here\":\n        \"Klistra in en URL till en .djvu-fil här\",\n    \"Open URL\":\n        \"Öppna URL\",\n    'Enter a valid URL (it should start with \"http(s)://\" | \"data:\")': // an alert shown when you try to open an empty URL\n        'Ange en giltig URL (den ska börja med \"http (s): //\")',\n\n    // Errors. Usually there is a header and a message for each error type.\n    // For the web request error there are different types of messages depending on the HTTP status.\n    // The ways to see the errors in the viewer are described in comments below.\n    // In case of web requests you can load links via the browser extension (via the URL field on the initial screen)\n    \"Error\":\n        \"Fel\",\n    \"Error on page\":\n        \"Felaktigheter på sidan\", // Open 'library/assets/czech_indirect/index.djvu\n    \"Network error\":\n        \"Nätverksfel\", // Disable internet connection and try to load something by URL\n    \"Check your network connection\":\n        \"Kontrollera nätverksanslutningen\",\n    // Load any URL to a nonexistent page on the Internet,\n    // e.g. https://djvu.js.org/nonexistentpage\n    \"Web request error\":\n        \"Fel vid webbegärning\",\n    \"404 Document not found\":\n        \"404 Dokument hittas inte\",\n    \"403 Access forbidden\":\n        \"403 Åtkomst förbjuden\",\n    \"500 Internal server error\":\n        \"500 Internt serverfel\",\n    \"The request failed with HTTP status #status\":\n        \"TBegäran misslyckades med http-status #status\",\n    \"DjVu file is corrupted\": // Open \"/library/assets/czech_indirect/dict0085.iff\"\n        \"DjVu-filen är korrupt:\",\n    \"The file doesn't comply with the DjVu format specification or it's not a whole DjVu document\":\n        \"Filen överensstämmer inte med specificerat DjVu-format eller så är dokumentet inte komplett\",\n    \"Incorrect file format\": // Open a not-djvu file.\n        \"Felaktigt filformat\",\n    \"The provided file is not a DjVu document\":\n        \"Den angivna filen är inte ett DjVu-dokument\",\n    // Load a URL to a DjVu file with \"#page=100500\" at the end (both in continuous scroll and single-page view modes)\n    // e.g. https://djvu.js.org/assets/djvu_examples/DjVu3Spec.djvu#page=100500\n    \"Incorrect page number\":\n        \"Felaktigt sidnummer\",\n    \"There is no page with the number #pageNumber\":\n        \"Felaktigt sidnummer #pageNumber\",\n    // \"baseURL\" is a URL to a document directory,\n    // all links inside the document index.djvu are considered relative to this URL.\n    // The term \"base URL\" can be translated as \"a URL to the document's folder\".\n    \"No base URL for an indirect DjVu document\":  // Open \"/library/assets/czech_indirect/index.djvu\"\n        \"Ingen bas-URL för ett indirekt DjVu-dokument\",\n    \"You probably opened an indirect (multi-file) DjVu document manually.\":\n        \"Du öppnade troligen ett indirekt DjVu-dokument (med flera filer) manuellt.\",\n    \"But such multi-file documents can be only loaded by URL.\":\n        \"Dokument med flera filer kan endast laddas med URL.\",\n    \"Unexpected error\": // Of course there is no standard way to produce this kind of error\n        \"Ett oväntat fel uppstod\",\n    \"Cannot print the error, look in the console\":\n        \"Kunde inte skriva ut felet, titta i konsolen\",\n\n    // Options and its tooltips\n    \"Options\":\n        \"Alternativ\",\n    \"Show options window\":\n        \"Visa fönster med alternativ\",\n    \"Color theme\":\n        \"Färgtema\",\n    \"Extension options\":\n        \"Förlängningsalternativ\", // the options of the browser extension\n    \"Open all links with .djvu at the end via the viewer\":\n        \"Öppna alla länkar med .djvu via webbläsaren\",\n    \"All links to .djvu files will be opened by the viewer via a simple click on a link\":\n        \"Alla länkar till .djvu-filer kommer öppnas i läsaren\",\n    \"Detect .djvu files by means of http headers\":\n        \"Upptäck .djvu-filer med hjälp av HTTP-rubriker\",\n    \"Analyze headers of every new tab in order to process even links which do not end with the .djvu extension\":\n        \"Analysera rubriker på alla nya flikar för att känna av om länkar är av .djvu-format (även länkar som inte slutar med \\\".djvu\\\"\",\n\n    // Footer: status bar\n    \"Ready\":\n        \"Klar\",\n    \"Loading\":\n        \"Laddar\",\n\n    // Footer: buttons' tooltips\n    \"Show help window\":\n        \"Visa hjälpsida\",\n    \"Switch full page mode\":\n        \"Ändra visningsläge till helsida\",\n\n    // File Block tooltips\n    \"Choose a file\":\n        \"Välj en fil\",\n    \"Close document\":\n        \"Stäng dokument\",\n    \"Save document\":\n        \"Spara dokument\",\n    \"Save\":\n        \"Spara\",\n    \"Open another .djvu file\":\n        \"Öppna ytterligare en .djvu-fil\",\n\n    // Help window\n    \"The application for viewing .djvu files in the browser.\":\n        \"Applikation för att se .djvu-filer i browsern.\",\n    \"If something doesn't work properly, feel free to write about the problem at #email.\":\n        \"Om något inte fungerar korrekt, vänligen kontakta #email.\",\n    \"The official website is #website.\":\n        \"Den officiella webbsidan är #website.\",\n    \"The source code is available on #link.\":\n        \"Källkoden är tillgänglig på #link.\",\n    \"Hotkeys\":\n        \"Tangebords-genvägar\",\n    \"save the document\":\n        \"spara dokument\",\n    \"go to the previous page\":\n        \"gå till föregående sida\",\n    \"go to the next page\":\n        \"gå till nästa sida\",\n    \"Controls\":\n        \"Kontroller\",\n    \"#expandIcon and #collapseIcon are to switch the viewer to the full page mode and back.\":\n        \"Knapparna #expandIcon och #collapseIcon används för att väcla visningsläge till helsida och tillbaka.\",\n    \"If you work with the browser extension, these buttons will cause no effect, since the viewer takes the whole page by default.\":\n        \"Om du använder tillägget i browsern kommer dessa knappar inte att fungera då visningsprogrammet hanterar hela sidan som standard.\",\n\n    // Toolbar tooltips\n    \"Continuous scroll view mode\":\n        \"Visningsläge med kontinuerlig scrolling\",\n    \"Number of pages in a row\":\n        null,\n    \"Number of pages in the first row\":\n        null,\n    \"Single page view mode\":\n        \"Visningsläge med en sida\",\n    \"Text view mode\":\n        \"Visningsläge med text\",\n    \"Click on the number to enter it manually\":\n        \"Du kan klicka på sidnumret för att ange det manuellt\",\n    \"Rotate the page\":\n        \"Rotera sidan\",\n    \"You also can scale the page via Ctrl+MouseWheel\":\n        \"Du kan också skala sidan genom att använda Ctrl + Skrollknappen på musen\",\n    \"Text cursor mode\":\n        \"Visningsläge med textmarkör\",\n    \"Grab cursor mode\":\n        \"Visningsläge med greppbart markörläge\",\n    \"Table of contents\":\n        null,\n    \"Toolbar is always shown\":\n        null,\n    \"Toolbar automatically hides\":\n        null,\n\n    // Contents\n    \"Contents\":\n        \"Innehåll\",\n    \"No contents provided\":\n        \"Inget innehåll har givits\",\n    // A rare case. Open /library/assets/links.djvu in the viewer on https://djvu.js.org/ (not in the extension!)\n    // and click the \"Absolute Link\" in the contents\n    \"The link points to another document. Do you want to proceed?\":\n        \"Länken vidarebefodrar till ett annat dokument. Vill du fortsätta?\",\n\n    // Text Block (shown in the text view mode)\n    \"No text on this page\":\n        \"Ingen text finns på denna sida\",\n\n    // Save dialog (shows when you save an indirect djvu)\n    \"You are trying to save an indirect (multi-file) document.\":\n        \"Du försöker spara ett indirekt dokument (flera filer).\",\n    \"What exactly do you want to do?\":\n        \"Vad vill du göra?\",\n    \"Save only index file\":\n        \"Spara endast index-fil\",\n    \"Download, bundle and save the whole document as one file\":\n        \"Ladda ner, packa ihop och spara hela dokumentet som en fil\",\n    \"Downloading and bundling the document\":\n        \"Laddar ner och packer ihop dokumentet\",\n    \"The document has been downloaded and bundled into one file successfully\":\n        \"Dokumentet har laddats ner och packats ihop till en fil\",\n\n    // Printing\n    \"Print document\":\n        \"Skriv ut\",\n    \"Pages must be rendered before printing.\":\n        \"Sidorna måste renderas före utskrift.\",\n    \"It may take a while.\":\n        \"Det kan ta en stund.\",\n    \"Select the pages you want to print.\":\n        \"Välj de sidor du vill skriva ut.\",\n    \"From\":\n        \"Från\",\n    \"to\":\n        \"till\",\n    \"Prepare pages for printing\":\n        \"Förbered sidor för utskrift\",\n    \"Preparing pages for printing\":\n        \"Förbereder sidor för utskrift\",\n\n    // Menu\n    \"Menu\":\n        null,\n    \"Document\":\n        null,\n    \"About\":\n        null,\n    \"Print\":\n        null,\n    \"Close\":\n        null,\n    \"View mode\":\n        null,\n    \"Scale\":\n        null,\n    \"Rotation\":\n        null,\n    \"Cursor mode\":\n        null,\n    \"Full page mode\":\n        null,\n    \"Fullscreen mode\":\n        null,\n};"
  },
  {
    "path": "viewer/src/locales/Ukrainian.js",
    "content": "/**\n * Some phrases contain insertions, e.g. icons and buttons, which are inserted in the code.\n * Here instead of visual components we use placeholders, e.g. #helpButton, which start with #.\n * Your translated phrase MUST also contain the same placeholder, but you can change its position.\n *\n * Some phrases are tooltips, that is, they are visible only when you hover the cursor over controls.\n *\n * Preserve the order of phrases and put the translation on a new line.\n * (for convenience of further additions and corrections).\n *\n * All null values mean that the corresponding strings need to be translated.\n * Such values are added automatically for convenience as placeholders.\n */\n\nexport default {\n    // language info\n    englishName:\n        \"Ukrainian\",\n    nativeName:\n        \"Українська\",\n\n    \"Language\":\n        \"Мова\", // not used now, but will be used in options afterwards\n\n    // Translation: tooltips and notification\n    // (to see the notification window, remove several phrases from any dictionary, except for the English one)\n    \"Add more\":\n        \"Додати ще\",\n    \"The translation isn't complete.\":\n        \"Переклад неповний.\",\n    \"The following phrases are not translated:\":\n        \"Наступні фрази не перекладені:\",\n    \"You can improve the translation here\":\n        \"Ви можете поліпшити переклад тут\",\n\n    // Initial screen\n    \"#helpButton - learn more about the app\":\n        \"#helpButton - дізнатися більше про застосунок\",\n    \"#optionsButton - see the available options\":\n        \"#optionsButton - переглянути налаштування\",\n    \"powered with\":\n        \"працює на базі\",\n    \"Drag & Drop a file here or click to choose manually\":\n        \"Перетягніть сюди файл або клацніть та оберіть ручним способом\",\n    \"Paste a URL to a djvu file here\":\n        \"Вставте URL до djvu-файла тут\",\n    \"Open URL\":\n        \"Відкрити URL\",\n    'Enter a valid URL (it should start with \"http(s)://\" | \"data:\")': // an alert shown when you try to open an empty URL\n        'Укажіть правильний URL (мусить починатися з \"http(s)://\" або \"data:\")',\n\n    // Errors. Usually there is a header and a message for each error type.\n    // For the web request error there are different types of messages depending on the HTTP status.\n    // The ways to see the errors in the viewer are described in comments below.\n    // In case of web requests you can load links via the browser extension (via the URL field on the initial screen)\n    \"Error\":\n        \"Помилка\",\n    \"Error on page\":\n        \"Помилка на сторінці\", // Open 'library/assets/czech_indirect/index.djvu\n    \"Network error\":\n        \"Помилка мережі\", // Disable internet connection and try to load something by URL\n    \"Check your network connection\":\n        \"Перевірте своє інтернет-з'єднання\",\n    // Load any URL to a nonexistent page on the Internet,\n    // e.g. https://djvu.js.org/nonexistentpage\n    \"Web request error\":\n        \"Помилка веб запиту\",\n    \"404 Document not found\":\n        \"404 Документ не знайдено\",\n    \"403 Access forbidden\":\n        \"403 Доступ заборонено\",\n    \"500 Internal server error\":\n        \"500 Внутрішня помилка сервера\",\n    \"The request failed with HTTP status #status\":\n        \"Запит не вдався з HTTP-статусом #status\",\n    \"DjVu file is corrupted\": // Open \"/library/assets/czech_indirect/dict0085.iff\"\n        \"DjVu-файл пошкоджено\",\n    \"The file doesn't comply with the DjVu format specification or it's not a whole DjVu document\":\n        \"Файл не відповідає специфікації формату DjVu або не є цілим DjVu-документом\",\n    \"Incorrect file format\": // Open a not-djvu file.\n        \"Неправильний формат файлу\",\n    \"The provided file is not a DjVu document\":\n        \"Наданий файл не є DjVu-документом\",\n    // Load a URL to a DjVu file with \"#page=100500\" at the end (both in continuous scroll and single-page view modes)\n    // e.g. https://djvu.js.org/assets/djvu_examples/DjVu3Spec.djvu#page=100500\n    \"Incorrect page number\":\n        \"Неправильний номер сторінки\",\n    \"There is no page with the number #pageNumber\":\n        \"Сторінки з номером #pageNumber не існує\",\n    // \"baseURL\" is a URL to a document directory,\n    // all links inside the document index.djvu are considered relative to this URL.\n    // The term \"base URL\" can be translated as \"a URL to the document's folder\".\n    \"No base URL for an indirect DjVu document\":  // Open \"/library/assets/czech_indirect/index.djvu\"\n        \"Немає ланки до кореневої теки документа\",\n    \"You probably opened an indirect (multi-file) DjVu document manually.\":\n        \"Імовірно, ви відкрили багатофайловий (indirect) DjVu-документ уручну.\",\n    \"But such multi-file documents can be only loaded by URL.\":\n        \"Проте, такі багатофайлові документи можна завантажити лише за URL.\",\n    \"Unexpected error\": // Of course there is no standard way to produce this kind of error\n        \"Неочікувана помилка\",\n    \"Cannot print the error, look in the console\":\n        \"Не вдається видрукувати помилку, подивіться в консоль\",\n\n    // Options and its tooltips\n    \"Options\":\n        \"Налаштування\",\n    \"Show options window\":\n        \"Показати вікно налаштувань\",\n    \"Color theme\":\n        \"Колірна схема\",\n    \"Extension options\":\n        \"Налаштування додатка\", // the options of the browser extension\n    \"Open all links with .djvu at the end via the viewer\":\n        \"Відкривати всі ланки з .djvu на кінці в переглядачі\",\n    \"All links to .djvu files will be opened by the viewer via a simple click on a link\":\n        \"Усі ланки до .djvu файлів відкриватимуться в переглядачі простим клацом по ланці\",\n    \"Detect .djvu files by means of http headers\":\n        \"Виявляти .djvu файли за http заголовками\",\n    \"Analyze headers of every new tab in order to process even links which do not end with the .djvu extension\":\n        \"Аналізувати заголовки кожної нової вкладки, щоби опрацьовувати навіть ланки без розширення .djvu\",\n\n    // Footer: status bar\n    \"Ready\":\n        \"Готово\",\n    \"Loading\":\n        \"Завантажування\",\n\n    // Footer: buttons' tooltips\n    \"Show help window\":\n        \"Показати довідку\",\n    \"Switch full page mode\":\n        \"Перемкнути повносторінковий режим\",\n\n    // File Block tooltips\n    \"Choose a file\":\n        \"Оберіть файл\",\n    \"Close document\":\n        \"Закрити документ\",\n    \"Save document\":\n        \"Зберегти документ\",\n    \"Save\":\n        \"Зберегти\",\n    \"Open another .djvu file\":\n        \"Відкрити інший файл .djvu\",\n\n    // Help window\n    \"The application for viewing .djvu files in the browser.\":\n        \"Застосунок для перегляду файлів .djvu в браузері.\",\n    \"If something doesn't work properly, feel free to write about the problem at #email.\":\n        \"Коли щось не працює, пишіть на #email.\",\n    \"The official website is #website.\":\n        \"Офіційний вебсайт #website.\",\n    \"The source code is available on #link.\":\n        \"Вихідний код доступний на #link.\",\n    \"Hotkeys\":\n        \"Гарячі клавіші\",\n    \"save the document\":\n        \"зберегти документ\",\n    \"go to the previous page\":\n        \"перейти до попередньої сторінки\",\n    \"go to the next page\":\n        \"перейти до наступної сторінки\",\n    \"Controls\":\n        \"Кнопки\",\n    \"#expandIcon and #collapseIcon are to switch the viewer to the full page mode and back.\":\n        \"#expandIcon та #collapseIcon перемикають переглядач у повносторінковий режим і назад.\",\n    \"If you work with the browser extension, these buttons will cause no effect, since the viewer takes the whole page by default.\":\n        \"Якщо ви використовуєте додаток до браузера, ці кнопки не працюватимуть, оскільки переглядач займає всю сторінку позавказом.\",\n\n    // Toolbar tooltips\n    \"Continuous scroll view mode\":\n        \"Режим неперервної прокрутки\",\n    \"Number of pages in a row\":\n        \"Число сторінок у рядку\",\n    \"Number of pages in the first row\":\n        \"Число сторінок у першім рядку\",\n    \"Single page view mode\":\n        \"Односторінковий режим\",\n    \"Text view mode\":\n        \"Текстовий режим\",\n    \"Click on the number to enter it manually\":\n        \"Клацніть по номеру, щоб увести його вручну\",\n    \"Rotate the page\":\n        \"Повернути сторінку\",\n    \"You also can scale the page via Ctrl+MouseWheel\":\n        \"Ви також можете масштабувати сторінку через Ctrl+КоліщаткоМиші\",\n    \"Text cursor mode\":\n        \"Курсор для виділення тексту\",\n    \"Grab cursor mode\":\n        \"Режим перетягування\",\n    \"Table of contents\":\n        \"Зміст\",\n    \"Toolbar is always shown\":\n        \"Панель інструментів завжди відображається\",\n    \"Toolbar automatically hides\":\n        \"Панель інструментів автоматично приховується\",\n\n    // Contents\n    \"Contents\":\n        \"Зміст\",\n    \"No contents provided\":\n        \"Зміст відсутній\",\n    // A rare case. Open /library/assets/links.djvu in the viewer on https://djvu.js.org/ (not in the extension!)\n    // and click the \"Absolute Link\" in the contents\n    \"The link points to another document. Do you want to proceed?\":\n        \"Ланка вказує на інший документ. Чи ви бажаєте продовжити?\",\n\n    // Text Block (shown in the text view mode)\n    \"No text on this page\":\n        \"На цій сторінці немає тексту\",\n\n    // Save dialog (shows when you save an indirect djvu)\n    \"You are trying to save an indirect (multi-file) document.\":\n        \"Ви намагаєтеся зберегти багатофайловий (indirect) документ.\",\n    \"What exactly do you want to do?\":\n        \"Що саме ви хочете зробити?\",\n    \"Save only index file\":\n        \"Зберегти тільки кореневий файл\",\n    \"Download, bundle and save the whole document as one file\":\n        \"Завантажити, об'єднати й зберегти ввесь документ одним файлом\",\n    \"Downloading and bundling the document\":\n        \"Завантажування та об'єднування документа\",\n    \"The document has been downloaded and bundled into one file successfully\":\n        \"Документ успішно завантажено й об'єднано в єдиний файл\",\n\n    // Printing\n    \"Print document\":\n        \"Видрукувати документ\",\n    \"Pages must be rendered before printing.\":\n        \"Сторінки мусять бути прорисовані перед друком.\",\n    \"It may take a while.\":\n        \"Це може зайняти певний час.\",\n    \"Select the pages you want to print.\":\n        \"Виберіть ті сторінки, котрі волієте видрукувати.\",\n    \"From\":\n        \"З\",\n    \"to\":\n        \"по\",\n    \"Prepare pages for printing\":\n        \"Підготувати сторінки до друку\",\n    \"Preparing pages for printing\":\n        \"Підготовка сторінок до друку\",\n\n    // Menu\n    \"Menu\":\n        \"Меню\",\n    \"Document\":\n        \"Документ\",\n    \"About\":\n        \"Про застосунок\",\n    \"Print\":\n        \"Друк\",\n    \"Close\":\n        \"Закрити\",\n    \"View mode\":\n        \"Режим перегляду\",\n    \"Scale\":\n        \"Масштаб\",\n    \"Rotation\":\n        \"Поворіт\",\n    \"Cursor mode\":\n        \"Курсор\",\n    \"Full page mode\":\n        \"Повносторінковий режим\",\n    \"Fullscreen mode\":\n        \"Повноекранний режим\",\n};"
  },
  {
    "path": "viewer/src/locales/index.js",
    "content": "/**\n * .js extension should be used in all imports, because this file is used in a node script (syncLocales.js)\n */\n\nimport English from './English.js';\nimport Russian from './Russian.js';\nimport Swedish from './Swedish.js';\nimport French from \"./French.js\";\nimport Italian from \"./Italian.js\";\nimport ChineseSimplified from \"./ChineseSimplified.js\"\nimport Spanish from \"./Spanish.js\";\nimport Portuguese from \"./Portuguese.js\";\nimport Ukrainian from './Ukrainian.js';\n\n/**\n * Here we use 2-character lowercase ISO 639-1 codes.\n * https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes\n * These codes are used in `navigator.languages`, so we can detect the preferred languages.\n */\nexport default {\n    'en': English,\n    'ru': Russian,\n    'sv': Swedish,\n    'fr': French,\n    'it': Italian,\n    'zh': ChineseSimplified,\n    'pt': Portuguese,\n    'es': Spanish,\n    'uk': Ukrainian,\n};"
  },
  {
    "path": "viewer/src/reducers/commonReducer.js",
    "content": "import Constants from '../constants';\nimport { ActionTypes } from \"../constants\";\nimport dictionaries from '../locales';\n\nconst initialState = Object.freeze({\n    documentId: 0, // required to detect the document change in the UI components\n    fileName: null,\n    userScale: 1,\n    pageRotation: 0,\n    isLoading: false,\n    viewMode: Constants.SINGLE_PAGE_MODE,\n    pagesQuantity: null,\n    isFullPageView: false,\n    error: null,\n    contents: null,\n    isHelpWindowShown: false,\n    isOptionsWindowOpened: false,\n    isIndirect: false,\n    isContentsOpened: false,\n    options: { // all these options are saved in localStorage\n        interceptHttpRequests: true, // this value MUST BE DUPLICATED in the extension code\n        analyzeHeaders: false, // this value MUST BE DUPLICATED in the extension code\n        locale: 'en',\n        theme: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',\n        preferContinuousScroll: false,\n        pageCountInRow: 1,\n        firstRowPageCount: 1,\n    },\n    uiOptions: { // aren't saved, should be set programmatically, if required\n        hideFullPageSwitch: false,\n        changePageOnScroll: true,\n        showContentsAutomatically: true,\n        hideOpenAndCloseButtons: false,\n        hidePrintButton: false,\n        hideSaveButton: false,\n    },\n    appContext: {},\n});\n\nfunction getInitialStateWithOptions(state) {\n    return {\n        ...initialState,\n        appContext: state.appContext,\n        isFullPageView: state.isFullPageView,\n        options: state.options,\n        uiOptions: state.uiOptions,\n    };\n}\n\nexport default (state = initialState, action) => {\n    const payload = action.payload;\n    switch (action.type) {\n\n        case ActionTypes.SET_UI_OPTIONS:\n            return { ...state, uiOptions: { ...state.uiOptions, ...payload } };\n\n        case ActionTypes.UPDATE_OPTIONS:\n            return { ...state, options: { ...state.options, ...payload } };\n\n        case ActionTypes.SET_VIEW_MODE:\n            return {\n                ...state,\n                viewMode: payload,\n                isLoading: payload === Constants.TEXT_MODE ? false : state.isLoading\n            };\n\n        case Constants.SET_PAGE_ROTATION_ACTION:\n            return {\n                ...state,\n                pageRotation: action.pageRotation\n            };\n\n        case ActionTypes.TOGGLE_OPTIONS_WINDOW:\n            return { ...state, isOptionsWindowOpened: payload };\n\n        case Constants.SHOW_HELP_WINDOW_ACTION:\n            return { ...state, isHelpWindowShown: true };\n\n        case Constants.CLOSE_HELP_WINDOW_ACTION:\n            return { ...state, isHelpWindowShown: false };\n\n        case Constants.SET_NEW_PAGE_NUMBER_ACTION:\n        case Constants.CREATE_DOCUMENT_FROM_ARRAY_BUFFER_ACTION:\n            return { ...state, isLoading: true }\n\n        case Constants.IMAGE_DATA_RECEIVED_ACTION:\n        case Constants.PAGE_TEXT_FETCHED_ACTION:\n        case Constants.PAGE_ERROR_ACTION:\n        case Constants.PAGES_SIZES_ARE_GOTTEN:\n        case ActionTypes.SET_IMAGE_PAGE_ERROR:\n            return { ...state, isLoading: false, };\n\n        case Constants.DOCUMENT_CREATED_ACTION:\n            return {\n                ...getInitialStateWithOptions(state),\n                documentId: state.documentId + 1,\n                isLoading: true,\n                viewMode: state.options.preferContinuousScroll ? Constants.CONTINUOUS_SCROLL_MODE : initialState.viewMode,\n                pagesQuantity: action.pagesQuantity,\n                fileName: action.fileName,\n                isIndirect: action.isIndirect,\n            };\n\n        case Constants.CLOSE_DOCUMENT_ACTION:\n            return getInitialStateWithOptions(state);\n\n        case Constants.CONTENTS_IS_GOTTEN_ACTION:\n            return {\n                ...state,\n                contents: action.contents,\n                isContentsOpened: state.uiOptions.showContentsAutomatically && !state.appContext.isMobile\n                    && !!action.contents,\n            };\n\n        case Constants.SET_USER_SCALE_ACTION:\n            return {\n                ...state,\n                userScale: action.scale\n            }\n\n        case Constants.TOGGLE_FULL_PAGE_VIEW_ACTION:\n            return {\n                ...state,\n                isFullPageView: action.isFullPageView\n            }\n\n        case ActionTypes.CLOSE_CONTENTS:\n            return { ...state, isContentsOpened: false };\n\n        case ActionTypes.TOGGLE_CONTENTS:\n            return { ...state, isContentsOpened: !state.isContentsOpened };\n\n        case ActionTypes.ERROR:\n            return {\n                ...state,\n                isLoading: false,\n                error: payload,\n            };\n\n        case ActionTypes.CLOSE_ERROR_WINDOW:\n            return { ...state, error: null }\n\n        case ActionTypes.UPDATE_APP_CONTEXT:\n            return { ...state, appContext: payload };\n\n        default:\n            return state;\n    }\n}\n\nexport const get = {\n    pageCountInRow: state => {\n        if (state.appContext.isMobile) return 1;\n        return Math.max(1, Math.min(state.options.pageCountInRow, state.pagesQuantity, Constants.MAX_PAGE_COUNT_IN_ROW));\n    },\n    firstRowPageCount: state => {\n        if (state.appContext.isMobile) return 1;\n        return Math.max(1, Math.min(state.options.firstRowPageCount, get.pageCountInRow(state)));\n    },\n    isContentsOpened: state => state.isContentsOpened,\n    dictionary: state => dictionaries[get.options(state).locale] || dictionaries.en,\n    isOptionsWindowOpened: state => state.isOptionsWindowOpened,\n    uiOptions: state => state.uiOptions,\n    documentId: state => state.documentId,\n    userScale: state => state.userScale,\n    pageRotation: state => state.pageRotation,\n    pagesQuantity: state => state.pagesQuantity,\n    contents: state => state.contents,\n    isIndirect: state => state.isIndirect,\n    isHelpWindowShown: state => state.isHelpWindowShown,\n    fileName: state => state.fileName,\n    error: state => state.error,\n    options: state => state.options,\n    isFullPageView: state => state.isFullPageView,\n    isLoading: state => state.isLoading,\n    isDocumentLoaded: state => !!state.pagesQuantity,\n    viewMode: state => {\n        if (state.isIndirect && state.viewMode === Constants.CONTINUOUS_SCROLL_MODE) {\n            return Constants.SINGLE_PAGE_MODE;\n        }\n        return state.viewMode;\n    }\n};"
  },
  {
    "path": "viewer/src/reducers/fileLoadingReducer.js",
    "content": "import { createSelector } from 'reselect';\nimport Constants, { ActionTypes } from '../constants';\n\nconst initialState = Object.freeze({\n    isFileLoading: false,\n    loadedBytes: 0,\n    totalBytes: 0\n});\n\nexport default function fileLoadingReducer(state = initialState, action) {\n    switch (action.type) {\n        case Constants.START_FILE_LOADING_ACTION:\n            return {\n                ...state,\n                isFileLoading: true,\n            }\n\n        case Constants.FILE_LOADING_PROGRESS_ACTION:\n            return {\n                ...state,\n                loadedBytes: action.loaded,\n                totalBytes: action.total\n            }\n\n        case Constants.END_FILE_LOADING_ACTION:\n        case ActionTypes.ERROR:\n            return initialState;\n\n        default:\n            return state;\n    }\n}\n\nconst $ = selector => createSelector(state => state.fileLoadingState, selector);\n\nexport const get = {\n    isFileLoading: $(s => s.isFileLoading),\n    loadedBytes: $(s => s.loadedBytes),\n    totalBytes: $(s => s.totalBytes),\n};"
  },
  {
    "path": "viewer/src/reducers/fileProcessingReducer.js",
    "content": "import { createSelector } from 'reselect';\nimport { ActionTypes } from '../constants';\n\nconst initialState = Object.freeze({\n    progress: 0,\n    buffer: null,\n    isBundling: false,\n    isSaveDialogShown: false,\n});\n\nexport default function fileProcessingReducer(state = initialState, action) {\n    const { type, payload } = action;\n    switch (type) {\n        case ActionTypes.OPEN_SAVE_DIALOG:\n            return { ...state, isSaveDialogShown: true };\n\n        case ActionTypes.START_TO_BUNDLE:\n            return { ...state, isBundling: true };\n\n        case ActionTypes.UPDATE_FILE_PROCESSING_PROGRESS:\n            return { ...state, progress: payload };\n\n        case ActionTypes.FINISH_TO_BUNDLE:\n            return { ...state, buffer: payload };\n\n        case ActionTypes.ERROR:\n        case ActionTypes.CLOSE_SAVE_DIALOG:\n            return initialState;\n\n        default:\n            return state;\n    }\n}\n\nconst $ = selector => createSelector(state => state.fileProcessingState, selector);\n\nexport const get = {\n    isSaveDialogShown: $(s => s.isSaveDialogShown),\n    isBundling: $(s => s.isBundling),\n    resultBuffer: $(s => s.buffer),\n    fileProcessingProgress: $(s => s.progress),\n};"
  },
  {
    "path": "viewer/src/reducers/index.js",
    "content": "import fileLoadingReducer, { get as fileLoadingGet } from './fileLoadingReducer';\nimport pageReducer, { get as pageGet } from './pageReducer';\nimport commonReducer, { get as commonGet } from './commonReducer';\nimport fileProcessingReducer, { get as fileProcessingGet } from \"./fileProcessingReducer\";\nimport printReducer, { get as printGet } from './printReducer';\n\nexport const get = {\n    ...commonGet,\n    ...pageGet,\n    ...fileLoadingGet,\n    ...fileProcessingGet,\n    ...printGet,\n};\n\nexport default (state, action) => {\n    state = commonReducer(state, action);\n    return {\n        ...state,\n        fileLoadingState: fileLoadingReducer(state.fileLoadingState, action),\n        pageState: pageReducer(state.pageState, action),\n        fileProcessingState: fileProcessingReducer(state.fileProcessingState, action),\n        printState: printReducer(state.printState, action),\n    }\n};"
  },
  {
    "path": "viewer/src/reducers/pageReducer.js",
    "content": "import { createSelector } from 'reselect';\nimport Constants from '../constants';\nimport { ActionTypes } from '../constants/index';\n\nconst singlePageInitialState = Object.freeze({\n    imageData: null,\n    imageDpi: null,\n    pageText: null,\n    textZones: null,\n    imagePageError: null,\n    textPageError: null,\n});\n\nconst initialState = Object.freeze({\n    ...singlePageInitialState,\n    cursorMode: Constants.GRAB_CURSOR_MODE,\n    currentPageNumber: 1,\n    shouldScrollToPage: false,\n    pageList: [],\n    pageSizeList: [],\n});\n\nexport default function pageReducer(state = initialState, action) {\n    const payload = action.payload;\n    switch (action.type) {\n        case Constants.DROP_PAGE_ACTION: {\n            const newPagesList = [...state.pageList];\n            const index = action.pageNumber - 1;\n            if (newPagesList[index]) { // some pages (loaded as \"last pages\" of the previous saga) can not be in the state, but only in the registry in the saga class\n                newPagesList[index] = {\n                    width: newPagesList[index].width,\n                    height: newPagesList[index].height,\n                    dpi: newPagesList[index].dpi,\n                };\n            }\n            return { ...state, pageList: newPagesList };\n        }\n\n        case Constants.DROP_ALL_PAGES_ACTION:\n            return {\n                ...state,\n                pageList: [...state.pageSizeList],\n            }\n\n        case Constants.PAGES_SIZES_ARE_GOTTEN:\n            return {\n                ...state,\n                isLoading: false,\n                pageSizeList: action.sizes,\n                pageList: action.sizes,\n            };\n\n        case Constants.PAGE_IS_LOADED_ACTION:\n            const page = state.pageList[action.pageNumber - 1];\n            if (page && page.url) { // if it has been already loaded we should avoid unnecessary updates\n                return state;\n            }\n            const newPagesList = [...state.pageList];\n            newPagesList[action.pageNumber - 1] = action.pageData;\n            return {\n                ...state,\n                pageList: newPagesList\n            };\n\n        case Constants.SET_CURSOR_MODE_ACTION:\n            return {\n                ...state,\n                cursorMode: action.cursorMode\n            };\n\n        case Constants.IMAGE_DATA_RECEIVED_ACTION:\n            return {\n                ...state,\n                imageData: action.imageData,\n                imageDpi: action.imageDpi\n            };\n\n        case Constants.SET_NEW_PAGE_NUMBER_ACTION:\n            return {\n                ...state,\n                ...((state.textPageError || state.imagePageError) ? singlePageInitialState : null),\n                shouldScrollToPage: action.shouldScrollToPage,\n                currentPageNumber: action.pageNumber\n            };\n\n        case ActionTypes.SET_VIEW_MODE:\n            if (payload === Constants.CONTINUOUS_SCROLL_MODE) {\n                return { ...state, ...singlePageInitialState };\n            }\n            break;\n\n        case Constants.PAGE_TEXT_FETCHED_ACTION:\n            return {\n                ...state,\n                pageText: action.pageText,\n                textZones: action.textZones\n            };\n\n        case ActionTypes.SET_IMAGE_PAGE_ERROR:\n            return { ...state, imagePageError: payload };\n\n        case ActionTypes.SET_TEXT_PAGE_ERROR:\n            return { ...state, textPageError: payload };\n    }\n\n    return state;\n}\n\n/** @returns {function} */\nconst $ = selector => createSelector(state => state.pageState, selector);\n\nexport const get = {\n    cursorMode: $(s => s.cursorMode),\n    pageText: $(s => s.pageText),\n    imageData: $(s => s.imageData),\n    imageDpi: $(s => s.imageDpi),\n    textZones: $(s => s.textZones),\n    currentPageNumber: $(s => s.currentPageNumber),\n    shouldScrollToPage: $(s => s.shouldScrollToPage),\n    imagePageError: $(s => s.imagePageError),\n    textPageError: $(s => s.textPageError),\n    pageList: $(s => s.pageList),\n    pageSizeList: $(s => s.pageSizeList),\n};"
  },
  {
    "path": "viewer/src/reducers/printReducer.js",
    "content": "import { createSelector } from 'reselect';\nimport { ActionTypes } from '../constants';\n\nconst initialState = Object.freeze({\n    isPrintDialogOpened: false,\n    isPreparingForPrinting: false,\n    printProgress: 0,\n    pagesForPrinting: null,\n});\n\nexport default function fileProcessingReducer(state = initialState, action) {\n    const { type, payload } = action;\n    switch (type) {\n        case ActionTypes.OPEN_PRINT_DIALOG:\n            // initial state is used just in case if there are pagesForPrinting from a previous attempt\n            // (if the saga wasn't cancelled, but it should be cancelled)\n            return { ...initialState, isPrintDialogOpened: true };\n\n        case ActionTypes.PREPARE_PAGES_FOR_PRINTING:\n            return { ...state, isPreparingForPrinting: true };\n\n        case ActionTypes.UPDATE_PRINT_PROGRESS:\n            return { ...state, printProgress: payload };\n\n        case ActionTypes.START_PRINTING:\n            return { ...state, pagesForPrinting: payload };\n\n        case ActionTypes.ERROR:\n        case ActionTypes.CLOSE_PRINT_DIALOG:\n            return initialState;\n\n        default:\n            return state;\n    }\n}\n\nconst $ = selector => createSelector(state => state.printState, selector);\n\nexport const get = {\n    isPrintDialogOpened: $(s => s.isPrintDialogOpened),\n    isPreparingForPrinting: $(s => s.isPreparingForPrinting),\n    printProgress: $(s => s.printProgress),\n    pagesForPrinting: $(s => s.pagesForPrinting),\n};"
  },
  {
    "path": "viewer/src/sagas/ContinuousScrollManager.js",
    "content": "/**\n * The logic related to page caching in the continuous scroll mode.\n * It pre-fetches pages depending on the current page number and the radius\n * (a number of pages before and after the current one).\n * All pages outside the radius are removed from the cache in order not to retain\n * too much memory, although each page is encoded as an Object URL to a PNG,\n * so it takes only several kilobytes.\n */\n\nimport { put, select } from 'redux-saga/effects';\nimport Actions from \"../actions/actions\";\nimport { get } from '../reducers';\nimport Constants from '../constants';\n\nconst radius = 20;\n\nexport default class ContinuousScrollManager {\n    constructor(djvuWorker, pagesCount, pageStorage) {\n        this._reset(djvuWorker, pagesCount, pageStorage);\n    }\n\n    _reset(djvuWorker, pagesCount, pageStorage) {\n        this.pagesCount = pagesCount;\n        this.pageStorage = pageStorage;\n        this.djvuWorker = djvuWorker;\n        this.obsoletePageNumbers = [];\n\n        // a previous saga is cancelled on a new action, so we should keep the last promise in order to avoid fetching the same page twice\n        this.lastLoadPagePromise = null;\n        this.lastLoadPageNumber = null;\n    }\n\n    * reset() {\n        yield* this.dropAllPages();\n        this._reset(this.djvuWorker, this.pagesCount, this.pageStorage);\n    }\n\n    setPageNumber(pageNumber) {\n        var unusedLength = 0;\n        this.pageNumber = pageNumber;\n\n        this.leftNumber = pageNumber - radius;\n        if (this.leftNumber < 1) {\n            unusedLength += 1 - this.leftNumber;\n            this.leftNumber = 1;\n        }\n\n        this.rightNumber = pageNumber + radius;\n        if (this.rightNumber > this.pagesCount) {\n            unusedLength += this.rightNumber - this.pagesCount;\n            this.rightNumber = this.pagesCount;\n        }\n\n        if (unusedLength) {\n            if (this.leftNumber > 1) {\n                this.leftNumber = Math.max(1, this.leftNumber - unusedLength);\n            } else if (this.rightNumber < this.pagesCount) {\n                this.rightNumber = Math.min(this.pagesCount, this.rightNumber + unusedLength);\n            }\n        }\n    }\n\n    updateRegistries() {\n        this.obsoletePageNumbers = [];\n        for (const pageNumber of this.pageStorage.getAllPageNumbers()) {\n            if (pageNumber < this.leftNumber || pageNumber > this.rightNumber) {\n                this.obsoletePageNumbers.push(pageNumber);\n            }\n        }\n    }\n\n    * dropAllPages() {\n        yield put({ type: Constants.DROP_ALL_PAGES_ACTION });\n        this.pageStorage.removeAllPages();\n    }\n\n    * dropPage(pageNumber) {\n        yield put(Actions.dropPageAction(pageNumber));\n        this.pageStorage.removePage(pageNumber);\n    }\n\n    * removeObsoletePagesIfRequired() {\n        const excess = this.pageStorage.getAllPageNumbers().length - 2 * radius - 1;\n        if (excess > 0) {\n            for (let i = 0; i < excess; i++) {\n                yield* this.dropPage(this.obsoletePageNumbers[i]);\n            }\n            this.obsoletePageNumbers.splice(0, excess);\n        }\n    }\n\n    * loadPageFromLastPromise() {\n        try {\n            const [page, textZones] = yield this.lastLoadPagePromise;\n            page.textZones = textZones;\n            this.pageStorage.addPage(this.lastLoadPageNumber, page);\n        } finally { // it's executed when the saga is cancelled too, so it won't hang on the \"lastLoadPagePromise\" if it's cancelled\n            this.lastLoadPagePromise = null;\n            this.lastLoadPageNumber = null;\n        }\n    }\n\n    * loadPage(pageNumber) {\n        const page = this.pageStorage.getPage(pageNumber);\n        if (!page || !page.hasOwnProperty('textZones')) {\n            this.lastLoadPagePromise = page ? Promise.all([\n                Promise.resolve(page),\n                this.djvuWorker.doc.getPage(pageNumber).getNormalizedTextZones().run(),\n            ]) : this.djvuWorker.run(\n                this.djvuWorker.doc.getPage(pageNumber).createPngObjectUrl(),\n                this.djvuWorker.doc.getPage(pageNumber).getNormalizedTextZones(),\n            );\n            this.lastLoadPageNumber = pageNumber;\n\n            yield* this.loadPageFromLastPromise();\n            yield* this.removeObsoletePagesIfRequired();\n        }\n\n        yield put(Actions.pageIsLoadedAction(this.pageStorage.getPage(pageNumber), pageNumber));\n    }\n\n    * startDataFetching() {\n        // the process of loading can't be stopped so it's by all means better\n        // to save the page to the registry, even if it will not be used soon\n        if (this.lastLoadPagePromise) {\n            yield* this.loadPageFromLastPromise();\n        }\n\n        const state = yield select();\n        const pageNumber = get.currentPageNumber(state);\n        this.setPageNumber(pageNumber);\n        this.updateRegistries();\n        this.djvuWorker.emptyTaskQueue();\n\n        const span = Math.min(this.pageNumber - this.leftNumber, this.rightNumber - this.pageNumber);\n\n        yield* this.loadPage(this.pageNumber);\n\n        for (let i = 1; i <= span; i++) {\n            yield* this.loadPage(this.pageNumber + i);\n            yield* this.loadPage(this.pageNumber - i);\n        }\n\n        if (this.pageNumber - this.leftNumber < this.rightNumber - this.pageNumber) { // if we are nearer to the beginning\n            for (let i = this.leftNumber + 2 * span + 1; i <= this.rightNumber; i++) { // load pages in a usual order\n                yield* this.loadPage(i);\n            }\n        } else {\n            for (let i = this.rightNumber - 2 * span - 1; i >= this.leftNumber; i--) { // else load pages in a reversed order\n                yield* this.loadPage(i);\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "viewer/src/sagas/PageStorage.js",
    "content": "/**\n * All URLs should be revoked after they are not needed any more.\n * Also these URLs can be used not only in the continuous scroll mode,\n * but for printing too.\n * Therefore, for convenience, they are all stored in one place.\n */\n\nexport default class PageStorage {\n    constructor() {\n        this.pages = {};\n    }\n\n    reset() {\n        this.removeAllPages();\n    }\n\n    getAllPageNumbers() {\n        return Object.keys(this.pages);\n    }\n\n    getPage(number) {\n        return this.pages[number];\n    }\n\n    addPage(number, data) {\n        this.pages[number] = data;\n    }\n\n    removePage(number) {\n        const page = this.pages[number];\n        if (!page) return;\n        URL.revokeObjectURL(page.url);\n        delete this.pages[number];\n    }\n\n    removeAllPages() {\n        for (const pageNumber of Object.keys(this.pages)) {\n            URL.revokeObjectURL(this.pages[pageNumber].url);\n        }\n        this.pages = {};\n    }\n}"
  },
  {
    "path": "viewer/src/sagas/PagesCache.js",
    "content": "/**\n * The logic of extracting and caching pages of a document in the single page view mode.\n * The current, the previous and the next pages are fetched (and pre-fetched).\n * A page in the format of ImageData may take about 30 MB of RAM,\n * so it's not good to cache more than 3.\n */\n\nimport { fork } from 'redux-saga/effects';\n//import { delay } from 'redux-saga';\n\nexport default class PagesCache {\n\n    constructor(djvuWorker) {\n        this.djvuWorker = djvuWorker;\n        this.pages = {};\n        this.imageDataPromise = null;\n        this.imageDataPromisePageNumber = null;\n\n        this.currentPageNumber = null;\n        this.lastCachingTask = null;\n    }\n\n    cancelCachingTask() {\n        // Actually, it should never happen, because the task is forked (not spawned),\n        // so it is cancelled automatically by \"takeLatest\".\n        // But it can happen, if we run the method via \"yield*\" in another saga rather than via dispatching the action.\n        if (this.lastCachingTask && this.lastCachingTask.isRunning()) {\n            console.warn(\"DjVu.js Viewer: Have to cancel the caching task manually!\", this.lastCachingTask);\n            this.lastCachingTask.cancel();\n        }\n        this.lastCachingTask = null;\n    }\n\n    resetPagesCache() {\n        this.cancelCachingTask();\n        this.pages = {};\n        this.imageDataPromise = null;\n        this.imageDataPromisePageNumber = null;\n    }\n\n    * fetchCurrentPageByNumber(currentPageNumber, pagesQuantity) {\n        const newPages = {\n            [currentPageNumber]: this.pages[currentPageNumber]\n        };\n        var pageNumbersToCache = null;\n\n        if (currentPageNumber > 1 && currentPageNumber < pagesQuantity) {\n            pageNumbersToCache = [currentPageNumber + 1, currentPageNumber - 1];\n        } else if (currentPageNumber === 1) {\n            pageNumbersToCache = pagesQuantity >= 3 ? [2, 3]\n                : pagesQuantity >= 2 ? [2] : null;\n        } else if (currentPageNumber === pagesQuantity) {\n            pageNumbersToCache = pagesQuantity >= 3 ? [currentPageNumber - 1, currentPageNumber - 2]\n                : pagesQuantity >= 2 ? [currentPageNumber - 1] : null;\n        }\n\n        if (pageNumbersToCache) {\n            for (var pageNumber of pageNumbersToCache) {\n                newPages[pageNumber] = this.pages[pageNumber];\n            }\n        }\n\n        this.pages = newPages;\n\n        this.cancelCachingTask();\n        yield* this.fetchImageDataByPageNumber(currentPageNumber);\n\n        if (pageNumbersToCache) {\n            // load other pages in a parallel task\n            this.lastCachingTask = yield fork([this, this.cachePages], pageNumbersToCache);\n        }\n\n        return this.pages[currentPageNumber];\n    }\n\n    * cachePages(pageNumbersToCache) {\n        for (var pageNumber of pageNumbersToCache) {\n            yield* this.fetchImageDataByPageNumber(pageNumber);\n        }\n    }\n\n    * fetchImageDataByPageNumber(pageNumber) {\n        if (pageNumber !== null && (!this.pages[pageNumber] || this.pages[pageNumber].error)) {\n            if (this.imageDataPromisePageNumber !== pageNumber) {\n                if (this.imageDataPromise) {\n                    this.djvuWorker.cancelAllTasks();\n                }\n\n                this.imageDataPromisePageNumber = pageNumber;\n                this.imageDataPromise = this.djvuWorker.run(\n                    this.djvuWorker.doc.getPage(pageNumber).getImageData(),\n                    this.djvuWorker.doc.getPage(pageNumber).getDpi(),\n                );\n            }\n\n            try {\n                //yield delay(2000);\n                let res = yield this.imageDataPromise;\n                const [imageData, dpi] = res;\n                this.pages[pageNumber] = { imageData, dpi };\n            } catch (e) {\n                this.pages[pageNumber] = { error: e };\n            }\n\n            // not in finally block, because the finally block is executed when a saga task is cancelled, but it's not needed\n            this.imageDataPromise = null;\n            this.imageDataPromisePageNumber = null;\n        }\n    }\n}"
  },
  {
    "path": "viewer/src/sagas/PrintManager.js",
    "content": "import { put } from \"redux-saga/effects\";\nimport { ActionTypes } from \"../constants\";\n\nexport default class PrintManager {\n    constructor(worker, pageStorage) {\n        this.djvuWorker = worker;\n        this.pageStorage = pageStorage;\n    }\n\n    * preparePagesForPrinting(from, to) {\n        this.djvuWorker.cancelAllTasks();\n        let loaded = 0;\n        const total = to - from + 1;\n        for (let i = from; i <= to; i++) {\n            if (this.pageStorage.getPage(i)) {\n                loaded++;\n            }\n        }\n\n        function* updateProgress() {\n            yield put({ type: ActionTypes.UPDATE_PRINT_PROGRESS, payload: Math.round(loaded / total * 100) });\n        }\n\n        yield* updateProgress();\n\n        if (loaded !== total) {\n            for (let i = from; i <= to; i++) {\n                if (!this.pageStorage.getPage(i)) {\n                    const page = yield this.djvuWorker.doc.getPage(i).createPngObjectUrl().run();\n                    this.pageStorage.addPage(i, page);\n                    loaded++;\n                    yield* updateProgress();\n                }\n            }\n        }\n\n        const pagesArray = [];\n        for (let i = from; i <= to; i++) {\n            pagesArray.push(this.pageStorage.getPage(i));\n        }\n\n        yield put({ type: ActionTypes.START_PRINTING, payload: pagesArray });\n    }\n}"
  },
  {
    "path": "viewer/src/sagas/rootSaga.js",
    "content": "/**\n * All side-effect logic is here (all logic which isn't related directly to the UI)\n */\nimport { parse as parseContentDisposition } from 'content-disposition-header';\nimport { put, select, takeLatest, take, cancel, fork } from 'redux-saga/effects';\nimport { get } from '../reducers';\n// import { delay } from 'redux-saga';\n\nimport Constants, { ActionTypes } from '../constants';\nimport Actions from \"../actions/actions\";\nimport PagesCache from './PagesCache';\nimport DjVu from '../DjVu';\nimport ContinuousScrollManager from './ContinuousScrollManager';\nimport { normalizeFileName, inExtension } from '../utils';\nimport { loadFile } from '../utils';\nimport dictionaries from \"../locales\";\nimport { createTranslator } from \"../components/Translation\";\nimport PrintManager from './PrintManager';\nimport PageStorage from \"./PageStorage\";\n\nclass RootSaga {\n    constructor(dispatch) {\n        this.dispatch = dispatch;\n        this.callbacks = {};\n\n        // Firefox's extension moderators asked me not to use \"content_security_policy\": \"script-src blob:\" permission,\n        // so just for them on the main page of the extension a script url should be provided manually.\n        // In all other cases no libURL is required - a blob URL will be generated automatically for the worker.\n        const libURL = inExtension ? document.querySelector('script#djvu_js_lib').src : undefined;\n        this.djvuWorker = new DjVu.Worker(libURL);\n\n        // it's needed to recreate the worker when the bundle() operation is cancelled (but save the document),\n        // because bundle() may take quite a while, if there are many pages in the document.\n        this.isBundling = false;\n        this.documentContructorData = null;\n\n        this.pageStorage = new PageStorage();\n        this.pagesCache = new PagesCache(this.djvuWorker);\n        this.printManager = new PrintManager(this.djvuWorker, this.pageStorage);\n        this.printTask = null;\n        this.continuousScrollManager = null;\n    }\n\n    * getImageData() {\n        const state = yield select();\n        const currentPageNumber = get.currentPageNumber(state);\n        const pagesQuantity = get.pagesQuantity(state);\n\n        const currentPageData = yield* this.pagesCache.fetchCurrentPageByNumber(currentPageNumber, pagesQuantity);\n\n        if (currentPageData.error) {\n            yield put({ type: ActionTypes.SET_IMAGE_PAGE_ERROR, payload: currentPageData.error });\n        } else {\n            //console.log('put Consts.IMAGE_DATA_RECEIVED_ACTION');\n            yield put({\n                type: Constants.IMAGE_DATA_RECEIVED_ACTION,\n                imageData: currentPageData.imageData,\n                imageDpi: currentPageData.dpi\n            });\n        }\n    }\n\n    * fetchPageData() {\n        const state = yield select();\n        const viewMode = get.viewMode(state);\n\n        // when an outer config is provided, and the continuous scroll isn't default,\n        // the page reset can start this saga before the manager has been crated.\n        // Must be fixed at the architectural level, maybe the manager should be created lazily.\n        if (viewMode === Constants.CONTINUOUS_SCROLL_MODE && this.continuousScrollManager) {\n            yield* this.continuousScrollManager.startDataFetching();\n        } else {\n            const pageNumber = get.currentPageNumber(state);\n\n            if (viewMode === Constants.TEXT_MODE) {\n                this.pagesCache.cancelCachingTask();\n                this.djvuWorker.cancelAllTasks();\n                yield* this.fetchPageText(pageNumber);\n            }\n\n            yield* this.getImageData();\n            yield* this.fetchPageText(pageNumber);\n        }\n    }\n\n    * fetchPageText(pageNumber) {\n        try {\n            //console.log('text ', pageNumber);\n            const [text, textZones] = yield this.djvuWorker.run(\n                this.djvuWorker.doc.getPage(pageNumber).getText(),\n                this.djvuWorker.doc.getPage(pageNumber).getNormalizedTextZones(),\n            );\n\n            yield put({\n                type: Constants.PAGE_TEXT_FETCHED_ACTION,\n                pageText: text,\n                textZones: textZones\n            });\n        } catch (e) {\n            yield put({ type: ActionTypes.SET_TEXT_PAGE_ERROR, payload: e });\n        }\n    }\n\n    * fetchPageTextIfRequired() {\n        const state = yield select();\n        const currentPageNumber = get.currentPageNumber(state);\n        const pageText = get.pageText(state);\n\n        if (pageText !== null) {\n            return; // already fetched\n        }\n\n        yield* this.fetchPageText(currentPageNumber);\n    }\n\n    * prepareForContinuousMode() {\n        const pagesSizes = yield this.djvuWorker.doc.getPagesSizes().run();\n        this.continuousScrollManager = new ContinuousScrollManager(this.djvuWorker, pagesSizes.length, this.pageStorage);\n        yield put(Actions.pagesSizesAreGottenAction(pagesSizes));\n    }\n\n    * configure({ pageNumber, pageRotation, viewMode, pageScale, language, theme, uiOptions }) {\n        if (viewMode) yield put({ type: ActionTypes.SET_VIEW_MODE, payload: viewMode, notSave: true });\n        if (pageNumber) yield put(Actions.setNewPageNumberAction(pageNumber, true));\n        if (pageRotation) yield put(Actions.setPageRotationAction(pageRotation));\n        if (pageScale) yield put(Actions.setUserScaleAction(pageScale));\n        if (uiOptions) yield put({\n            type: ActionTypes.SET_UI_OPTIONS,\n            payload: uiOptions,\n        });\n\n        const options = {};\n        if (language) {\n            if (language in dictionaries) {\n                options.locale = language;\n            } else {\n                console.warn(`DjVu.js Viewer: only ${Object.keys(dictionaries)} languages are available! Got ${language}`);\n            }\n        }\n        if (theme) {\n            if (theme === 'dark' || theme === 'light') {\n                options.theme = theme;\n            } else {\n                console.warn('DjVu.js Viewer: only \"dark\" or \"light\" themes are supported! Got ' + theme);\n            }\n        }\n\n        if (Object.keys(options).length) {\n            yield put({\n                type: ActionTypes.UPDATE_OPTIONS,\n                payload: options,\n                notSave: true,\n            });\n        }\n    }\n\n    * createDocumentFromArrayBuffer({ arrayBuffer, fileName, config }) {\n        this.resetWorkerAndStorages();\n\n        this.documentContructorData = {\n            buffer: arrayBuffer.slice(0),\n            options: config && config.djvuOptions,\n        };\n        yield this.djvuWorker.createDocument(arrayBuffer, config && config.djvuOptions);\n        const [pagesQuantity, isBundled] = yield this.djvuWorker.run(\n            this.djvuWorker.doc.getPagesQuantity(),\n            this.djvuWorker.doc.isBundled(),\n        );\n        if (isBundled) {\n            this.documentContructorData = null;\n        }\n\n        yield put({\n            type: Constants.DOCUMENT_CREATED_ACTION,\n            pagesQuantity: pagesQuantity,\n            fileName: fileName,\n            isIndirect: !isBundled,\n        });\n\n        yield* this.loadContents();\n\n        const state = yield select();\n        if (get.viewMode(state) === Constants.CONTINUOUS_SCROLL_MODE) {\n            yield* this.prepareForContinuousMode();\n        }\n\n        // perhaps it's better to configure the viewer before DOCUMENT_CREATED_ACTION \n        // (since the promise of the viewer.loadDocument is resolved on this action)\n        // But currently DOCUMENT_CREATED_ACTION reset the state of the viewer, so the configuration is done after it.\n        // The optimal variant is to resolve the promise on another action, but I'm not sure is it needed to anybody at all.\n        // Also, configuration can start some heavy sagas (e.g. viewMode or pageNumber change) which cancel all worker tasks.\n        // Thus, it's done after all other tasks (including loading of the contents) are done.\n        if (config) {\n            yield* this.configure(config);\n        }\n\n        yield* this.resetCurrentPageNumber();\n    }\n\n    * loadContents() {\n        const contents = yield this.djvuWorker.doc.getContents().run();\n        yield put({\n            type: Constants.CONTENTS_IS_GOTTEN_ACTION,\n            contents: contents\n        });\n    }\n\n    * resetCurrentPageNumber() {\n        // set the current number to start page fetching saga\n        // fetchPageData shouldn't be called via yield* directly, otherwise it won't be cancelled by takeLatest effect\n        const state = yield select();\n        yield put(Actions.setNewPageNumberAction(get.currentPageNumber(state), true)); // set the current number to start page fetching saga\n    }\n\n    * setPageByUrl(action) {\n        const url = action.url;\n        if (url && url[0] !== '#') { // urls can be empty strings sometimes\n            // right now the constructor options are saved for indirect documents only\n            const data = this.documentContructorData;\n            const baseUrl = data && data.options && data.options.baseUrl;\n            const absoluteUrl = (\n                /^https?:\\/\\/.+/.test(url) ? url :\n                    baseUrl ? new URL(url, baseUrl).href :\n                        !inExtension ? new URL(url, location.href) : null\n            );\n\n            if (absoluteUrl) {\n                if (inExtension) {\n                    chrome.runtime.sendMessage({ command: 'open_viewer_tab', url: absoluteUrl });\n                } else {\n                    const t = createTranslator(yield select(get.dictionary));\n                    if (confirm(t('The link points to another document. Do you want to proceed?'))) {\n                        yield put({\n                            type: ActionTypes.LOAD_DOCUMENT_BY_URL,\n                            url: absoluteUrl,\n                        })\n                    }\n                }\n                return;\n            }\n        }\n\n        const pageNumber = yield this.djvuWorker.doc.getPageNumberByUrl(action.url).run();\n        if (pageNumber !== null) {\n            yield put(Actions.setNewPageNumberAction(pageNumber, true));\n            if (action.closeContentsOnSuccess) {\n                yield put({ type: ActionTypes.CLOSE_CONTENTS });\n            }\n        }\n    }\n\n    withErrorHandler(func) {\n        func = func.bind(this);\n        return function* (action) {\n            try {\n                const gen = func(action);\n                if (gen && gen.next) yield* gen;\n            } catch (error) {\n                yield put(Actions.errorAction(error))\n            }\n        }\n    }\n\n    * saveDocument() {\n        const state = yield select();\n        const fileName = get.fileName(state);\n\n        if (fileName) {\n            const url = yield this.djvuWorker.doc.createObjectURL().run();\n            const a = document.createElement('a');\n            a.href = url;\n            a.download = normalizeFileName(fileName);\n            a.dispatchEvent(new MouseEvent(\"click\"));\n        }\n    }\n\n    resetWorkerAndStorages() {\n        this.pageStorage.reset();\n        this.pagesCache.resetPagesCache();\n        this.djvuWorker.reset();\n        // we don't have to reset it, since the worker was recreated and all memory was released in any case\n        this.continuousScrollManager = null;\n        this.isBundling = false;\n        this.documentContructorData = null;\n    }\n\n    setCallback(action) {\n        this.callbacks[action.callbackName] = action.callback;\n    }\n\n    * switchToContinuousScrollMode(notSave = false) {\n        this.djvuWorker.cancelAllTasks();\n        if (!this.continuousScrollManager) {\n            yield* this.prepareForContinuousMode();\n        }\n        this.pagesCache.resetPagesCache();\n        yield* this.resetCurrentPageNumber();\n        if (!notSave) yield put({ type: ActionTypes.UPDATE_OPTIONS, payload: { preferContinuousScroll: true } });\n    }\n\n    * switchToSinglePageMode(notSave = false) {\n        this.djvuWorker.cancelAllTasks();\n        if (this.continuousScrollManager) {\n            yield* this.continuousScrollManager.reset();\n        }\n        yield* this.resetCurrentPageNumber();\n        if (!notSave) yield put({ type: ActionTypes.UPDATE_OPTIONS, payload: { preferContinuousScroll: false } });\n    }\n\n    * switchToTextMode() {\n        this.djvuWorker.cancelAllTasks();\n        if (this.continuousScrollManager) {\n            yield* this.continuousScrollManager.reset();\n        }\n        yield* this.fetchPageTextIfRequired();\n    }\n\n    * handleViewModeSwitch({ notSave = false } = {}) {\n        const isLoaded = yield select(get.isDocumentLoaded);\n        const viewMode = yield select(get.viewMode);\n        if (!isLoaded) return;\n\n        switch (viewMode) {\n            case Constants.CONTINUOUS_SCROLL_MODE:\n                return yield* this.switchToContinuousScrollMode(notSave);\n            case Constants.SINGLE_PAGE_MODE:\n                return yield* this.switchToSinglePageMode(notSave);\n            case Constants.TEXT_MODE:\n                return yield* this.switchToTextMode();\n            default:\n                throw new Error('Invalid view mode: ' + payload);\n        }\n    }\n\n    * updateOptions(action) {\n        if (action.notSave) return;\n\n        const state = yield select();\n        const options = get.options(state);\n\n        if (inExtension) {\n            yield new Promise(resolve => window.chrome.storage.local.set({ 'djvu_js_options': JSON.stringify(options) }, resolve));\n        } else {\n            localStorage.setItem('djvu_js_options', JSON.stringify(options));\n        }\n    }\n\n    * loadOptions() {\n        try {\n            let options = {};\n            if (inExtension) {\n                options = yield new Promise(resolve => window.chrome.storage.local.get('djvu_js_options', resolve));\n                options = options['djvu_js_options'];\n            } else {\n                options = localStorage.getItem('djvu_js_options');\n            }\n\n            try {\n                options = options ? JSON.parse(options) : {};\n            } catch (e) {\n                options = {};\n            }\n\n            if (!options.locale) {\n                for (const code of navigator.languages) {\n                    const shortCode = code.slice(0, 2);\n                    if (shortCode in dictionaries) {\n                        options.locale = shortCode;\n                        break;\n                    }\n                }\n            }\n\n            if (Object.keys(options).length) {\n                yield put({\n                    type: ActionTypes.UPDATE_OPTIONS,\n                    payload: options,\n                    notSave: true,\n                });\n            }\n        } catch (e) { }\n    }\n\n    * loadDocumentByUrl({ url, config }) {\n        config = config || {};\n\n        const getFileNameFromUrl = (url) => {\n            try {\n                const res = /[^/#]*(?=#|$)/.exec(url.trim());\n                return res ? decodeURIComponent(res[0]) : '***';\n            } catch (e) {\n                return '***';\n            }\n        };\n\n        try {\n            const a = document.createElement('a');\n            a.href = url;\n            url = a.href; // converting of a relative url to an absolute one\n            yield put(Actions.startFileLoadingAction());\n\n            const xhr = yield loadFile(url, (e) => {\n                this.dispatch(Actions.fileLoadingProgressAction(e.loaded, e.total));\n            });\n            const { response: buffer, responseURL } = xhr;\n\n            // Try to get file name from Content-Disposition header if it's there\n            const cdHeader = xhr.getResponseHeader('Content-Disposition');\n            if (cdHeader) {\n                const parsedCd = parseContentDisposition(cdHeader);\n                if (parsedCd.parameters.filename) {\n                    config.name = parsedCd.parameters.filename;\n                }\n            }\n\n            // responseUrl is the URL after all redirects\n            config.djvuOptions = { baseUrl: url.startsWith('data:') ? null : new URL('./', responseURL).href };\n            yield* this.createDocumentFromArrayBuffer({\n                arrayBuffer: buffer,\n                fileName: config.name === undefined ? getFileNameFromUrl(url) : config.name,\n                config: config\n            });\n\n            // now we should process #page=page_number and ?page=page_number\n            const urlObject = new URL(url.toLowerCase());\n            const pageNumber = +urlObject.searchParams.get('page') || +new URLSearchParams(urlObject.hash).get('#page');\n\n            if (pageNumber) {\n                yield put(Actions.setNewPageNumberAction(pageNumber, true));\n            }\n        } catch (e) {\n            yield put(Actions.errorAction(e));\n        } finally {\n            yield put(Actions.endFileLoadingAction());\n        }\n    }\n\n    * bundleDocument() {\n        try {\n            this.djvuWorker.cancelAllTasks();\n            this.isBundling = true;\n            const buffer = yield this.djvuWorker.doc.bundle(progress => {\n                this.dispatch({\n                    type: ActionTypes.UPDATE_FILE_PROCESSING_PROGRESS,\n                    payload: progress,\n                });\n            }).run();\n            yield put({\n                type: ActionTypes.FINISH_TO_BUNDLE,\n                payload: buffer,\n            });\n        } finally {\n            this.isBundling = false;\n        }\n    }\n\n    * hardReloadIfRequired() {\n        if (this.isBundling && this.documentContructorData) {\n            this.djvuWorker.reset();\n            this.isBundling = false;\n            yield this.djvuWorker.createDocument(\n                this.documentContructorData.buffer.slice(0),\n                this.documentContructorData.options\n            );\n            //console.warn('HARD RELOAD');\n            yield* this.resetCurrentPageNumber();\n        }\n    }\n\n    * preparePagesForPrinting(action) {\n        const { from, to } = action.payload;\n        this.printTask = yield fork(this.printManager.preparePagesForPrinting.bind(this.printManager), from, to);\n    }\n\n    * cancelPrintTaskIfRequired() {\n        if (this.printTask) {\n            this.printTask.cancel();\n            this.printTask = null;\n            const viewMode = yield select(get.viewMode);\n            if (viewMode !== Constants.CONTINUOUS_SCROLL_MODE) {\n                // in the continuous scroll mode redundant pages will be removed automatically\n                this.pageStorage.removeAllPages();\n            }\n            yield* this.resetCurrentPageNumber();\n        }\n    }\n\n    * main() {\n        yield takeLatest(Constants.CREATE_DOCUMENT_FROM_ARRAY_BUFFER_ACTION, this.withErrorHandler(this.createDocumentFromArrayBuffer));\n        yield takeLatest(Constants.SET_NEW_PAGE_NUMBER_ACTION, this.withErrorHandler(this.fetchPageData));\n        yield takeLatest(Constants.SET_PAGE_BY_URL_ACTION, this.withErrorHandler(this.setPageByUrl));\n        yield takeLatest(ActionTypes.SAVE_DOCUMENT, this.withErrorHandler(this.saveDocument));\n        yield takeLatest(Constants.CLOSE_DOCUMENT_ACTION, this.withErrorHandler(this.resetWorkerAndStorages));\n        yield takeLatest(Constants.SET_API_CALLBACK_ACTION, this.withErrorHandler(this.setCallback));\n        yield takeLatest(ActionTypes.SET_VIEW_MODE, this.withErrorHandler(this.handleViewModeSwitch));\n        yield takeLatest(ActionTypes.UPDATE_OPTIONS, this.withErrorHandler(this.updateOptions));\n        yield takeLatest(ActionTypes.CONFIGURE, this.withErrorHandler(this.configure));\n        yield takeLatest(ActionTypes.LOAD_DOCUMENT_BY_URL, this.loadDocumentByUrl.bind(this));\n\n        yield takeLatest(ActionTypes.START_TO_BUNDLE, this.withErrorHandler(this.bundleDocument));\n        yield takeLatest([\n                ActionTypes.ERROR,\n                ActionTypes.CLOSE_SAVE_DIALOG,\n            ],\n            this.withErrorHandler(this.hardReloadIfRequired)\n        );\n\n        yield takeLatest([\n                ActionTypes.ERROR,\n                ActionTypes.CLOSE_PRINT_DIALOG,\n            ], this.withErrorHandler(this.cancelPrintTaskIfRequired)\n        );\n\n        yield takeLatest(ActionTypes.PREPARE_PAGES_FOR_PRINTING, this.withErrorHandler(this.preparePagesForPrinting));\n\n        yield* this.withErrorHandler(this.loadOptions)();\n\n        yield take(ActionTypes.DESTROY);\n        this.djvuWorker.terminate();\n        // actually it's not needed since, all URLs will be revoked when the worker is terminated\n        // this.pageStorage.removeAllPages();\n        yield cancel();\n    }\n}\n\n// a new object should be created each time in order to provide an ability to create many independent instances of the viewer\nexport default (dispatch) => RootSaga.prototype.main.bind(new RootSaga(dispatch));"
  },
  {
    "path": "viewer/src/store.js",
    "content": "import { createStore, applyMiddleware } from 'redux';\nimport thunkMiddleware from 'redux-thunk';\nimport createSagaMiddleware from 'redux-saga';\n\nimport rootReducer from './reducers';\nimport createRootSaga from './sagas/rootSaga';\nimport initHotkeys from './hotkeys';\n\nconst configureStore = (eventMiddleware = undefined) => {\n    const sagaMiddleware = createSagaMiddleware();\n    const store = createStore(rootReducer, applyMiddleware(\n        // store => next => action => {\n        //     const state = store.getState();\n        //     console.log(action);\n        //     next(action);\n        // },\n        thunkMiddleware,\n        sagaMiddleware,\n        eventMiddleware\n    ));\n    sagaMiddleware.run(createRootSaga(store.dispatch));\n    initHotkeys(store);\n    return store;\n};\n\nexport default configureStore;"
  },
  {
    "path": "viewer/src/utils.js",
    "content": "import DjVu from './DjVu';\n\n/**\n * We use the error codes form the library just to unify the structure of error objects and\n * make it easier to show an appropriate error message for each case. These codes are used in the\n * ErrorWindow component.\n */\nexport function loadFile(url, progressHandler) {\n    return new Promise((resolve, reject) => {\n        var xhr = new XMLHttpRequest();\n        xhr.open(\"GET\", url);\n        xhr.responseType = 'arraybuffer';\n        xhr.onload = (e) => {\n            if (xhr.status && xhr.status !== 200) { // при загрузке файла status === 0\n                return reject({\n                    code: DjVu.ErrorCodes.UNSUCCESSFUL_REQUEST,\n                    status: xhr.status,\n                    header: `Response status code: ${xhr.status}`,\n                    message: `Response status text: ${xhr.statusText}`\n                });\n            }\n            resolve(xhr);\n        };\n\n        xhr.onerror = (e) => {\n            reject({\n                code: DjVu.ErrorCodes.NETWORK_ERROR,\n                header: \"Network error\",\n                message: \"You should check your network connection\",\n            });\n        }\n\n        xhr.onprogress = progressHandler;\n        xhr.send();\n    });\n}\n\nexport const inExtension = !!(document.querySelector('input#djvu_js_extension_main_page')\n    && window.chrome && window.chrome.runtime && window.chrome.runtime.id);\nexport const isManifestV3 = inExtension && chrome.runtime.getManifest().manifest_version === 3;\nexport const isFirefox = /Firefox/.test(navigator.userAgent);\n\nexport const normalizeFileName = fileName => {\n    return /\\.(djv|djvu)$/.test(fileName) ? fileName : (fileName + '.djvu');\n};"
  },
  {
    "path": "viewer/syncLocales.js",
    "content": "/**\n * A script for automatic synchronization of the Russian dictionary with others.\n * It automatically generates English.js from Russian.js, adds missing phrases with null values\n * to other dictionaries, and also can generate a template file for a new translation\n * if the name of a new language is provided as the first command-line argument.\n */\n\nimport dictionaries from './src/locales/index.js';\nimport fs from 'fs';\n\nconst mainRegex = /(?<=(?:\"|'|`|\\b)(.+)(?:\"|'|`|\\b):\\s*(?:\\/\\/.+$)?\\s*)([\"'`].*[\"'`])(?=,)/gm;\nconst cleanedRussianText = fs.readFileSync('./src/locales/Russian.js', 'utf8')\n    .replace(/^\\/\\*\\*[^]+?\\*\\/[\\n\\r]+/, '');\n\nfunction getFilePathByDict(dict) {\n    const name = dict.englishName === 'Simplified Chinese' ? \"ChineseSimplified\" : dict.englishName;\n    return `./src/locales/${name}.js`;\n}\n\nfunction getDictWithQuotes(dict) {\n    const regex = new RegExp(mainRegex.source, mainRegex.flags);\n\n    const dictWithQuotes = {};\n    const text = fs.readFileSync(getFilePathByDict(dict), 'utf8');\n\n    let result;\n    while (result = regex.exec(text)) {\n        const [, key, value] = result;\n        dictWithQuotes[key] = value;\n    }\n\n    return dictWithQuotes;\n}\n\nfunction syncDict(dict, dictWithQuotes) {\n    let count = 0;\n    const text = cleanedRussianText.replace(new RegExp(mainRegex.source, mainRegex.flags), (match, key) => {\n        let translation = dictWithQuotes[key];\n        if (translation == null) {\n            if (dict[key] != null) {\n                console.warn(`WARNING: dict with quotes doesn't contain a key-value pair: ${key} : ${dict[key]}`);\n                translation = JSON.stringify(dict[key]);\n            } else if (dict.englishName === 'English') {\n                translation = JSON.stringify(key);\n            }\n        }\n        count++;\n        return translation == null ? 'null' : translation;\n    });\n\n    fs.writeFileSync(getFilePathByDict(dict), text, 'utf8');\n\n    return count;\n}\n\nfunction main(newFileName) {\n    if (newFileName) {\n        syncDict({ englishName: newFileName }, { englishName: `\"${newFileName}\"` });\n        console.info(`New file ${newFileName}.js has been generated`);\n    } else {\n        delete dictionaries.ru;\n        for (const dict of Object.values(dictionaries)) {\n            const dictWithQuotes = getDictWithQuotes(dict);\n            const count = syncDict(dict, dictWithQuotes);\n            console.info(`${dict.englishName} synced. ${count} key-value pairs processed.`);\n        }\n    }\n}\n\nmain(process.argv[2]);"
  },
  {
    "path": "viewer/vite.config.js",
    "content": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\nimport fs from 'fs';\nimport serveStatic from 'serve-static';\nimport { create as createContentDisposition } from 'content-disposition-header';\nimport URL from 'url';\n\n// https://vitejs.dev/config/\nexport default defineConfig(({ command }) => ({\n    plugins: [react({\n        babel: {\n            plugins: ['babel-plugin-styled-components'],\n        }\n    }), {\n        name: 'custom-middlewares',\n        configureServer(server) {\n            server.middlewares.use('/djvufile',\n                (req, res, next) => {\n                    const query = URL.parse(req.url, true).query;\n                    const contentDispositionType = query.cd || 'inline';\n                    let filename = query.fname || 'TheMap.djvu';\n                    const cdHeader = createContentDisposition(filename, { type: contentDispositionType });\n                    res.setHeader('Content-Disposition', cdHeader);\n\n                    fs.createReadStream('../library/assets/carte.djvu').pipe(res);\n                }\n            );\n\n            server.middlewares.use(serveStatic('../library/assets'));\n        },\n    }],\n    build: {\n        rollupOptions: {\n            output: {\n                entryFileNames: 'djvu_viewer.js',\n            }\n        }\n    },\n    server: {\n        port: 8000,\n    }\n}));\n"
  }
]