├── scanprep ├── __init__.py └── scanprep.py ├── separator-page.pdf ├── requirements.txt ├── Pipfile ├── PACKAGING.md ├── setup.py ├── LICENSE ├── snapcraft.yaml ├── README.md ├── .gitignore └── Pipfile.lock /scanprep/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /separator-page.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baltpeter/scanprep/HEAD/separator-page.pdf -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Installs dependencies from ./setup.py, and the package itself in editable mode (see: https://stackoverflow.com/a/49684835). 2 | -e . 3 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | Pillow = "*" 8 | PyMuPDF = "*" 9 | numpy = "*" 10 | pyzbar = "*" 11 | 12 | [dev-packages] 13 | autopep8 = "*" 14 | 15 | [requires] 16 | python_version = "3.8" 17 | -------------------------------------------------------------------------------- /PACKAGING.md: -------------------------------------------------------------------------------- 1 | # Packaging scanprep 2 | 3 | ## Pip 4 | 5 | Prerequisites: `python3 -m pip install --user --upgrade setuptools wheel twine` 6 | 7 | 1. Make sure the correct versions of the required packages are listed in `setup.py` under `install_requires`. 8 | 2. Increment the `version` in `setup.py`. 9 | 3. Run `python3 setup.py sdist bdist_wheel` to generate the dist files. 10 | 4. Upload to PyPI using `python3 -m twine upload dist/*`. Login using `__token__` as the username and the API token as the password. 11 | 5. Create a new release on GitHub. 12 | 13 | ### References 14 | 15 | * 16 | * 17 | * 18 | 19 | ## Snap 20 | 21 | Updates to the snap are built automatically once pushed to GitHub. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="scanprep", 8 | version="1.0.2", 9 | author="Benjamin Altpeter", 10 | author_email="hi@bn.al", 11 | description="Small utility to prepare scanned documents. Supports separating PDF files by separator pages and removing blank pages.", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/baltpeter/scanprep", 15 | packages=setuptools.find_packages(), 16 | entry_points={ 17 | 'console_scripts': ['scanprep=scanprep.scanprep:main'] 18 | }, 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | ], 24 | python_requires='>=3.6', 25 | install_requires=[ 26 | 'numpy==1.19.5', 27 | 'pillow==8.1.0', 28 | 'pymupdf==1.18.6', 29 | 'pyzbar==0.1.8' 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2021 Benjamin Altpeter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: scanprep 2 | version: git 3 | summary: Small utility to prepare scanned documents. 4 | description: | 5 | Scanprep can be used to prepare scanned documents for further processing with existing tools (like the great OCRmyPDF) or directly for archival. It allows splitting multiple documents that were scanned in a single batch into multiple files. In addition, it can also remove blank pages from the output (this is especially helpful if using a duplex scanner). 6 | 7 | For document separation, separator pages need to be inserted between the different documents before scanning. These pages tell the program where to split. You can either use the included separator page or create your own. The separator page simply needs to have a barcode that encodes the text SCANPREP_SEP (you can use any barcode type supported by zbar). 8 | confinement: strict 9 | grade: stable 10 | base: core18 11 | architectures: 12 | - build-on: amd64 13 | parts: 14 | scanprep: 15 | plugin: python 16 | source: . 17 | stage-packages: 18 | - libzbar-dev 19 | 20 | apps: 21 | scanprep: 22 | command: scanprep 23 | plugs: 24 | - home 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scanprep – Prepare scanned PDF documents 2 | 3 | > Small utility to prepare scanned documents. Supports separating PDF files by separator pages and removing blank pages. 4 | 5 | 6 | 7 | Scanprep can be used to prepare scanned documents for further processing with existing tools (like the great [OCRmyPDF](https://github.com/jbarlow83/OCRmyPDF)) or directly for archival. It allows splitting multiple documents that were scanned in a single batch into multiple files. In addition, it can also remove blank pages from the output (this is especially helpful if using a duplex scanner). 8 | 9 | For document separation, separator pages need to be inserted between the different documents before scanning. These pages tell the program where to split. You can either use the [included separator page](/separator-page.pdf) or create your own. The separator page simply needs to have a barcode that encodes the text `SCANPREP_SEP` (you can use any [barcode type supported by zbar](http://zbar.sourceforge.net/about.html)). 10 | 11 | ## Installation 12 | 13 | ### Via Snap 14 | 15 | You can install scanprep from the [Snap Store](https://snapcraft.io/scanprep): 16 | 17 | ```sh 18 | snap install scanprep 19 | 20 | scanprep -h 21 | ``` 22 | 23 | ### Via PyPI 24 | 25 | You can install scanprep using `pip` (consider doing that in a venv): 26 | 27 | ```sh 28 | pip3 install scanprep 29 | 30 | # If you see an error like "ImportError: Unable to find zbar shared library", you need to install zbar yourself. See: https://pypi.org/project/pyzbar/ 31 | scanprep -h 32 | ``` 33 | 34 | ### From source 35 | 36 | To install scanprep from source, clone this repository and install the dependencies: 37 | 38 | ```sh 39 | git clone https://github.com/baltpeter/scanprep.git 40 | cd scanprep 41 | pip3 install -r requirements.txt # You may want to do this in a venv. 42 | # You may also need to install the zbar shared library. See: https://pypi.org/project/pyzbar/ 43 | 44 | python3 scanprep/scanprep.py -h 45 | ``` 46 | 47 | ## Usage 48 | 49 | Most simply, you can run scanprep via `scanprep `. This will process the input file and output the results into your current working directory. To specify a different output directory, use `scanprep `. 50 | The output files will be called `0-`, `1-`, and so on. 51 | 52 | By default, both page separation and blank page removal will be performed. To turn them off, use `--no-page-separation` or `--no-blank-removal`, respectively. 53 | 54 | Use `scanprep -h` to show the help: 55 | 56 | ``` 57 | usage: scanprep [-h] [--page-separation] [--blank-removal] input_pdf [output_dir] 58 | 59 | positional arguments: 60 | input_pdf The PDF document to process. 61 | output_dir The directory where the output documents will be saved. (defaults to the 62 | current directory) 63 | 64 | optional arguments: 65 | -h, --help show this help message and exit 66 | --page-separation, --no-page-separation 67 | Do (or do not) split document into separate files by the included 68 | separator pages. (default yes) 69 | --blank-removal, --no-blank-removal 70 | Do (or do not) remove empty pages from the output. (default yes) 71 | ``` 72 | 73 | ## License 74 | 75 | Scanprep is licensed under the MIT license, see the [`LICENSE`](/LICENSE) file for details. Issues and pull requests are welcome! 76 | -------------------------------------------------------------------------------- /scanprep/scanprep.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import fitz 3 | from PIL import Image, ImageFilter, ImageStat 4 | import numpy as np 5 | import os 6 | import pathlib 7 | from pyzbar.pyzbar import decode 8 | 9 | 10 | # Algorithm inspired by: https://dsp.stackexchange.com/a/48837 11 | def page_is_empty(img): 12 | threshold = np.mean(ImageStat.Stat(img).mean) - 50 13 | img = img.convert('L').point(lambda x: 255 if x > threshold else 0) 14 | 15 | # Staples, folds, punch holes et al. tend to be confined to the left and right margin, so we crop off 10% there. 16 | # Also, we crop off 5% at the top and bottom to get rid of the page borders. 17 | lr_margin = img.width * 0.10 18 | tb_margin = img.height * 0.05 19 | img = img.crop((lr_margin, tb_margin, img.width - 20 | lr_margin, img.height - tb_margin)) 21 | 22 | # Use erosion and dilation to get rid of small specks but make actual text/content more significant. 23 | img = img.filter(ImageFilter.MaxFilter(1)) 24 | img = img.filter(ImageFilter.MinFilter(3)) 25 | 26 | white_pixels = np.count_nonzero(img) 27 | total_pixels = img.size[0] * img.size[1] 28 | ratio = (total_pixels - white_pixels) / total_pixels 29 | 30 | return ratio < 0.005 31 | 32 | 33 | def page_is_separator(img): 34 | detected_barcodes = decode(img) 35 | for barcode in detected_barcodes: 36 | if barcode.data == b'SCANPREP_SEP': 37 | return True 38 | return False 39 | 40 | 41 | def get_new_docs_pages(doc, separate=True, remove_blank=True): 42 | docs = [[]] 43 | 44 | for page in doc: 45 | pixmap = page.getPixmap() 46 | img = Image.frombytes( 47 | "RGB", (pixmap.width, pixmap.height), pixmap.samples) 48 | 49 | if separate and page_is_separator(img): 50 | docs.append([]) 51 | continue 52 | if remove_blank and page_is_empty(img): 53 | continue 54 | 55 | docs[-1].append(page.number) 56 | 57 | return list(filter(lambda d: len(d) > 0, docs)) 58 | 59 | 60 | def emit_new_documents(doc, filename, out_dir, separate=True, remove_blank=True): 61 | pathlib.Path(out_dir).mkdir(parents=True, exist_ok=True) 62 | 63 | new_docs = get_new_docs_pages(doc, separate, remove_blank) 64 | for i, pages in enumerate(new_docs): 65 | new_doc = fitz.open() # Will create a new, blank document. 66 | for j, page_no in enumerate(pages): 67 | new_doc.insertPDF(doc, from_page=page_no, 68 | to_page=page_no, final=(j == len(pages) - 1)) 69 | new_doc.save(os.path.join(out_dir, f"{i}-{filename}")) 70 | 71 | 72 | # Taken from: https://stackoverflow.com/a/9236426 73 | class ActionNoYes(argparse.Action): 74 | def __init__(self, opt_name, dest, default=True, required=False, help=None): 75 | super(ActionNoYes, self).__init__(['--' + opt_name, '--no-' + opt_name], 76 | dest, nargs=0, const=None, default=default, required=required, help=help) 77 | 78 | def __call__(self, p, namespace, values, option_string=None): 79 | if option_string.startswith('--no-'): 80 | setattr(namespace, self.dest, False) 81 | else: 82 | setattr(namespace, self.dest, True) 83 | 84 | 85 | def main(): 86 | parser = argparse.ArgumentParser() 87 | parser.add_argument('input_pdf', help='The PDF document to process.') 88 | parser.add_argument( 89 | 'output_dir', help='The directory where the output documents will be saved. (defaults to the current directory)', nargs='?', default=os.getcwd()) 90 | parser._add_action(ActionNoYes('page-separation', 'separate', 91 | help='Do (or do not) split document into separate files by the included separator pages. (default yes)')) 92 | parser._add_action(ActionNoYes('blank-removal', 'remove_blank', 93 | help='Do (or do not) remove empty pages from the output. (default yes)')) 94 | args = parser.parse_args() 95 | 96 | emit_new_documents(fitz.open(os.path.abspath(args.input_pdf)), os.path.basename( 97 | args.input_pdf), os.path.abspath(args.output_dir), args.separate, args.remove_blank) 98 | 99 | 100 | if __name__ == '__main__': 101 | main() 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | *.tmp 3 | tmp.* 4 | separator-page.indd 5 | *.snap 6 | 7 | # Created by https://www.toptal.com/developers/gitignore/api/linux,macos,windows,visualstudiocode,sublimetext,jetbrains+all,python 8 | # Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,windows,visualstudiocode,sublimetext,jetbrains+all,python 9 | 10 | ### JetBrains+all ### 11 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 12 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 13 | 14 | # User-specific stuff 15 | .idea/**/workspace.xml 16 | .idea/**/tasks.xml 17 | .idea/**/usage.statistics.xml 18 | .idea/**/dictionaries 19 | .idea/**/shelf 20 | 21 | # Generated files 22 | .idea/**/contentModel.xml 23 | 24 | # Sensitive or high-churn files 25 | .idea/**/dataSources/ 26 | .idea/**/dataSources.ids 27 | .idea/**/dataSources.local.xml 28 | .idea/**/sqlDataSources.xml 29 | .idea/**/dynamic.xml 30 | .idea/**/uiDesigner.xml 31 | .idea/**/dbnavigator.xml 32 | 33 | # Gradle 34 | .idea/**/gradle.xml 35 | .idea/**/libraries 36 | 37 | # Gradle and Maven with auto-import 38 | # When using Gradle or Maven with auto-import, you should exclude module files, 39 | # since they will be recreated, and may cause churn. Uncomment if using 40 | # auto-import. 41 | # .idea/artifacts 42 | # .idea/compiler.xml 43 | # .idea/jarRepositories.xml 44 | # .idea/modules.xml 45 | # .idea/*.iml 46 | # .idea/modules 47 | # *.iml 48 | # *.ipr 49 | 50 | # CMake 51 | cmake-build-*/ 52 | 53 | # Mongo Explorer plugin 54 | .idea/**/mongoSettings.xml 55 | 56 | # File-based project format 57 | *.iws 58 | 59 | # IntelliJ 60 | out/ 61 | 62 | # mpeltonen/sbt-idea plugin 63 | .idea_modules/ 64 | 65 | # JIRA plugin 66 | atlassian-ide-plugin.xml 67 | 68 | # Cursive Clojure plugin 69 | .idea/replstate.xml 70 | 71 | # Crashlytics plugin (for Android Studio and IntelliJ) 72 | com_crashlytics_export_strings.xml 73 | crashlytics.properties 74 | crashlytics-build.properties 75 | fabric.properties 76 | 77 | # Editor-based Rest Client 78 | .idea/httpRequests 79 | 80 | # Android studio 3.1+ serialized cache file 81 | .idea/caches/build_file_checksums.ser 82 | 83 | ### JetBrains+all Patch ### 84 | # Ignores the whole .idea folder and all .iml files 85 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 86 | 87 | .idea/ 88 | 89 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 90 | 91 | *.iml 92 | modules.xml 93 | .idea/misc.xml 94 | *.ipr 95 | 96 | # Sonarlint plugin 97 | .idea/sonarlint 98 | 99 | ### Linux ### 100 | *~ 101 | 102 | # temporary files which can be created if a process still has a handle open of a deleted file 103 | .fuse_hidden* 104 | 105 | # KDE directory preferences 106 | .directory 107 | 108 | # Linux trash folder which might appear on any partition or disk 109 | .Trash-* 110 | 111 | # .nfs files are created when an open file is removed but is still being accessed 112 | .nfs* 113 | 114 | ### macOS ### 115 | # General 116 | .DS_Store 117 | .AppleDouble 118 | .LSOverride 119 | 120 | # Icon must end with two \r 121 | Icon 122 | 123 | 124 | # Thumbnails 125 | ._* 126 | 127 | # Files that might appear in the root of a volume 128 | .DocumentRevisions-V100 129 | .fseventsd 130 | .Spotlight-V100 131 | .TemporaryItems 132 | .Trashes 133 | .VolumeIcon.icns 134 | .com.apple.timemachine.donotpresent 135 | 136 | # Directories potentially created on remote AFP share 137 | .AppleDB 138 | .AppleDesktop 139 | Network Trash Folder 140 | Temporary Items 141 | .apdisk 142 | 143 | ### Python ### 144 | # Byte-compiled / optimized / DLL files 145 | __pycache__/ 146 | *.py[cod] 147 | *$py.class 148 | 149 | # C extensions 150 | *.so 151 | 152 | # Distribution / packaging 153 | .Python 154 | build/ 155 | develop-eggs/ 156 | dist/ 157 | downloads/ 158 | eggs/ 159 | .eggs/ 160 | lib/ 161 | lib64/ 162 | parts/ 163 | sdist/ 164 | var/ 165 | wheels/ 166 | pip-wheel-metadata/ 167 | share/python-wheels/ 168 | *.egg-info/ 169 | .installed.cfg 170 | *.egg 171 | MANIFEST 172 | 173 | # PyInstaller 174 | # Usually these files are written by a python script from a template 175 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 176 | *.manifest 177 | *.spec 178 | 179 | # Installer logs 180 | pip-log.txt 181 | pip-delete-this-directory.txt 182 | 183 | # Unit test / coverage reports 184 | htmlcov/ 185 | .tox/ 186 | .nox/ 187 | .coverage 188 | .coverage.* 189 | .cache 190 | nosetests.xml 191 | coverage.xml 192 | *.cover 193 | *.py,cover 194 | .hypothesis/ 195 | .pytest_cache/ 196 | pytestdebug.log 197 | 198 | # Translations 199 | *.mo 200 | *.pot 201 | 202 | # Django stuff: 203 | *.log 204 | local_settings.py 205 | db.sqlite3 206 | db.sqlite3-journal 207 | 208 | # Flask stuff: 209 | instance/ 210 | .webassets-cache 211 | 212 | # Scrapy stuff: 213 | .scrapy 214 | 215 | # Sphinx documentation 216 | docs/_build/ 217 | doc/_build/ 218 | 219 | # PyBuilder 220 | target/ 221 | 222 | # Jupyter Notebook 223 | .ipynb_checkpoints 224 | 225 | # IPython 226 | profile_default/ 227 | ipython_config.py 228 | 229 | # pyenv 230 | .python-version 231 | 232 | # pipenv 233 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 234 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 235 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 236 | # install all needed dependencies. 237 | #Pipfile.lock 238 | 239 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 240 | __pypackages__/ 241 | 242 | # Celery stuff 243 | celerybeat-schedule 244 | celerybeat.pid 245 | 246 | # SageMath parsed files 247 | *.sage.py 248 | 249 | # Environments 250 | .env 251 | .venv 252 | env/ 253 | venv/ 254 | ENV/ 255 | env.bak/ 256 | venv.bak/ 257 | pythonenv* 258 | 259 | # Spyder project settings 260 | .spyderproject 261 | .spyproject 262 | 263 | # Rope project settings 264 | .ropeproject 265 | 266 | # mkdocs documentation 267 | /site 268 | 269 | # mypy 270 | .mypy_cache/ 271 | .dmypy.json 272 | dmypy.json 273 | 274 | # Pyre type checker 275 | .pyre/ 276 | 277 | # pytype static type analyzer 278 | .pytype/ 279 | 280 | # profiling data 281 | .prof 282 | 283 | ### SublimeText ### 284 | # Cache files for Sublime Text 285 | *.tmlanguage.cache 286 | *.tmPreferences.cache 287 | *.stTheme.cache 288 | 289 | # Workspace files are user-specific 290 | *.sublime-workspace 291 | 292 | # Project files should be checked into the repository, unless a significant 293 | # proportion of contributors will probably not be using Sublime Text 294 | # *.sublime-project 295 | 296 | # SFTP configuration file 297 | sftp-config.json 298 | 299 | # Package control specific files 300 | Package Control.last-run 301 | Package Control.ca-list 302 | Package Control.ca-bundle 303 | Package Control.system-ca-bundle 304 | Package Control.cache/ 305 | Package Control.ca-certs/ 306 | Package Control.merged-ca-bundle 307 | Package Control.user-ca-bundle 308 | oscrypto-ca-bundle.crt 309 | bh_unicode_properties.cache 310 | 311 | # Sublime-github package stores a github token in this file 312 | # https://packagecontrol.io/packages/sublime-github 313 | GitHub.sublime-settings 314 | 315 | ### VisualStudioCode ### 316 | .vscode/* 317 | !.vscode/tasks.json 318 | !.vscode/launch.json 319 | *.code-workspace 320 | 321 | ### VisualStudioCode Patch ### 322 | # Ignore all local history of files 323 | .history 324 | .ionide 325 | 326 | ### Windows ### 327 | # Windows thumbnail cache files 328 | Thumbs.db 329 | Thumbs.db:encryptable 330 | ehthumbs.db 331 | ehthumbs_vista.db 332 | 333 | # Dump file 334 | *.stackdump 335 | 336 | # Folder config file 337 | [Dd]esktop.ini 338 | 339 | # Recycle Bin used on file shares 340 | $RECYCLE.BIN/ 341 | 342 | # Windows Installer files 343 | *.cab 344 | *.msi 345 | *.msix 346 | *.msm 347 | *.msp 348 | 349 | # Windows shortcuts 350 | *.lnk 351 | 352 | # End of https://www.toptal.com/developers/gitignore/api/linux,macos,windows,visualstudiocode,sublimetext,jetbrains+all,python 353 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "3b710942927012ba019661f2b943f83ce2f9b34566536c429b98f6836183c48e" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "numpy": { 20 | "hashes": [ 21 | "sha256:012426a41bc9ab63bb158635aecccc7610e3eff5d31d1eb43bc099debc979d94", 22 | "sha256:06fab248a088e439402141ea04f0fffb203723148f6ee791e9c75b3e9e82f080", 23 | "sha256:0eef32ca3132a48e43f6a0f5a82cb508f22ce5a3d6f67a8329c81c8e226d3f6e", 24 | "sha256:1ded4fce9cfaaf24e7a0ab51b7a87be9038ea1ace7f34b841fe3b6894c721d1c", 25 | "sha256:2e55195bc1c6b705bfd8ad6f288b38b11b1af32f3c8289d6c50d47f950c12e76", 26 | "sha256:2ea52bd92ab9f768cc64a4c3ef8f4b2580a17af0a5436f6126b08efbd1838371", 27 | "sha256:36674959eed6957e61f11c912f71e78857a8d0604171dfd9ce9ad5cbf41c511c", 28 | "sha256:384ec0463d1c2671170901994aeb6dce126de0a95ccc3976c43b0038a37329c2", 29 | "sha256:39b70c19ec771805081578cc936bbe95336798b7edf4732ed102e7a43ec5c07a", 30 | "sha256:400580cbd3cff6ffa6293df2278c75aef2d58d8d93d3c5614cd67981dae68ceb", 31 | "sha256:43d4c81d5ffdff6bae58d66a3cd7f54a7acd9a0e7b18d97abb255defc09e3140", 32 | "sha256:50a4a0ad0111cc1b71fa32dedd05fa239f7fb5a43a40663269bb5dc7877cfd28", 33 | "sha256:603aa0706be710eea8884af807b1b3bc9fb2e49b9f4da439e76000f3b3c6ff0f", 34 | "sha256:6149a185cece5ee78d1d196938b2a8f9d09f5a5ebfbba66969302a778d5ddd1d", 35 | "sha256:759e4095edc3c1b3ac031f34d9459fa781777a93ccc633a472a5468587a190ff", 36 | "sha256:7fb43004bce0ca31d8f13a6eb5e943fa73371381e53f7074ed21a4cb786c32f8", 37 | "sha256:811daee36a58dc79cf3d8bdd4a490e4277d0e4b7d103a001a4e73ddb48e7e6aa", 38 | "sha256:8b5e972b43c8fc27d56550b4120fe6257fdc15f9301914380b27f74856299fea", 39 | "sha256:99abf4f353c3d1a0c7a5f27699482c987cf663b1eac20db59b8c7b061eabd7fc", 40 | "sha256:a0d53e51a6cb6f0d9082decb7a4cb6dfb33055308c4c44f53103c073f649af73", 41 | "sha256:a12ff4c8ddfee61f90a1633a4c4afd3f7bcb32b11c52026c92a12e1325922d0d", 42 | "sha256:a4646724fba402aa7504cd48b4b50e783296b5e10a524c7a6da62e4a8ac9698d", 43 | "sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4", 44 | "sha256:a9d17f2be3b427fbb2bce61e596cf555d6f8a56c222bd2ca148baeeb5e5c783c", 45 | "sha256:ab83f24d5c52d60dbc8cd0528759532736b56db58adaa7b5f1f76ad551416a1e", 46 | "sha256:aeb9ed923be74e659984e321f609b9ba54a48354bfd168d21a2b072ed1e833ea", 47 | "sha256:c843b3f50d1ab7361ca4f0b3639bf691569493a56808a0b0c54a051d260b7dbd", 48 | "sha256:cae865b1cae1ec2663d8ea56ef6ff185bad091a5e33ebbadd98de2cfa3fa668f", 49 | "sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff", 50 | "sha256:cf2402002d3d9f91c8b01e66fbb436a4ed01c6498fffed0e4c7566da1d40ee1e", 51 | "sha256:d051ec1c64b85ecc69531e1137bb9751c6830772ee5c1c426dbcfe98ef5788d7", 52 | "sha256:d6631f2e867676b13026e2846180e2c13c1e11289d67da08d71cacb2cd93d4aa", 53 | "sha256:dbd18bcf4889b720ba13a27ec2f2aac1981bd41203b3a3b27ba7a33f88ae4827", 54 | "sha256:df609c82f18c5b9f6cb97271f03315ff0dbe481a2a02e56aeb1b1a985ce38e60" 55 | ], 56 | "index": "pypi", 57 | "version": "==1.19.5" 58 | }, 59 | "pillow": { 60 | "hashes": [ 61 | "sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6", 62 | "sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded", 63 | "sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865", 64 | "sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174", 65 | "sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032", 66 | "sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a", 67 | "sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e", 68 | "sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378", 69 | "sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17", 70 | "sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c", 71 | "sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913", 72 | "sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7", 73 | "sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0", 74 | "sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820", 75 | "sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba", 76 | "sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2", 77 | "sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b", 78 | "sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9", 79 | "sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234", 80 | "sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d", 81 | "sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5", 82 | "sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206", 83 | "sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9", 84 | "sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8", 85 | "sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59", 86 | "sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d", 87 | "sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7", 88 | "sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a", 89 | "sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0", 90 | "sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b", 91 | "sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d", 92 | "sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae" 93 | ], 94 | "index": "pypi", 95 | "version": "==8.1.0" 96 | }, 97 | "pymupdf": { 98 | "hashes": [ 99 | "sha256:1cc607a0feedd28f94e0181df65e7ddfda370f3538ab2d9a02ac10b2b5bf2fc1", 100 | "sha256:2656bbf7fa530f9e32c6f650d0c71dd3e553da3836b95c66df5f305e0ebdfb7d", 101 | "sha256:4079fbc1d99c9227aa677dc721fc7ad3cdf29e9104aaa8c566c80f339e80fefe", 102 | "sha256:516660eceaf484d1577caccc14e714470070a4196b99ad929f6e9640d8219801", 103 | "sha256:56d950708a9ab081a8ced1615127bc73378234e28b75a8715a56dd1652c0519e", 104 | "sha256:6cb627d95b7db291d88222b44da9c4537bf1f60c332f8146b48538f9cb101fde", 105 | "sha256:703d7f7748ed5d55448d4ab87de6cc5f22b2c298bd9ceff696a90b547553b848", 106 | "sha256:70d8b97489d558d91013ac6853247aadcf677fc859fa7610c5e13a50d2b9dbfb", 107 | "sha256:739b213c28c923f6ffc11efef80b7a7842a8ed92b64c13e46a97d439a1fbca75", 108 | "sha256:78be0230279719f35640782377b4b8b3f27b133d82b98c2cd87bba9cca8e608e", 109 | "sha256:91930aa0ad7a2c960be2a98d1dcb3c660a7aa0fc50d6a6c3287d6fde9e7e82e1", 110 | "sha256:94e6a4ccbdb18e7d39cce3172f71d3dc2c1b161a5ec4eefe07610046f4dcf7ae", 111 | "sha256:b6f552704ce62188ef8c3de102f9ca85f35f94b12f119d64701ca646f67da39c", 112 | "sha256:c024946d6f4ce06e2ab9f9d0409877136dd43031b2c7d9c2f4ab3b4332b84e12", 113 | "sha256:cd4658edcb7c6bd089a8b8a973c84aaf1822708b238ff1c7d2b7e7fad2482003", 114 | "sha256:e3776a1905eccd0cd26cd673d19787e5f5f5186b2cb884e25a9ee35d695cc273", 115 | "sha256:f59524b7aa85c3ee4fab1dc4ed0e6164ffc125175fd55c6579c13dd420403578" 116 | ], 117 | "index": "pypi", 118 | "version": "==1.18.6" 119 | }, 120 | "pyzbar": { 121 | "hashes": [ 122 | "sha256:0e204b904e093e5e75aa85e0203bb0e02888105732a509b51f31cff400f34265", 123 | "sha256:496249b546be70ec98c0ff0ad9151e73daaffff129266df86150a15dcd8dac4c", 124 | "sha256:7d6c01d2c0a352fa994aa91b5540d1caeaeaac466656eb41468ca5df33be9f2e" 125 | ], 126 | "index": "pypi", 127 | "version": "==0.1.8" 128 | } 129 | }, 130 | "develop": { 131 | "autopep8": { 132 | "hashes": [ 133 | "sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094" 134 | ], 135 | "index": "pypi", 136 | "version": "==1.5.4" 137 | }, 138 | "pycodestyle": { 139 | "hashes": [ 140 | "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", 141 | "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" 142 | ], 143 | "version": "==2.6.0" 144 | }, 145 | "toml": { 146 | "hashes": [ 147 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 148 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 149 | ], 150 | "version": "==0.10.2" 151 | } 152 | } 153 | } 154 | --------------------------------------------------------------------------------