├── .gitignore ├── COPYING ├── README.md ├── build-aux └── meson │ ├── merge-xml.py │ └── move.py ├── desktop ├── com.github.unrud.djpdf.desktop.in ├── com.github.unrud.djpdf.metainfo-releases.xml.in ├── com.github.unrud.djpdf.metainfo.xml.in ├── com.github.unrud.djpdf.png └── meson.build ├── djpdf ├── __init__.py ├── __main__.py ├── argyllcms-srgb.icm ├── djpdf.py ├── hocr.py ├── scans2pdf.py ├── scans2pdfcli.py ├── tesseract-pdf.ttf ├── to-unicode.cmap └── util.py ├── flatpak ├── com.github.unrud.djpdf.yaml ├── ocr-extension.metainfo.xml.in ├── ocr-extensions-generator.py ├── ocr-extensions-installer.py ├── ocr-extensions.json └── tesseract-wrapper.py ├── meson.build ├── po ├── LINGUAS ├── POTFILES ├── de.po ├── djpdfgui.pot ├── es.po ├── et.po ├── hu.po ├── meson.build ├── nb_NO.po ├── nl.po ├── ru.po ├── sk.po └── ta.po ├── scans2pdf_gui ├── __init__.py ├── main.py ├── meson.build ├── qml │ ├── detail.qml │ ├── main.qml │ ├── meson.build │ └── overview.qml └── scans2pdf-gui.in ├── screenshots ├── 1.png └── 2.png ├── setup.py └── snap └── snapcraft.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.qmlc 3 | /po/*.mo 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scans to PDF 2 | 3 | [![Translation status](https://hosted.weblate.org/widgets/djpdf/-/djpdfgui/svg-badge.svg)](https://hosted.weblate.org/engage/djpdf/) 4 | 5 | Create small, searchable PDFs from scanned documents. 6 | The program divides images into bitonal foreground images (text) 7 | and a color background image, then compresses them separately. 8 | An invisible OCR text layer is added, making the PDF searchable. 9 | 10 | Color and grayscale scans need some preparation for good results. 11 | Recommended tools are Scan Tailor or GIMP. 12 | 13 | A GUI and command line interface are included. 14 | 15 | ## Installation 16 | 17 | Download on Flathub 18 | 19 | ### Alternative installation methods 20 | 21 | * [Snap Store](https://snapcraft.io/djpdf) 22 | * Manual: 23 | * Dependencies: [ImageMagick](http://www.imagemagick.org/), [QPDF](https://github.com/qpdf/qpdf), 24 | [jbig2enc](https://github.com/agl/jbig2enc), [Tesseract](https://github.com/tesseract-ocr/tesseract) 25 | * Install library and CLI: `pip3 install .` 26 | * Install GUI: `meson builddir && meson install -C builddir` 27 | 28 | ## Translation 29 | 30 | We're using [Weblate](https://hosted.weblate.org/engage/djpdf/) to translate the UI. So feel free, to contribute translations over there. 31 | 32 | ## Screenshots 33 | 34 | ![screenshot 1](https://raw.githubusercontent.com/Unrud/djpdf/master/screenshots/1.png) 35 | 36 | ![screenshot 2](https://raw.githubusercontent.com/Unrud/djpdf/master/screenshots/2.png) 37 | -------------------------------------------------------------------------------- /build-aux/meson/merge-xml.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import xml.etree.ElementTree as ET 3 | 4 | 5 | class CommentTreeBuilder(ET.TreeBuilder): 6 | def comment(self, data): 7 | self.start(ET.Comment, {}) 8 | self.data(data) 9 | self.end(ET.Comment) 10 | 11 | 12 | def make_xml_parser(): 13 | return ET.XMLParser(target=CommentTreeBuilder()) 14 | 15 | 16 | _, main_xml_path, insert_xml_path, insert_xpath = sys.argv 17 | main_xml = ET.parse(main_xml_path, parser=make_xml_parser()) 18 | insert_xml = ET.parse(insert_xml_path, parser=make_xml_parser()) 19 | main_xml.find(insert_xpath).append(insert_xml.getroot()) 20 | main_xml.write(sys.stdout.buffer, encoding='utf-8', xml_declaration=True) 21 | -------------------------------------------------------------------------------- /build-aux/meson/move.py: -------------------------------------------------------------------------------- 1 | from os import environ, path, sys 2 | from shutil import move 3 | 4 | destdir = environ.get('DESTDIR', '') 5 | prefix = environ['MESON_INSTALL_PREFIX'] 6 | source, dest = sys.argv[1:] 7 | move(destdir + path.join(prefix, source), destdir + path.join(prefix, dest)) 8 | -------------------------------------------------------------------------------- /desktop/com.github.unrud.djpdf.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Scans to PDF 4 | Comment=Create small, searchable PDFs from scanned documents 5 | Terminal=false 6 | Exec=scans2pdf-gui 7 | Icon=com.github.unrud.djpdf 8 | Categories=Graphics;Scanning;OCR; 9 | # TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 10 | Keywords=djpdf;OCR;convert;PDF;image; 11 | -------------------------------------------------------------------------------- /desktop/com.github.unrud.djpdf.metainfo-releases.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
    6 |
  • Add Russian translation
  • 7 |
8 |
9 |
10 | 11 | 12 |
    13 |
  • Add Tamil translation
  • 14 |
  • Update Estonian translation
  • 15 |
16 |
17 |
18 | 19 | 20 |
    21 |
  • Add Slovak translation
  • 22 |
  • Update German translation
  • 23 |
  • Update Hungarian translation
  • 24 |
25 |
26 |
27 | 28 | 29 |
    30 |
  • Add Spanish translation
  • 31 |
32 |
33 |
34 | 35 | 36 |
    37 |
  • Add Estonian translation
  • 38 |
39 |
40 |
41 | 42 | 43 |
    44 |
  • Update runtime and dependencies
  • 45 |
  • Update metadata
  • 46 |
47 |
48 |
49 | 50 | 51 |
    52 |
  • Fix layout error causing buttons to be covered on details page
  • 53 |
  • Fix JBIG2 threshold display
  • 54 |
  • Replace experimental QML types (Qt Labs)
  • 55 |
  • Refactor QML code
  • 56 |
57 |
58 |
59 | 60 | 61 |
    62 |
  • Fix PDF conformity (CMap: bfrange < 256)
  • 63 |
64 |
65 |
66 | 67 | 68 |
    69 |
  • Add German translation
  • 70 |
  • Add Norwegian translation
  • 71 |
72 |
73 |
74 | 75 | 76 |
    77 |
  • Fix QML layout issue
  • 78 |
79 |
80 |
81 | 82 | 83 |
    84 |
  • Add Dutch translation
  • 85 |
  • Add Hungarian translation
  • 86 |
  • Build djpdfgui with Meson
  • 87 |
  • Make djpdfgui translatable
  • 88 |
89 |
90 |
91 | 92 | 93 |
    94 |
  • Remove custom FileChooser integration
  • 95 |
96 |
97 |
98 | 99 | 100 |
    101 |
  • Add support for python>=3.10
  • 102 |
  • Drop support for python<3.8
  • 103 |
104 |
105 |
106 | 107 | 108 |
    109 |
  • Qt: Show thumbnails for large images
  • 110 |
111 |
112 |
113 | 114 | 115 |
    116 |
  • Switch to Qt6
  • 117 |
  • Automatically detect FileChooser portal
  • 118 |
119 |
120 |
121 | 122 | 123 |
    124 |
  • Make default values configurable
  • 125 |
  • Fix typo in help
  • 126 |
  • Raise default JBIG2 symbol threshold to 90%
  • 127 |
  • Fix race when using FileChooser dialog
  • 128 |
129 |
130 |
131 | 132 | 133 |
    134 |
  • Fixed bug that messed up the page order when dragging and dropping pages
  • 135 |
  • Display file names in overview
  • 136 |
  • Make PDF creation cancelable
  • 137 |
  • Show error log when PDF creation fails
  • 138 |
139 |
140 |
141 | 142 | 143 |
    144 |
  • Qt: Prefer x11 to wayland
  • 145 |
  • xdg-desktop-portal: No parent_window under wayland
  • 146 |
147 |
148 |
149 | 150 | 151 |
    152 |
  • Kill remaining processes
  • 153 |
  • Color logging for all command line tools
  • 154 |
  • Log to stderr
  • 155 |
  • Hide python warnings if verbose output is not enabled
  • 156 |
  • Properly cancel remaining tasks and close unused coroutines
  • 157 |
  • Use async-await syntax
  • 158 |
159 |
160 |
161 | 162 | 163 |
    164 |
  • Fix compatibility with Python>=3.9
  • 165 |
166 |
167 |
168 | 169 | 170 |
    171 |
  • Add setuptools requirement
  • 172 |
173 |
174 |
175 | 176 | 177 |
    178 |
  • Fix save file dialog filter
  • 179 |
180 |
181 |
182 | 183 | 184 |
    185 |
  • Fix compatibility with PySide 5.15.2
  • 186 |
  • Improve file dialog filters
  • 187 |
188 |
189 |
190 | 191 | 192 |
    193 |
  • Prefer /var/tmp for big files
  • 194 |
  • Set signal handlers to delete temporary files
  • 195 |
196 |
197 |
198 | 199 | 200 |
    201 |
  • Fix import of TIFF images with color palette
  • 202 |
  • Fix compatibility with new ImageMagick version
  • 203 |
  • Limit ImageMagick and Tesseract subprocesses to one thread
  • 204 |
205 |
206 |
207 | 208 | 209 |
    210 |
  • Set window icon
  • 211 |
212 |
213 |
214 | 215 | 216 |
    217 |
  • Convert input images to sRGB color space
  • 218 |
  • Pass PDF/A-2A verification test
  • 219 |
220 |
221 |
222 | 223 | 224 |
    225 |
  • Update dependencies
  • 226 |
  • AppStream: Fix release descriptions
  • 227 |
228 |
229 |
230 | 231 | 232 |
    233 |
  • Minor improvements
  • 234 |
235 |
236 |
237 | 238 | 239 |
    240 |
  • Improve OCR by passing DPI to tesseract
  • 241 |
242 |
243 |
244 | 245 | 246 |
    247 |
  • Update dependencies
  • 248 |
249 |
250 |
251 | 252 | 253 |
    254 |
  • Use SpinBox instead of Slider
  • 255 |
256 |
257 |
258 | 259 | 260 |
    261 |
  • show progress while creating PDF
  • 262 |
263 |
264 |
265 | 266 | 267 |
    268 |
  • Display slider values
  • 269 |
  • Add --verbose argument to scans2pdf-gui
  • 270 |
  • Use correct value from background quality slider
  • 271 |
272 |
273 |
274 | 275 | 276 |
    277 |
  • Run jobs in parallel based on available memory
  • 278 |
  • Fix crash when closing GUI with Flatpak integration
  • 279 |
280 |
281 |
282 | 283 | 284 |
    285 |
  • reduce memory usage
  • 286 |
287 |
288 |
289 | 290 |
291 | -------------------------------------------------------------------------------- /desktop/com.github.unrud.djpdf.metainfo.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.github.unrud.djpdf 5 | CC0-1.0 6 | GPL-3.0-or-later 7 | Scans to PDF 8 | Create small, searchable PDFs from scanned documents 9 | 10 |

11 | Create small, searchable PDFs from scanned documents. 12 | The program divides images into bitonal foreground images (text) 13 | and a color background image, then compresses them separately. 14 | An invisible OCR text layer is added, making the PDF searchable. 15 |

16 |

17 | Color and grayscale scans need some preparation for good results. 18 | Recommended tools are Scan Tailor or GIMP. 19 |

20 |

21 | A GUI and command line interface are included. 22 |

23 |
24 | com.github.unrud.djpdf.desktop 25 | https://github.com/Unrud/djpdf/issues 26 | https://github.com/Unrud/djpdf 27 | https://github.com/Unrud/djpdf 28 | https://hosted.weblate.org/engage/djpdf/ 29 | Unrud 30 | 31 | Unrud 32 | 33 | unrud_AT_outlook.com 34 | djpdfgui 35 | 36 | 37 | https://raw.githubusercontent.com/Unrud/djpdf/v0.5.10/screenshots/1.png 38 | 39 | 40 | https://raw.githubusercontent.com/Unrud/djpdf/v0.5.10/screenshots/2.png 41 | 42 | 43 | 44 |
45 | -------------------------------------------------------------------------------- /desktop/com.github.unrud.djpdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unrud/djpdf/e304dc337d1da218130223b3b3c9f550e286e6aa/desktop/com.github.unrud.djpdf.png -------------------------------------------------------------------------------- /desktop/meson.build: -------------------------------------------------------------------------------- 1 | desktop_file = i18n.merge_file( 2 | input: 'com.github.unrud.djpdf.desktop.in', 3 | output: 'com.github.unrud.djpdf.desktop', 4 | type: 'desktop', 5 | po_dir: '../po', 6 | install: true, 7 | install_dir: get_option('datadir') / 'applications', 8 | ) 9 | 10 | desktop_utils = find_program('desktop-file-validate', required: false) 11 | if desktop_utils.found() 12 | test('Validate desktop file', desktop_utils, 13 | args: [desktop_file], 14 | ) 15 | endif 16 | 17 | base_appstream_file = i18n.merge_file( 18 | input: 'com.github.unrud.djpdf.metainfo.xml.in', 19 | output: 'com.github.unrud.djpdf-base.metainfo.xml', 20 | po_dir: '../po', 21 | ) 22 | 23 | appstream_file = custom_target('com.github.unrud.djpdf.metainfo.xml', 24 | input: [ 25 | base_appstream_file, 26 | 'com.github.unrud.djpdf.metainfo-releases.xml.in', 27 | ], 28 | output: 'com.github.unrud.djpdf.metainfo.xml', 29 | command: [ 30 | python, merge_xml_aux, 31 | '@INPUT0@', '@INPUT1@', '.', 32 | ], 33 | capture: true, 34 | install: true, 35 | install_dir: get_option('datadir') / 'metainfo', 36 | ) 37 | 38 | appstream_util = find_program('appstream-util', required: false) 39 | if appstream_util.found() 40 | test('Validate appstream file', appstream_util, 41 | args: ['validate-relax', appstream_file], 42 | ) 43 | endif 44 | 45 | icon_id = 'com.github.unrud.djpdf' 46 | icon_src = icon_id + '.png' 47 | convert_util = find_program('convert') 48 | foreach res : [16, 32, 48, 64, 128, 256, 512] 49 | icon_install_dir = ( 50 | get_option('datadir') / 'icons' / 'hicolor' / '@0@x@0@'.format(res) / 'apps' 51 | ) 52 | icon_png = '@0@_@1@.png'.format(icon_id, res) 53 | custom_target(icon_png, 54 | input: icon_src, 55 | output: icon_png, 56 | command: [ 57 | convert_util, 58 | '+set', 'date:create', '+set', 'date:modify', 59 | '@INPUT@', '-resize', '@0@x@0@'.format(res), '@OUTPUT@', 60 | ], 61 | install: true, 62 | install_dir: icon_install_dir, 63 | ) 64 | # rename icon after installation 65 | meson.add_install_script(python, move_aux, 66 | icon_install_dir / icon_png, 67 | icon_install_dir / icon_id + '.png', 68 | ) 69 | endforeach 70 | -------------------------------------------------------------------------------- /djpdf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unrud/djpdf/e304dc337d1da218130223b3b3c9f550e286e6aa/djpdf/__init__.py -------------------------------------------------------------------------------- /djpdf/__main__.py: -------------------------------------------------------------------------------- 1 | from djpdf.scans2pdfcli import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /djpdf/argyllcms-srgb.icm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unrud/djpdf/e304dc337d1da218130223b3b3c9f550e286e6aa/djpdf/argyllcms-srgb.icm -------------------------------------------------------------------------------- /djpdf/hocr.py: -------------------------------------------------------------------------------- 1 | # This file is part of djpdf. 2 | # 3 | # djpdf is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # djpdf is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with djpdf. If not, see . 15 | 16 | # Copyright 2015, 2017 Unrud 17 | 18 | import json 19 | import logging 20 | import re 21 | import sys 22 | import traceback 23 | from argparse import ArgumentParser 24 | from xml.etree.ElementTree import ElementTree 25 | 26 | from djpdf.util import cli_set_verbosity, cli_setup 27 | 28 | try: 29 | from PIL import Image, ImageDraw 30 | except ImportError: 31 | HAS_PIL = False 32 | else: 33 | HAS_PIL = True 34 | 35 | 36 | def extract_text(hocr_filename): 37 | bbox_regex = re.compile(r"bbox((\s+\d+){4})") 38 | textangle_regex = re.compile(r"textangle(\s+\d+)") 39 | hocr = ElementTree() 40 | hocr.parse(hocr_filename) 41 | texts = [] 42 | for line in hocr.iter(): 43 | if line.attrib.get("class") != "ocr_line": 44 | continue 45 | try: 46 | textangle = textangle_regex.search(line.attrib["title"]).group(1) 47 | except Exception: 48 | logging.info("Can't extract textangle from ocr_line: %s" % 49 | line.attrib.get("title")) 50 | logging.debug("Exception occurred:\n%s" % traceback.format_exc()) 51 | textangle = 0 52 | textangle = int(textangle) 53 | for word in line.iter(): 54 | if word.attrib.get("class") != "ocrx_word": 55 | continue 56 | text = "" 57 | # Sometimes word has children like "text" 58 | for e in word.iter(): 59 | if e.text: 60 | text += e.text 61 | text = text.strip() 62 | if not text: 63 | logging.info("ocrx_word with empty text found") 64 | continue 65 | try: 66 | box = bbox_regex.search(word.attrib["title"]).group(1).split() 67 | except Exception: 68 | logging.info("Can't extract bbox from ocrx_word: %s" % 69 | word.attrib.get("title")) 70 | logging.debug( 71 | "Exception occurred:\n%s" % traceback.format_exc()) 72 | continue 73 | box = [int(i) for i in box] 74 | textdirection = word.get("dir", "ltr") 75 | if textdirection not in ("ltr", "rtl", "ttb"): 76 | logging.info("ocrx_word with unknown textdirection found: %s" % 77 | textdirection) 78 | textdirection = "ltr" 79 | texts.append({ 80 | "x": box[0], 81 | "y": box[1], 82 | "width": box[2] - box[0], 83 | "height": box[3] - box[1], 84 | "rotation": textangle, 85 | "text": text, 86 | "direction": textdirection 87 | }) 88 | return texts 89 | 90 | 91 | def _draw_image(image_filename, texts): 92 | im = Image.open(image_filename).convert("RGB") 93 | d = ImageDraw.Draw(im) 94 | for text in texts: 95 | x = text["x"] 96 | y = text["y"] 97 | w = text["width"] 98 | h = text["height"] 99 | # t = text["text"] 100 | # r = text["rotation"] 101 | d.polygon([(x, y), (x+w, y), (x+w, y+h), (x, y+h)], outline="red") 102 | im.show() 103 | 104 | 105 | def main(): 106 | cli_setup() 107 | parser = ArgumentParser() 108 | parser.add_argument("-v", "--verbose", help="increase output verbosity", 109 | action="store_true") 110 | if HAS_PIL: 111 | parser.add_argument('--image', metavar='IMAGE_FILE', action="store") 112 | args = parser.parse_args() 113 | cli_set_verbosity(args.verbose) 114 | try: 115 | texts = extract_text(sys.stdin) 116 | print(json.dumps(texts)) 117 | if HAS_PIL and args.image is not None: 118 | _draw_image(args.image, texts) 119 | except Exception: 120 | logging.debug("Exception occurred:\n%s" % traceback.format_exc()) 121 | logging.fatal("Operation failed") 122 | sys.exit(1) 123 | -------------------------------------------------------------------------------- /djpdf/scans2pdf.py: -------------------------------------------------------------------------------- 1 | # This file is part of djpdf. 2 | # 3 | # djpdf is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # djpdf is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with djpdf. If not, see . 15 | 16 | # Copyright 2015, 2017 Unrud 17 | 18 | import asyncio 19 | import json 20 | import logging 21 | import os 22 | import re 23 | import subprocess 24 | import sys 25 | import traceback 26 | from argparse import ArgumentParser 27 | from os import path 28 | 29 | from djpdf import hocr 30 | from djpdf.djpdf import (CONVERT_CMD, JOB_MEMORY, PARALLEL_JOBS, 31 | RESERVED_MEMORY, SRGB_ICC_RESOURCE, 32 | BigTemporaryDirectory, PdfBuilder) 33 | from djpdf.util import (AsyncCache, MemoryBoundedSemaphore, cli_set_verbosity, 34 | cli_setup, format_number, run_command) 35 | 36 | if sys.version_info < (3, 9): 37 | import importlib_resources 38 | else: 39 | import importlib.resources as importlib_resources 40 | 41 | DEFAULT_SETTINGS = { 42 | "dpi": "auto", 43 | "bg_color": (0xff, 0xff, 0xff), 44 | "bg_enabled": True, 45 | "bg_resize": 0.5, 46 | "bg_compression": "jp2", 47 | "bg_quality": 50, 48 | "fg_enabled": True, 49 | "fg_colors": [(0, 0, 0)], 50 | "fg_compression": "jbig2", 51 | "fg_jbig2_threshold": 0.9, 52 | "ocr_enabled": True, 53 | "ocr_language": "eng", 54 | "ocr_colors": [(0, 0, 0)], 55 | "filename": None 56 | } 57 | IDENTIFY_CMD = "identify" 58 | TESSERACT_CMD = "tesseract" 59 | PDF_DPI = 72 60 | 61 | 62 | def find_ocr_languages(): 63 | try: 64 | return sorted(subprocess.check_output( 65 | [TESSERACT_CMD, "--list-langs"], stderr=subprocess.STDOUT, 66 | universal_newlines=True).rstrip("\n").split("\n")[1:]) 67 | except (FileNotFoundError, PermissionError, subprocess.CalledProcessError): 68 | return [] 69 | 70 | 71 | def _color_to_hex(color): 72 | return "#%02x%02x%02x" % color 73 | 74 | 75 | class RecipeFactory: 76 | def __init__(self): 77 | self._cleaners = [] 78 | self._cache = [] 79 | 80 | def add_cleaner(self, callback): 81 | self._cleaners.append(callback) 82 | 83 | def cleanup(self): 84 | for callback in self._cleaners: 85 | callback() 86 | self._cleaners.clear() 87 | 88 | def _from_cache(self, obj): 89 | try: 90 | index = self._cache.index(obj) 91 | obj = self._cache[index] 92 | except ValueError: 93 | self._cache.append(obj) 94 | return obj 95 | 96 | def make_input_image(self, page): 97 | obj = InputImage(self, page) 98 | return self._from_cache(obj) 99 | 100 | def make_background_image(self, page): 101 | obj = BackgroundImage(self, page) 102 | return self._from_cache(obj) 103 | 104 | def make_background(self, page): 105 | obj = Background(self, page) 106 | return self._from_cache(obj) 107 | 108 | def make_foreground_image(self, color_index, page): 109 | obj = ForegroundImage(color_index, self, page) 110 | return self._from_cache(obj) 111 | 112 | def make_foreground(self, color_index, page): 113 | obj = Foreground(color_index, self, page) 114 | return self._from_cache(obj) 115 | 116 | def make_ocr_image(self, page): 117 | obj = OcrImage(self, page) 118 | return self._from_cache(obj) 119 | 120 | def make_ocr(self, page): 121 | obj = Ocr(self, page) 122 | return self._from_cache(obj) 123 | 124 | def make_page(self, page): 125 | obj = Page(self, page) 126 | return self._from_cache(obj) 127 | 128 | 129 | class BasePageObject: 130 | _factory = None 131 | _page = None 132 | _temp_dir = None 133 | _cache = None 134 | 135 | def __init__(self, factory, page): 136 | self._factory = factory 137 | self._page = page 138 | temp_dir = BigTemporaryDirectory(prefix="djpdf-") 139 | self._temp_dir = temp_dir.name 140 | factory.add_cleaner(temp_dir.cleanup) 141 | self._cache = AsyncCache() 142 | 143 | 144 | class BaseImageObject(BasePageObject): 145 | _size_cache = None 146 | _dpi_cache = None 147 | 148 | def __init__(self, *args): 149 | super().__init__(*args) 150 | self._size_cache = AsyncCache() 151 | self._dpi_cache = AsyncCache() 152 | 153 | async def size(self, psem): 154 | return await self._size_cache.get(self._size(psem)) 155 | 156 | async def _size(self, psem): 157 | outs = await run_command([ 158 | IDENTIFY_CMD, "-format", "%w %h", 159 | path.abspath(await self.filename(psem))], psem) 160 | outs = outs.decode("ascii") 161 | outss = outs.split() 162 | w, h = int(outss[0]), int(outss[1]) 163 | return w, h 164 | 165 | async def dpi(self, psem): 166 | return await self._dpi_cache.get(self._dpi(psem)) 167 | 168 | async def _dpi(self, psem): 169 | outs = await run_command([ 170 | IDENTIFY_CMD, "-units", "PixelsPerInch", "-format", "%x %y", 171 | path.abspath(await self.filename(psem))], psem) 172 | outs = outs.decode("ascii") 173 | outss = outs.split() 174 | if len(outss) == 2: 175 | x, y = float(outss[0]), float(outss[1]) 176 | elif len(outss) == 4: 177 | assert outss[1] == "PixelsPerInch" 178 | assert outss[3] == "PixelsPerInch" 179 | x, y = float(outss[0]), float(outss[2]) 180 | else: 181 | raise Exception("Can't extract dpi: %s" % outs) 182 | return x, y 183 | 184 | @staticmethod 185 | async def _is_plain_color_file(filename, color, psem): 186 | outs = await run_command([ 187 | CONVERT_CMD, "-format", "%c", path.abspath(filename), 188 | "histogram:info:-"], psem) 189 | outs = outs.decode("ascii") 190 | histogram_re = re.compile(r"\s*(?P\d+(?:(?:\.\d+)?e\+\d+)?):\s+" 191 | r"\(\s*(?P\d+),\s*(?P\d+)," 192 | r"\s*(?P\d+)\)") 193 | colors = [] 194 | for line in outs.split("\n"): 195 | if not line.strip(): 196 | continue 197 | mo = histogram_re.match(line) 198 | try: 199 | colors.append((int(mo.group("r")), 200 | int(mo.group("g")), 201 | int(mo.group("b")))) 202 | except Exception as e: 203 | raise Exception("Can't extract color: %s" % line) from e 204 | return len(colors) == 1 and colors[0] == tuple(color) 205 | 206 | 207 | class InputImage(BaseImageObject): 208 | def __eq__(self, other): 209 | if not isinstance(other, InputImage): 210 | return False 211 | p = self._page 212 | op = other._page 213 | return (p["filename"] == op["filename"] and 214 | p["bg_color"] == op["bg_color"]) 215 | 216 | async def filename(self, psem): 217 | return await self._cache.get(self._filename(psem)) 218 | 219 | async def _filename(self, psem): 220 | fname = path.join(self._temp_dir, "image.png") 221 | with importlib_resources.as_file(SRGB_ICC_RESOURCE) as srgb_icc_path: 222 | await run_command([ 223 | CONVERT_CMD, 224 | "-colorspace", "sRGB", 225 | "-profile", os.fspath(srgb_icc_path), 226 | "-background", _color_to_hex(self._page["bg_color"]), 227 | "-alpha", "remove", 228 | "-alpha", "off", 229 | "-type", "TrueColor", 230 | path.abspath(self._page["filename"]), 231 | path.abspath(fname)], psem) 232 | return fname 233 | 234 | 235 | class BackgroundImage(BaseImageObject): 236 | def __init__(self, *args): 237 | super().__init__(*args) 238 | self._input_image = self._factory.make_input_image(self._page) 239 | 240 | def __eq__(self, other): 241 | if not isinstance(other, BackgroundImage): 242 | return False 243 | p = self._page 244 | op = other._page 245 | return (p["bg_resize"] == op["bg_resize"] and 246 | (not p["fg_enabled"] and not op["fg_enabled"] or 247 | p["fg_enabled"] and op["fg_enabled"] and 248 | p["fg_colors"] == op["fg_colors"]) and 249 | self._input_image == other._input_image) 250 | 251 | async def filename(self, psem): 252 | return await self._cache.get(self._filename(psem)) 253 | 254 | async def _filename(self, psem): 255 | if (self._page["fg_enabled"] and self._page["fg_colors"] or 256 | self._page["bg_resize"] != 1): 257 | fname = path.join(self._temp_dir, "image.png") 258 | cmd = [CONVERT_CMD, 259 | "-fill", _color_to_hex(self._page["bg_color"])] 260 | if self._page["fg_enabled"]: 261 | for color in self._page["fg_colors"]: 262 | cmd.extend(["-opaque", _color_to_hex(color)]) 263 | cmd.extend(["-resize", format_number(self._page["bg_resize"], 2, 264 | percentage=True), 265 | path.abspath(await self._input_image.filename(psem)), 266 | path.abspath(fname)]) 267 | await run_command(cmd, psem) 268 | else: 269 | fname = await self._input_image.filename(psem) 270 | if await self._is_plain_color_file(fname, self._page["bg_color"], 271 | psem): 272 | return None 273 | return fname 274 | 275 | 276 | class Background(BasePageObject): 277 | def __init__(self, *args): 278 | super().__init__(*args) 279 | self._background_image = self._factory.make_background_image( 280 | self._page) 281 | 282 | def __eq__(self, other): 283 | if not isinstance(other, Background): 284 | return False 285 | p = self._page 286 | op = other._page 287 | return (not p["bg_enabled"] and not op["bg_enabled"] or 288 | p["bg_enabled"] and op["bg_enabled"] and 289 | p["bg_compression"] == op["bg_compression"] and 290 | p["bg_quality"] == op["bg_quality"] and 291 | self._background_image == other._background_image) 292 | 293 | async def json(self, psem): 294 | return await self._cache.get(self._json(psem)) 295 | 296 | async def _json(self, psem): 297 | if (not self._page["bg_enabled"] or 298 | await self._background_image.filename(psem) is None): 299 | return None 300 | return { 301 | "compression": self._page["bg_compression"], 302 | "quality": self._page["bg_quality"], 303 | "filename": await self._background_image.filename(psem) 304 | } 305 | 306 | 307 | class ForegroundImage(BaseImageObject): 308 | def __init__(self, color_index, *args): 309 | super().__init__(*args) 310 | self._color_index = color_index 311 | self._input_image = self._factory.make_input_image(self._page) 312 | 313 | def __eq__(self, other): 314 | if not isinstance(other, ForegroundImage): 315 | return False 316 | p = self._page 317 | op = other._page 318 | return (p["fg_colors"][self._color_index] == 319 | op["fg_colors"][other._color_index] and 320 | self._input_image == other._input_image) 321 | 322 | async def filename(self, psem): 323 | return await self._cache.get(self._filename(psem)) 324 | 325 | async def _filename(self, psem): 326 | fname = path.join(self._temp_dir, "image.png") 327 | color = self._page["fg_colors"][self._color_index] 328 | cmd = [CONVERT_CMD] 329 | new_black = (0x00, 0x00, 0x00) 330 | if color != new_black: 331 | if color != (0x00, 0x00, 0x01): 332 | new_black = (0x00, 0x00, 0x01) 333 | else: 334 | new_black = (0x00, 0x00, 0x02) 335 | cmd.extend(["-fill", _color_to_hex(new_black), 336 | "-opaque", "#000000", 337 | "-fill", "#000000", 338 | "-opaque", _color_to_hex(color), 339 | "-threshold", "0", 340 | path.abspath(await self._input_image.filename(psem)), 341 | path.abspath(fname)]) 342 | await run_command(cmd, psem) 343 | if await self._is_plain_color_file(fname, (0xff, 0xff, 0xff), psem): 344 | return None 345 | return fname 346 | 347 | 348 | class Foreground(BasePageObject): 349 | def __init__(self, color_index, *args): 350 | super().__init__(*args) 351 | self._color_index = color_index 352 | self._foreground_image = self._factory.make_foreground_image( 353 | self._color_index, self._page) 354 | 355 | def __eq__(self, other): 356 | if not isinstance(other, Foreground): 357 | return False 358 | p = self._page 359 | op = other._page 360 | return (not p["fg_enabled"] and not op["fg_enabled"] or 361 | p["fg_enabled"] and op["fg_enabled"] and 362 | p["fg_compression"] == op["fg_compression"] and 363 | p["fg_jbig2_threshold"] == op["fg_jbig2_threshold"] and 364 | p["fg_colors"][self._color_index] == 365 | op["fg_colors"][other._color_index] and 366 | self._foreground_image == other._foreground_image) 367 | 368 | async def json(self, psem): 369 | return await self._cache.get(self._json(psem)) 370 | 371 | async def _json(self, psem): 372 | if (not self._page["fg_enabled"] or 373 | await self._foreground_image.filename(psem) is None): 374 | return None 375 | color = self._page["fg_colors"][self._color_index] 376 | return { 377 | "compression": self._page["fg_compression"], 378 | "jbig2_threshold": self._page["fg_jbig2_threshold"], 379 | "filename": await self._foreground_image.filename(psem), 380 | "color": color 381 | } 382 | 383 | 384 | class OcrImage(BaseImageObject): 385 | def __init__(self, *args): 386 | super().__init__(*args) 387 | self._input_image = self._factory.make_input_image(self._page) 388 | 389 | def __eq__(self, other): 390 | if not isinstance(other, OcrImage): 391 | return False 392 | p = self._page 393 | op = other._page 394 | return (p["ocr_colors"] == op["ocr_colors"] and 395 | self._input_image == other._input_image) 396 | 397 | async def filename(self, psem): 398 | return await self._cache.get(self._filename(psem)) 399 | 400 | async def _filename(self, psem): 401 | if self._page["ocr_colors"] != "all": 402 | fname = path.join(self._temp_dir, "image.png") 403 | 404 | def contains_color(color, cs): 405 | color = tuple(color) 406 | return any(map(lambda c: tuple(c) == color, cs)) 407 | new_black = (0x00, 0x00, 0x00) 408 | if not contains_color(new_black, self._page["ocr_colors"]): 409 | while (new_black == (0x00, 0x00, 0x00) or 410 | contains_color(new_black, self._page["ocr_colors"])): 411 | v = ((new_black[0] << 16) + 412 | (new_black[1] << 8) + 413 | (new_black[2] << 0)) 414 | v += 1 415 | new_black = ((v >> 16) & 0xff, (v >> 8) & 0xff, 416 | (v >> 0) & 0xff) 417 | cmd = [CONVERT_CMD] 418 | cmd.extend(["-fill", _color_to_hex(new_black), 419 | "-opaque", "#000000", 420 | "-fill", "#000000"]) 421 | for color in self._page["ocr_colors"]: 422 | cmd.extend(["-opaque", _color_to_hex(color)]) 423 | cmd.extend(["-threshold", "0"]) 424 | cmd.extend([path.abspath(await self._input_image.filename(psem)), 425 | path.abspath(fname)]) 426 | await run_command(cmd, psem) 427 | else: 428 | fname = await self._input_image.filename(psem) 429 | return fname 430 | 431 | 432 | class Ocr(BasePageObject): 433 | def __init__(self, *args): 434 | super().__init__(*args) 435 | self._input_image = self._factory.make_input_image(self._page) 436 | self._ocr_image = self._factory.make_ocr_image(self._page) 437 | 438 | def __eq__(self, other): 439 | if not isinstance(other, Ocr): 440 | return False 441 | p = self._page 442 | op = other._page 443 | return (not p["ocr_enabled"] and not op["ocr_enabled"] or 444 | p["ocr_enabled"] and op["ocr_enabled"] and 445 | p["ocr_language"] == op["ocr_language"] and 446 | self._ocr_image == other._ocr_image) 447 | 448 | async def texts(self, psem): 449 | return await self._cache.get(self._texts(psem)) 450 | 451 | async def _texts(self, psem): 452 | if not self._page["ocr_enabled"]: 453 | return None 454 | if self._page["dpi"] == "auto": 455 | dpi_x, _ = await self._input_image.dpi(psem) 456 | else: 457 | dpi_x = self._page["dpi"] 458 | await run_command([ 459 | TESSERACT_CMD, "-l", self._page["ocr_language"], 460 | "--dpi", "%.0f" % dpi_x, 461 | path.abspath(await self._ocr_image.filename(psem)), 462 | path.abspath(path.join(self._temp_dir, "ocr")), "hocr"], psem) 463 | return hocr.extract_text(path.join(self._temp_dir, "ocr.hocr")) 464 | 465 | 466 | class Page(BasePageObject): 467 | def __init__(self, factory, page): 468 | page = self._check_and_sanitize_recipe(page) 469 | super().__init__(factory, page) 470 | self._input_image = self._factory.make_input_image(self._page) 471 | self._foregrounds = [] 472 | for color_index, _ in enumerate(page["fg_colors"]): 473 | foreground = self._factory.make_foreground(color_index, self._page) 474 | self._foregrounds.append(foreground) 475 | self._background = self._factory.make_background(self._page) 476 | self._ocr = self._factory.make_ocr(self._page) 477 | 478 | def __eq__(self, other): 479 | if not isinstance(other, Page): 480 | return False 481 | p = self._page 482 | op = other._page 483 | return (p["bg_color"] == op["bg_color"] and 484 | p["dpi"] == op["dpi"] and 485 | self._input_image == other._input_image and 486 | self._foregrounds == other._foregrounds and 487 | self._background == other._background and 488 | self._ocr == other._ocr) 489 | 490 | async def json(self, psem): 491 | return await self._cache.get(self._json(psem)) 492 | 493 | async def _json(self, psem): 494 | # Prepare everything in parallel 495 | async def get_dpi(psem): 496 | if self._page["dpi"] == "auto": 497 | return await self._input_image.dpi(psem) 498 | return self._page["dpi"], self._page["dpi"] 499 | (texts, background, foregrounds_json, (width, height), 500 | (dpi_x, dpi_y)) = await asyncio.gather( 501 | self._ocr.texts(psem), 502 | self._background.json(psem), 503 | asyncio.gather(*[fg.json(psem) for fg in self._foregrounds]), 504 | self._input_image.size(psem), 505 | get_dpi(psem)) 506 | if texts is not None: 507 | for text in texts: 508 | text["x"] *= (PDF_DPI / dpi_x) 509 | text["y"] = ((height - text["y"] - text["height"]) * 510 | (PDF_DPI / dpi_y)) 511 | text["width"] *= (PDF_DPI / dpi_x) 512 | text["height"] *= (PDF_DPI / dpi_y) 513 | # Filter empty foregrounds 514 | foregrounds_json = [fg for fg in foregrounds_json if fg is not None] 515 | 516 | return { 517 | "width": width * (PDF_DPI / dpi_x), 518 | "height": height * (PDF_DPI / dpi_y), 519 | "background": background, 520 | "foreground": foregrounds_json, 521 | "color": self._page["bg_color"], 522 | "text": texts 523 | } 524 | 525 | @staticmethod 526 | def _check_and_sanitize_recipe(page): 527 | def is_color(c): 528 | return (isinstance(c, (list, tuple)) and 529 | len(c) == 3 and 530 | all(map(lambda v: isinstance(v, int), c))) 531 | 532 | def is_colors(cs): 533 | return (isinstance(cs, (list, tuple)) and 534 | all(map(lambda c: is_color(c), cs))) 535 | assert (isinstance(page.get("dpi"), (float, int)) and 536 | page["dpi"] > 0 or 537 | page.get("dpi") == "auto") 538 | assert is_color(page.get("bg_color")) 539 | assert isinstance(page.get("bg_enabled"), bool) 540 | assert (isinstance(page.get("bg_resize"), (int, float)) and 541 | page["bg_resize"] >= 0) 542 | assert page.get("bg_compression") in ("deflate", "jp2", "jpeg") 543 | assert (isinstance(page.get("bg_quality"), int) and 544 | 1 <= page["bg_quality"] and 545 | page["bg_quality"] <= 100) 546 | assert isinstance(page.get("fg_enabled"), bool) 547 | assert is_colors(page.get("fg_colors")) 548 | assert page.get("fg_compression") in ("jbig2", "fax") 549 | assert (isinstance(page.get("fg_jbig2_threshold"), (float, int)) and 550 | (page["fg_jbig2_threshold"] == 1 or 551 | 0.4 <= page["fg_jbig2_threshold"] and 552 | page["fg_jbig2_threshold"] <= 0.9)) 553 | assert isinstance(page.get("ocr_enabled"), bool) 554 | assert isinstance(page.get("ocr_language"), str) 555 | assert (page.get("ocr_colors") == "all" or 556 | is_colors(page.get("ocr_colors"))) 557 | assert isinstance(page.get("filename"), str) 558 | # sanitize 559 | page["bg_color"] = tuple(page["bg_color"]) 560 | page["fg_colors"] = tuple(map(tuple, page["fg_colors"])) 561 | if page["ocr_colors"] != "all": 562 | page["ocr_colors"] = tuple(map(tuple, page["ocr_colors"])) 563 | return page 564 | 565 | 566 | async def build_pdf(pages, pdf_filename, process_semaphore=None, 567 | progress_cb=None): 568 | if process_semaphore is None: 569 | process_semaphore = MemoryBoundedSemaphore( 570 | PARALLEL_JOBS, JOB_MEMORY, RESERVED_MEMORY) 571 | 572 | factory = RecipeFactory() 573 | 574 | finished_pages = 0 575 | 576 | async def progress_wrapper(fut): 577 | nonlocal finished_pages 578 | res = await fut 579 | finished_pages += 1 580 | if progress_cb: 581 | progress_cb(finished_pages / len(pages) * 0.5) 582 | return res 583 | 584 | try: 585 | djpdf_pages = await asyncio.gather(*[ 586 | progress_wrapper(factory.make_page(page).json(process_semaphore)) 587 | for page in pages]) 588 | pdf_builder = PdfBuilder({"pages": djpdf_pages}) 589 | return await pdf_builder.write( 590 | pdf_filename, process_semaphore, 591 | lambda f: progress_cb(0.5 + f * 0.5) if progress_cb else None) 592 | finally: 593 | factory.cleanup() 594 | 595 | 596 | def main(): 597 | cli_setup() 598 | parser = ArgumentParser() 599 | parser.add_argument("-v", "--verbose", help="increase output verbosity", 600 | action="store_true") 601 | parser.add_argument("OUTFILE") 602 | args = parser.parse_args() 603 | cli_set_verbosity(args.verbose) 604 | 605 | def progress_cb(fraction): 606 | json.dump({"fraction": fraction}, sys.stdout) 607 | print() 608 | sys.stdout.flush() 609 | try: 610 | recipe = json.load(sys.stdin) 611 | asyncio.run(build_pdf(recipe, args.OUTFILE, progress_cb=progress_cb)) 612 | except Exception: 613 | logging.debug("Exception occurred:\n%s" % traceback.format_exc()) 614 | logging.fatal("Operation failed") 615 | sys.exit(1) 616 | -------------------------------------------------------------------------------- /djpdf/scans2pdfcli.py: -------------------------------------------------------------------------------- 1 | # This file is part of djpdf. 2 | # 3 | # djpdf is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # djpdf is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with djpdf. If not, see . 15 | 16 | # Copyright 2015, 2017 Unrud 17 | 18 | import asyncio 19 | import copy 20 | import logging 21 | import os 22 | import re 23 | import subprocess 24 | import sys 25 | import traceback 26 | from argparse import ArgumentParser, ArgumentTypeError 27 | from importlib import metadata 28 | 29 | import webcolors 30 | 31 | from djpdf.djpdf import CONVERT_CMD, JBIG2_CMD, QPDF_CMD 32 | from djpdf.scans2pdf import (DEFAULT_SETTINGS, IDENTIFY_CMD, TESSERACT_CMD, 33 | build_pdf, find_ocr_languages) 34 | from djpdf.util import cli_set_verbosity, cli_setup, format_number 35 | 36 | VERSION = metadata.version("djpdf") 37 | 38 | 39 | def type_fraction(var): 40 | mobj = re.fullmatch( 41 | r"(?P\+?(?:\d+|\d*\.\d+))(?P%?)", 42 | var) 43 | if not mobj: 44 | raise ArgumentTypeError("invalid fraction value: '%s'" % var) 45 | d = float(mobj.group("value")) 46 | if mobj.group("percentage"): 47 | d /= 100 48 | if d < 0: 49 | raise ArgumentTypeError("invalid fraction value: '%s' " 50 | "(must be ≥ 0)" % var) 51 | return d 52 | 53 | 54 | def type_jbig2_threshold(var): 55 | f = type_fraction(var) 56 | if f == 1 or (0.4 <= f and f <= 0.9): 57 | return f 58 | raise ArgumentTypeError(("invalid threshold value: '%s' " 59 | "(must be 1 or between 0.4 and 0.9") % var) 60 | 61 | 62 | def type_quality(var): 63 | try: 64 | q = int(var) 65 | except ValueError as e: 66 | raise ArgumentTypeError("invalid int value: '%s'" % var) from e 67 | if 1 <= q and q <= 100: 68 | return q 69 | raise ArgumentTypeError(("invalid quality value: '%s' " 70 | "(must be between 1 and 100") % var) 71 | 72 | 73 | def type_color(var): 74 | try: 75 | return webcolors.name_to_rgb(var) 76 | except ValueError: 77 | pass 78 | try: 79 | return webcolors.hex_to_rgb(var) 80 | except ValueError: 81 | pass 82 | raise ArgumentTypeError("invalid color value: '%s'" % var) 83 | 84 | 85 | def type_colors(var): 86 | if not var: 87 | return () 88 | cs = [] 89 | for v in var.split(","): 90 | try: 91 | c = type_color(v) 92 | if c not in cs: 93 | cs.append(c) 94 | except ArgumentTypeError as e: 95 | raise ArgumentTypeError("invalid colors value: '%s'" % var) from e 96 | return cs 97 | 98 | 99 | def type_ocr_colors(var): 100 | if var == "all": 101 | return var 102 | return type_colors(var) 103 | 104 | 105 | def type_dpi(var): 106 | if var == "auto": 107 | return var 108 | try: 109 | d = float(var) 110 | except ValueError: 111 | raise ArgumentTypeError("invalid dpi value: '%s'" % var) 112 | if d <= 0: 113 | raise ArgumentTypeError("invalid dpi value: '%s' " 114 | "(must be > 0)" % var) 115 | return d 116 | 117 | 118 | def type_bool(var): 119 | if var.lower() in ("yes", "y", "on", "true", "t", "1"): 120 | return True 121 | if var.lower() in ("no", "n", "off", "false", "f", "0"): 122 | return False 123 | raise ArgumentTypeError("invalid bool value: '%s'" % var) 124 | 125 | 126 | def type_infile(var): 127 | eids = os.access in os.supports_effective_ids 128 | if os.path.exists(var) and not os.path.isfile(var): 129 | raise ArgumentTypeError("not a regular file: '%s'" % var) 130 | if not os.path.exists(var): 131 | raise ArgumentTypeError("file does not exist: '%s'" % var) 132 | if not os.access(var, os.R_OK, effective_ids=eids): 133 | raise ArgumentTypeError("file access is denied: '%s'" % var) 134 | return var 135 | 136 | 137 | def type_outfile(var): 138 | eids = os.access in os.supports_effective_ids 139 | if os.path.exists(var) and not os.path.isfile(var): 140 | raise ArgumentTypeError("not a regular file: '%s'" % var) 141 | if not os.path.exists(var): 142 | dir = os.path.dirname(var) 143 | if not dir: 144 | dir = "." 145 | if not os.path.isdir(dir): 146 | raise ArgumentTypeError("containing directory does " 147 | "not exist: '%s'" % var) 148 | if not os.access(dir, os.W_OK, effective_ids=eids): 149 | raise ArgumentTypeError("file access is denied: '%s'" % var) 150 | elif not os.access(var, os.W_OK, effective_ids=eids): 151 | raise ArgumentTypeError("file access is denied: '%s'" % var) 152 | return var 153 | 154 | 155 | def update_page_from_namespace(page, ns): 156 | if ns.dpi is not None: 157 | page["dpi"] = ns.dpi 158 | if ns.bg_color is not None: 159 | page["bg_color"] = ns.bg_color 160 | if ns.bg is not None: 161 | page["bg_enabled"] = ns.bg 162 | if ns.bg_resize is not None: 163 | page["bg_resize"] = ns.bg_resize 164 | if ns.bg_compression is not None: 165 | page["bg_compression"] = ns.bg_compression 166 | if ns.bg_quality is not None: 167 | page["bg_quality"] = ns.bg_quality 168 | if ns.fg is not None: 169 | page["fg_enabled"] = ns.fg 170 | if ns.fg_colors is not None: 171 | page["fg_colors"] = ns.fg_colors 172 | if ns.fg_compression is not None: 173 | page["fg_compression"] = ns.fg_compression 174 | if ns.fg_jbig2_threshold is not None: 175 | page["fg_jbig2_threshold"] = ns.fg_jbig2_threshold 176 | if ns.ocr is not None: 177 | page["ocr_enabled"] = ns.ocr 178 | if ns.ocr_lang is not None: 179 | page["ocr_language"] = ns.ocr_lang 180 | if ns.ocr_colors is not None: 181 | page["ocr_colors"] = ns.ocr_colors 182 | page["filename"] = ns.INFILE 183 | 184 | 185 | def test_command_exists(args, fatal=False): 186 | try: 187 | subprocess.call( 188 | args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 189 | except (FileNotFoundError, PermissionError): 190 | if fatal: 191 | logging.fatal("Program not found: %s" % args[0]) 192 | sys.exit(1) 193 | else: 194 | logging.warning("Program not found: %s" % args[0]) 195 | return False 196 | else: 197 | return True 198 | 199 | 200 | def main(): 201 | cli_setup() 202 | 203 | def rgb_to_name_or_hex(rgb): 204 | try: 205 | return webcolors.rgb_to_name(rgb) 206 | except ValueError: 207 | pass 208 | return webcolors.rgb_to_hex(rgb) 209 | 210 | def bool_to_name(b): 211 | if b: 212 | return "yes" 213 | else: 214 | return "no" 215 | 216 | def format_number_percentage(d): 217 | return format_number(d, 2, percentage=True) 218 | 219 | df = copy.deepcopy(DEFAULT_SETTINGS) 220 | # Autodetect features 221 | ocr_languages = find_ocr_languages() 222 | if not ocr_languages: 223 | df["ocr_enabled"] = False 224 | if test_command_exists([TESSERACT_CMD]): 225 | logging.warning("'%s' is missing language files" % TESSERACT_CMD) 226 | elif df["ocr_language"] not in ocr_languages: 227 | df["ocr_language"] = ocr_languages[0] 228 | if not test_command_exists([JBIG2_CMD]): 229 | df["fg_compression"] = "fax" 230 | test_command_exists([QPDF_CMD], fatal=True) 231 | test_command_exists([CONVERT_CMD], fatal=True) 232 | test_command_exists([IDENTIFY_CMD], fatal=True) 233 | 234 | parser = ArgumentParser( 235 | description="Options are valid for all following images.", 236 | usage="%(prog)s [options] INFILE [[options] INFILE ...] OUTFILE") 237 | 238 | parser.add_argument( 239 | "--version", action="version", version="%%(prog)s %s" % VERSION, 240 | help="show version info and exit") 241 | 242 | parser.add_argument("-v", "--verbose", help="increase output verbosity", 243 | action="store_true") 244 | 245 | parser.add_argument( 246 | "--dpi", type=type_dpi, 247 | help="specify the dpi of the input image. If 'auto' is given the " 248 | "value gets read from the input file " 249 | "(default: %s)" % (df["dpi"] if isinstance(df["dpi"], str) else 250 | format_number(df["dpi"], 2))) 251 | 252 | parser.add_argument( 253 | "--bg-color", type=type_color, action="store", metavar="COLOR", 254 | help="sets the background color of the page. Colors can be either " 255 | "specified by name (e.g. white) or as a hash mark '#' followed " 256 | "by three pairs of hexadecimal digits, specifying values for " 257 | "red, green and blue components (e.g. #ffffff) " 258 | "(default: %s)" % rgb_to_name_or_hex(df["bg_color"])) 259 | parser.add_argument( 260 | "--bg", type=type_bool, action="store", metavar="BOOLEAN", 261 | help="sets if a low quality background image gets included, " 262 | "containing all the colors that are not in the foreground " 263 | "layer " 264 | "(default: %s)" % bool_to_name(df["bg_enabled"])) 265 | parser.add_argument( 266 | "--bg-resize", type=type_fraction, action="store", 267 | metavar="FRACTION", 268 | help=("sets the percentage by which the background image gets " 269 | "resized. A value of 100%%%% means that the resolution is not " 270 | "changed. " 271 | "(default: %s)" % 272 | format_number_percentage(df["bg_resize"]).replace("%", "%%"))) 273 | parser.add_argument( 274 | "--bg-compression", choices=["deflate", "jp2", "jpeg"], 275 | help=("specify the compression algorithm to use for the background " 276 | "layer. 'deflate' is lossless. 'jp2' and 'jpeg' are lossy " 277 | "depending on the quality setting. " 278 | "(default: %s)" % df["bg_compression"])) 279 | parser.add_argument( 280 | "--bg-quality", metavar="INTEGER", type=type_quality, 281 | help="for 'jp2' and 'jpeg' compression, quality is 1 (lowest image " 282 | "quality and highest compression) to 100 (best quality but " 283 | "least effective compression) " 284 | "(default: %d)" % df["bg_quality"]) 285 | 286 | parser.add_argument( 287 | "--fg", type=type_bool, action="store", metavar="BOOLEAN", 288 | help="sets if a high quality foreground layer gets included, " 289 | "containing only a limited set of colors " 290 | "(default: %s)" % bool_to_name(df["fg_enabled"])) 291 | parser.add_argument( 292 | "--fg-colors", type=type_colors, action="store", metavar="COLORS", 293 | help="specify the colors to separate into the foreground layer. " 294 | "Colors can be specified as described at '--bg-color'. " 295 | "Multiple colors must be comma-separated. " 296 | "(default: %s)" % ",".join(map(lambda c: rgb_to_name_or_hex(c), 297 | df["fg_colors"]))) 298 | parser.add_argument( 299 | "--fg-compression", choices=["fax", "jbig2"], 300 | help="specify the compression algorithm to use for the bitonal " 301 | "foreground layer. 'fax' is lossless. 'jbig2' is " 302 | "lossy depending on the threshold setting. " 303 | "(default: %s)" % df["fg_compression"]) 304 | parser.add_argument( 305 | "--fg-jbig2-threshold", type=type_jbig2_threshold, action="store", 306 | metavar="FRACTION", 307 | help=("sets the fraction of pixels which have to match in order for " 308 | "two symbols to be classed the same. This isn't strictly true, " 309 | "as there are other tests as well, but increasing this will " 310 | "generally increase the number of symbol classes. A value of " 311 | "100%%%% means lossless compression. " 312 | "(default: %s)" % format_number_percentage( 313 | df["fg_jbig2_threshold"]).replace("%", "%%"))) 314 | 315 | parser.add_argument( 316 | "--ocr", type=type_bool, action="store", metavar="BOOLEAN", 317 | help="optical character recognition with tesseract " 318 | "(default: %s)" % bool_to_name(df["ocr_enabled"])) 319 | parser.add_argument( 320 | "--ocr-lang", action="store", metavar="LANG", 321 | help="specify language used for OCR. " 322 | "Multiple languages may be specified, separated " 323 | "by plus characters. " 324 | "(default: %s)" % df["ocr_language"]) 325 | parser.add_argument( 326 | "--ocr-list-langs", action="store_true", 327 | help="list available languages for OCR ") 328 | parser.add_argument( 329 | "--ocr-colors", type=type_ocr_colors, action="store", 330 | metavar="COLORS", 331 | help="specify the colors for ocr. 'all' specifies all colors " 332 | "(default: %s)" % ( 333 | df["ocr_colors"] if isinstance(df["ocr_colors"], str) 334 | else ",".join(map(lambda c: rgb_to_name_or_hex(c), 335 | df["ocr_colors"])))) 336 | 337 | global_args = ("--vers", "-h", "--h", "-v", "--verb", "--ocr-li") 338 | global_argv = list(filter( 339 | lambda arg: any( 340 | [arg.startswith(s) for s in global_args]), 341 | sys.argv[1:])) 342 | remaining_argv = list(filter( 343 | lambda arg: not any( 344 | [arg.startswith(s) for s in global_args]), 345 | sys.argv[1:])) 346 | 347 | # handle global arguments 348 | ns = parser.parse_args(global_argv) 349 | cli_set_verbosity(ns.verbose) 350 | 351 | if ns.ocr_list_langs: 352 | print("\n".join(ocr_languages)) 353 | sys.exit(0) 354 | 355 | infile_parser = ArgumentParser(usage=parser.usage, prog=parser.prog, 356 | parents=(parser,), add_help=False) 357 | infile_parser.add_argument("INFILE", type=type_infile) 358 | outfile_parser = ArgumentParser(usage=parser.usage, prog=parser.prog, 359 | parents=(parser,), add_help=False) 360 | outfile_parser.add_argument("OUTFILE", type=type_outfile) 361 | 362 | def is_arg(s): 363 | if re.fullmatch(r"-\d+", s): 364 | return False 365 | return s.startswith("-") 366 | 367 | def expects_arg(s): 368 | # all non-global arguments expect one argument 369 | return is_arg(s) and s.startswith("--") 370 | 371 | pages = [] 372 | while True: 373 | current_argv = [] 374 | while (not current_argv or 375 | (current_argv and is_arg(current_argv[-1])) or 376 | (len(current_argv) >= 2 and expects_arg(current_argv[-2]))): 377 | if not remaining_argv: 378 | parser.error("the following arguments are required: " 379 | "INFILE, OUTFILE") 380 | current_argv.append(remaining_argv[0]) 381 | del remaining_argv[0] 382 | ns = infile_parser.parse_args(current_argv) 383 | update_page_from_namespace(df, ns) 384 | pages.append(df.copy()) 385 | if (not remaining_argv or len(remaining_argv) == 1 and 386 | not is_arg(remaining_argv[0])): 387 | break 388 | ns = outfile_parser.parse_args(remaining_argv) 389 | out_file = ns.OUTFILE 390 | 391 | try: 392 | asyncio.run(build_pdf(pages, out_file)) 393 | except Exception: 394 | logging.debug("Exception occurred:\n%s" % traceback.format_exc()) 395 | logging.fatal("Operation failed") 396 | sys.exit(1) 397 | -------------------------------------------------------------------------------- /djpdf/tesseract-pdf.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unrud/djpdf/e304dc337d1da218130223b3b3c9f550e286e6aa/djpdf/tesseract-pdf.ttf -------------------------------------------------------------------------------- /djpdf/to-unicode.cmap: -------------------------------------------------------------------------------- 1 | /CIDInit/ProcSet findresource begin 2 | 12 dict begin 3 | begincmap 4 | /CIDSystemInfo<< 5 | /Registry (Adobe) 6 | /Ordering (UCS) 7 | /Supplement 0 8 | >> def 9 | /CMapName/Adobe-Identity-UCS def 10 | /CMapType 2 def 11 | 1 begincodespacerange 12 | <0000> 13 | endcodespacerange 14 | 100 beginbfrange 15 | <0000> <00FF> <0000> 16 | <0100> <01FF> <0100> 17 | <0200> <02FF> <0200> 18 | <0300> <03FF> <0300> 19 | <0400> <04FF> <0400> 20 | <0500> <05FF> <0500> 21 | <0600> <06FF> <0600> 22 | <0700> <07FF> <0700> 23 | <0800> <08FF> <0800> 24 | <0900> <09FF> <0900> 25 | <0A00> <0AFF> <0A00> 26 | <0B00> <0BFF> <0B00> 27 | <0C00> <0CFF> <0C00> 28 | <0D00> <0DFF> <0D00> 29 | <0E00> <0EFF> <0E00> 30 | <0F00> <0FFF> <0F00> 31 | <1000> <10FF> <1000> 32 | <1100> <11FF> <1100> 33 | <1200> <12FF> <1200> 34 | <1300> <13FF> <1300> 35 | <1400> <14FF> <1400> 36 | <1500> <15FF> <1500> 37 | <1600> <16FF> <1600> 38 | <1700> <17FF> <1700> 39 | <1800> <18FF> <1800> 40 | <1900> <19FF> <1900> 41 | <1A00> <1AFF> <1A00> 42 | <1B00> <1BFF> <1B00> 43 | <1C00> <1CFF> <1C00> 44 | <1D00> <1DFF> <1D00> 45 | <1E00> <1EFF> <1E00> 46 | <1F00> <1FFF> <1F00> 47 | <2000> <20FF> <2000> 48 | <2100> <21FF> <2100> 49 | <2200> <22FF> <2200> 50 | <2300> <23FF> <2300> 51 | <2400> <24FF> <2400> 52 | <2500> <25FF> <2500> 53 | <2600> <26FF> <2600> 54 | <2700> <27FF> <2700> 55 | <2800> <28FF> <2800> 56 | <2900> <29FF> <2900> 57 | <2A00> <2AFF> <2A00> 58 | <2B00> <2BFF> <2B00> 59 | <2C00> <2CFF> <2C00> 60 | <2D00> <2DFF> <2D00> 61 | <2E00> <2EFF> <2E00> 62 | <2F00> <2FFF> <2F00> 63 | <3000> <30FF> <3000> 64 | <3100> <31FF> <3100> 65 | <3200> <32FF> <3200> 66 | <3300> <33FF> <3300> 67 | <3400> <34FF> <3400> 68 | <3500> <35FF> <3500> 69 | <3600> <36FF> <3600> 70 | <3700> <37FF> <3700> 71 | <3800> <38FF> <3800> 72 | <3900> <39FF> <3900> 73 | <3A00> <3AFF> <3A00> 74 | <3B00> <3BFF> <3B00> 75 | <3C00> <3CFF> <3C00> 76 | <3D00> <3DFF> <3D00> 77 | <3E00> <3EFF> <3E00> 78 | <3F00> <3FFF> <3F00> 79 | <4000> <40FF> <4000> 80 | <4100> <41FF> <4100> 81 | <4200> <42FF> <4200> 82 | <4300> <43FF> <4300> 83 | <4400> <44FF> <4400> 84 | <4500> <45FF> <4500> 85 | <4600> <46FF> <4600> 86 | <4700> <47FF> <4700> 87 | <4800> <48FF> <4800> 88 | <4900> <49FF> <4900> 89 | <4A00> <4AFF> <4A00> 90 | <4B00> <4BFF> <4B00> 91 | <4C00> <4CFF> <4C00> 92 | <4D00> <4DFF> <4D00> 93 | <4E00> <4EFF> <4E00> 94 | <4F00> <4FFF> <4F00> 95 | <5000> <50FF> <5000> 96 | <5100> <51FF> <5100> 97 | <5200> <52FF> <5200> 98 | <5300> <53FF> <5300> 99 | <5400> <54FF> <5400> 100 | <5500> <55FF> <5500> 101 | <5600> <56FF> <5600> 102 | <5700> <57FF> <5700> 103 | <5800> <58FF> <5800> 104 | <5900> <59FF> <5900> 105 | <5A00> <5AFF> <5A00> 106 | <5B00> <5BFF> <5B00> 107 | <5C00> <5CFF> <5C00> 108 | <5D00> <5DFF> <5D00> 109 | <5E00> <5EFF> <5E00> 110 | <5F00> <5FFF> <5F00> 111 | <6000> <60FF> <6000> 112 | <6100> <61FF> <6100> 113 | <6200> <62FF> <6200> 114 | <6300> <63FF> <6300> 115 | endbfrange 116 | 100 beginbfrange 117 | <6400> <64FF> <6400> 118 | <6500> <65FF> <6500> 119 | <6600> <66FF> <6600> 120 | <6700> <67FF> <6700> 121 | <6800> <68FF> <6800> 122 | <6900> <69FF> <6900> 123 | <6A00> <6AFF> <6A00> 124 | <6B00> <6BFF> <6B00> 125 | <6C00> <6CFF> <6C00> 126 | <6D00> <6DFF> <6D00> 127 | <6E00> <6EFF> <6E00> 128 | <6F00> <6FFF> <6F00> 129 | <7000> <70FF> <7000> 130 | <7100> <71FF> <7100> 131 | <7200> <72FF> <7200> 132 | <7300> <73FF> <7300> 133 | <7400> <74FF> <7400> 134 | <7500> <75FF> <7500> 135 | <7600> <76FF> <7600> 136 | <7700> <77FF> <7700> 137 | <7800> <78FF> <7800> 138 | <7900> <79FF> <7900> 139 | <7A00> <7AFF> <7A00> 140 | <7B00> <7BFF> <7B00> 141 | <7C00> <7CFF> <7C00> 142 | <7D00> <7DFF> <7D00> 143 | <7E00> <7EFF> <7E00> 144 | <7F00> <7FFF> <7F00> 145 | <8000> <80FF> <8000> 146 | <8100> <81FF> <8100> 147 | <8200> <82FF> <8200> 148 | <8300> <83FF> <8300> 149 | <8400> <84FF> <8400> 150 | <8500> <85FF> <8500> 151 | <8600> <86FF> <8600> 152 | <8700> <87FF> <8700> 153 | <8800> <88FF> <8800> 154 | <8900> <89FF> <8900> 155 | <8A00> <8AFF> <8A00> 156 | <8B00> <8BFF> <8B00> 157 | <8C00> <8CFF> <8C00> 158 | <8D00> <8DFF> <8D00> 159 | <8E00> <8EFF> <8E00> 160 | <8F00> <8FFF> <8F00> 161 | <9000> <90FF> <9000> 162 | <9100> <91FF> <9100> 163 | <9200> <92FF> <9200> 164 | <9300> <93FF> <9300> 165 | <9400> <94FF> <9400> 166 | <9500> <95FF> <9500> 167 | <9600> <96FF> <9600> 168 | <9700> <97FF> <9700> 169 | <9800> <98FF> <9800> 170 | <9900> <99FF> <9900> 171 | <9A00> <9AFF> <9A00> 172 | <9B00> <9BFF> <9B00> 173 | <9C00> <9CFF> <9C00> 174 | <9D00> <9DFF> <9D00> 175 | <9E00> <9EFF> <9E00> 176 | <9F00> <9FFF> <9F00> 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | endbfrange 218 | 56 beginbfrange 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | endbfrange 276 | endcmap 277 | CMapName currentdict /CMap defineresource pop 278 | end 279 | end -------------------------------------------------------------------------------- /djpdf/util.py: -------------------------------------------------------------------------------- 1 | # This file is part of djpdf. 2 | # 3 | # djpdf is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # djpdf is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with djpdf. If not, see . 15 | 16 | # Copyright 2015, 2017 Unrud 17 | 18 | import asyncio 19 | import contextlib 20 | import logging 21 | import os 22 | import signal 23 | import sys 24 | import warnings 25 | from subprocess import PIPE, CalledProcessError 26 | 27 | import colorama 28 | import psutil 29 | 30 | 31 | class MemoryBoundedSemaphore(): 32 | 33 | def __init__(self, value, job_memory, reserved_memory, *, loop=None): 34 | if value < 0: 35 | raise ValueError("value must be >= 0") 36 | if job_memory < 0: 37 | raise ValueError("job_memory must be >= 0") 38 | if reserved_memory < 0: 39 | raise ValueError("reserved_memory must be >= 0") 40 | self._value = self._bound_value = value 41 | self._job_memory = job_memory 42 | self._reserved_memory = reserved_memory 43 | self._waiters = [] 44 | self._pids = set() 45 | if loop is not None: 46 | self._loop = loop 47 | else: 48 | self._loop = asyncio.get_event_loop() 49 | 50 | def _wake_up_next(self, count=1): 51 | while count > 0 and self._waiters: 52 | waiter = self._waiters.pop(0) 53 | if not waiter.done(): 54 | waiter.set_result(None) 55 | count -= 1 56 | 57 | def _available_jobs(self): 58 | available_memory = psutil.virtual_memory().free 59 | available_memory -= self._reserved_memory 60 | available_memory -= self._job_memory * ( 61 | self._bound_value - self._value) 62 | for pid in self._pids: 63 | with contextlib.suppress(psutil.NoSuchProcess): 64 | process_memory = psutil.Process(pid).memory_info().rss 65 | available_memory += min(self._job_memory, process_memory) 66 | available_memory = max(0, available_memory) 67 | jobs = available_memory // self._job_memory 68 | if self._value == self._bound_value: 69 | # Allow at least one job, when low on memory 70 | jobs = max(1, jobs) 71 | return min(self._value, jobs) 72 | 73 | async def acquire(self): 74 | while self._available_jobs() == 0: 75 | waiter = self._loop.create_future() 76 | self._waiters.append(waiter) 77 | try: 78 | await waiter 79 | except BaseException: 80 | waiter.cancel() 81 | if not waiter.cancelled() and self._available_jobs() > 0: 82 | self._wake_up_next() 83 | raise 84 | self._value -= 1 85 | 86 | def release(self): 87 | if self._value >= self._bound_value: 88 | raise ValueError("Semaphore released too many times") 89 | self._value += 1 90 | self._wake_up_next(self._available_jobs()) 91 | 92 | def add_pid(self, pid): 93 | if pid in self._pids: 94 | raise ValueError("PID already exists") 95 | self._pids.add(pid) 96 | 97 | def remove_pid(self, pid): 98 | self._pids.remove(pid) 99 | 100 | async def __aenter__(self): 101 | await self.acquire() 102 | return None 103 | 104 | async def __aexit__(self, exc_type, exc, tb): 105 | self.release() 106 | 107 | 108 | class AsyncCache: 109 | _cached = None 110 | _content = None 111 | _lock = None 112 | 113 | def __init__(self): 114 | self._cached = False 115 | self._content = None 116 | self._lock = asyncio.Lock() 117 | 118 | async def get(self, content_future): 119 | async with self._lock: 120 | if not self._cached: 121 | self._content = await content_future 122 | self._cached = True 123 | if asyncio.iscoroutine(content_future): 124 | content_future.close() 125 | return self._content 126 | 127 | 128 | def format_number(f, decimal_places, percentage=False, 129 | trim_leading_zero=False): 130 | if percentage: 131 | f *= 100 132 | s = ("%%.%df" % decimal_places) % f 133 | if "." in s: 134 | s = s.rstrip("0").rstrip(".") 135 | if trim_leading_zero and "." in s: 136 | s = s.lstrip("0") 137 | if percentage: 138 | s += "%" 139 | return s 140 | 141 | 142 | async def run_command(args, process_semaphore, cwd=None): 143 | logging.debug("Running command: %s", args) 144 | env = { 145 | **os.environ, 146 | "MAGICK_THREAD_LIMIT": "1", 147 | "OMP_THREAD_LIMIT": "1" 148 | } 149 | async with process_semaphore: 150 | try: 151 | proc = await asyncio.create_subprocess_exec( 152 | *args, stdout=PIPE, stderr=PIPE, env=env, cwd=cwd) 153 | except (FileNotFoundError, PermissionError) as e: 154 | logging.error("Program not found: %s" % args[0]) 155 | raise Exception("Program not found: %s" % args[0]) from e 156 | process_semaphore.add_pid(proc.pid) 157 | try: 158 | outs, errs = await proc.communicate() 159 | finally: 160 | with contextlib.suppress(ProcessLookupError): 161 | proc.kill() 162 | process_semaphore.remove_pid(proc.pid) 163 | errs = errs.decode(sys.stderr.encoding, sys.stderr.errors) 164 | if errs: 165 | logging.debug(errs) 166 | if proc.returncode != 0: 167 | logging.error("Command '%s' returned non-zero exit status %d", 168 | args, proc.returncode) 169 | raise CalledProcessError(proc.returncode, args, None) 170 | return outs 171 | 172 | 173 | class ColorStreamHandler(logging.StreamHandler): 174 | def __init__(self, stream=None): 175 | super().__init__(stream=stream) 176 | tty = hasattr(self.stream, 'isatty') and self.stream.isatty() 177 | self.stream = colorama.AnsiToWin32( 178 | self.stream, 179 | strip=None if tty else True, 180 | autoreset=True).stream 181 | 182 | def emit(self, record): 183 | try: 184 | level = record.levelno 185 | f = b = "" 186 | if level >= logging.WARNING: 187 | f = colorama.Fore.YELLOW 188 | if level >= logging.ERROR: 189 | f = colorama.Fore.RED 190 | msg = self.format(record) 191 | stream = self.stream 192 | stream.write(f + b + msg) 193 | stream.write(self.terminator) 194 | self.flush() 195 | except Exception: 196 | self.handleError(record) 197 | 198 | 199 | def cli_setup(): 200 | # Setup signals: 201 | # Raise SystemExit when signal arrives to run cleanup code 202 | # (like destructors, try-finish etc.), otherwise the process exits 203 | # without running any of them 204 | def signal_handler(signal_number, stack_frame): 205 | sys.exit(1) 206 | signal.signal(signal.SIGTERM, signal_handler) 207 | signal.signal(signal.SIGINT, signal_handler) 208 | if sys.platform != "win32": 209 | signal.signal(signal.SIGHUP, signal_handler) 210 | 211 | # Setup logging: 212 | ch = ColorStreamHandler(sys.stderr) 213 | fmt = logging.Formatter('%(levelname)s:%(message)s') 214 | ch.setFormatter(fmt) 215 | logging.getLogger().addHandler(ch) 216 | 217 | 218 | def cli_set_verbosity(verbose): 219 | if verbose: 220 | logging.getLogger().setLevel(logging.DEBUG) 221 | warnings.simplefilter("default") 222 | else: 223 | logging.getLogger().setLevel(logging.WARNING) 224 | warnings.simplefilter("ignore") 225 | -------------------------------------------------------------------------------- /flatpak/ocr-extension.metainfo.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | {id} 4 | com.github.unrud.djpdf 5 | CC0-1.0 6 | GPL-3.0-or-later 7 | OCR {title} 8 | OCR extension for {title} ({name}) 9 | https://github.com/Unrud/djpdf/issues 10 | https://github.com/Unrud/djpdf 11 | Unrud 12 | 13 | 14 | -------------------------------------------------------------------------------- /flatpak/ocr-extensions-generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import json 5 | import re 6 | 7 | EXTENSIONS_RELPATH = 'extensions/ocr' 8 | BASE_ID = 'com.github.unrud.djpdf.OCR' 9 | 10 | 11 | def main(): 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument('extensions_json', type=argparse.FileType('r')) 14 | args = parser.parse_args() 15 | for name, d in json.load(args.extensions_json).items(): 16 | camel_name = ''.join(p[:1].upper() + p[1:] 17 | for p in re.split(r'[_/\s]+', name)) 18 | _, mode = d['t'], d['m'] 19 | if mode in ('include', 'skip'): 20 | continue 21 | if mode not in ('auto', 'no-auto'): 22 | raise ValueError(f'unsupported mode: {mode!r}') 23 | print(f'{BASE_ID}.{camel_name}:', json.dumps({ 24 | 'directory': f'{EXTENSIONS_RELPATH}/{camel_name}', 25 | 'bundle': True, 26 | 'no-autodownload': mode == 'no-auto', 27 | 'autodelete': True, 28 | })) 29 | 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /flatpak/ocr-extensions-installer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import json 5 | import re 6 | from os import makedirs, remove, walk 7 | from os.path import dirname, join 8 | from shutil import move 9 | from subprocess import run 10 | 11 | PREFIX = '/app' 12 | TESSDATA_RELPATH = 'share/tessdata' 13 | METAINFO_RELPATH = 'share/metainfo' 14 | EXTENSIONS_RELPATH = 'extensions/ocr' 15 | BASE_ID = 'com.github.unrud.djpdf.OCR' 16 | 17 | 18 | def main(): 19 | parser = argparse.ArgumentParser() 20 | parser.add_argument('extensions_json', type=argparse.FileType('r')) 21 | parser.add_argument('metainfo_template', type=argparse.FileType('r')) 22 | parser.add_argument('src_dir') 23 | args = parser.parse_args() 24 | metainfo_tpl = args.metainfo_template.read() 25 | for name, d in json.load(args.extensions_json).items(): 26 | camel_name = ''.join(p[:1].upper() + p[1:] 27 | for p in re.split(r'[_/\s]+', name)) 28 | title, mode = d['t'], d['m'] 29 | src_path = join(args.src_dir, name + '.traineddata') 30 | if mode == 'include': 31 | dst_path = join(PREFIX, TESSDATA_RELPATH, name + '.traineddata') 32 | makedirs(dirname(dst_path), exist_ok=True) 33 | move(src_path, dst_path) 34 | continue 35 | if mode == 'skip': 36 | remove(src_path) 37 | continue 38 | if mode not in ('auto', 'no-auto'): 39 | raise ValueError(f'unsupported mode: {mode!r}') 40 | id_ = f'{BASE_ID}.{camel_name}' 41 | base_path = join(PREFIX, EXTENSIONS_RELPATH, camel_name) 42 | meta_path = join(base_path, METAINFO_RELPATH, id_ + '.metainfo.xml') 43 | makedirs(dirname(meta_path), exist_ok=True) 44 | with open(meta_path, 'w') as f: 45 | f.write(metainfo_tpl.format(id=id_, name=name, title=title)) 46 | run(['appstreamcli', 'compose', f'--components={id_}', '--prefix=/', 47 | f'--origin={id_}', f'--result-root={base_path}', 48 | f'--data-dir={join(base_path, 'share/app-info/xmls')}', 49 | base_path], check=True) 50 | dst_path = join(base_path, TESSDATA_RELPATH, name + '.traineddata') 51 | makedirs(dirname(dst_path), exist_ok=True) 52 | move(src_path, dst_path) 53 | for root, _, files in walk(args.src_dir): 54 | for name in files: 55 | if name.endswith('.traineddata'): 56 | raise RuntimeError(f'Missing entry: {join(root, name)!r}') 57 | 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /flatpak/ocr-extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "afr": {"t": "Afrikaans", "m": "no-auto"}, 3 | "amh": {"t": "Amharic", "m": "no-auto"}, 4 | "ara": {"t": "Arabic", "m": "auto"}, 5 | "asm": {"t": "Assamese", "m": "no-auto"}, 6 | "aze": {"t": "Azerbaijani", "m": "no-auto"}, 7 | "aze_cyrl": {"t": "Azerbaijani - Cyrilic", "m": "no-auto"}, 8 | "bel": {"t": "Belarusian", "m": "no-auto"}, 9 | "ben": {"t": "Bengali", "m": "no-auto"}, 10 | "bod": {"t": "Tibetan", "m": "no-auto"}, 11 | "bos": {"t": "Bosnian", "m": "no-auto"}, 12 | "bre": {"t": "Breton", "m": "no-auto"}, 13 | "bul": {"t": "Bulgarian", "m": "no-auto"}, 14 | "cat": {"t": "Catalan; Valencian", "m": "no-auto"}, 15 | "ceb": {"t": "Cebuano", "m": "no-auto"}, 16 | "ces": {"t": "Czech", "m": "no-auto"}, 17 | "chi_sim": {"t": "Chinese - Simplified", "m": "auto"}, 18 | "chi_sim_vert": {"t": "Chinese - Simplified (vertical)", "m": "no-auto"}, 19 | "chi_tra": {"t": "Chinese - Traditional", "m": "no-auto"}, 20 | "chi_tra_vert": {"t": "Chinese - Traditional (vertical)", "m": "no-auto"}, 21 | "chr": {"t": "Cherokee", "m": "no-auto"}, 22 | "cos": {"t": "Corsican", "m": "no-auto"}, 23 | "cym": {"t": "Welsh", "m": "no-auto"}, 24 | "dan": {"t": "Danish", "m": "no-auto"}, 25 | "dan_frak": {"t": "Danish - Fraktur", "m": "no-auto"}, 26 | "deu": {"t": "German", "m": "auto"}, 27 | "deu_frak": {"t": "German - Fraktur", "m": "no-auto"}, 28 | "div": {"t": "Divehi", "m": "no-auto"}, 29 | "dzo": {"t": "Dzongkha", "m": "no-auto"}, 30 | "ell": {"t": "Greek, Modern (1453-)", "m": "no-auto"}, 31 | "eng": {"t": "English", "m": "include"}, 32 | "enm": {"t": "English, Middle (1100-1500)", "m": "no-auto"}, 33 | "epo": {"t": "Esperanto", "m": "no-auto"}, 34 | "est": {"t": "Estonian", "m": "no-auto"}, 35 | "eus": {"t": "Basque", "m": "no-auto"}, 36 | "fao": {"t": "Faroese", "m": "no-auto"}, 37 | "fas": {"t": "Persian", "m": "no-auto"}, 38 | "fil": {"t": "Filipino (old - Tagalog)", "m": "no-auto"}, 39 | "fin": {"t": "Finnish", "m": "no-auto"}, 40 | "fra": {"t": "French", "m": "auto"}, 41 | "frk": {"t": "German - Fraktur", "m": "no-auto"}, 42 | "frm": {"t": "French, Middle (ca.1400-1600)", "m": "no-auto"}, 43 | "fry": {"t": "Western Frisian", "m": "no-auto"}, 44 | "gla": {"t": "Scottish Gaelic", "m": "no-auto"}, 45 | "gle": {"t": "Irish", "m": "no-auto"}, 46 | "glg": {"t": "Galician", "m": "no-auto"}, 47 | "grc": {"t": "Greek, Ancient (to 1453)", "m": "no-auto"}, 48 | "guj": {"t": "Gujarati", "m": "no-auto"}, 49 | "hat": {"t": "Haitian; Haitian Creole", "m": "no-auto"}, 50 | "heb": {"t": "Hebrew", "m": "no-auto"}, 51 | "hin": {"t": "Hindi", "m": "auto"}, 52 | "hrv": {"t": "Croatian", "m": "no-auto"}, 53 | "hun": {"t": "Hungarian", "m": "no-auto"}, 54 | "hye": {"t": "Armenian", "m": "no-auto"}, 55 | "iku": {"t": "Inuktitut", "m": "no-auto"}, 56 | "ind": {"t": "Indonesian", "m": "no-auto"}, 57 | "isl": {"t": "Icelandic", "m": "no-auto"}, 58 | "ita": {"t": "Italian", "m": "no-auto"}, 59 | "ita_old": {"t": "Italian - Old", "m": "no-auto"}, 60 | "jav": {"t": "Javanese", "m": "no-auto"}, 61 | "jpn": {"t": "Japanese", "m": "auto"}, 62 | "jpn_vert": {"t": "Japanese (vertical)", "m": "no-auto"}, 63 | "kan": {"t": "Kannada", "m": "no-auto"}, 64 | "kat": {"t": "Georgian", "m": "no-auto"}, 65 | "kat_old": {"t": "Georgian - Old", "m": "no-auto"}, 66 | "kaz": {"t": "Kazakh", "m": "no-auto"}, 67 | "khm": {"t": "Central Khmer", "m": "no-auto"}, 68 | "kir": {"t": "Kirghiz; Kyrgyz", "m": "no-auto"}, 69 | "kmr": {"t": "Kurmanji (Kurdish - Latin Script)", "m": "no-auto"}, 70 | "kor": {"t": "Korean", "m": "no-auto"}, 71 | "kor_vert": {"t": "Korean (vertical)", "m": "no-auto"}, 72 | "lao": {"t": "Lao", "m": "no-auto"}, 73 | "lat": {"t": "Latin", "m": "no-auto"}, 74 | "lav": {"t": "Latvian", "m": "no-auto"}, 75 | "lit": {"t": "Lithuanian", "m": "no-auto"}, 76 | "ltz": {"t": "Luxembourgish", "m": "no-auto"}, 77 | "mal": {"t": "Malayalam", "m": "no-auto"}, 78 | "mar": {"t": "Marathi", "m": "no-auto"}, 79 | "mkd": {"t": "Macedonian", "m": "no-auto"}, 80 | "mlt": {"t": "Maltese", "m": "no-auto"}, 81 | "mon": {"t": "Mongolian", "m": "no-auto"}, 82 | "mri": {"t": "Maori", "m": "no-auto"}, 83 | "msa": {"t": "Malay", "m": "no-auto"}, 84 | "mya": {"t": "Burmese", "m": "no-auto"}, 85 | "nep": {"t": "Nepali", "m": "no-auto"}, 86 | "nld": {"t": "Dutch; Flemish", "m": "no-auto"}, 87 | "nor": {"t": "Norwegian", "m": "no-auto"}, 88 | "oci": {"t": "Occitan (post 1500)", "m": "no-auto"}, 89 | "ori": {"t": "Oriya", "m": "no-auto"}, 90 | "pan": {"t": "Panjabi; Punjabi", "m": "no-auto"}, 91 | "pol": {"t": "Polish", "m": "no-auto"}, 92 | "por": {"t": "Portuguese", "m": "auto"}, 93 | "pus": {"t": "Pushto; Pashto", "m": "no-auto"}, 94 | "que": {"t": "Quechua", "m": "no-auto"}, 95 | "ron": {"t": "Romanian; Moldavian; Moldovan", "m": "no-auto"}, 96 | "rus": {"t": "Russian", "m": "auto"}, 97 | "san": {"t": "Sanskrit", "m": "no-auto"}, 98 | "sin": {"t": "Sinhala; Sinhalese", "m": "no-auto"}, 99 | "slk": {"t": "Slovak", "m": "no-auto"}, 100 | "slk_frak": {"t": "Slovak - Fraktur", "m": "no-auto"}, 101 | "slv": {"t": "Slovenian", "m": "no-auto"}, 102 | "snd": {"t": "Sindhi", "m": "no-auto"}, 103 | "spa": {"t": "Spanish; Castilian", "m": "auto"}, 104 | "spa_old": {"t": "Spanish; Castilian - Old", "m": "no-auto"}, 105 | "sqi": {"t": "Albanian", "m": "no-auto"}, 106 | "srp": {"t": "Serbian", "m": "no-auto"}, 107 | "srp_latn": {"t": "Serbian - Latin", "m": "no-auto"}, 108 | "sun": {"t": "Sundanese", "m": "no-auto"}, 109 | "swa": {"t": "Swahili", "m": "no-auto"}, 110 | "swe": {"t": "Swedish", "m": "no-auto"}, 111 | "syr": {"t": "Syriac", "m": "no-auto"}, 112 | "tam": {"t": "Tamil", "m": "no-auto"}, 113 | "tat": {"t": "Tatar", "m": "no-auto"}, 114 | "tel": {"t": "Telugu", "m": "no-auto"}, 115 | "tgk": {"t": "Tajik", "m": "no-auto"}, 116 | "tgl": {"t": "Tagalog", "m": "no-auto"}, 117 | "tha": {"t": "Thai", "m": "no-auto"}, 118 | "tir": {"t": "Tigrinya", "m": "no-auto"}, 119 | "ton": {"t": "Tonga", "m": "no-auto"}, 120 | "tur": {"t": "Turkish", "m": "no-auto"}, 121 | "uig": {"t": "Uighur; Uyghur", "m": "no-auto"}, 122 | "ukr": {"t": "Ukrainian", "m": "no-auto"}, 123 | "urd": {"t": "Urdu", "m": "no-auto"}, 124 | "uzb": {"t": "Uzbek", "m": "no-auto"}, 125 | "uzb_cyrl": {"t": "Uzbek - Cyrilic", "m": "no-auto"}, 126 | "vie": {"t": "Vietnamese", "m": "no-auto"}, 127 | "yid": {"t": "Yiddish", "m": "no-auto"}, 128 | "yor": {"t": "Yoruba", "m": "no-auto"}, 129 | "script/Arabic": {"t": "Arabic script", "m": "no-auto"}, 130 | "script/Armenian": {"t": "Armenian script", "m": "no-auto"}, 131 | "script/Bengali": {"t": "Bengali script", "m": "no-auto"}, 132 | "script/Canadian_Aboriginal": {"t": "Canadian Aboriginal script", "m": "no-auto"}, 133 | "script/Cherokee": {"t": "Cherokee script", "m": "no-auto"}, 134 | "script/Cyrillic": {"t": "Cyrillic script", "m": "no-auto"}, 135 | "script/Devanagari": {"t": "Devanagari script", "m": "no-auto"}, 136 | "script/Ethiopic": {"t": "Ethiopic script", "m": "no-auto"}, 137 | "script/Fraktur": {"t": "Fraktur script", "m": "no-auto"}, 138 | "script/Georgian": {"t": "Georgian script", "m": "no-auto"}, 139 | "script/Greek": {"t": "Greek script", "m": "no-auto"}, 140 | "script/Gujarati": {"t": "Gujarati script", "m": "no-auto"}, 141 | "script/Gurmukhi": {"t": "Gurmukhi script", "m": "no-auto"}, 142 | "script/HanS": {"t": "Han simplified script", "m": "no-auto"}, 143 | "script/HanS_vert": {"t": "Han simplified vertical script", "m": "no-auto"}, 144 | "script/HanT": {"t": "Han traditional script", "m": "no-auto"}, 145 | "script/HanT_vert": {"t": "Han traditional vertical script", "m": "no-auto"}, 146 | "script/Hangul": {"t": "Hangul script", "m": "no-auto"}, 147 | "script/Hangul_vert": {"t": "Hangul vertical script", "m": "no-auto"}, 148 | "script/Hebrew": {"t": "Hebrew script", "m": "no-auto"}, 149 | "script/Japanese": {"t": "Japanese script", "m": "no-auto"}, 150 | "script/Japanese_vert": {"t": "Japanese vertical script", "m": "no-auto"}, 151 | "script/Kannada": {"t": "Kannada script", "m": "no-auto"}, 152 | "script/Khmer": {"t": "Khmer script", "m": "no-auto"}, 153 | "script/Lao": {"t": "Lao script", "m": "no-auto"}, 154 | "script/Latin": {"t": "Latin script", "m": "no-auto"}, 155 | "script/Malayalam": {"t": "Malayalam script", "m": "no-auto"}, 156 | "script/Myanmar": {"t": "Myanmar script", "m": "no-auto"}, 157 | "script/Oriya": {"t": "Oriya (Odia) script", "m": "no-auto"}, 158 | "script/Sinhala": {"t": "Sinhala script", "m": "no-auto"}, 159 | "script/Syriac": {"t": "Syriac script", "m": "no-auto"}, 160 | "script/Tamil": {"t": "Tamil script", "m": "no-auto"}, 161 | "script/Telugu": {"t": "Telugu script", "m": "no-auto"}, 162 | "script/Thaana": {"t": "Thaana script", "m": "no-auto"}, 163 | "script/Thai": {"t": "Thai script", "m": "no-auto"}, 164 | "script/Tibetan": {"t": "Tibetan script", "m": "no-auto"}, 165 | "script/Vietnamese": {"t": "Vietnamese script", "m": "no-auto"}, 166 | "equ": {"t": "Math / equation detection", "m": "no-auto"}, 167 | "osd": {"t": "Orientation and script detection module", "m": "include"} 168 | } 169 | -------------------------------------------------------------------------------- /flatpak/tesseract-wrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import subprocess 5 | import sys 6 | import tempfile 7 | 8 | PREFIX = '/app' 9 | TESSERACT_RELPATH = 'bin/tesseract.real' 10 | TESSDATA_RELPATH = 'share/tessdata' 11 | EXTENSIONS_RELPATH = 'extensions/ocr' 12 | 13 | 14 | def merge_directories(target, sources): 15 | for source in sources: 16 | for dirpath, dirnames, filenames in os.walk(source): 17 | rel_dirpath = os.path.relpath(dirpath, start=source) 18 | for name in dirnames: 19 | os.makedirs(os.path.join(target, rel_dirpath, name), 20 | exist_ok=True) 21 | for name in filenames: 22 | os.symlink(os.path.join(dirpath, name), 23 | os.path.join(target, rel_dirpath, name)) 24 | 25 | 26 | def exec_tesseract(tessdata_path=None): 27 | env = os.environ.copy() 28 | if tessdata_path is not None: 29 | env['TESSDATA_PREFIX'] = tessdata_path 30 | tesseract = os.path.join(PREFIX, TESSERACT_RELPATH) 31 | exit(subprocess.run(sys.argv, executable=tesseract, env=env).returncode) 32 | 33 | 34 | def main(): 35 | if 'TESSDATA_PREFIX' in os.environ: 36 | exec_tesseract() 37 | tessdata_paths = [os.path.join(PREFIX, TESSDATA_RELPATH)] 38 | for entry in os.scandir(os.path.join(PREFIX, EXTENSIONS_RELPATH)): 39 | tessdata_paths.append(os.path.join(entry.path, TESSDATA_RELPATH)) 40 | with tempfile.TemporaryDirectory(prefix='tessdata-') as tempdir: 41 | merge_directories(tempdir, tessdata_paths) 42 | exec_tesseract(tempdir) 43 | 44 | 45 | if __name__ == '__main__': 46 | main() 47 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('djpdfgui', 2 | version: '0.5.10', 3 | meson_version: '>= 0.62.0', 4 | default_options: ['warning_level=2', 'werror=false'], 5 | ) 6 | 7 | i18n = import('i18n') 8 | python = import('python').find_installation('python3') 9 | merge_xml_aux = meson.current_source_dir() / 'build-aux' / 'meson' / 'merge-xml.py' 10 | move_aux = meson.current_source_dir() / 'build-aux' / 'meson' / 'move.py' 11 | 12 | subdir('desktop') 13 | subdir('scans2pdf_gui') 14 | subdir('po') 15 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | nl 2 | hu 3 | nb_NO 4 | de 5 | sk 6 | et 7 | es 8 | ta 9 | ru 10 | -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | desktop/com.github.unrud.djpdf.desktop.in 2 | desktop/com.github.unrud.djpdf.metainfo.xml.in 3 | scans2pdf_gui/main.py 4 | scans2pdf_gui/qml/detail.qml 5 | scans2pdf_gui/qml/main.qml 6 | scans2pdf_gui/qml/overview.qml 7 | -------------------------------------------------------------------------------- /po/de.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the djpdfgui package. 4 | # Unrud , 2023. 5 | # ssantos , 2024. 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: djpdfgui\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-07-17 03:33+0200\n" 11 | "PO-Revision-Date: 2024-10-20 19:15+0000\n" 12 | "Last-Translator: ssantos \n" 13 | "Language-Team: German \n" 15 | "Language: de\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 20 | "X-Generator: Weblate 5.8-rc\n" 21 | 22 | #: desktop/com.github.unrud.djpdf.desktop.in:4 23 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:7 24 | #: scans2pdf_gui/qml/main.qml:29 25 | msgid "Scans to PDF" 26 | msgstr "Scans zu PDF" 27 | 28 | #: desktop/com.github.unrud.djpdf.desktop.in:5 29 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:8 30 | msgid "Create small, searchable PDFs from scanned documents" 31 | msgstr "Erstelle kleine, durchsuchbare PDFs aus gescannten Dokumenten" 32 | 33 | #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 34 | #: desktop/com.github.unrud.djpdf.desktop.in:11 35 | msgid "djpdf;OCR;convert;PDF;image;" 36 | msgstr "djpdf;OCR;konvertieren;PDF;Bild;" 37 | 38 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:10 39 | msgid "" 40 | "Create small, searchable PDFs from scanned documents. The program divides " 41 | "images into bitonal foreground images (text) and a color background image, " 42 | "then compresses them separately. An invisible OCR text layer is added, " 43 | "making the PDF searchable." 44 | msgstr "" 45 | "Erstelle kleine, durchsuchbare PDFs aus gescannten Dokumenten. Das Programm " 46 | "teilt Bilder in einfarbige Vordergrundbilder (Text) und ein farbiges " 47 | "Hintergrundbild auf und komprimiert sie dann separat. Eine unsichtbare OCR-" 48 | "Textebene wird hinzugefügt, um das PDF durchsuchbar zu machen." 49 | 50 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:16 51 | msgid "" 52 | "Color and grayscale scans need some preparation for good results. " 53 | "Recommended tools are Scan Tailor or GIMP." 54 | msgstr "" 55 | "Farb- und Graustufen-Scans müssen für gute Ergebnisse vorbereitet werden. " 56 | "Empfohlene Werkzeuge dafür sind Scan Tailor oder GIMP." 57 | 58 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:20 59 | msgid "A GUI and command line interface are included." 60 | msgstr "" 61 | "Eine grafische Oberfläche und ein Kommandozeilen-Werkzeug sind enthalten." 62 | 63 | #: scans2pdf_gui/qml/detail.qml:43 scans2pdf_gui/qml/detail.qml:253 64 | #: scans2pdf_gui/qml/detail.qml:392 65 | msgid "Remove" 66 | msgstr "Entfernen" 67 | 68 | #: scans2pdf_gui/qml/detail.qml:66 69 | msgid "Please choose a color" 70 | msgstr "Wähle eine Farbe" 71 | 72 | #: scans2pdf_gui/qml/detail.qml:98 73 | msgid "DPI:" 74 | msgstr "DPI:" 75 | 76 | #: scans2pdf_gui/qml/detail.qml:103 77 | msgid "auto" 78 | msgstr "auto" 79 | 80 | #: scans2pdf_gui/qml/detail.qml:113 81 | msgid "Background color:" 82 | msgstr "Hintergrundfarbe:" 83 | 84 | #: scans2pdf_gui/qml/detail.qml:138 85 | msgid "Background" 86 | msgstr "Hintergrund" 87 | 88 | #: scans2pdf_gui/qml/detail.qml:155 89 | msgid "Resize:" 90 | msgstr "Größe ändern:" 91 | 92 | #: scans2pdf_gui/qml/detail.qml:172 scans2pdf_gui/qml/detail.qml:269 93 | msgid "Compression:" 94 | msgstr "Komprimierung:" 95 | 96 | #: scans2pdf_gui/qml/detail.qml:193 97 | msgid "Quality:" 98 | msgstr "Qualität:" 99 | 100 | #: scans2pdf_gui/qml/detail.qml:212 101 | msgid "Foreground" 102 | msgstr "Vordergrund" 103 | 104 | #: scans2pdf_gui/qml/detail.qml:229 scans2pdf_gui/qml/detail.qml:369 105 | msgid "Colors:" 106 | msgstr "Farben:" 107 | 108 | #: scans2pdf_gui/qml/detail.qml:260 109 | msgid "No colors" 110 | msgstr "Keine Farben" 111 | 112 | #: scans2pdf_gui/qml/detail.qml:260 scans2pdf_gui/qml/detail.qml:399 113 | msgid "Add" 114 | msgstr "Hinzufügen" 115 | 116 | #: scans2pdf_gui/qml/detail.qml:290 117 | msgid "JBIG2 Threshold:" 118 | msgstr "JBIG2 Schwellenwert:" 119 | 120 | #: scans2pdf_gui/qml/detail.qml:314 121 | msgid "Warning" 122 | msgstr "Warnung" 123 | 124 | #: scans2pdf_gui/qml/detail.qml:319 125 | msgid "" 126 | "Lossy JBIG2 compression can alter text in a way that is not noticeable as " 127 | "corruption (e.g. the numbers '6' and '8' get replaced)" 128 | msgstr "" 129 | "Verlustbehaftete JBIG2-Komprimierung kann Text auf eine Art verändern, die " 130 | "nicht als Verfälschung erkennbar ist (z.B. die Zahlen '6' und '8' werden " 131 | "ersetzt)" 132 | 133 | #: scans2pdf_gui/qml/detail.qml:329 134 | msgid "OCR" 135 | msgstr "OCR" 136 | 137 | #: scans2pdf_gui/qml/detail.qml:348 138 | msgid "Language" 139 | msgstr "Sprache" 140 | 141 | #: scans2pdf_gui/qml/detail.qml:399 142 | msgid "All colors" 143 | msgstr "Alle Farben" 144 | 145 | #: scans2pdf_gui/qml/detail.qml:410 146 | msgid "Apply to all" 147 | msgstr "Auf alle übernehmen" 148 | 149 | #: scans2pdf_gui/qml/detail.qml:416 150 | msgid "Apply to following" 151 | msgstr "Auf folgende übernehmen" 152 | 153 | #: scans2pdf_gui/qml/detail.qml:424 154 | msgid "Load default settings" 155 | msgstr "Standard-Einstellungen laden" 156 | 157 | #: scans2pdf_gui/qml/detail.qml:430 158 | msgid "Overwrite?" 159 | msgstr "Überschreiben?" 160 | 161 | #: scans2pdf_gui/qml/detail.qml:431 162 | msgid "Replace default settings?" 163 | msgstr "Standard-Einstellungen ersetzen?" 164 | 165 | #: scans2pdf_gui/qml/detail.qml:437 166 | msgid "Save default settings" 167 | msgstr "Standard-Einstellungen speichern" 168 | 169 | #: scans2pdf_gui/qml/overview.qml:30 170 | msgid "Open" 171 | msgstr "Öffnen" 172 | 173 | #: scans2pdf_gui/qml/overview.qml:32 174 | msgid "Images" 175 | msgstr "Bilder" 176 | 177 | #: scans2pdf_gui/qml/overview.qml:33 178 | msgid "All files" 179 | msgstr "Alle Dateien" 180 | 181 | #: scans2pdf_gui/qml/overview.qml:42 182 | msgid "Save" 183 | msgstr "Speichern" 184 | 185 | #: scans2pdf_gui/qml/overview.qml:43 186 | msgid "PDF" 187 | msgstr "PDF" 188 | 189 | #: scans2pdf_gui/qml/overview.qml:75 190 | msgid "Failed to create PDF" 191 | msgstr "Erstellung von PDF fehlgeschlagen" 192 | 193 | #: scans2pdf_gui/qml/overview.qml:91 194 | msgid "Close" 195 | msgstr "Schließen" 196 | 197 | #: scans2pdf_gui/qml/overview.qml:108 198 | msgid "Saving…" 199 | msgstr "Speichern…" 200 | 201 | #: scans2pdf_gui/qml/overview.qml:121 202 | msgid "Cancel" 203 | msgstr "Abbrechen" 204 | 205 | #: scans2pdf_gui/qml/overview.qml:140 206 | msgid "Create" 207 | msgstr "Erstellen" 208 | -------------------------------------------------------------------------------- /po/djpdfgui.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the djpdfgui package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: djpdfgui\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-07-17 03:33+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: desktop/com.github.unrud.djpdf.desktop.in:4 21 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:7 22 | #: scans2pdf_gui/qml/main.qml:29 23 | msgid "Scans to PDF" 24 | msgstr "" 25 | 26 | #: desktop/com.github.unrud.djpdf.desktop.in:5 27 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:8 28 | msgid "Create small, searchable PDFs from scanned documents" 29 | msgstr "" 30 | 31 | #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 32 | #: desktop/com.github.unrud.djpdf.desktop.in:11 33 | msgid "djpdf;OCR;convert;PDF;image;" 34 | msgstr "" 35 | 36 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:10 37 | msgid "" 38 | "Create small, searchable PDFs from scanned documents. The program divides " 39 | "images into bitonal foreground images (text) and a color background image, " 40 | "then compresses them separately. An invisible OCR text layer is added, " 41 | "making the PDF searchable." 42 | msgstr "" 43 | 44 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:16 45 | msgid "" 46 | "Color and grayscale scans need some preparation for good results. " 47 | "Recommended tools are Scan Tailor or GIMP." 48 | msgstr "" 49 | 50 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:20 51 | msgid "A GUI and command line interface are included." 52 | msgstr "" 53 | 54 | #: scans2pdf_gui/qml/detail.qml:43 scans2pdf_gui/qml/detail.qml:253 55 | #: scans2pdf_gui/qml/detail.qml:392 56 | msgid "Remove" 57 | msgstr "" 58 | 59 | #: scans2pdf_gui/qml/detail.qml:66 60 | msgid "Please choose a color" 61 | msgstr "" 62 | 63 | #: scans2pdf_gui/qml/detail.qml:98 64 | msgid "DPI:" 65 | msgstr "" 66 | 67 | #: scans2pdf_gui/qml/detail.qml:103 68 | msgid "auto" 69 | msgstr "" 70 | 71 | #: scans2pdf_gui/qml/detail.qml:113 72 | msgid "Background color:" 73 | msgstr "" 74 | 75 | #: scans2pdf_gui/qml/detail.qml:138 76 | msgid "Background" 77 | msgstr "" 78 | 79 | #: scans2pdf_gui/qml/detail.qml:155 80 | msgid "Resize:" 81 | msgstr "" 82 | 83 | #: scans2pdf_gui/qml/detail.qml:172 scans2pdf_gui/qml/detail.qml:269 84 | msgid "Compression:" 85 | msgstr "" 86 | 87 | #: scans2pdf_gui/qml/detail.qml:193 88 | msgid "Quality:" 89 | msgstr "" 90 | 91 | #: scans2pdf_gui/qml/detail.qml:212 92 | msgid "Foreground" 93 | msgstr "" 94 | 95 | #: scans2pdf_gui/qml/detail.qml:229 scans2pdf_gui/qml/detail.qml:369 96 | msgid "Colors:" 97 | msgstr "" 98 | 99 | #: scans2pdf_gui/qml/detail.qml:260 100 | msgid "No colors" 101 | msgstr "" 102 | 103 | #: scans2pdf_gui/qml/detail.qml:260 scans2pdf_gui/qml/detail.qml:399 104 | msgid "Add" 105 | msgstr "" 106 | 107 | #: scans2pdf_gui/qml/detail.qml:290 108 | msgid "JBIG2 Threshold:" 109 | msgstr "" 110 | 111 | #: scans2pdf_gui/qml/detail.qml:314 112 | msgid "Warning" 113 | msgstr "" 114 | 115 | #: scans2pdf_gui/qml/detail.qml:319 116 | msgid "" 117 | "Lossy JBIG2 compression can alter text in a way that is not noticeable as " 118 | "corruption (e.g. the numbers '6' and '8' get replaced)" 119 | msgstr "" 120 | 121 | #: scans2pdf_gui/qml/detail.qml:329 122 | msgid "OCR" 123 | msgstr "" 124 | 125 | #: scans2pdf_gui/qml/detail.qml:348 126 | msgid "Language" 127 | msgstr "" 128 | 129 | #: scans2pdf_gui/qml/detail.qml:399 130 | msgid "All colors" 131 | msgstr "" 132 | 133 | #: scans2pdf_gui/qml/detail.qml:410 134 | msgid "Apply to all" 135 | msgstr "" 136 | 137 | #: scans2pdf_gui/qml/detail.qml:416 138 | msgid "Apply to following" 139 | msgstr "" 140 | 141 | #: scans2pdf_gui/qml/detail.qml:424 142 | msgid "Load default settings" 143 | msgstr "" 144 | 145 | #: scans2pdf_gui/qml/detail.qml:430 146 | msgid "Overwrite?" 147 | msgstr "" 148 | 149 | #: scans2pdf_gui/qml/detail.qml:431 150 | msgid "Replace default settings?" 151 | msgstr "" 152 | 153 | #: scans2pdf_gui/qml/detail.qml:437 154 | msgid "Save default settings" 155 | msgstr "" 156 | 157 | #: scans2pdf_gui/qml/overview.qml:30 158 | msgid "Open" 159 | msgstr "" 160 | 161 | #: scans2pdf_gui/qml/overview.qml:32 162 | msgid "Images" 163 | msgstr "" 164 | 165 | #: scans2pdf_gui/qml/overview.qml:33 166 | msgid "All files" 167 | msgstr "" 168 | 169 | #: scans2pdf_gui/qml/overview.qml:42 170 | msgid "Save" 171 | msgstr "" 172 | 173 | #: scans2pdf_gui/qml/overview.qml:43 174 | msgid "PDF" 175 | msgstr "" 176 | 177 | #: scans2pdf_gui/qml/overview.qml:75 178 | msgid "Failed to create PDF" 179 | msgstr "" 180 | 181 | #: scans2pdf_gui/qml/overview.qml:91 182 | msgid "Close" 183 | msgstr "" 184 | 185 | #: scans2pdf_gui/qml/overview.qml:108 186 | msgid "Saving…" 187 | msgstr "" 188 | 189 | #: scans2pdf_gui/qml/overview.qml:121 190 | msgid "Cancel" 191 | msgstr "" 192 | 193 | #: scans2pdf_gui/qml/overview.qml:140 194 | msgid "Create" 195 | msgstr "" 196 | -------------------------------------------------------------------------------- /po/es.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the djpdfgui package. 4 | # José Luis Figueroa , 2024. 5 | # gallegonovato , 2024. 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: djpdfgui\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-07-17 03:33+0200\n" 11 | "PO-Revision-Date: 2024-08-08 13:09+0000\n" 12 | "Last-Translator: gallegonovato \n" 13 | "Language-Team: Spanish \n" 15 | "Language: es\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 20 | "X-Generator: Weblate 5.7-dev\n" 21 | 22 | #: desktop/com.github.unrud.djpdf.desktop.in:4 23 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:7 24 | #: scans2pdf_gui/qml/main.qml:29 25 | msgid "Scans to PDF" 26 | msgstr "Escanear a un PDF" 27 | 28 | #: desktop/com.github.unrud.djpdf.desktop.in:5 29 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:8 30 | msgid "Create small, searchable PDFs from scanned documents" 31 | msgstr "" 32 | "Crea pequeños archivos PDF a partir de documentos escaneados donde puedas " 33 | "buscar texto" 34 | 35 | #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 36 | #: desktop/com.github.unrud.djpdf.desktop.in:11 37 | msgid "djpdf;OCR;convert;PDF;image;" 38 | msgstr "djpdf;OCR;convertir;PDF;imagen;" 39 | 40 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:10 41 | msgid "" 42 | "Create small, searchable PDFs from scanned documents. The program divides " 43 | "images into bitonal foreground images (text) and a color background image, " 44 | "then compresses them separately. An invisible OCR text layer is added, " 45 | "making the PDF searchable." 46 | msgstr "" 47 | "Crea archivos PDF pequeños y con capacidad para buscar texto en los " 48 | "documentos escaneados. El programa divide las imágenes en imágenes bitonales " 49 | "de primer plano (texto) y una imagen de fondo en color, y luego las comprime " 50 | "por separado. Se agrega una capa de texto OCR invisible, lo que permite " 51 | "realizar búsquedas en el PDF." 52 | 53 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:16 54 | msgid "" 55 | "Color and grayscale scans need some preparation for good results. " 56 | "Recommended tools are Scan Tailor or GIMP." 57 | msgstr "" 58 | "Para obtener buenos resultados, los archivos escaneados en color y escala de " 59 | "grises deben prepararse adecuadamente. Para ello, recomendamos Scan Tailor o " 60 | "GIMP." 61 | 62 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:20 63 | msgid "A GUI and command line interface are included." 64 | msgstr "" 65 | "Se incluye una interfaz gráfica de usuario y una interfaz de línea de " 66 | "comandos." 67 | 68 | #: scans2pdf_gui/qml/detail.qml:43 scans2pdf_gui/qml/detail.qml:253 69 | #: scans2pdf_gui/qml/detail.qml:392 70 | msgid "Remove" 71 | msgstr "Remover" 72 | 73 | #: scans2pdf_gui/qml/detail.qml:66 74 | msgid "Please choose a color" 75 | msgstr "Por favor, selecciona un color" 76 | 77 | #: scans2pdf_gui/qml/detail.qml:98 78 | msgid "DPI:" 79 | msgstr "Puntos por pulgada:" 80 | 81 | #: scans2pdf_gui/qml/detail.qml:103 82 | msgid "auto" 83 | msgstr "automático" 84 | 85 | #: scans2pdf_gui/qml/detail.qml:113 86 | msgid "Background color:" 87 | msgstr "Color del fondo:" 88 | 89 | #: scans2pdf_gui/qml/detail.qml:138 90 | msgid "Background" 91 | msgstr "Fondo" 92 | 93 | #: scans2pdf_gui/qml/detail.qml:155 94 | msgid "Resize:" 95 | msgstr "Cambiar tamaño:" 96 | 97 | #: scans2pdf_gui/qml/detail.qml:172 scans2pdf_gui/qml/detail.qml:269 98 | msgid "Compression:" 99 | msgstr "Compresión:" 100 | 101 | #: scans2pdf_gui/qml/detail.qml:193 102 | msgid "Quality:" 103 | msgstr "Calidad:" 104 | 105 | #: scans2pdf_gui/qml/detail.qml:212 106 | msgid "Foreground" 107 | msgstr "Primer plano" 108 | 109 | #: scans2pdf_gui/qml/detail.qml:229 scans2pdf_gui/qml/detail.qml:369 110 | msgid "Colors:" 111 | msgstr "Colores:" 112 | 113 | #: scans2pdf_gui/qml/detail.qml:260 114 | msgid "No colors" 115 | msgstr "Sin colores" 116 | 117 | #: scans2pdf_gui/qml/detail.qml:260 scans2pdf_gui/qml/detail.qml:399 118 | msgid "Add" 119 | msgstr "Añadir" 120 | 121 | #: scans2pdf_gui/qml/detail.qml:290 122 | msgid "JBIG2 Threshold:" 123 | msgstr "Umbral JBIG2:" 124 | 125 | #: scans2pdf_gui/qml/detail.qml:314 126 | msgid "Warning" 127 | msgstr "Advertencia" 128 | 129 | #: scans2pdf_gui/qml/detail.qml:319 130 | msgid "" 131 | "Lossy JBIG2 compression can alter text in a way that is not noticeable as " 132 | "corruption (e.g. the numbers '6' and '8' get replaced)" 133 | msgstr "" 134 | "Si se utiliza compresión con pérdida JBIG2, el texto puede cambiar para que " 135 | "los errores no se noten fácilmente (por ejemplo, se pueden reemplazar los " 136 | "números \"6\" y \"8\")" 137 | 138 | #: scans2pdf_gui/qml/detail.qml:329 139 | msgid "OCR" 140 | msgstr "OCR" 141 | 142 | #: scans2pdf_gui/qml/detail.qml:348 143 | msgid "Language" 144 | msgstr "Idioma" 145 | 146 | #: scans2pdf_gui/qml/detail.qml:399 147 | msgid "All colors" 148 | msgstr "Todos los colores" 149 | 150 | #: scans2pdf_gui/qml/detail.qml:410 151 | msgid "Apply to all" 152 | msgstr "Aplicar a todo" 153 | 154 | #: scans2pdf_gui/qml/detail.qml:416 155 | msgid "Apply to following" 156 | msgstr "Aplicar a lo siguiente" 157 | 158 | #: scans2pdf_gui/qml/detail.qml:424 159 | msgid "Load default settings" 160 | msgstr "Carga las configuraciones por defecto" 161 | 162 | #: scans2pdf_gui/qml/detail.qml:430 163 | msgid "Overwrite?" 164 | msgstr "¿Sobrescribir?" 165 | 166 | #: scans2pdf_gui/qml/detail.qml:431 167 | msgid "Replace default settings?" 168 | msgstr "¿Reemplazar la configuración predeterminada?" 169 | 170 | #: scans2pdf_gui/qml/detail.qml:437 171 | msgid "Save default settings" 172 | msgstr "Guardar la configuración predeterminada" 173 | 174 | #: scans2pdf_gui/qml/overview.qml:30 175 | msgid "Open" 176 | msgstr "Abrir" 177 | 178 | #: scans2pdf_gui/qml/overview.qml:32 179 | msgid "Images" 180 | msgstr "Imágenes" 181 | 182 | #: scans2pdf_gui/qml/overview.qml:33 183 | msgid "All files" 184 | msgstr "Todos los archivos" 185 | 186 | #: scans2pdf_gui/qml/overview.qml:42 187 | msgid "Save" 188 | msgstr "Guardar" 189 | 190 | #: scans2pdf_gui/qml/overview.qml:43 191 | msgid "PDF" 192 | msgstr "PDF" 193 | 194 | #: scans2pdf_gui/qml/overview.qml:75 195 | msgid "Failed to create PDF" 196 | msgstr "No se pudo crear el PDF" 197 | 198 | #: scans2pdf_gui/qml/overview.qml:91 199 | msgid "Close" 200 | msgstr "Cerrar" 201 | 202 | #: scans2pdf_gui/qml/overview.qml:108 203 | msgid "Saving…" 204 | msgstr "Guardando…" 205 | 206 | #: scans2pdf_gui/qml/overview.qml:121 207 | msgid "Cancel" 208 | msgstr "Cancelar" 209 | 210 | #: scans2pdf_gui/qml/overview.qml:140 211 | msgid "Create" 212 | msgstr "Crear" 213 | -------------------------------------------------------------------------------- /po/et.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the djpdfgui package. 4 | # Priit Jõerüüt , 2024. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: djpdfgui\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2024-07-17 03:33+0200\n" 10 | "PO-Revision-Date: 2024-11-19 22:00+0000\n" 11 | "Last-Translator: Priit Jõerüüt \n" 12 | "Language-Team: Estonian \n" 14 | "Language: et\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 19 | "X-Generator: Weblate 5.9-dev\n" 20 | 21 | #: desktop/com.github.unrud.djpdf.desktop.in:4 22 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:7 23 | #: scans2pdf_gui/qml/main.qml:29 24 | msgid "Scans to PDF" 25 | msgstr "Skaneeringute konverter PDF-failideks" 26 | 27 | #: desktop/com.github.unrud.djpdf.desktop.in:5 28 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:8 29 | msgid "Create small, searchable PDFs from scanned documents" 30 | msgstr "" 31 | "Loo skaneeritud dokumentidest väikeseid PDF-faile, kust saad teksti otsida" 32 | 33 | #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 34 | #: desktop/com.github.unrud.djpdf.desktop.in:11 35 | msgid "djpdf;OCR;convert;PDF;image;" 36 | msgstr "" 37 | "djpdf;OCR;konverter;PDF;PDF-tekst;pildid;skaneerimine;tekstituvastus;" 38 | "konvertimine;konverteerimine;" 39 | 40 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:10 41 | msgid "" 42 | "Create small, searchable PDFs from scanned documents. The program divides " 43 | "images into bitonal foreground images (text) and a color background image, " 44 | "then compresses them separately. An invisible OCR text layer is added, " 45 | "making the PDF searchable." 46 | msgstr "" 47 | "Loo skaneeritud failidest väikesed ja otsitava sisuga PDF-failid. See " 48 | "rakendus jagab pildid kaheks pildiks: esiplaanil mustvalge pilt (tekst) ja " 49 | "taustal värviline pilt ning pakib nad kokku eraldi. Lisaks muudame optilise " 50 | "tekstituvastuse abil tekstipildid tavatekstiks ja salvestame ta nähtamatu " 51 | "andmekihina ning sellega muudame PDF-faili otsitavaks." 52 | 53 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:16 54 | msgid "" 55 | "Color and grayscale scans need some preparation for good results. " 56 | "Recommended tools are Scan Tailor or GIMP." 57 | msgstr "" 58 | "Heade tulemuste jaoks peavad värvilised ja halltoonides skaneeritud failid " 59 | "olema korralikult ette valmistatud. Selleks soovitame rakendusi Scan Tailor " 60 | "või GIMP." 61 | 62 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:20 63 | msgid "A GUI and command line interface are included." 64 | msgstr "Rakendusel on nii graafiline kasutajaliides, kui ka käsurea klient." 65 | 66 | #: scans2pdf_gui/qml/detail.qml:43 scans2pdf_gui/qml/detail.qml:253 67 | #: scans2pdf_gui/qml/detail.qml:392 68 | msgid "Remove" 69 | msgstr "Eemalda" 70 | 71 | #: scans2pdf_gui/qml/detail.qml:66 72 | msgid "Please choose a color" 73 | msgstr "Palun vali värv" 74 | 75 | #: scans2pdf_gui/qml/detail.qml:98 76 | msgid "DPI:" 77 | msgstr "Punkte tolli kohta:" 78 | 79 | #: scans2pdf_gui/qml/detail.qml:103 80 | msgid "auto" 81 | msgstr "automaatne" 82 | 83 | #: scans2pdf_gui/qml/detail.qml:113 84 | msgid "Background color:" 85 | msgstr "Taustavärv:" 86 | 87 | #: scans2pdf_gui/qml/detail.qml:138 88 | msgid "Background" 89 | msgstr "Taust" 90 | 91 | #: scans2pdf_gui/qml/detail.qml:155 92 | msgid "Resize:" 93 | msgstr "Muuda suurust:" 94 | 95 | #: scans2pdf_gui/qml/detail.qml:172 scans2pdf_gui/qml/detail.qml:269 96 | msgid "Compression:" 97 | msgstr "Pakkimisvorming:" 98 | 99 | #: scans2pdf_gui/qml/detail.qml:193 100 | msgid "Quality:" 101 | msgstr "Kvaliteet:" 102 | 103 | #: scans2pdf_gui/qml/detail.qml:212 104 | msgid "Foreground" 105 | msgstr "Esiplaan" 106 | 107 | #: scans2pdf_gui/qml/detail.qml:229 scans2pdf_gui/qml/detail.qml:369 108 | msgid "Colors:" 109 | msgstr "Värvid:" 110 | 111 | #: scans2pdf_gui/qml/detail.qml:260 112 | msgid "No colors" 113 | msgstr "Värve pole" 114 | 115 | #: scans2pdf_gui/qml/detail.qml:260 scans2pdf_gui/qml/detail.qml:399 116 | msgid "Add" 117 | msgstr "Lisa" 118 | 119 | #: scans2pdf_gui/qml/detail.qml:290 120 | msgid "JBIG2 Threshold:" 121 | msgstr "JBIG2 lävi:" 122 | 123 | #: scans2pdf_gui/qml/detail.qml:314 124 | msgid "Warning" 125 | msgstr "Hoiatus" 126 | 127 | #: scans2pdf_gui/qml/detail.qml:319 128 | msgid "" 129 | "Lossy JBIG2 compression can alter text in a way that is not noticeable as " 130 | "corruption (e.g. the numbers '6' and '8' get replaced)" 131 | msgstr "" 132 | "Kui kasutusel on JBIG2 kadudega pakkimine, siis tekst võib muutuda nii, et " 133 | "vigu pole kerge märgata (näiteks numbrid „6“ ja „8“ võivad asenduda)" 134 | 135 | #: scans2pdf_gui/qml/detail.qml:329 136 | msgid "OCR" 137 | msgstr "Optiline tekstituvastus" 138 | 139 | #: scans2pdf_gui/qml/detail.qml:348 140 | msgid "Language" 141 | msgstr "Keel" 142 | 143 | #: scans2pdf_gui/qml/detail.qml:399 144 | msgid "All colors" 145 | msgstr "Kõik värvid" 146 | 147 | #: scans2pdf_gui/qml/detail.qml:410 148 | msgid "Apply to all" 149 | msgstr "Kohalda kõigile" 150 | 151 | #: scans2pdf_gui/qml/detail.qml:416 152 | msgid "Apply to following" 153 | msgstr "Kohalda järgmistele" 154 | 155 | #: scans2pdf_gui/qml/detail.qml:424 156 | msgid "Load default settings" 157 | msgstr "Laadi vaikimisi seadistused" 158 | 159 | #: scans2pdf_gui/qml/detail.qml:430 160 | msgid "Overwrite?" 161 | msgstr "Kirjutame üle?" 162 | 163 | #: scans2pdf_gui/qml/detail.qml:431 164 | msgid "Replace default settings?" 165 | msgstr "Kas kirjutame vaikimisi seadistused üle?" 166 | 167 | #: scans2pdf_gui/qml/detail.qml:437 168 | msgid "Save default settings" 169 | msgstr "Salvesta vaikimisi seadistused" 170 | 171 | #: scans2pdf_gui/qml/overview.qml:30 172 | msgid "Open" 173 | msgstr "Ava" 174 | 175 | #: scans2pdf_gui/qml/overview.qml:32 176 | msgid "Images" 177 | msgstr "Pildid" 178 | 179 | #: scans2pdf_gui/qml/overview.qml:33 180 | msgid "All files" 181 | msgstr "Kõik failid" 182 | 183 | #: scans2pdf_gui/qml/overview.qml:42 184 | msgid "Save" 185 | msgstr "Salvesta" 186 | 187 | #: scans2pdf_gui/qml/overview.qml:43 188 | msgid "PDF" 189 | msgstr "PDF-failid" 190 | 191 | #: scans2pdf_gui/qml/overview.qml:75 192 | msgid "Failed to create PDF" 193 | msgstr "PDF-faili loomine ei õnnestunud" 194 | 195 | #: scans2pdf_gui/qml/overview.qml:91 196 | msgid "Close" 197 | msgstr "Sulge" 198 | 199 | #: scans2pdf_gui/qml/overview.qml:108 200 | msgid "Saving…" 201 | msgstr "Salvestame…" 202 | 203 | #: scans2pdf_gui/qml/overview.qml:121 204 | msgid "Cancel" 205 | msgstr "Katkesta" 206 | 207 | #: scans2pdf_gui/qml/overview.qml:140 208 | msgid "Create" 209 | msgstr "Loo uus PDF-fail" 210 | -------------------------------------------------------------------------------- /po/hu.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the djpdfgui package. 4 | # ovari , 2023, 2024. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: djpdfgui\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2024-07-17 03:33+0200\n" 10 | "PO-Revision-Date: 2024-09-07 17:09+0000\n" 11 | "Last-Translator: ovari \n" 12 | "Language-Team: Hungarian \n" 14 | "Language: hu\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 19 | "X-Generator: Weblate 5.8-dev\n" 20 | 21 | #: desktop/com.github.unrud.djpdf.desktop.in:4 22 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:7 23 | #: scans2pdf_gui/qml/main.qml:29 24 | msgid "Scans to PDF" 25 | msgstr "PDF-fájlba beolvas" 26 | 27 | #: desktop/com.github.unrud.djpdf.desktop.in:5 28 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:8 29 | msgid "Create small, searchable PDFs from scanned documents" 30 | msgstr "" 31 | "Kisméretű, kereshető PDF-fájlok létrehozása a beolvasott dokumentumokból" 32 | 33 | #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 34 | #: desktop/com.github.unrud.djpdf.desktop.in:11 35 | msgid "djpdf;OCR;convert;PDF;image;" 36 | msgstr "djpdf;OCR;optikai karakterfelismerés;konvertálás;PDF;kép;" 37 | 38 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:10 39 | msgid "" 40 | "Create small, searchable PDFs from scanned documents. The program divides " 41 | "images into bitonal foreground images (text) and a color background image, " 42 | "then compresses them separately. An invisible OCR text layer is added, " 43 | "making the PDF searchable." 44 | msgstr "" 45 | "Kisméretű, kereshető PDF-fájlok létrehozása a beolvasott dokumentumokból. Az " 46 | "alkalmazás a képeket bitonális előtérképekre (szövegekre) és színes " 47 | "háttérképekre osztja, majd külön tömöríti őket. Egy láthatatlan OCR " 48 | "szövegréteg került hozzáadásra, így a PDF-fájl kereshetővé válik." 49 | 50 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:16 51 | msgid "" 52 | "Color and grayscale scans need some preparation for good results. " 53 | "Recommended tools are Scan Tailor or GIMP." 54 | msgstr "" 55 | "A színes és szürkeárnyalatos szkenneléshez némi előkészület szükséges a jó " 56 | "eredmény eléréséhez. A Scan Tailor és a GIMP ajánlott eszközök." 57 | 58 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:20 59 | msgid "A GUI and command line interface are included." 60 | msgstr "Grafikus felhasználói felület és parancssori felület tartozik hozzá." 61 | 62 | #: scans2pdf_gui/qml/detail.qml:43 scans2pdf_gui/qml/detail.qml:253 63 | #: scans2pdf_gui/qml/detail.qml:392 64 | msgid "Remove" 65 | msgstr "Eltávolítás" 66 | 67 | #: scans2pdf_gui/qml/detail.qml:66 68 | msgid "Please choose a color" 69 | msgstr "Szín kijelölése" 70 | 71 | #: scans2pdf_gui/qml/detail.qml:98 72 | msgid "DPI:" 73 | msgstr "Pont/hüvelyk:" 74 | 75 | #: scans2pdf_gui/qml/detail.qml:103 76 | msgid "auto" 77 | msgstr "Önműködő" 78 | 79 | #: scans2pdf_gui/qml/detail.qml:113 80 | msgid "Background color:" 81 | msgstr "Háttérszín:" 82 | 83 | #: scans2pdf_gui/qml/detail.qml:138 84 | msgid "Background" 85 | msgstr "Háttér" 86 | 87 | #: scans2pdf_gui/qml/detail.qml:155 88 | msgid "Resize:" 89 | msgstr "Átméretezés:" 90 | 91 | #: scans2pdf_gui/qml/detail.qml:172 scans2pdf_gui/qml/detail.qml:269 92 | msgid "Compression:" 93 | msgstr "Tömörítés:" 94 | 95 | #: scans2pdf_gui/qml/detail.qml:193 96 | msgid "Quality:" 97 | msgstr "Minőség:" 98 | 99 | #: scans2pdf_gui/qml/detail.qml:212 100 | msgid "Foreground" 101 | msgstr "Előtér" 102 | 103 | #: scans2pdf_gui/qml/detail.qml:229 scans2pdf_gui/qml/detail.qml:369 104 | msgid "Colors:" 105 | msgstr "Színek:" 106 | 107 | #: scans2pdf_gui/qml/detail.qml:260 108 | msgid "No colors" 109 | msgstr "Nincsenek színek" 110 | 111 | #: scans2pdf_gui/qml/detail.qml:260 scans2pdf_gui/qml/detail.qml:399 112 | msgid "Add" 113 | msgstr "Hozzáadás" 114 | 115 | #: scans2pdf_gui/qml/detail.qml:290 116 | msgid "JBIG2 Threshold:" 117 | msgstr "JBIG2-küszöbérték:" 118 | 119 | #: scans2pdf_gui/qml/detail.qml:314 120 | msgid "Warning" 121 | msgstr "Figyelem" 122 | 123 | #: scans2pdf_gui/qml/detail.qml:319 124 | msgid "" 125 | "Lossy JBIG2 compression can alter text in a way that is not noticeable as " 126 | "corruption (e.g. the numbers '6' and '8' get replaced)" 127 | msgstr "" 128 | "A veszteséges JBIG2-tömörítés olyan módon módosíthatja a szöveget, amely nem " 129 | "észrevehető sérülésként (például a „6” és „8” számok lecserélődnek)" 130 | 131 | #: scans2pdf_gui/qml/detail.qml:329 132 | msgid "OCR" 133 | msgstr "Optikai karakter felismerés" 134 | 135 | #: scans2pdf_gui/qml/detail.qml:348 136 | msgid "Language" 137 | msgstr "Nyelv" 138 | 139 | #: scans2pdf_gui/qml/detail.qml:399 140 | msgid "All colors" 141 | msgstr "Minden szín" 142 | 143 | #: scans2pdf_gui/qml/detail.qml:410 144 | msgid "Apply to all" 145 | msgstr "Alkalmazás mindre" 146 | 147 | #: scans2pdf_gui/qml/detail.qml:416 148 | msgid "Apply to following" 149 | msgstr "Alkalmazás a következőre" 150 | 151 | #: scans2pdf_gui/qml/detail.qml:424 152 | msgid "Load default settings" 153 | msgstr "Alapértelmezett beállítások betöltése" 154 | 155 | #: scans2pdf_gui/qml/detail.qml:430 156 | msgid "Overwrite?" 157 | msgstr "Felülírás?" 158 | 159 | #: scans2pdf_gui/qml/detail.qml:431 160 | msgid "Replace default settings?" 161 | msgstr "Alapértelmezett beállítások lecserélése?" 162 | 163 | #: scans2pdf_gui/qml/detail.qml:437 164 | msgid "Save default settings" 165 | msgstr "Alapértelmezett beállítások mentése" 166 | 167 | #: scans2pdf_gui/qml/overview.qml:30 168 | msgid "Open" 169 | msgstr "Megnyitás" 170 | 171 | #: scans2pdf_gui/qml/overview.qml:32 172 | msgid "Images" 173 | msgstr "Képek" 174 | 175 | #: scans2pdf_gui/qml/overview.qml:33 176 | msgid "All files" 177 | msgstr "Minden fájl" 178 | 179 | #: scans2pdf_gui/qml/overview.qml:42 180 | msgid "Save" 181 | msgstr "Mentés" 182 | 183 | #: scans2pdf_gui/qml/overview.qml:43 184 | msgid "PDF" 185 | msgstr "PDF-fájl" 186 | 187 | #: scans2pdf_gui/qml/overview.qml:75 188 | msgid "Failed to create PDF" 189 | msgstr "Nem sikerült a PDF-fájl létrehozása" 190 | 191 | #: scans2pdf_gui/qml/overview.qml:91 192 | msgid "Close" 193 | msgstr "Bezárás" 194 | 195 | #: scans2pdf_gui/qml/overview.qml:108 196 | msgid "Saving…" 197 | msgstr "Mentés folyamatban van…" 198 | 199 | #: scans2pdf_gui/qml/overview.qml:121 200 | msgid "Cancel" 201 | msgstr "Mégse" 202 | 203 | #: scans2pdf_gui/qml/overview.qml:140 204 | msgid "Create" 205 | msgstr "Létrehozás" 206 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('djpdfgui', preset: 'glib') 2 | -------------------------------------------------------------------------------- /po/nb_NO.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the djpdfgui package. 4 | # Allan Nordhøy , 2023. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: djpdfgui\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2024-07-17 03:33+0200\n" 10 | "PO-Revision-Date: 2023-08-23 08:49+0000\n" 11 | "Last-Translator: Allan Nordhøy \n" 12 | "Language-Team: Norwegian Bokmål \n" 14 | "Language: nb_NO\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 19 | "X-Generator: Weblate 5.0-dev\n" 20 | 21 | #: desktop/com.github.unrud.djpdf.desktop.in:4 22 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:7 23 | #: scans2pdf_gui/qml/main.qml:29 24 | msgid "Scans to PDF" 25 | msgstr "Skanner til PDF" 26 | 27 | #: desktop/com.github.unrud.djpdf.desktop.in:5 28 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:8 29 | msgid "Create small, searchable PDFs from scanned documents" 30 | msgstr "Opprett små, søkbare PDF-er fra skannede dokumenter" 31 | 32 | #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 33 | #: desktop/com.github.unrud.djpdf.desktop.in:11 34 | msgid "djpdf;OCR;convert;PDF;image;" 35 | msgstr "" 36 | 37 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:10 38 | msgid "" 39 | "Create small, searchable PDFs from scanned documents. The program divides " 40 | "images into bitonal foreground images (text) and a color background image, " 41 | "then compresses them separately. An invisible OCR text layer is added, " 42 | "making the PDF searchable." 43 | msgstr "" 44 | "Opprett små, søkbare PDF-er fra skannede dokumenter. Programmet deler bilder " 45 | "inn i bitonale forgrunnsbilder (tekst) og et fargelagt bakgrunnsbilde, og " 46 | "sammenpakker dem hver for seg. Et usynlig OCR-tekstlag legges til, noe som " 47 | "gjør PDF-en søkbar." 48 | 49 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:16 50 | msgid "" 51 | "Color and grayscale scans need some preparation for good results. " 52 | "Recommended tools are Scan Tailor or GIMP." 53 | msgstr "" 54 | "Farge- og gråskalaskanninger trenger litt forberedelse for gode resultat. " 55 | "Verktøy som «Scan Tailor» eller «GIMP» anbefales." 56 | 57 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:20 58 | msgid "A GUI and command line interface are included." 59 | msgstr "Et grensesnitt og en kommandolinje er inkludert." 60 | 61 | #: scans2pdf_gui/qml/detail.qml:43 scans2pdf_gui/qml/detail.qml:253 62 | #: scans2pdf_gui/qml/detail.qml:392 63 | msgid "Remove" 64 | msgstr "Fjern" 65 | 66 | #: scans2pdf_gui/qml/detail.qml:66 67 | msgid "Please choose a color" 68 | msgstr "Velg en farge" 69 | 70 | #: scans2pdf_gui/qml/detail.qml:98 71 | msgid "DPI:" 72 | msgstr "DPI:" 73 | 74 | #: scans2pdf_gui/qml/detail.qml:103 75 | msgid "auto" 76 | msgstr "auto" 77 | 78 | #: scans2pdf_gui/qml/detail.qml:113 79 | msgid "Background color:" 80 | msgstr "Bakgrunnsfarge:" 81 | 82 | #: scans2pdf_gui/qml/detail.qml:138 83 | msgid "Background" 84 | msgstr "Bakgrunn" 85 | 86 | #: scans2pdf_gui/qml/detail.qml:155 87 | msgid "Resize:" 88 | msgstr "Endre størrelse:" 89 | 90 | #: scans2pdf_gui/qml/detail.qml:172 scans2pdf_gui/qml/detail.qml:269 91 | msgid "Compression:" 92 | msgstr "Sammenpakking:" 93 | 94 | #: scans2pdf_gui/qml/detail.qml:193 95 | msgid "Quality:" 96 | msgstr "Kvalitet:" 97 | 98 | #: scans2pdf_gui/qml/detail.qml:212 99 | msgid "Foreground" 100 | msgstr "Forgrunn" 101 | 102 | #: scans2pdf_gui/qml/detail.qml:229 scans2pdf_gui/qml/detail.qml:369 103 | msgid "Colors:" 104 | msgstr "Farger:" 105 | 106 | #: scans2pdf_gui/qml/detail.qml:260 107 | msgid "No colors" 108 | msgstr "Ingen farger" 109 | 110 | #: scans2pdf_gui/qml/detail.qml:260 scans2pdf_gui/qml/detail.qml:399 111 | msgid "Add" 112 | msgstr "Legg til" 113 | 114 | #: scans2pdf_gui/qml/detail.qml:290 115 | msgid "JBIG2 Threshold:" 116 | msgstr "JBIG2-terskel:" 117 | 118 | #: scans2pdf_gui/qml/detail.qml:314 119 | msgid "Warning" 120 | msgstr "Advarsel" 121 | 122 | #: scans2pdf_gui/qml/detail.qml:319 123 | msgid "" 124 | "Lossy JBIG2 compression can alter text in a way that is not noticeable as " 125 | "corruption (e.g. the numbers '6' and '8' get replaced)" 126 | msgstr "" 127 | "Tapsbasert JBIG2-sammenpakking kan forandre tekst som ikke merkes som " 128 | "forvrengning (f.eks. at «6» erstattes med «8»)" 129 | 130 | #: scans2pdf_gui/qml/detail.qml:329 131 | msgid "OCR" 132 | msgstr "OCR" 133 | 134 | #: scans2pdf_gui/qml/detail.qml:348 135 | msgid "Language" 136 | msgstr "Språk" 137 | 138 | #: scans2pdf_gui/qml/detail.qml:399 139 | msgid "All colors" 140 | msgstr "Alle farger" 141 | 142 | #: scans2pdf_gui/qml/detail.qml:410 143 | #, fuzzy 144 | msgid "Apply to all" 145 | msgstr "Bruk for alt" 146 | 147 | #: scans2pdf_gui/qml/detail.qml:416 148 | msgid "Apply to following" 149 | msgstr "Bruk for følgende" 150 | 151 | #: scans2pdf_gui/qml/detail.qml:424 152 | msgid "Load default settings" 153 | msgstr "Last inn forvalgte innstillinger" 154 | 155 | #: scans2pdf_gui/qml/detail.qml:430 156 | msgid "Overwrite?" 157 | msgstr "Overskriv?" 158 | 159 | #: scans2pdf_gui/qml/detail.qml:431 160 | msgid "Replace default settings?" 161 | msgstr "Erstatt forvalgte innstillinger?" 162 | 163 | #: scans2pdf_gui/qml/detail.qml:437 164 | msgid "Save default settings" 165 | msgstr "Lagre forvalgte innstillinger" 166 | 167 | #: scans2pdf_gui/qml/overview.qml:30 168 | msgid "Open" 169 | msgstr "Åpne" 170 | 171 | #: scans2pdf_gui/qml/overview.qml:32 172 | msgid "Images" 173 | msgstr "Bilder" 174 | 175 | #: scans2pdf_gui/qml/overview.qml:33 176 | msgid "All files" 177 | msgstr "Alle filer" 178 | 179 | #: scans2pdf_gui/qml/overview.qml:42 180 | msgid "Save" 181 | msgstr "Lagre" 182 | 183 | #: scans2pdf_gui/qml/overview.qml:43 184 | msgid "PDF" 185 | msgstr "PDF" 186 | 187 | #: scans2pdf_gui/qml/overview.qml:75 188 | #, fuzzy 189 | msgid "Failed to create PDF" 190 | msgstr "Klarte ikke å opprette PDF" 191 | 192 | #: scans2pdf_gui/qml/overview.qml:91 193 | msgid "Close" 194 | msgstr "Lukk" 195 | 196 | #: scans2pdf_gui/qml/overview.qml:108 197 | msgid "Saving…" 198 | msgstr "Lagrer …" 199 | 200 | #: scans2pdf_gui/qml/overview.qml:121 201 | msgid "Cancel" 202 | msgstr "Avbryt" 203 | 204 | #: scans2pdf_gui/qml/overview.qml:140 205 | msgid "Create" 206 | msgstr "Opprett" 207 | -------------------------------------------------------------------------------- /po/nl.po: -------------------------------------------------------------------------------- 1 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 2 | # This file is distributed under the same license as the djpdfgui package. 3 | # 4 | # Heimen Stoffels , 2023. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: djpdfgui\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2024-07-17 03:33+0200\n" 10 | "PO-Revision-Date: 2023-08-16 14:12+0200\n" 11 | "Last-Translator: Heimen Stoffels \n" 12 | "Language-Team: Dutch\n" 13 | "Language: nl\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 18 | "X-Generator: Poedit 3.3.2\n" 19 | 20 | #: desktop/com.github.unrud.djpdf.desktop.in:4 21 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:7 22 | #: scans2pdf_gui/qml/main.qml:29 23 | msgid "Scans to PDF" 24 | msgstr "Scans-naar-pdf" 25 | 26 | #: desktop/com.github.unrud.djpdf.desktop.in:5 27 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:8 28 | msgid "Create small, searchable PDFs from scanned documents" 29 | msgstr "Maak korte, doorzoekbare pdf-bestanden van gescande documenten" 30 | 31 | #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 32 | #: desktop/com.github.unrud.djpdf.desktop.in:11 33 | msgid "djpdf;OCR;convert;PDF;image;" 34 | msgstr "" 35 | 36 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:10 37 | msgid "" 38 | "Create small, searchable PDFs from scanned documents. The program divides " 39 | "images into bitonal foreground images (text) and a color background image, " 40 | "then compresses them separately. An invisible OCR text layer is added, " 41 | "making the PDF searchable." 42 | msgstr "" 43 | "Maak korte, doorzoekbare pdf-bestanden van gescande documenten. Deze " 44 | "toepassing zet afbeeldingen om naar bitonale voorgrondafbeeldingen (tekst) " 45 | "en een gekleurde achtergrond. Vervolgens wordt op beide (los van elkaar) " 46 | "compressie toegepast. Tot slot wordt een onzichtbare ocr-tekstlaag " 47 | "toegevoegd, waarmee de pdf-bestanden doorzoekbaar worden gemaakt." 48 | 49 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:16 50 | msgid "" 51 | "Color and grayscale scans need some preparation for good results. " 52 | "Recommended tools are Scan Tailor or GIMP." 53 | msgstr "" 54 | "Voor het verwerken van kleuren- en grijswaardenscans is enige voorbereiding " 55 | "nodig. Aanbevolen hulpmiddelen: Scan Tailor, GIMP." 56 | 57 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:20 58 | msgid "A GUI and command line interface are included." 59 | msgstr "Meegeleverd: grafische en terminaltoepassing (cli)." 60 | 61 | #: scans2pdf_gui/qml/detail.qml:43 scans2pdf_gui/qml/detail.qml:253 62 | #: scans2pdf_gui/qml/detail.qml:392 63 | msgid "Remove" 64 | msgstr "Verwijderen" 65 | 66 | #: scans2pdf_gui/qml/detail.qml:66 67 | msgid "Please choose a color" 68 | msgstr "Kies een kleur" 69 | 70 | #: scans2pdf_gui/qml/detail.qml:98 71 | msgid "DPI:" 72 | msgstr "Dpi:" 73 | 74 | #: scans2pdf_gui/qml/detail.qml:103 75 | msgid "auto" 76 | msgstr "Automatisch" 77 | 78 | #: scans2pdf_gui/qml/detail.qml:113 79 | msgid "Background color:" 80 | msgstr "Achtergrondkleur:" 81 | 82 | #: scans2pdf_gui/qml/detail.qml:138 83 | msgid "Background" 84 | msgstr "Achtergrond" 85 | 86 | #: scans2pdf_gui/qml/detail.qml:155 87 | msgid "Resize:" 88 | msgstr "Grootte aanpassen:" 89 | 90 | #: scans2pdf_gui/qml/detail.qml:172 scans2pdf_gui/qml/detail.qml:269 91 | msgid "Compression:" 92 | msgstr "Compressie:" 93 | 94 | #: scans2pdf_gui/qml/detail.qml:193 95 | msgid "Quality:" 96 | msgstr "Kwaliteit:" 97 | 98 | #: scans2pdf_gui/qml/detail.qml:212 99 | msgid "Foreground" 100 | msgstr "Voorgrond" 101 | 102 | #: scans2pdf_gui/qml/detail.qml:229 scans2pdf_gui/qml/detail.qml:369 103 | msgid "Colors:" 104 | msgstr "Kleuren:" 105 | 106 | #: scans2pdf_gui/qml/detail.qml:260 107 | msgid "No colors" 108 | msgstr "Kleurloos" 109 | 110 | #: scans2pdf_gui/qml/detail.qml:260 scans2pdf_gui/qml/detail.qml:399 111 | msgid "Add" 112 | msgstr "Toevoegen" 113 | 114 | #: scans2pdf_gui/qml/detail.qml:290 115 | msgid "JBIG2 Threshold:" 116 | msgstr "JBIG2-drempelwaarde:" 117 | 118 | #: scans2pdf_gui/qml/detail.qml:314 119 | msgid "Warning" 120 | msgstr "Waarschuwing" 121 | 122 | #: scans2pdf_gui/qml/detail.qml:319 123 | msgid "" 124 | "Lossy JBIG2 compression can alter text in a way that is not noticeable as " 125 | "corruption (e.g. the numbers '6' and '8' get replaced)" 126 | msgstr "" 127 | "JBIG2-compressie met verlies kan tekst dusdanig aanpassen dat storingen " 128 | "onzichtbaar worden. Voorbeeld: nummer 6 en 8." 129 | 130 | #: scans2pdf_gui/qml/detail.qml:329 131 | msgid "OCR" 132 | msgstr "Tekstherkenning" 133 | 134 | #: scans2pdf_gui/qml/detail.qml:348 135 | msgid "Language" 136 | msgstr "Taal" 137 | 138 | #: scans2pdf_gui/qml/detail.qml:399 139 | msgid "All colors" 140 | msgstr "Alle kleuren" 141 | 142 | #: scans2pdf_gui/qml/detail.qml:410 143 | msgid "Apply to all" 144 | msgstr "Toepassen op alles" 145 | 146 | #: scans2pdf_gui/qml/detail.qml:416 147 | msgid "Apply to following" 148 | msgstr "Toepassen op volgende" 149 | 150 | #: scans2pdf_gui/qml/detail.qml:424 151 | msgid "Load default settings" 152 | msgstr "Standaardvoorkeuren laden" 153 | 154 | #: scans2pdf_gui/qml/detail.qml:430 155 | msgid "Overwrite?" 156 | msgstr "Overschrijven?" 157 | 158 | #: scans2pdf_gui/qml/detail.qml:431 159 | msgid "Replace default settings?" 160 | msgstr "Wilt u de standaardvoorkeuren vervangen?" 161 | 162 | #: scans2pdf_gui/qml/detail.qml:437 163 | msgid "Save default settings" 164 | msgstr "Opslaan als standaardvoorkeuren" 165 | 166 | #: scans2pdf_gui/qml/overview.qml:30 167 | msgid "Open" 168 | msgstr "Openen" 169 | 170 | #: scans2pdf_gui/qml/overview.qml:32 171 | msgid "Images" 172 | msgstr "Afbeeldingen" 173 | 174 | #: scans2pdf_gui/qml/overview.qml:33 175 | msgid "All files" 176 | msgstr "Alle bestanden" 177 | 178 | #: scans2pdf_gui/qml/overview.qml:42 179 | msgid "Save" 180 | msgstr "Opslaan" 181 | 182 | #: scans2pdf_gui/qml/overview.qml:43 183 | msgid "PDF" 184 | msgstr "Pdf" 185 | 186 | #: scans2pdf_gui/qml/overview.qml:75 187 | msgid "Failed to create PDF" 188 | msgstr "Het pdf-bestand kan niet worden aangemaakt" 189 | 190 | #: scans2pdf_gui/qml/overview.qml:91 191 | msgid "Close" 192 | msgstr "Sluiten" 193 | 194 | #: scans2pdf_gui/qml/overview.qml:108 195 | msgid "Saving…" 196 | msgstr "Bezig met opslaan…" 197 | 198 | #: scans2pdf_gui/qml/overview.qml:121 199 | msgid "Cancel" 200 | msgstr "Annuleren" 201 | 202 | #: scans2pdf_gui/qml/overview.qml:140 203 | msgid "Create" 204 | msgstr "Maken" 205 | -------------------------------------------------------------------------------- /po/ru.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the djpdfgui package. 4 | # Сергей , 2025. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: djpdfgui\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2024-07-17 03:33+0200\n" 10 | "PO-Revision-Date: 2025-02-03 11:01+0000\n" 11 | "Last-Translator: Сергей \n" 12 | "Language-Team: Russian \n" 14 | "Language: ru\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " 19 | "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" 20 | "X-Generator: Weblate 5.10-dev\n" 21 | 22 | #: desktop/com.github.unrud.djpdf.desktop.in:4 23 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:7 24 | #: scans2pdf_gui/qml/main.qml:29 25 | msgid "Scans to PDF" 26 | msgstr "Сканы в PDF" 27 | 28 | #: desktop/com.github.unrud.djpdf.desktop.in:5 29 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:8 30 | msgid "Create small, searchable PDFs from scanned documents" 31 | msgstr "" 32 | "Создание из отсканированных документов небольших PDF-файлов с возможностью " 33 | "поиска по их содержимому" 34 | 35 | #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 36 | #: desktop/com.github.unrud.djpdf.desktop.in:11 37 | msgid "djpdf;OCR;convert;PDF;image;" 38 | msgstr "djpdf;OCR;convert;PDF;image;изображение;конвертировать;распознавание;" 39 | 40 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:10 41 | msgid "" 42 | "Create small, searchable PDFs from scanned documents. The program divides " 43 | "images into bitonal foreground images (text) and a color background image, " 44 | "then compresses them separately. An invisible OCR text layer is added, " 45 | "making the PDF searchable." 46 | msgstr "" 47 | "Создание из отсканированных документов небольших PDF-файлов с возможностью " 48 | "поиска по их содержимому. Программа разделяет изображение на битональный " 49 | "слой переднего плана (текст) и цветовое фоновое изображение, затем по " 50 | "отдельности их сжимает. Добавляется невидимый текстовый слой OCR, что делает " 51 | "возможным поиск по содержимому документа PDF." 52 | 53 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:16 54 | msgid "" 55 | "Color and grayscale scans need some preparation for good results. " 56 | "Recommended tools are Scan Tailor or GIMP." 57 | msgstr "" 58 | "Для получения хороших результатов цветные и в градациях серого сканы требуют " 59 | "некоторой предварительной подготовки. Рекомендуемые инструменты - это Scan " 60 | "Tailor или Gimp." 61 | 62 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:20 63 | msgid "A GUI and command line interface are included." 64 | msgstr "В комплект входит графический интерфейс и интерфейс командной строки." 65 | 66 | #: scans2pdf_gui/qml/detail.qml:43 scans2pdf_gui/qml/detail.qml:253 67 | #: scans2pdf_gui/qml/detail.qml:392 68 | msgid "Remove" 69 | msgstr "Удалить" 70 | 71 | #: scans2pdf_gui/qml/detail.qml:66 72 | msgid "Please choose a color" 73 | msgstr "Выберите цвет" 74 | 75 | #: scans2pdf_gui/qml/detail.qml:98 76 | msgid "DPI:" 77 | msgstr "DPI:" 78 | 79 | #: scans2pdf_gui/qml/detail.qml:103 80 | msgid "auto" 81 | msgstr "авто" 82 | 83 | #: scans2pdf_gui/qml/detail.qml:113 84 | msgid "Background color:" 85 | msgstr "Цвет фона:" 86 | 87 | #: scans2pdf_gui/qml/detail.qml:138 88 | msgid "Background" 89 | msgstr "Фон" 90 | 91 | #: scans2pdf_gui/qml/detail.qml:155 92 | msgid "Resize:" 93 | msgstr "Размер:" 94 | 95 | #: scans2pdf_gui/qml/detail.qml:172 scans2pdf_gui/qml/detail.qml:269 96 | msgid "Compression:" 97 | msgstr "Сжатие:" 98 | 99 | #: scans2pdf_gui/qml/detail.qml:193 100 | msgid "Quality:" 101 | msgstr "Качество:" 102 | 103 | #: scans2pdf_gui/qml/detail.qml:212 104 | msgid "Foreground" 105 | msgstr "Передний план" 106 | 107 | #: scans2pdf_gui/qml/detail.qml:229 scans2pdf_gui/qml/detail.qml:369 108 | msgid "Colors:" 109 | msgstr "Цвета:" 110 | 111 | #: scans2pdf_gui/qml/detail.qml:260 112 | msgid "No colors" 113 | msgstr "Нет цвета" 114 | 115 | #: scans2pdf_gui/qml/detail.qml:260 scans2pdf_gui/qml/detail.qml:399 116 | msgid "Add" 117 | msgstr "Добавить" 118 | 119 | #: scans2pdf_gui/qml/detail.qml:290 120 | msgid "JBIG2 Threshold:" 121 | msgstr "Порог сжатия JBIG2:" 122 | 123 | #: scans2pdf_gui/qml/detail.qml:314 124 | msgid "Warning" 125 | msgstr "Предупреждение" 126 | 127 | #: scans2pdf_gui/qml/detail.qml:319 128 | msgid "" 129 | "Lossy JBIG2 compression can alter text in a way that is not noticeable as " 130 | "corruption (e.g. the numbers '6' and '8' get replaced)" 131 | msgstr "" 132 | "Режим сжатия «Lossy JBIG2» может привести к изменению текста таким образом, " 133 | "что некоторые различия не будут заметны (например, цифры «6» и «8» могут " 134 | "заменяться)" 135 | 136 | #: scans2pdf_gui/qml/detail.qml:329 137 | msgid "OCR" 138 | msgstr "OCR" 139 | 140 | #: scans2pdf_gui/qml/detail.qml:348 141 | msgid "Language" 142 | msgstr "Язык" 143 | 144 | #: scans2pdf_gui/qml/detail.qml:399 145 | msgid "All colors" 146 | msgstr "Все цвета" 147 | 148 | #: scans2pdf_gui/qml/detail.qml:410 149 | msgid "Apply to all" 150 | msgstr "Применить ко всем" 151 | 152 | #: scans2pdf_gui/qml/detail.qml:416 153 | msgid "Apply to following" 154 | msgstr "Применить ко всем за текущей" 155 | 156 | #: scans2pdf_gui/qml/detail.qml:424 157 | msgid "Load default settings" 158 | msgstr "Загрузить настройки по умолчанию" 159 | 160 | #: scans2pdf_gui/qml/detail.qml:430 161 | msgid "Overwrite?" 162 | msgstr "Переписать?" 163 | 164 | #: scans2pdf_gui/qml/detail.qml:431 165 | msgid "Replace default settings?" 166 | msgstr "Заменить настройки по умолчанию?" 167 | 168 | #: scans2pdf_gui/qml/detail.qml:437 169 | msgid "Save default settings" 170 | msgstr "Сохранить настройки по умолчанию" 171 | 172 | #: scans2pdf_gui/qml/overview.qml:30 173 | msgid "Open" 174 | msgstr "Открыть" 175 | 176 | #: scans2pdf_gui/qml/overview.qml:32 177 | msgid "Images" 178 | msgstr "Изображения" 179 | 180 | #: scans2pdf_gui/qml/overview.qml:33 181 | msgid "All files" 182 | msgstr "Все файлы" 183 | 184 | #: scans2pdf_gui/qml/overview.qml:42 185 | msgid "Save" 186 | msgstr "Сохранить" 187 | 188 | #: scans2pdf_gui/qml/overview.qml:43 189 | msgid "PDF" 190 | msgstr "PDF" 191 | 192 | #: scans2pdf_gui/qml/overview.qml:75 193 | msgid "Failed to create PDF" 194 | msgstr "Не удалось создать PDF" 195 | 196 | #: scans2pdf_gui/qml/overview.qml:91 197 | msgid "Close" 198 | msgstr "Закрыть" 199 | 200 | #: scans2pdf_gui/qml/overview.qml:108 201 | msgid "Saving…" 202 | msgstr "Сохранение…" 203 | 204 | #: scans2pdf_gui/qml/overview.qml:121 205 | msgid "Cancel" 206 | msgstr "Отменить" 207 | 208 | #: scans2pdf_gui/qml/overview.qml:140 209 | msgid "Create" 210 | msgstr "Создать" 211 | -------------------------------------------------------------------------------- /po/sk.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the djpdfgui package. 4 | # Milan Šalka , 2023, 2024. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: djpdfgui\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2024-07-17 03:33+0200\n" 10 | "PO-Revision-Date: 2024-11-02 20:00+0000\n" 11 | "Last-Translator: Milan Šalka \n" 12 | "Language-Team: Slovak \n" 14 | "Language: sk\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" 19 | "X-Generator: Weblate 5.8.2\n" 20 | 21 | #: desktop/com.github.unrud.djpdf.desktop.in:4 22 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:7 23 | #: scans2pdf_gui/qml/main.qml:29 24 | msgid "Scans to PDF" 25 | msgstr "Skeny do PDF" 26 | 27 | #: desktop/com.github.unrud.djpdf.desktop.in:5 28 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:8 29 | msgid "Create small, searchable PDFs from scanned documents" 30 | msgstr "Vytvorte malé, vyhľadávateľné PDF z skenovaných dokumentov" 31 | 32 | #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 33 | #: desktop/com.github.unrud.djpdf.desktop.in:11 34 | msgid "djpdf;OCR;convert;PDF;image;" 35 | msgstr "djpdf;OCR;convert;PDF;image;" 36 | 37 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:10 38 | msgid "" 39 | "Create small, searchable PDFs from scanned documents. The program divides " 40 | "images into bitonal foreground images (text) and a color background image, " 41 | "then compresses them separately. An invisible OCR text layer is added, " 42 | "making the PDF searchable." 43 | msgstr "" 44 | "Vytvorte malé, vyhľadávateľné PDF z skenovaných dokumentov. Program " 45 | "rozdeľuje obrázky do bitonal foreground obrázkov (text) a farebný obrázok " 46 | "pozadia, potom ich komprimuje samostatne. Neviditeľný OCR textová vrstva je " 47 | "pridaná, takže vyhľadávanie PDF." 48 | 49 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:16 50 | msgid "" 51 | "Color and grayscale scans need some preparation for good results. " 52 | "Recommended tools are Scan Tailor or GIMP." 53 | msgstr "" 54 | "Skeny farieb a šedej oblasti potrebujú nejakú prípravu pre dobré výsledky. " 55 | "Odporúčané nástroje sú Scan Tailor alebo GIMP." 56 | 57 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:20 58 | msgid "A GUI and command line interface are included." 59 | msgstr "Rozhranie GUI a príkazového riadku sú zahrnuté." 60 | 61 | #: scans2pdf_gui/qml/detail.qml:43 scans2pdf_gui/qml/detail.qml:253 62 | #: scans2pdf_gui/qml/detail.qml:392 63 | msgid "Remove" 64 | msgstr "Odstrániť" 65 | 66 | #: scans2pdf_gui/qml/detail.qml:66 67 | msgid "Please choose a color" 68 | msgstr "Vyberte farbu" 69 | 70 | #: scans2pdf_gui/qml/detail.qml:98 71 | msgid "DPI:" 72 | msgstr "DPI:" 73 | 74 | #: scans2pdf_gui/qml/detail.qml:103 75 | msgid "auto" 76 | msgstr "auto" 77 | 78 | #: scans2pdf_gui/qml/detail.qml:113 79 | msgid "Background color:" 80 | msgstr "Farba pozadia:" 81 | 82 | #: scans2pdf_gui/qml/detail.qml:138 83 | msgid "Background" 84 | msgstr "Slovenčina" 85 | 86 | #: scans2pdf_gui/qml/detail.qml:155 87 | msgid "Resize:" 88 | msgstr "Veľkosť:" 89 | 90 | #: scans2pdf_gui/qml/detail.qml:172 scans2pdf_gui/qml/detail.qml:269 91 | msgid "Compression:" 92 | msgstr "Kompresia:" 93 | 94 | #: scans2pdf_gui/qml/detail.qml:193 95 | msgid "Quality:" 96 | msgstr "Kvalita:" 97 | 98 | #: scans2pdf_gui/qml/detail.qml:212 99 | msgid "Foreground" 100 | msgstr "Všeobecný" 101 | 102 | #: scans2pdf_gui/qml/detail.qml:229 scans2pdf_gui/qml/detail.qml:369 103 | msgid "Colors:" 104 | msgstr "Farby:" 105 | 106 | #: scans2pdf_gui/qml/detail.qml:260 107 | msgid "No colors" 108 | msgstr "Žiadne farby" 109 | 110 | #: scans2pdf_gui/qml/detail.qml:260 scans2pdf_gui/qml/detail.qml:399 111 | msgid "Add" 112 | msgstr "Pridať" 113 | 114 | #: scans2pdf_gui/qml/detail.qml:290 115 | msgid "JBIG2 Threshold:" 116 | msgstr "JBIG2 Threshold:" 117 | 118 | #: scans2pdf_gui/qml/detail.qml:314 119 | msgid "Warning" 120 | msgstr "Varovanie" 121 | 122 | #: scans2pdf_gui/qml/detail.qml:319 123 | msgid "" 124 | "Lossy JBIG2 compression can alter text in a way that is not noticeable as " 125 | "corruption (e.g. the numbers '6' and '8' get replaced)" 126 | msgstr "" 127 | "Strata JBIG2 kompresie môže zmeniť text spôsobom, ktorý nie je viditeľný ako " 128 | "korupcia (napr. čísla \"6\" a \"8\" sa nahradí)" 129 | 130 | #: scans2pdf_gui/qml/detail.qml:329 131 | msgid "OCR" 132 | msgstr "OCR" 133 | 134 | #: scans2pdf_gui/qml/detail.qml:348 135 | msgid "Language" 136 | msgstr "Jazyk" 137 | 138 | #: scans2pdf_gui/qml/detail.qml:399 139 | msgid "All colors" 140 | msgstr "Všetky farby" 141 | 142 | #: scans2pdf_gui/qml/detail.qml:410 143 | msgid "Apply to all" 144 | msgstr "Použiť všetky" 145 | 146 | #: scans2pdf_gui/qml/detail.qml:416 147 | msgid "Apply to following" 148 | msgstr "Použiť nasledujúce" 149 | 150 | #: scans2pdf_gui/qml/detail.qml:424 151 | msgid "Load default settings" 152 | msgstr "Nastavenie nastavenia zaťaženia" 153 | 154 | #: scans2pdf_gui/qml/detail.qml:430 155 | msgid "Overwrite?" 156 | msgstr "Prepísať?" 157 | 158 | #: scans2pdf_gui/qml/detail.qml:431 159 | msgid "Replace default settings?" 160 | msgstr "Nahradiť predvolené nastavenia?" 161 | 162 | #: scans2pdf_gui/qml/detail.qml:437 163 | msgid "Save default settings" 164 | msgstr "Uložiť predvolené nastavenia" 165 | 166 | #: scans2pdf_gui/qml/overview.qml:30 167 | msgid "Open" 168 | msgstr "Otvoriť" 169 | 170 | #: scans2pdf_gui/qml/overview.qml:32 171 | msgid "Images" 172 | msgstr "Obrázky" 173 | 174 | #: scans2pdf_gui/qml/overview.qml:33 175 | msgid "All files" 176 | msgstr "Všetky súbory" 177 | 178 | #: scans2pdf_gui/qml/overview.qml:42 179 | msgid "Save" 180 | msgstr "Uložiť" 181 | 182 | #: scans2pdf_gui/qml/overview.qml:43 183 | msgid "PDF" 184 | msgstr "PDF" 185 | 186 | #: scans2pdf_gui/qml/overview.qml:75 187 | msgid "Failed to create PDF" 188 | msgstr "Failed na vytvorenie PDF" 189 | 190 | #: scans2pdf_gui/qml/overview.qml:91 191 | msgid "Close" 192 | msgstr "Zavrieť" 193 | 194 | #: scans2pdf_gui/qml/overview.qml:108 195 | msgid "Saving…" 196 | msgstr "Ukladanie…" 197 | 198 | #: scans2pdf_gui/qml/overview.qml:121 199 | msgid "Cancel" 200 | msgstr "Zrušiť" 201 | 202 | #: scans2pdf_gui/qml/overview.qml:140 203 | msgid "Create" 204 | msgstr "Vytvorte" 205 | -------------------------------------------------------------------------------- /po/ta.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the djpdfgui package. 4 | # தமிழ்நேரம் , 2024. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: djpdfgui\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2024-07-17 03:33+0200\n" 10 | "PO-Revision-Date: 2024-12-21 13:00+0000\n" 11 | "Last-Translator: தமிழ்நேரம் \n" 12 | "Language-Team: Tamil " 13 | "\n" 14 | "Language: ta\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 19 | "X-Generator: Weblate 5.10-dev\n" 20 | 21 | #: desktop/com.github.unrud.djpdf.desktop.in:4 22 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:7 23 | #: scans2pdf_gui/qml/main.qml:29 24 | msgid "Scans to PDF" 25 | msgstr "PDF க்கு ச்கேன்" 26 | 27 | #: desktop/com.github.unrud.djpdf.desktop.in:5 28 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:8 29 | msgid "Create small, searchable PDFs from scanned documents" 30 | msgstr "" 31 | "ச்கேன் செய்யப்பட்ட ஆவணங்களிலிருந்து சிறிய, தேடக்கூடிய PDF களை உருவாக்கவும்" 32 | 33 | #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 34 | #: desktop/com.github.unrud.djpdf.desktop.in:11 35 | msgid "djpdf;OCR;convert;PDF;image;" 36 | msgstr "djpdf; ocr; மாற்ற; pdf; படம்;" 37 | 38 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:10 39 | msgid "" 40 | "Create small, searchable PDFs from scanned documents. The program divides " 41 | "images into bitonal foreground images (text) and a color background image, " 42 | "then compresses them separately. An invisible OCR text layer is added, " 43 | "making the PDF searchable." 44 | msgstr "" 45 | "ச்கேன் செய்யப்பட்ட ஆவணங்களிலிருந்து சிறிய, தேடக்கூடிய PDF களை உருவாக்கவும். நிரல் படங்களை" 46 | " பிட்டோனல் முன்புற படங்கள் (உரை) மற்றும் வண்ண பின்னணி படமாக பிரிக்கிறது, பின்னர் அவற்றை " 47 | "தனித்தனியாக சுருக்குகிறது. கண்ணுக்கு தெரியாத OCR உரை அடுக்கு சேர்க்கப்பட்டு, PDF " 48 | "தேடக்கூடியதாக இருக்கும்." 49 | 50 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:16 51 | msgid "" 52 | "Color and grayscale scans need some preparation for good results. " 53 | "Recommended tools are Scan Tailor or GIMP." 54 | msgstr "" 55 | "வண்ணம் மற்றும் கிரேச்கேல் ச்கேன்களுக்கு நல்ல முடிவுகளுக்கு சில தயாரிப்புகள் தேவை. " 56 | "பரிந்துரைக்கப்பட்ட கருவிகள் ச்கேன் தையல்காரர் அல்லது சிம்ப்." 57 | 58 | #: desktop/com.github.unrud.djpdf.metainfo.xml.in:20 59 | msgid "A GUI and command line interface are included." 60 | msgstr "ஒரு GUI மற்றும் கட்டளை வரி இடைமுகம் சேர்க்கப்பட்டுள்ளது." 61 | 62 | #: scans2pdf_gui/qml/detail.qml:43 scans2pdf_gui/qml/detail.qml:253 63 | #: scans2pdf_gui/qml/detail.qml:392 64 | msgid "Remove" 65 | msgstr "அகற்று" 66 | 67 | #: scans2pdf_gui/qml/detail.qml:66 68 | msgid "Please choose a color" 69 | msgstr "தயவுசெய்து ஒரு வண்ணத்தைத் தேர்வுசெய்க" 70 | 71 | #: scans2pdf_gui/qml/detail.qml:98 72 | msgid "DPI:" 73 | msgstr "டிபிஐ:" 74 | 75 | #: scans2pdf_gui/qml/detail.qml:103 76 | msgid "auto" 77 | msgstr "தானி" 78 | 79 | #: scans2pdf_gui/qml/detail.qml:113 80 | msgid "Background color:" 81 | msgstr "பின்னணி நிறம்:" 82 | 83 | #: scans2pdf_gui/qml/detail.qml:138 84 | msgid "Background" 85 | msgstr "பின்னணி" 86 | 87 | #: scans2pdf_gui/qml/detail.qml:155 88 | msgid "Resize:" 89 | msgstr "மறுஅளவிடுதல்:" 90 | 91 | #: scans2pdf_gui/qml/detail.qml:172 scans2pdf_gui/qml/detail.qml:269 92 | msgid "Compression:" 93 | msgstr "சுருக்க:" 94 | 95 | #: scans2pdf_gui/qml/detail.qml:193 96 | msgid "Quality:" 97 | msgstr "தரம்:" 98 | 99 | #: scans2pdf_gui/qml/detail.qml:212 100 | msgid "Foreground" 101 | msgstr "முன்புறம்" 102 | 103 | #: scans2pdf_gui/qml/detail.qml:229 scans2pdf_gui/qml/detail.qml:369 104 | msgid "Colors:" 105 | msgstr "நிறங்கள்:" 106 | 107 | #: scans2pdf_gui/qml/detail.qml:260 108 | msgid "No colors" 109 | msgstr "வண்ணங்கள் இல்லை" 110 | 111 | #: scans2pdf_gui/qml/detail.qml:260 scans2pdf_gui/qml/detail.qml:399 112 | msgid "Add" 113 | msgstr "கூட்டு" 114 | 115 | #: scans2pdf_gui/qml/detail.qml:290 116 | msgid "JBIG2 Threshold:" 117 | msgstr "JBIG2 வாசல்:" 118 | 119 | #: scans2pdf_gui/qml/detail.qml:314 120 | msgid "Warning" 121 | msgstr "எச்சரிக்கை" 122 | 123 | #: scans2pdf_gui/qml/detail.qml:319 124 | msgid "" 125 | "Lossy JBIG2 compression can alter text in a way that is not noticeable as " 126 | "corruption (e.g. the numbers '6' and '8' get replaced)" 127 | msgstr "" 128 | "இழப்பு JBIG2 சுருக்கமானது ஊழல் என கவனிக்க முடியாத வகையில் உரையை மாற்ற முடியும் (எ.கா" 129 | ". '6' மற்றும் '8' எண்கள் மாற்றப்படுகின்றன)" 130 | 131 | #: scans2pdf_gui/qml/detail.qml:329 132 | msgid "OCR" 133 | msgstr "OCR" 134 | 135 | #: scans2pdf_gui/qml/detail.qml:348 136 | msgid "Language" 137 | msgstr "மொழி" 138 | 139 | #: scans2pdf_gui/qml/detail.qml:399 140 | msgid "All colors" 141 | msgstr "அனைத்து வண்ணங்களும்" 142 | 143 | #: scans2pdf_gui/qml/detail.qml:410 144 | msgid "Apply to all" 145 | msgstr "அனைவருக்கும் பொருந்தும்" 146 | 147 | #: scans2pdf_gui/qml/detail.qml:416 148 | msgid "Apply to following" 149 | msgstr "பின்வருவன விண்ணப்பிக்கவும்" 150 | 151 | #: scans2pdf_gui/qml/detail.qml:424 152 | msgid "Load default settings" 153 | msgstr "இயல்புநிலை அமைப்புகளை ஏற்றவும்" 154 | 155 | #: scans2pdf_gui/qml/detail.qml:430 156 | msgid "Overwrite?" 157 | msgstr "மேலெழுதவா?" 158 | 159 | #: scans2pdf_gui/qml/detail.qml:431 160 | msgid "Replace default settings?" 161 | msgstr "இயல்புநிலை அமைப்புகளை மாற்றவா?" 162 | 163 | #: scans2pdf_gui/qml/detail.qml:437 164 | msgid "Save default settings" 165 | msgstr "இயல்புநிலை அமைப்புகளைச் சேமிக்கவும்" 166 | 167 | #: scans2pdf_gui/qml/overview.qml:30 168 | msgid "Open" 169 | msgstr "திற" 170 | 171 | #: scans2pdf_gui/qml/overview.qml:32 172 | msgid "Images" 173 | msgstr "படங்கள்" 174 | 175 | #: scans2pdf_gui/qml/overview.qml:33 176 | msgid "All files" 177 | msgstr "அனைத்து கோப்புகள்" 178 | 179 | #: scans2pdf_gui/qml/overview.qml:42 180 | msgid "Save" 181 | msgstr "சேமி" 182 | 183 | #: scans2pdf_gui/qml/overview.qml:43 184 | msgid "PDF" 185 | msgstr "பி.டி.எஃப்" 186 | 187 | #: scans2pdf_gui/qml/overview.qml:75 188 | msgid "Failed to create PDF" 189 | msgstr "PDF ஐ உருவாக்குவதில் தோல்வி" 190 | 191 | #: scans2pdf_gui/qml/overview.qml:91 192 | msgid "Close" 193 | msgstr "மூடு" 194 | 195 | #: scans2pdf_gui/qml/overview.qml:108 196 | msgid "Saving…" 197 | msgstr "சேமிப்பு…" 198 | 199 | #: scans2pdf_gui/qml/overview.qml:121 200 | msgid "Cancel" 201 | msgstr "ரத்துசெய்" 202 | 203 | #: scans2pdf_gui/qml/overview.qml:140 204 | msgid "Create" 205 | msgstr "உருவாக்கு" 206 | -------------------------------------------------------------------------------- /scans2pdf_gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unrud/djpdf/e304dc337d1da218130223b3b3c9f550e286e6aa/scans2pdf_gui/__init__.py -------------------------------------------------------------------------------- /scans2pdf_gui/main.py: -------------------------------------------------------------------------------- 1 | # This file is part of djpdf. 2 | # 3 | # djpdf is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # djpdf is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with djpdf. If not, see . 15 | 16 | # Copyright 2018 Unrud 17 | 18 | import contextlib 19 | import copy 20 | import gettext 21 | import json 22 | import os 23 | import sys 24 | import tempfile 25 | from argparse import ArgumentParser 26 | 27 | from PySide6 import QtQml 28 | from PySide6.QtGui import QIcon, QImage, QImageReader 29 | from PySide6.QtCore import (Property, QAbstractListModel, QModelIndex, 30 | QObject, QProcess, QUrl, Qt, Signal, Slot) 31 | from PySide6.QtQuick import QQuickImageProvider 32 | from PySide6.QtQml import QQmlApplicationEngine 33 | from PySide6.QtWidgets import QApplication 34 | 35 | from djpdf.scans2pdf import DEFAULT_SETTINGS, find_ocr_languages 36 | 37 | if sys.version_info < (3, 9): 38 | import importlib_resources 39 | else: 40 | import importlib.resources as importlib_resources 41 | 42 | QML_RESOURCE = importlib_resources.files("scans2pdf_gui").joinpath("qml") 43 | IMAGE_FILE_EXTENSIONS = ("bmp", "gif", "jpeg", "jpg", "png", "pnm", 44 | "ppm", "pbm", "pgm", "xbm", "xpm", "tif", 45 | "tiff", "webp", "jp2") 46 | PDF_FILE_EXTENSION = "pdf" 47 | THUMBNAIL_SIZE = 256 48 | USER_SETTINGS_PATH = os.path.join( 49 | os.environ.get("XDG_CONFIG_HOME", 50 | os.path.join(os.path.expanduser("~"), ".config")), 51 | "djpdf", "default.json") 52 | 53 | 54 | class QmlPage(QObject): 55 | 56 | _BG_COMPRESSIONS = ("deflate", "jp2", "jpeg") 57 | _FG_COMPRESSIONS = ("fax", "jbig2") 58 | _OCR_LANGS = tuple(find_ocr_languages()) 59 | 60 | def __init__(self): 61 | super().__init__() 62 | self._data = copy.deepcopy(DEFAULT_SETTINGS) 63 | 64 | @Slot() 65 | def loadUserDefaults(self): 66 | self._update(DEFAULT_SETTINGS) 67 | try: 68 | with open(USER_SETTINGS_PATH) as f: 69 | user_settings = json.load(f) 70 | except FileNotFoundError: 71 | pass 72 | except (IsADirectoryError, PermissionError, 73 | UnicodeDecodeError, json.JSONDecodeError) as e: 74 | print("Failed to load settings: %r" % e, file=sys.stderr) 75 | else: 76 | self._update(user_settings) 77 | 78 | @Slot() 79 | def saveUserDefaults(self): 80 | user_defaults = {} 81 | for key, default_value in DEFAULT_SETTINGS.items(): 82 | if key == "filename": 83 | continue 84 | value = self._data[key] 85 | if value == default_value: 86 | continue 87 | user_defaults[key] = value 88 | dir = os.path.dirname(USER_SETTINGS_PATH) 89 | os.makedirs(dir, exist_ok=True) 90 | temp = tempfile.NamedTemporaryFile("w", dir=dir, delete=False) 91 | try: 92 | json.dump(user_defaults, temp) 93 | temp.close() 94 | os.replace(temp.name, USER_SETTINGS_PATH) 95 | except BaseException: 96 | temp.close() 97 | with contextlib.suppress(FileNotFoundError): 98 | os.remove(temp.name) 99 | raise 100 | 101 | def _update(self, d): 102 | def log_settings_error(key=None): 103 | if key is None: 104 | print("Invalid settings: %r" % d, file=sys.stderr) 105 | return 106 | value = d.get(key) 107 | if value is None: 108 | return 109 | print("invalid settings [%r]: %r" % (key, value), file=sys.stderr) 110 | if not isinstance(d, dict): 111 | log_settings_error() 112 | return 113 | 114 | def set_(key, signal, value): 115 | self._data[key] = value 116 | signal.emit() 117 | 118 | dpi = d.get("dpi") 119 | if dpi == "auto": 120 | set_("dpi", self.dpiChanged, dpi) 121 | elif isinstance(dpi, int): 122 | set_("dpi", self.dpiChanged, max(1, dpi)) 123 | else: 124 | log_settings_error("dpi") 125 | bg_color = d.get("bg_color") 126 | if (isinstance(bg_color, (list, tuple)) and len(bg_color) == 3 and 127 | all(isinstance(v, int) for v in bg_color)): 128 | set_("bg_color", self.bgColorChanged, 129 | tuple(max(0, min(0xff, v)) for v in bg_color)) 130 | else: 131 | log_settings_error("bg_color") 132 | bg_enabled = d.get("bg_enabled") 133 | if isinstance(bg_enabled, bool): 134 | set_("bg_enabled", self.bgChanged, bg_enabled) 135 | else: 136 | log_settings_error("bg_enabled") 137 | bg_resize = d.get("bg_resize") 138 | if isinstance(bg_resize, (float, int)): 139 | set_("bg_resize", self.bgResizeChanged, 140 | max(0.01, min(1, bg_resize))) 141 | else: 142 | log_settings_error("bg_resize") 143 | bg_compression = d.get("bg_compression") 144 | if bg_compression in self._BG_COMPRESSIONS: 145 | set_("bg_compression", self.bgCompressionChanged, bg_compression) 146 | else: 147 | log_settings_error("bg_compression") 148 | bg_quality = d.get("bg_quality") 149 | if isinstance(bg_quality, int): 150 | set_("bg_quality", self.bgQualityChanged, 151 | max(1, min(100, bg_quality))) 152 | else: 153 | log_settings_error("bg_quality") 154 | fg_enabled = d.get("fg_enabled") 155 | if isinstance(fg_enabled, bool): 156 | set_("fg_enabled", self.fgChanged, fg_enabled) 157 | else: 158 | log_settings_error("fg_enabled") 159 | fg_colors = d.get("fg_colors") 160 | if (isinstance(fg_colors, list) and all( 161 | isinstance(c, (list, tuple)) and len(c) == 3 162 | and all(isinstance(v, int) for v in c) for c in fg_colors)): 163 | set_("fg_colors", self.fgColorsChanged, 164 | [tuple(max(0, min(0xff, v)) for v in c) for c in fg_colors]) 165 | else: 166 | log_settings_error("fg_colors") 167 | fg_compression = d.get("fg_compression") 168 | if fg_compression in self._FG_COMPRESSIONS: 169 | set_("fg_compression", self.fgCompressionChanged, fg_compression) 170 | else: 171 | log_settings_error("fg_compression") 172 | fg_jbig2_threshold = d.get("fg_jbig2_threshold") 173 | if isinstance(fg_jbig2_threshold, (float, int)): 174 | set_("fg_jbig2_threshold", self.fgJbig2ThresholdChanged, 175 | 1 if fg_jbig2_threshold >= 1 176 | else max(0.4, min(0.9, fg_jbig2_threshold))) 177 | else: 178 | log_settings_error("fg_jbig2_threshold") 179 | ocr_enabled = d.get("ocr_enabled") 180 | if isinstance(ocr_enabled, bool): 181 | set_("ocr_enabled", self.ocrChanged, ocr_enabled) 182 | else: 183 | log_settings_error("ocr_enabled") 184 | ocr_language = d.get("ocr_language") 185 | if ocr_language in self._OCR_LANGS: 186 | set_("ocr_language", self.ocrLangChanged, ocr_language) 187 | else: 188 | log_settings_error("ocr_language") 189 | ocr_colors = d.get("ocr_colors") 190 | if ocr_colors == "all": 191 | set_("ocr_colors", self.ocrColorsChanged, ocr_colors) 192 | elif (isinstance(ocr_colors, list) and all( 193 | isinstance(c, (list, tuple)) and len(c) == 3 and 194 | all(isinstance(v, int) for v in c) for c in ocr_colors)): 195 | set_("ocr_colors", self.ocrColorsChanged, 196 | [tuple(max(0, min(0xff, v)) for v in c) for c in ocr_colors]) 197 | else: 198 | log_settings_error("ocr_colors") 199 | 200 | def apply_page_settings(self, qml_page): 201 | self._update(qml_page._data) 202 | 203 | urlChanged = Signal() 204 | 205 | def readUrl(self): 206 | return QUrl.fromLocalFile(self._data["filename"]) 207 | 208 | def setUrl(self, val): 209 | self._data["filename"] = val.toLocalFile() 210 | self.urlChanged.emit() 211 | 212 | url = Property("QUrl", readUrl, setUrl, notify=urlChanged) 213 | 214 | @Property(str, notify=urlChanged) 215 | def displayName(self): 216 | return os.path.basename(self._data["filename"]) 217 | 218 | dpiChanged = Signal() 219 | 220 | def readDpi(self): 221 | val = self._data["dpi"] 222 | if val == "auto": 223 | return 0 224 | return val 225 | 226 | def setDpi(self, val): 227 | self._update({"dpi": "auto" if val == 0 else val}) 228 | 229 | dpi = Property(int, readDpi, setDpi, notify=dpiChanged) 230 | 231 | bgColorChanged = Signal() 232 | 233 | def readBgColor(self): 234 | return "#%02x%02x%02x" % self._data["bg_color"] 235 | return self._bgColor 236 | 237 | def setBgColor(self, val): 238 | assert val[0] == "#" and len(val) == 7 239 | self._update({"bg_color": ( 240 | int(val[1:3], 16), int(val[3:5], 16), int(val[5:], 16))}) 241 | 242 | bgColor = Property(str, readBgColor, setBgColor, notify=bgColorChanged) 243 | 244 | bgChanged = Signal() 245 | 246 | def readBg(self): 247 | return self._data["bg_enabled"] 248 | 249 | def setBg(self, val): 250 | self._update({"bg_enabled": val}) 251 | 252 | bg = Property(bool, readBg, setBg, notify=bgChanged) 253 | 254 | bgResizeChanged = Signal() 255 | 256 | def readBgResize(self): 257 | return self._data["bg_resize"] 258 | 259 | def setBgResize(self, val): 260 | self._update({"bg_resize": val}) 261 | 262 | bgResize = Property(float, readBgResize, setBgResize, 263 | notify=bgResizeChanged) 264 | 265 | bgCompressionsChanged = Signal() 266 | 267 | @Property("QStringList", notify=bgCompressionsChanged) 268 | def bgCompressions(self): 269 | return self._BG_COMPRESSIONS 270 | 271 | bgCompressionChanged = Signal() 272 | 273 | def readBgCompression(self): 274 | return self._data["bg_compression"] 275 | 276 | def setBgCompression(self, val): 277 | self._update({"bg_compression": val}) 278 | 279 | bgCompression = Property(str, readBgCompression, setBgCompression, 280 | notify=bgCompressionChanged) 281 | 282 | bgQualityChanged = Signal() 283 | 284 | def readBgQuality(self): 285 | return self._data["bg_quality"] 286 | 287 | def setBgQuality(self, val): 288 | self._update({"bg_quality": val}) 289 | 290 | bgQuality = Property(int, readBgQuality, setBgQuality, 291 | notify=bgQualityChanged) 292 | 293 | fgChanged = Signal() 294 | 295 | def readFg(self): 296 | return self._data["fg_enabled"] 297 | 298 | def setFg(self, val): 299 | self._update({"fg_enabled": val}) 300 | 301 | fg = Property(bool, readFg, setFg, notify=fgChanged) 302 | 303 | fgColorsChanged = Signal() 304 | 305 | @Property("QStringList", notify=fgColorsChanged) 306 | def fgColors(self): 307 | return ["#%02x%02x%02x" % c for c in self._data["fg_colors"]] 308 | 309 | @Slot(str) 310 | def addFgColor(self, val): 311 | assert val[0] == "#" and len(val) == 7 312 | self._update({"fg_colors": [ 313 | *self._data["fg_colors"], 314 | (int(val[1:3], 16), int(val[3:5], 16), int(val[5:], 16))]}) 315 | 316 | @Slot(int) 317 | def removeFgColor(self, index): 318 | self._update({"fg_colors": [ 319 | *self._data["fg_colors"][:index], 320 | *self._data["fg_colors"][index+1:]]}) 321 | 322 | @Slot(int, str) 323 | def changeFgColor(self, index, val): 324 | assert val[0] == "#" and len(val) == 7 325 | self._update({"fg_colors": [ 326 | *self._data["fg_colors"][:index], 327 | (int(val[1:3], 16), int(val[3:5], 16), int(val[5:], 16)), 328 | *self._data["fg_colors"][index+1:]]}) 329 | 330 | fgCompressionsChanged = Signal() 331 | 332 | @Property("QStringList", notify=fgCompressionsChanged) 333 | def fgCompressions(self): 334 | return self._FG_COMPRESSIONS 335 | 336 | fgCompressionChanged = Signal() 337 | 338 | def readFgCompression(self): 339 | return self._data["fg_compression"] 340 | 341 | def setFgCompression(self, val): 342 | self._update({"fg_compression": val}) 343 | 344 | fgCompression = Property(str, readFgCompression, setFgCompression, 345 | notify=fgCompressionChanged) 346 | 347 | fgJbig2ThresholdChanged = Signal() 348 | 349 | def readFgJbig2Threshold(self): 350 | return self._data["fg_jbig2_threshold"] 351 | 352 | def setFgJbig2Threshold(self, val): 353 | self._update({"fg_jbig2_threshold": val}) 354 | 355 | fgJbig2Threshold = Property(float, readFgJbig2Threshold, 356 | setFgJbig2Threshold, 357 | notify=fgJbig2ThresholdChanged) 358 | 359 | ocrChanged = Signal() 360 | 361 | def readOcr(self): 362 | return self._data["ocr_enabled"] 363 | 364 | def setOcr(self, val): 365 | self._update({"ocr_enabled": val}) 366 | 367 | ocr = Property(bool, readOcr, setOcr, notify=ocrChanged) 368 | 369 | ocrLangsChanged = Signal() 370 | 371 | @Property("QStringList", notify=ocrLangsChanged) 372 | def ocrLangs(self): 373 | return self._OCR_LANGS 374 | 375 | ocrLangChanged = Signal() 376 | 377 | def readOcrLang(self): 378 | return self._data["ocr_language"] 379 | 380 | def setOcrLang(self, val): 381 | self._update({"ocr_language": val}) 382 | 383 | ocrLang = Property(str, readOcrLang, setOcrLang, notify=ocrLangChanged) 384 | 385 | ocrColorsChanged = Signal() 386 | 387 | @Property("QStringList", notify=ocrColorsChanged) 388 | def ocrColors(self): 389 | if self._data["ocr_colors"] == "all": 390 | return [] 391 | return ["#%02x%02x%02x" % c for c in self._data["ocr_colors"]] 392 | 393 | @Slot(str) 394 | def addOcrColor(self, val): 395 | ocr_colors = self._data["ocr_colors"] 396 | if ocr_colors == "all": 397 | ocr_colors = [] 398 | self._update({"ocr_colors": [ 399 | *ocr_colors, 400 | (int(val[1:3], 16), int(val[3:5], 16), int(val[5:], 16))]}) 401 | 402 | @Slot(int) 403 | def removeOcrColor(self, index): 404 | self._update({"ocr_colors": [ 405 | *self._data["ocr_colors"][:index], 406 | *self._data["ocr_colors"][index+1:]] or "all"}) 407 | 408 | @Slot(int, str) 409 | def changeOcrColor(self, index, val): 410 | self._update({"ocr_colors": [ 411 | *self._data["ocr_colors"][:index], 412 | (int(val[1:3], 16), int(val[3:5], 16), int(val[5:], 16)), 413 | *self._data["ocr_colors"][index+1:]]}) 414 | 415 | 416 | class QmlPagesModel(QAbstractListModel): 417 | 418 | _MODEL_DATA_ROLE = Qt.UserRole + 1 419 | 420 | def __init__(self, verbose=False, parent=None): 421 | super().__init__(parent) 422 | self._pages = [] 423 | self._process = None 424 | self._process_canceled = False 425 | self._saving = False 426 | self._savingProgress = 0 427 | self._verbose = verbose 428 | 429 | def roleNames(self): 430 | return { 431 | self._MODEL_DATA_ROLE: b"modelData" 432 | } 433 | 434 | def rowCount(self, index): 435 | return len(self._pages) 436 | 437 | def data(self, index, role): 438 | if not index.isValid(): 439 | return None 440 | if role == self._MODEL_DATA_ROLE: 441 | return self._pages[index.row()] 442 | return None 443 | 444 | countChanged = Signal() 445 | 446 | @Property(int, notify=countChanged) 447 | def count(self): 448 | return len(self._pages) 449 | 450 | @Slot("QList") 451 | def extend(self, urls): 452 | if not urls: 453 | return 454 | self.beginInsertRows(QModelIndex(), len(self._pages), 455 | len(self._pages) + len(urls) - 1) 456 | 457 | def create_page(url): 458 | p = QmlPage() 459 | p.loadUserDefaults() 460 | p.url = url 461 | return p 462 | self._pages.extend(map(create_page, urls)) 463 | 464 | self.endInsertRows() 465 | self.countChanged.emit() 466 | 467 | @Slot(int, int) 468 | def move(self, from_index, to_index): 469 | if from_index == to_index: 470 | return 471 | move_rows_dest_index = to_index 472 | if from_index < to_index: 473 | move_rows_dest_index += 1 474 | self.beginMoveRows(QModelIndex(), from_index, from_index, 475 | QModelIndex(), move_rows_dest_index) 476 | self._pages.insert(to_index, self._pages.pop(from_index)) 477 | self.endMoveRows() 478 | 479 | @Slot(int) 480 | def remove(self, index): 481 | self.beginRemoveRows(QModelIndex(), index, index) 482 | self._pages.pop(index) 483 | self.endRemoveRows() 484 | self.countChanged.emit() 485 | 486 | @Slot(QmlPage) 487 | def applyToAll(self, qml_page): 488 | for p in self._pages: 489 | if qml_page is not p: 490 | p.apply_page_settings(qml_page) 491 | 492 | @Slot(int, QmlPage) 493 | def applyToFollowing(self, index, qml_page): 494 | for p in self._pages[index:]: 495 | if qml_page is not p: 496 | p.apply_page_settings(qml_page) 497 | 498 | savingError = Signal(str) 499 | 500 | savingChanged = Signal() 501 | 502 | @Property(bool, notify=savingChanged) 503 | def saving(self): 504 | return self._saving 505 | 506 | savingProgressChanged = Signal() 507 | 508 | @Property(float, notify=savingProgressChanged) 509 | def savingProgress(self): 510 | return self._savingProgress 511 | 512 | savingCancelableChanged = Signal() 513 | 514 | @Property(bool, notify=savingCancelableChanged) 515 | def savingCancelable(self): 516 | return bool(self._process and not self._process_canceled) 517 | 518 | @Slot() 519 | def cancelSaving(self): 520 | if self.savingCancelable: 521 | self._process.terminate() 522 | self._process_canceled = True 523 | self.savingCancelableChanged.emit() 524 | 525 | @Slot("QUrl") 526 | def save(self, url): 527 | self._saving = True 528 | self.savingChanged.emit() 529 | self._savingProgress = 0 530 | self.savingProgressChanged.emit() 531 | self._process_canceled = False 532 | self.savingCancelableChanged.emit() 533 | p = QProcess(self) 534 | p.setProcessChannelMode(QProcess.SeparateChannels) 535 | 536 | stdout_buffer = b"" 537 | stderr_buffer = b"" 538 | 539 | def ready_read_stdout(): 540 | nonlocal stdout_buffer 541 | stdout_buffer += p.readAllStandardOutput().data() 542 | *messages, stdout_buffer = stdout_buffer.split(b"\n") 543 | for message in messages: 544 | progress = json.loads(messages[-1].decode(sys.stdout.encoding)) 545 | self._savingProgress = progress["fraction"] 546 | self.savingProgressChanged.emit() 547 | 548 | def ready_read_stderr(): 549 | nonlocal stderr_buffer 550 | stderr_data = p.readAllStandardError().data() 551 | stderr_buffer += stderr_data 552 | sys.stderr.buffer.write(stderr_data) 553 | sys.stderr.buffer.flush() 554 | 555 | def process_finished(status): 556 | self._process = None 557 | self._saving = False 558 | self.savingChanged.emit() 559 | self.savingCancelableChanged.emit() 560 | if not self._process_canceled and status != 0: 561 | message = stderr_buffer.decode(sys.stderr.encoding, 562 | sys.stderr.errors) 563 | self.savingError.emit(message) 564 | 565 | p.readyReadStandardOutput.connect(ready_read_stdout) 566 | p.readyReadStandardError.connect(ready_read_stderr) 567 | p.finished.connect(process_finished) 568 | args = ["-c", "from djpdf.scans2pdf import main; main()", 569 | os.path.abspath(url.toLocalFile())] 570 | if self._verbose: 571 | args.append("--verbose") 572 | p.start(sys.executable, args) 573 | self._process = p 574 | self.savingCancelableChanged.emit() 575 | p.write(json.dumps([p._data for p in self._pages]).encode()) 576 | p.closeWriteChannel() 577 | 578 | pdfFileExtensionChanged = Signal() 579 | 580 | @Property(str, notify=pdfFileExtensionChanged) 581 | def pdfFileExtension(self): 582 | return PDF_FILE_EXTENSION 583 | 584 | imageFileExtensionsChanged = Signal() 585 | 586 | @Property("QStringList", notify=imageFileExtensionsChanged) 587 | def imageFileExtensions(self): 588 | return IMAGE_FILE_EXTENSIONS 589 | 590 | def shutdown(self): 591 | if self._process: 592 | self._process.terminate() 593 | self._process.waitForFinished(-1) 594 | 595 | 596 | class ThumbnailImageProvider(QQuickImageProvider): 597 | 598 | def __init__(self): 599 | super().__init__(QQuickImageProvider.Image) 600 | 601 | def requestImage(self, url, size, requestedSize): 602 | url = QUrl(url) 603 | image = QImage(url.toLocalFile()) 604 | width, height = image.width(), image.height() 605 | if size: 606 | size.setWidth(width) 607 | size.setHeight(height) 608 | if requestedSize.width() > 0: 609 | width = requestedSize.width() 610 | if requestedSize.height() > 0: 611 | height = requestedSize.height() 612 | return image.scaled(min(width, THUMBNAIL_SIZE), 613 | min(height, THUMBNAIL_SIZE), Qt.KeepAspectRatio) 614 | 615 | 616 | class JsBridge(QObject): 617 | 618 | @Slot(str, result=str) 619 | def gettext(self, s): 620 | return gettext.gettext(s) 621 | 622 | 623 | def main(): 624 | parser = ArgumentParser() 625 | parser.add_argument("-v", "--verbose", help="increase output verbosity", 626 | action="store_true") 627 | args = parser.parse_args() 628 | QtQml.qmlRegisterType(QmlPage, "djpdf", 1, 0, "DjpdfPage") 629 | app = QApplication([]) 630 | app.setWindowIcon(QIcon.fromTheme("com.github.unrud.djpdf")) 631 | engine = QQmlApplicationEngine() 632 | thumbnail_image_provider = ThumbnailImageProvider() 633 | # Disable image memory limit to be able to load thumbnails for large images 634 | QImageReader.setAllocationLimit(0) 635 | engine.addImageProvider("thumbnails", thumbnail_image_provider) 636 | jsBridge = JsBridge() 637 | jsBridgeObj = engine.newQObject(jsBridge) 638 | ctx = engine.rootContext() 639 | engine.globalObject().setProperty("N_", jsBridgeObj.property("gettext")) 640 | pages_model = QmlPagesModel(verbose=args.verbose) 641 | try: 642 | ctx.setContextProperty("pagesModel", pages_model) 643 | with importlib_resources.as_file(QML_RESOURCE) as qml_dir: 644 | engine.load(QUrl.fromLocalFile( 645 | os.path.join(os.fspath(qml_dir), "main.qml"))) 646 | return app.exec_() 647 | finally: 648 | pages_model.shutdown() 649 | -------------------------------------------------------------------------------- /scans2pdf_gui/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name() 2 | moduledir = pkgdatadir / 'scans2pdf_gui' 3 | 4 | conf = configuration_data() 5 | conf.set('PYTHON', python.full_path()) 6 | conf.set_quoted('localedir', get_option('prefix') / get_option('localedir')) 7 | conf.set_quoted('pkgdatadir', pkgdatadir) 8 | 9 | scans2pdf_gui_bin = configure_file( 10 | input: 'scans2pdf-gui.in', 11 | output: 'scans2pdf-gui', 12 | configuration: conf, 13 | install: true, 14 | install_dir: get_option('bindir'), 15 | install_mode: 'r-xr-xr-x', 16 | ) 17 | 18 | scans2pdf_gui_sources = files([ 19 | '__init__.py', 20 | 'main.py', 21 | ]) 22 | 23 | install_data(scans2pdf_gui_sources, install_dir: moduledir) 24 | 25 | 26 | subdir('qml') 27 | -------------------------------------------------------------------------------- /scans2pdf_gui/qml/detail.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of djpdf. 3 | * 4 | * djpdf is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Foobar is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with djpdf. If not, see . 16 | * 17 | * Copyright 2018 Unrud 18 | */ 19 | 20 | import QtQuick 21 | import QtQuick.Controls 22 | import QtQuick.Layouts 23 | import QtQuick.Dialogs 24 | import djpdf 25 | 26 | Page { 27 | id: sv 28 | property int modelIndex: 0 29 | property DjpdfPage p: DjpdfPage{} 30 | property real leftColumnWidth: Math.max( 31 | l1.implicitWidth, l2.implicitWidth, l3.implicitWidth, l4.implicitWidth, 32 | l5.implicitWidth, l6.implicitWidth, l7.implicitWidth, l8.implicitWidth, 33 | l9.implicitWidth, l10.implicitWidth) 34 | 35 | header: ToolBar { 36 | RowLayout { 37 | anchors.fill: parent 38 | ToolButton { 39 | text: "‹" 40 | onClicked: { stack.pop() } 41 | } 42 | ToolButton { 43 | text: N_("Remove") 44 | highlighted: true 45 | onClicked: { 46 | stack.pop() 47 | pagesModel.remove(sv.modelIndex) 48 | } 49 | } 50 | Label { 51 | text: sv.p.displayName 52 | font.bold: true 53 | elide: Text.ElideMiddle 54 | textFormat: Text.PlainText 55 | horizontalAlignment: Text.AlignHCenter 56 | leftPadding: 5 57 | rightPadding: 5 + x 58 | Layout.fillWidth: true 59 | } 60 | } 61 | } 62 | 63 | ColorDialog { 64 | property var callback: null 65 | id: colorDialog 66 | title: N_("Please choose a color") 67 | onAccepted: { 68 | const t = callback 69 | callback = null 70 | t?.(selectedColor) 71 | } 72 | onRejected: { callback = null } 73 | function show(callback, color) { 74 | this.callback = callback 75 | selectedColor = color 76 | open() 77 | } 78 | } 79 | 80 | ScrollView { 81 | anchors.fill: parent 82 | padding: 5 83 | contentWidth: availableWidth 84 | // WORKAROUND: Update contentHeight manually 85 | contentHeight: contentChildren.reduce((acc, c) => Math.max(acc, c.implicitHeight), 0) 86 | 87 | ColumnLayout { 88 | anchors.fill: parent 89 | 90 | Pane { 91 | Layout.fillWidth: true 92 | ColumnLayout { 93 | anchors.fill: parent 94 | RowLayout { 95 | Label { 96 | id: l1 97 | Layout.preferredWidth: sv.leftColumnWidth 98 | text: N_("DPI:") 99 | } 100 | TextField { 101 | Layout.fillWidth: true 102 | selectByMouse: true 103 | placeholderText: N_("auto") 104 | text: sv.p.dpi !== 0 ? sv.p.dpi : "" 105 | validator: RegularExpressionValidator { regularExpression: /[0-9]*/ } 106 | onEditingFinished: { sv.p.dpi = text === "" ? 0 : parseInt(text) } 107 | } 108 | } 109 | RowLayout { 110 | Label { 111 | id: l2 112 | Layout.preferredWidth: sv.leftColumnWidth 113 | text: N_("Background color:") 114 | } 115 | Button { 116 | Layout.fillWidth: true 117 | onClicked: { colorDialog.show((color) => { sv.p.bgColor = color }, sv.p.bgColor) } 118 | Rectangle { 119 | color: parent.enabled ? paletteActive.buttonText : paletteDisabled.buttonText 120 | anchors.fill: parent 121 | anchors.margins: 5 122 | Rectangle { 123 | color: sv.p.bgColor 124 | anchors.fill: parent 125 | anchors.margins: 1 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | GroupBox { 134 | Layout.fillWidth: true 135 | label: RowLayout { 136 | width: parent.width 137 | Label { 138 | text: N_("Background") 139 | font.bold: true 140 | } 141 | Switch { 142 | Layout.alignment: Qt.AlignVCenter | Qt.AlignRight 143 | checked: sv.p.bg 144 | onToggled: { sv.p.bg = checked && true } 145 | } 146 | } 147 | 148 | ColumnLayout { 149 | enabled: sv.p.bg 150 | anchors.fill: parent 151 | RowLayout { 152 | Label { 153 | id: l3 154 | Layout.preferredWidth: sv.leftColumnWidth 155 | text: N_("Resize:") 156 | } 157 | SpinBox { 158 | Layout.fillWidth: true 159 | editable: true 160 | from: 1 161 | to: 100 162 | onValueModified: { sv.p.bgResize = value / 100 } 163 | value: Number(sv.p.bgResize * 100).toFixed(0) 164 | textFromValue: (value, locale) => `${Number(value).toLocaleString(locale, "f", 0)}%` 165 | valueFromText: (text, locale) => Number.fromLocaleString(locale, text.replace(/%$/, "")) 166 | } 167 | } 168 | RowLayout { 169 | Label { 170 | id: l4 171 | Layout.preferredWidth: sv.leftColumnWidth 172 | text: N_("Compression:") 173 | } 174 | ComboBox { 175 | Layout.fillWidth: true 176 | id: bgCompressionComboBox 177 | model: sv.p.bgCompressions 178 | Component.onCompleted: { currentIndex = indexOfValue(sv.p.bgCompression) } 179 | onActivated: { sv.p.bgCompression = currentValue } 180 | Connections { 181 | target: sv.p 182 | function onBgCompressionChanged() { 183 | bgCompressionComboBox.currentIndex = bgCompressionComboBox.indexOfValue(sv.p.bgCompression) 184 | } 185 | } 186 | } 187 | } 188 | RowLayout { 189 | enabled: sv.p.bgCompression === "jp2" || sv.p.bgCompression === "jpeg" 190 | Label { 191 | id: l5 192 | Layout.preferredWidth: sv.leftColumnWidth 193 | text: N_("Quality:") 194 | } 195 | SpinBox { 196 | Layout.fillWidth: true 197 | editable: true 198 | from: 1 199 | to: 100 200 | onValueModified: { sv.p.bgQuality = value } 201 | value: sv.p.bgQuality 202 | } 203 | } 204 | } 205 | } 206 | 207 | GroupBox { 208 | Layout.fillWidth: true 209 | label: RowLayout { 210 | width: parent.width 211 | Label { 212 | text: N_("Foreground") 213 | font.bold: true 214 | } 215 | Switch { 216 | Layout.alignment: Qt.AlignVCenter | Qt.AlignRight 217 | checked: sv.p.fg 218 | onToggled: { sv.p.fg = checked && true } 219 | } 220 | } 221 | 222 | ColumnLayout { 223 | anchors.fill: parent 224 | enabled: sv.p.fg 225 | RowLayout { 226 | Label { 227 | id: l6 228 | Layout.preferredWidth: sv.leftColumnWidth 229 | text: N_("Colors:") 230 | } 231 | ColumnLayout { 232 | Layout.fillWidth: true 233 | Repeater { 234 | model: sv.p.fgColors 235 | RowLayout { 236 | Layout.fillWidth: true 237 | Button { 238 | Layout.fillWidth: true 239 | onClicked: { colorDialog.show((color) => { sv.p.changeFgColor(index, color) }, sv.p.fgColors[index]) } 240 | Rectangle { 241 | color: parent.enabled ? paletteActive.buttonText : paletteDisabled.buttonText 242 | anchors.fill: parent 243 | anchors.margins: 5 244 | Rectangle { 245 | color: modelData 246 | anchors.fill: parent 247 | anchors.margins: 1 248 | } 249 | } 250 | } 251 | Button { 252 | Layout.fillWidth: true 253 | text: N_("Remove") 254 | onClicked: { sv.p.removeFgColor(index) } 255 | } 256 | } 257 | } 258 | Button { 259 | Layout.fillWidth: true 260 | text: sv.p.fgColors.length === 0 ? N_("No colors") : N_("Add") 261 | onClicked: { colorDialog.show(sv.p.addFgColor, "#ffffff") } 262 | } 263 | } 264 | } 265 | RowLayout { 266 | Label { 267 | id: l7 268 | Layout.preferredWidth: sv.leftColumnWidth 269 | text: N_("Compression:") 270 | } 271 | ComboBox { 272 | Layout.fillWidth: true 273 | id: fgCompressionComboBox 274 | model: sv.p.fgCompressions 275 | Component.onCompleted: currentIndex = indexOfValue(sv.p.fgCompression) 276 | onActivated: { sv.p.fgCompression = currentValue } 277 | Connections { 278 | target: sv.p 279 | function onFgCompressionChanged() { 280 | fgCompressionComboBox.currentIndex = fgCompressionComboBox.indexOfValue(sv.p.fgCompression) 281 | } 282 | } 283 | } 284 | } 285 | RowLayout { 286 | enabled: sv.p.fgCompression === "jbig2" 287 | Label { 288 | id: l8 289 | Layout.preferredWidth: sv.leftColumnWidth 290 | text: N_("JBIG2 Threshold:") 291 | } 292 | SpinBox { 293 | Layout.fillWidth: true 294 | editable: true 295 | from: 40 296 | to: 100 297 | onValueModified: { 298 | let newValue = value / 100 299 | if (0.9 < newValue && newValue < 1) { 300 | newValue = sv.p.fgJbig2Threshold < newValue ? 1 : 0.9 301 | } 302 | sv.p.fgJbig2Threshold = newValue 303 | } 304 | value: Number(sv.p.fgJbig2Threshold * 100).toFixed(0) 305 | textFromValue: (value, locale) => `${Number(value).toLocaleString(locale, "f", 0)}%` 306 | valueFromText: (text, locale) => Number.fromLocaleString(locale, text.replace(/%$/, "")) 307 | } 308 | } 309 | ColumnLayout { 310 | visible: sv.p.fgCompression === "jbig2" && sv.p.fgJbig2Threshold < 1 311 | Label { 312 | Layout.fillWidth: true 313 | font.bold: true 314 | text: N_("Warning") 315 | } 316 | Label { 317 | Layout.fillWidth: true 318 | wrapMode: Label.Wrap 319 | text: N_("Lossy JBIG2 compression can alter text in a way that is not noticeable as corruption (e.g. the numbers '6' and '8' get replaced)") 320 | } 321 | } 322 | } 323 | } 324 | 325 | GroupBox { 326 | label: RowLayout { 327 | width: parent.width 328 | Label { 329 | text: N_("OCR") 330 | font.bold: true 331 | } 332 | Switch { 333 | Layout.alignment: Qt.AlignVCenter | Qt.AlignRight 334 | checked: sv.p.ocr 335 | onToggled: { sv.p.ocr = checked && true } 336 | } 337 | } 338 | Layout.fillWidth: true 339 | 340 | ColumnLayout { 341 | anchors.fill: parent 342 | enabled: sv.p.ocr 343 | RowLayout { 344 | Layout.fillWidth: true 345 | Label { 346 | id: l9 347 | Layout.preferredWidth: sv.leftColumnWidth 348 | text: N_("Language") 349 | } 350 | ComboBox { 351 | Layout.fillWidth: true 352 | id: ocrLangComboBox 353 | model: sv.p.ocrLangs 354 | Component.onCompleted: currentIndex = indexOfValue(sv.p.ocrLang) 355 | onActivated: { sv.p.ocrLang = currentValue } 356 | Connections { 357 | target: sv.p 358 | function onOcrLangChanged() { 359 | ocrLangComboBox.currentIndex = ocrLangComboBox.indexOfValue(sv.p.ocrLang) 360 | } 361 | } 362 | } 363 | } 364 | RowLayout { 365 | Layout.fillWidth: true 366 | Label { 367 | id: l10 368 | Layout.preferredWidth: sv.leftColumnWidth 369 | text: N_("Colors:") 370 | } 371 | ColumnLayout { 372 | Layout.fillWidth: true 373 | Repeater { 374 | model: sv.p.ocrColors 375 | RowLayout { 376 | Button { 377 | Layout.fillWidth: true 378 | onClicked: { colorDialog.show((color) => { sv.p.changeOcrColor(index, color) }, sv.p.ocrColors[index]) } 379 | Rectangle { 380 | color: parent.enabled ? paletteActive.buttonText : paletteDisabled.buttonText 381 | anchors.fill: parent 382 | anchors.margins: 5 383 | Rectangle { 384 | color: modelData 385 | anchors.fill: parent 386 | anchors.margins: 1 387 | } 388 | } 389 | } 390 | Button { 391 | Layout.fillWidth: true 392 | text: N_("Remove") 393 | onClicked: { sv.p.removeOcrColor(index) } 394 | } 395 | } 396 | } 397 | Button { 398 | Layout.fillWidth: true 399 | text: sv.p.ocrColors.length === 0 ? N_("All colors") : N_("Add") 400 | onClicked: { colorDialog.show(sv.p.addOcrColor, "#ffffff") } 401 | } 402 | } 403 | } 404 | } 405 | } 406 | RowLayout { 407 | Button { 408 | Layout.fillWidth: true 409 | Layout.preferredWidth: (parent.width-parent.spacing) / 2 410 | text: N_("Apply to all") 411 | onClicked: { pagesModel.applyToAll(sv.p) } 412 | } 413 | Button { 414 | Layout.fillWidth: true 415 | Layout.preferredWidth: (parent.width-parent.spacing) / 2 416 | text: N_("Apply to following") 417 | onClicked: { pagesModel.applyToFollowing(sv.modelIndex, sv.p) } 418 | } 419 | } 420 | RowLayout { 421 | Button { 422 | Layout.fillWidth: true 423 | Layout.preferredWidth: (parent.width-parent.spacing) / 2 424 | text: N_("Load default settings") 425 | onClicked: { sv.p.loadUserDefaults() } 426 | } 427 | Button { 428 | MessageDialog { 429 | id: saveUserDefaultsDialog 430 | title: N_("Overwrite?") 431 | text: N_("Replace default settings?") 432 | buttons: Dialog.Yes | Dialog.No 433 | onAccepted: { sv.p.saveUserDefaults() } 434 | } 435 | Layout.fillWidth: true 436 | Layout.preferredWidth: (parent.width-parent.spacing) / 2 437 | text: N_("Save default settings") 438 | onClicked: { saveUserDefaultsDialog.open() } 439 | } 440 | } 441 | } 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /scans2pdf_gui/qml/main.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of djpdf. 3 | * 4 | * djpdf is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Foobar is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with djpdf. If not, see . 16 | * 17 | * Copyright 2018 Unrud 18 | */ 19 | 20 | import QtQuick 21 | import QtQuick.Controls 22 | 23 | ApplicationWindow { 24 | SystemPalette { id: paletteActive; colorGroup: SystemPalette.Active } 25 | SystemPalette { id: paletteDisabled; colorGroup: SystemPalette.Disabled } 26 | visible: true 27 | width: 640 28 | height: 480 29 | title: N_("Scans to PDF") 30 | 31 | StackView { 32 | id: stack 33 | initialItem: "overview.qml" 34 | anchors.fill: parent 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scans2pdf_gui/qml/meson.build: -------------------------------------------------------------------------------- 1 | scans2pdf_gui_qml = files([ 2 | 'detail.qml', 3 | 'main.qml', 4 | 'overview.qml', 5 | ]) 6 | 7 | install_data(scans2pdf_gui_qml, install_dir: moduledir / 'qml') 8 | -------------------------------------------------------------------------------- /scans2pdf_gui/qml/overview.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of djpdf. 3 | * 4 | * djpdf is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Foobar is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with djpdf. If not, see . 16 | * 17 | * Copyright 2018 Unrud 18 | */ 19 | 20 | import QtCore 21 | import QtQuick 22 | import QtQuick.Controls 23 | import QtQuick.Layouts 24 | import QtQuick.Dialogs 25 | import djpdf 26 | 27 | Page { 28 | FileDialog { 29 | id: openDialog 30 | title: N_("Open") 31 | nameFilters: [ 32 | `${N_("Images")} (${pagesModel.imageFileExtensions.map((s) => `*.${s}`).join(" ")})`, 33 | `${N_("All files")} (*)`, 34 | ] 35 | fileMode: FileDialog.OpenFiles 36 | currentFolder: StandardPaths.writableLocation(StandardPaths.HomeLocation) 37 | onAccepted: { pagesModel.extend(selectedFiles) } 38 | } 39 | 40 | FileDialog { 41 | id: saveDialog 42 | title: N_("Save") 43 | nameFilters: [ `${N_("PDF")} (*.${pagesModel.pdfFileExtension})` ] 44 | fileMode: FileDialog.SaveFile 45 | currentFolder: StandardPaths.writableLocation(StandardPaths.HomeLocation) 46 | onAccepted: { pagesModel.save(selectedFile) } 47 | } 48 | 49 | Connections { 50 | target: pagesModel 51 | function onSavingError(message) { 52 | errorDialog.show(message) 53 | } 54 | } 55 | 56 | Popup { 57 | property string text 58 | 59 | id: errorDialog 60 | parent: stack 61 | anchors.centerIn: parent 62 | modal: true 63 | focus: true 64 | width: parent.width * 0.8 65 | height: Math.min(parent.height * 0.8, implicitHeight) 66 | closePolicy: Popup.CloseOnEscape 67 | onClosed: { text = "" } 68 | function show(text) { 69 | this.text = text 70 | open() 71 | } 72 | ColumnLayout { 73 | anchors.fill: parent 74 | Label { 75 | text: N_("Failed to create PDF") 76 | font.bold: true 77 | horizontalAlignment: Text.AlignHCenter 78 | Layout.fillWidth: true 79 | } 80 | ScrollView { 81 | Layout.fillWidth: true 82 | Layout.fillHeight: true 83 | TextArea { 84 | text: errorDialog.text 85 | wrapMode: TextEdit.Wrap 86 | selectByMouse: true 87 | readOnly: true 88 | } 89 | } 90 | Button { 91 | text: N_("Close") 92 | Layout.alignment: Qt.AlignRight 93 | onClicked: { errorDialog.close() } 94 | } 95 | } 96 | } 97 | 98 | Popup { 99 | parent: stack 100 | anchors.centerIn: parent 101 | modal: true 102 | focus: true 103 | visible: pagesModel.saving 104 | closePolicy: Popup.NoAutoClose 105 | ColumnLayout { 106 | anchors.fill: parent 107 | Label { 108 | text: N_("Saving…") 109 | font.bold: true 110 | horizontalAlignment: Text.AlignHCenter 111 | Layout.fillWidth: true 112 | } 113 | ProgressBar { 114 | Layout.fillWidth: true 115 | Layout.fillHeight: true 116 | value: pagesModel.savingProgress 117 | topPadding: 15 118 | bottomPadding: 15 119 | } 120 | Button { 121 | text: N_("Cancel") 122 | Layout.alignment: Qt.AlignRight 123 | onClicked: { pagesModel.cancelSaving() } 124 | enabled: pagesModel.savingCancelable 125 | } 126 | } 127 | } 128 | 129 | header: ToolBar { 130 | RowLayout { 131 | anchors.fill: parent 132 | ToolButton { 133 | text: "+" 134 | onClicked: { openDialog.open() } 135 | } 136 | Item { 137 | Layout.fillWidth: true 138 | } 139 | ToolButton { 140 | text: N_("Create") 141 | enabled: pagesModel.count > 0 142 | onClicked: { saveDialog.open() } 143 | } 144 | } 145 | } 146 | 147 | ScrollView { 148 | anchors.fill: parent 149 | background: Rectangle { 150 | color: paletteActive.base 151 | } 152 | 153 | GridView { 154 | property string dragKey: "9e8acb18cd58e838" 155 | 156 | id: pagesView 157 | focus: true 158 | activeFocusOnTab: true 159 | model: pagesModel 160 | 161 | Keys.onSpacePressed: { 162 | event.accepted = true 163 | stack.push("detail.qml", {p: pagesView.currentItem.p, 164 | modelIndex: pagesView.currentItem.modelIndex}) 165 | } 166 | 167 | cellWidth: 100 168 | cellHeight: 150 169 | delegate: MouseArea { 170 | id: pageDelegate 171 | 172 | property int modelIndex: index 173 | property DjpdfPage p: model.modelData 174 | property bool active: GridView.isCurrentItem && pagesView.activeFocus 175 | 176 | onClicked: { stack.push("detail.qml", {p: p, modelIndex: modelIndex}) } 177 | 178 | onPressed: { 179 | pagesView.forceActiveFocus(Qt.MouseFocusReason) 180 | pagesView.currentIndex = modelIndex 181 | } 182 | 183 | width: pagesView.cellWidth 184 | height: pagesView.cellHeight 185 | drag.target: pageItem 186 | Item { 187 | id: pageItem 188 | width: pageDelegate.width 189 | height: pageDelegate.height 190 | anchors.horizontalCenter: parent.horizontalCenter 191 | anchors.verticalCenter: parent.verticalCenter 192 | 193 | Drag.active: pageDelegate.drag.active 194 | Drag.source: pageDelegate 195 | Drag.hotSpot.x: width/2 196 | Drag.hotSpot.y: height/2 197 | Drag.keys: [ pagesView.dragKey ] 198 | 199 | states: [ 200 | State { 201 | when: pageItem.Drag.active 202 | ParentChange { 203 | target: pageItem 204 | parent: pagesView 205 | } 206 | 207 | AnchorChanges { 208 | target: pageItem 209 | anchors.horizontalCenter: undefined 210 | anchors.verticalCenter: undefined 211 | } 212 | } 213 | ] 214 | 215 | Image { 216 | id: image 217 | anchors { 218 | left: parent.left 219 | right: parent.right 220 | top: parent.top 221 | bottom: title.top 222 | margins: 6 223 | } 224 | asynchronous: true 225 | source: `image://thumbnails/${model.modelData.url}` 226 | fillMode: Image.PreserveAspectFit 227 | verticalAlignment: Image.AlignBottom 228 | z: 1 229 | } 230 | Rectangle { 231 | anchors { 232 | horizontalCenter: image.horizontalCenter 233 | bottom: image.bottom 234 | bottomMargin: (image.paintedHeight-height)/2 235 | } 236 | width: image.paintedWidth + 4 237 | height: image.paintedHeight + 4 238 | visible: image.status === Image.Ready 239 | color: paletteActive.text 240 | } 241 | BusyIndicator { 242 | anchors.centerIn: image 243 | running: image.status !== Image.Ready 244 | } 245 | 246 | Label { 247 | id: title 248 | anchors { fill: parent; topMargin: 100 } 249 | color: pageDelegate.active ? paletteActive.highlightedText : paletteActive.text 250 | text: pageDelegate.p.displayName 251 | wrapMode: Text.Wrap 252 | horizontalAlignment: Text.AlignHCenter 253 | elide: Text.ElideRight 254 | leftPadding: 5 255 | rightPadding: 5 256 | bottomPadding: 3 257 | z: 1 258 | } 259 | Rectangle { 260 | anchors { horizontalCenter: title.horizontalCenter; top: title.top } 261 | color: paletteActive.highlight 262 | visible: pageDelegate.active 263 | height: title.contentHeight + 3 264 | width: title.contentWidth + 6 265 | } 266 | } 267 | 268 | DropArea { 269 | anchors { fill: parent; margins: 5 } 270 | keys: [ pagesView.dragKey ] 271 | onEntered: (drag) => { pagesModel.move(drag.source.modelIndex, pageDelegate.modelIndex) } 272 | } 273 | } 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /scans2pdf_gui/scans2pdf-gui.in: -------------------------------------------------------------------------------- 1 | #!@PYTHON@ 2 | 3 | # Copyright (C) 2023 Unrud 4 | # 5 | # This file is part of Video Downloader. 6 | # 7 | # Video Downloader is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Video Downloader is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with Video Downloader. If not, see . 19 | 20 | import contextlib 21 | import gettext 22 | import locale 23 | import signal 24 | import sys 25 | 26 | pkgdatadir = @pkgdatadir@ 27 | localedir = @localedir@ 28 | 29 | sys.path.insert(1, pkgdatadir) 30 | signal.signal(signal.SIGINT, signal.SIG_DFL) 31 | locale.bindtextdomain('djpdfgui', localedir) 32 | locale.textdomain('djpdfgui') 33 | gettext.bindtextdomain('djpdfgui', localedir) 34 | gettext.textdomain('djpdfgui') 35 | with contextlib.suppress(locale.Error): 36 | locale.setlocale(locale.LC_ALL, '') 37 | 38 | if __name__ == '__main__': 39 | from scans2pdf_gui import main 40 | sys.exit(main.main()) 41 | -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unrud/djpdf/e304dc337d1da218130223b3b3c9f550e286e6aa/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unrud/djpdf/e304dc337d1da218130223b3b3c9f550e286e6aa/screenshots/2.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="djpdf", 5 | version="0.5.10", 6 | packages=["djpdf"], 7 | package_data={"djpdf": ["argyllcms-srgb.icm", "tesseract-pdf.ttf", 8 | "to-unicode.cmap"]}, 9 | entry_points={"console_scripts": ["scans2pdf = djpdf.scans2pdfcli:main", 10 | "scans2pdf-json = djpdf.scans2pdf:main", 11 | "djpdf-json = djpdf.djpdf:main", 12 | "hocr-json = djpdf.hocr:main"]}, 13 | python_requires=">=3.8", 14 | install_requires=["webcolors", "colorama", "pdfrw", "psutil", 15 | "python-xmp-toolkit", 16 | "importlib_resources>=1.4; python_version<'3.9'"]) 17 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | # Build by running "snapcraft". 2 | 3 | # WARNING: 4 | # Snapcraft uses caching for already build steps but it's buggy and can cause strange problems. 5 | # Clean the cache by running "snapcraft clean". 6 | 7 | name: djpdf 8 | license: GPL-3.0+ 9 | grade: stable 10 | adopt-info: djpdfgui 11 | 12 | base: core22 13 | confinement: strict 14 | 15 | architectures: 16 | - build-on: amd64 17 | 18 | environment: 19 | # WORKAROUND: Add python modules in Snap to search path 20 | PYTHONPATH: ${SNAP}/lib/python3.10/site-packages:${SNAP}/usr/lib/python3/dist-packages 21 | 22 | apps: 23 | djpdf: 24 | command: usr/bin/scans2pdf-gui 25 | extensions: 26 | # HINT: Adds plugs and changes environment variables when building and running 27 | - kde-neon 28 | common-id: com.github.unrud.djpdf 29 | desktop: usr/share/applications/com.github.unrud.djpdf.desktop 30 | 31 | parts: 32 | jbig2enc: 33 | plugin: autotools 34 | source: https://github.com/agl/jbig2enc/archive/refs/tags/0.30.tar.gz 35 | source-type: tar 36 | source-checksum: sha256/4468442f666edc2cc4d38b11cde2123071a94edc3b403ebe60eb20ea3b2cc67b 37 | autotools-configure-parameters: 38 | # WORKAROUND: Install to /usr instead of /usr/local because it's not in search paths 39 | - --prefix=/usr 40 | build-packages: 41 | - g++ 42 | - libleptonica-dev 43 | - zlib1g-dev 44 | stage-packages: 45 | - liblept5 46 | - zlib1g 47 | override-build: | 48 | ./autogen.sh 49 | craftctl default 50 | 51 | imagemagick: 52 | plugin: autotools 53 | source: https://github.com/ImageMagick/ImageMagick/archive/refs/tags/7.1.1-47.tar.gz 54 | source-type: tar 55 | source-checksum: sha256/818e21a248986f15a6ba0221ab3ccbaed3d3abee4a6feb4609c6f2432a30d7ed 56 | autotools-configure-parameters: 57 | # WORKAROUND: Install to /usr instead of /usr/local because it's not in search paths 58 | - --prefix=/usr 59 | build-packages: 60 | - libjpeg-dev 61 | - libpng-dev 62 | - libtiff-dev 63 | - libwebp-dev 64 | - libopenjp2-7-dev 65 | stage-packages: 66 | - libjpeg8 67 | - libpng16-16 68 | - libtiff5 69 | - libwebpmux3 70 | - libgomp1 71 | - libwebpdemux2 72 | - libopenjp2-7 73 | 74 | tesseract: 75 | plugin: autotools 76 | source: https://github.com/tesseract-ocr/tesseract/archive/refs/tags/5.5.0.tar.gz 77 | source-checksum: sha256/f2fb34ca035b6d087a42875a35a7a5c4155fa9979c6132365b1e5a28ebc3fc11 78 | autotools-configure-parameters: 79 | # WORKAROUND: Fake installation location to find dependencies at runtime 80 | - --prefix=/snap/djpdf/current/usr 81 | build-packages: 82 | - pkg-config 83 | - libleptonica-dev 84 | - libcurl4-openssl-dev 85 | stage-packages: 86 | - liblept5 87 | - libcurl4 88 | override-build: | 89 | ./autogen.sh 90 | craftctl default 91 | organize: 92 | # WORKAROUND: Move files from fake installation location to actual target 93 | snap/djpdf/current/usr: usr 94 | 95 | tessdata: 96 | plugin: nil 97 | build-packages: [wget] 98 | override-pull: | 99 | wget https://github.com/tesseract-ocr/tessdata/raw/4.1.0/eng.traineddata -O eng.traineddata 100 | wget https://github.com/tesseract-ocr/tessdata/raw/4.1.0/chi_sim.traineddata -O chi_sim.traineddata 101 | wget https://github.com/tesseract-ocr/tessdata/raw/4.1.0/hin.traineddata -O hin.traineddata 102 | wget https://github.com/tesseract-ocr/tessdata/raw/4.1.0/spa.traineddata -O spa.traineddata 103 | wget https://github.com/tesseract-ocr/tessdata/raw/4.1.0/fra.traineddata -O fra.traineddata 104 | wget https://github.com/tesseract-ocr/tessdata/raw/4.1.0/ara.traineddata -O ara.traineddata 105 | wget https://github.com/tesseract-ocr/tessdata/raw/4.1.0/rus.traineddata -O rus.traineddata 106 | wget https://github.com/tesseract-ocr/tessdata/raw/4.1.0/por.traineddata -O por.traineddata 107 | wget https://github.com/tesseract-ocr/tessdata/raw/4.1.0/deu.traineddata -O deu.traineddata 108 | wget https://github.com/tesseract-ocr/tessdata/raw/4.1.0/jpn.traineddata -O jpn.traineddata 109 | wget https://github.com/tesseract-ocr/tessdata/raw/4.1.0/osd.traineddata -O osd.traineddata 110 | echo 'daa0c97d651c19fba3b25e81317cd697e9908c8208090c94c3905381c23fc047 *eng.traineddata 111 | fc05d89ab31d8b4e226910f16a8bcbf78e43bae3e2580bb5feefd052efdab363 *chi_sim.traineddata 112 | cc76d09fa4fed1c7a4674046e25e63760d0c9bfdce390a52113462c34a556ee6 *hin.traineddata 113 | 0b0fcbb4665189e01ab8019e591f014dd7260460de072543edd4b2cb4ede7c96 *spa.traineddata 114 | eac01c1d72540d6090facb7b2f42dd0a2ee8fc57c5be1b20548ae668e2761913 *fra.traineddata 115 | 2005976778bbc14fc56a4ea8d43c6080847aeee72fcc2201488f240daca15c5b *ara.traineddata 116 | 681be2c2bead1bc7bd235df88c44e8e60ae73ae866840c0ad4e3b4c247bd37c2 *rus.traineddata 117 | 016c6a371bb1e4c48fe521908cf3ba3d751fade0ab846ad5d4086b563f5c528c *por.traineddata 118 | 896b3b4956503ab9daa10285db330881b2d74b70d889b79262cc534b9ec699a4 *deu.traineddata 119 | 6f416b902d129d8cc28e99c33244034b1cf52549e8560f6320b06d317852159a *jpn.traineddata 120 | e19f2ae860792fdf372cf48d8ce70ae5da3c4052962fe22e9de1f680c374bb0e *osd.traineddata' > SHA256SUMS 121 | sha256sum -c SHA256SUMS 122 | override-build: | 123 | install -Dm0644 -t "${CRAFT_PART_INSTALL}/usr/share/tessdata" * 124 | 125 | djpdf: 126 | plugin: python 127 | source: . 128 | source-type: git 129 | python-packages: 130 | # Dependencies for djpdfgui 131 | - PySide6<6.8 132 | stage-packages: 133 | - python3-pkg-resources 134 | - qpdf 135 | - libexempi8 136 | override-build: | 137 | craftctl default 138 | # WORKAROUND: Hardcode libexempi path 139 | libexempi_path="/snap/djpdf/current/$(realpath --relative-base="${CRAFT_PART_INSTALL}" "$(find "${CRAFT_PART_INSTALL}" -name 'libexempi.so*' -print -quit)")" 140 | sed -e "s|[^=]*find_library('exempi')| '$libexempi_path'|" -i "${CRAFT_PART_INSTALL}"/lib/python3.*/site-packages/libxmp/exempi.py 141 | stage: 142 | # WORKAROUND: Skip venv from python plugin 143 | - -bin/activate 144 | - -bin/activate.csh 145 | - -bin/activate.fish 146 | - -bin/Activate.ps1 147 | - -bin/python 148 | - -bin/python3 149 | - -pyvenv.cfg 150 | 151 | meson-deps: 152 | # Copied from gnome-42-2204-sdk 153 | plugin: nil 154 | source: https://github.com/mesonbuild/meson.git 155 | source-tag: '1.2.3' 156 | source-depth: 1 157 | override-build: | 158 | python3 -m pip install . 159 | mkdir -p $CRAFT_PART_INSTALL/usr/lib/python3/dist-packages 160 | rm -rf $CRAFT_PART_INSTALL/usr/lib/python3/dist-packages/meson* 161 | python3 -m pip install --target=$CRAFT_PART_INSTALL/usr . 162 | mv $CRAFT_PART_INSTALL/usr/meson* $CRAFT_PART_INSTALL/usr/lib/python3/dist-packages/ 163 | sed -i "s%^#!/usr/bin/python3$%#!/usr/bin/env python3%g" /usr/local/bin/meson 164 | sed -i "s%^#!/usr/bin/python3$%#!/usr/bin/env python3%g" $CRAFT_PART_INSTALL/usr/bin/meson 165 | build-packages: 166 | - python3-pip 167 | prime: [] 168 | 169 | djpdfgui: 170 | # WORKAROUND: meson in repository too old 171 | after: [ meson-deps ] 172 | plugin: meson 173 | source: . 174 | source-type: git 175 | # WORKAROUND: Fake installation location to find dependencies at runtime 176 | meson-parameters: [--prefix=/snap/djpdf/current/usr] 177 | build-packages: 178 | - ninja-build 179 | - gettext 180 | - appstream 181 | - graphicsmagick-imagemagick-compat 182 | stage-packages: 183 | # WORKAROUND: Dependencies required for Qt 184 | - libxcb-cursor0 185 | override-pull: | 186 | craftctl default 187 | # WORKAROUND: Point icon directly to PNG otherwise snapcraft can't find it 188 | sed -e 's|Icon=com.github.unrud.djpdf|Icon=/usr/share/icons/hicolor/512x512/apps/com.github.unrud.djpdf.png|' -i desktop/com.github.unrud.djpdf.desktop.in 189 | override-build: | 190 | craftctl default 191 | # WORKAROUND: Use python from search path, the path detected by meson doesn't exist when running the Snap 192 | sed -e '1c#!/usr/bin/env python3' -i "${CRAFT_PART_INSTALL}/snap/djpdf/current/usr/bin/scans2pdf-gui" 193 | organize: 194 | # WORKAROUND: Move files from fake installation location to actual target 195 | snap/djpdf/current/usr: usr 196 | parse-info: [usr/share/metainfo/com.github.unrud.djpdf.metainfo.xml] 197 | --------------------------------------------------------------------------------