diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000000..d6b8aa2879 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,38 @@ +name: .NET + +on: [push, pull_request] + +jobs: + build: + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [windows-2022, ubuntu-24.04, macos-15] + steps: + - name: Install OS dependencies + if: matrix.os == 'ubuntu-24.04' + run: sudo apt-get install -y fonts-liberation2 fonts-noto-core fonts-noto-cjk + - uses: actions/checkout@v4 + - name: Setup .NET 9 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Install workloads + if: matrix.os == 'macos-15' + run: dotnet workload install macos + - name: Build + run: dotnet run --project NAPS2.Tools -- build debug -v + - name: Test + if: matrix.os != 'macos-15' + run: dotnet run --project NAPS2.Tools -- test -v --nogui + - name: Test (mac) + if: matrix.os == 'macos-15' + run: dotnet run --project NAPS2.Tools -- test -v --nogui --nonetwork + - name: Test (WPF images) + if: matrix.os == 'windows-2022' + run: dotnet run --project NAPS2.Tools -- test -v --nogui --images wpf --scope sdk + - name: Test (ImageSharp images) + if: matrix.os == 'ubuntu-24.04' + run: dotnet run --project NAPS2.Tools -- test -v --nogui --images is --scope sdk diff --git a/.gitignore b/.gitignore index e93598953e..4cb7c8c5bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ Desktop.ini +.DS_Store packages/ *.user *.suo diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..ec96756d6d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,798 @@ +Changes in 8.2.1: +- Added a review prompt in the Microsoft Store version after a month's use +- Windows: Added TWAIN support to arm64 installer +- Windows: Fixed some OCR compatibility issues +- Windows: Fixed preview window being cut off +- Mac: Fixed SANE crash +- Escl: Fixed some compatibility issues +- Sane: Fixed some duplex compability issues +- Linux: Fixed Gmail/Outlook Web options not showing +- Linux: Fixed hidden buttons after disabling PDF encryption +- Sdk: Fixed extraneous error log + +Changes in 8.2.0: +- NAPS2 is [now available](https://apps.microsoft.com/detail/9N3QQ9W0B23Q?cid=changelog) on the Microsoft Store + - It costs a small fee to support the developer and provide automatic updates + - NAPS2 will continue to be freely available at www.naps2.com +- Added "Edit with" under the "Image" menu for using an external image editor +- Added "Share even when NAPS2 is closed" option for Scanner Sharing + - This will show a system tray icon and restart on login +- Imported file names are now used as the default file name when saving +- The "Apply to all selected" checkbox now stays checked +- Escl: Increased maximum time searching for devices from 5s to 60s +- Escl: Scanner IPs are now cached for faster and more reliable scanning +- Windows: Added an arm64 installer +- Windows: Replaced the "No friendly name" device name from some drivers with "Unknown Scanner" +- Mac: Fixed an issue where saved files didn't always have the right extension +- Mac: Disabled the "Apple Mail" email provider when not the default email reader +- Mac: Updated icons for Split/Combine +- Linux: Fixed issues with the Save dialog + +Changes in 8.1.4: +- Windows: Added a "Theme" setting to switch between Light and Dark mode +- Linux: Fixed OCR on older Linux versions (e.g. Ubuntu 20.04) + - Ubuntu 18.04 is no longer supported (use NAPS2 7.5.3 if needed) + +Changes in 8.1.3: +- Twain: Fixed issues with Kyocera Ecosys scanners +- Sane: Fixed duplex with some scanners +- Windows: Fixed issues with multiple monitors with different DPI + +Changes in 8.1.2: +- Added "Apply to all selected" for Split action +- Fixed "unsaved changes" prompt after auto saving +- Fixed issues with Placeholders in Default File Path +- Windows: Fixed OCR errors on some systems +- Windows: Fixed crash with right-to-left languages + +Changes in 8.1.1: +- Fixed an issue with dpi selection + +Changes in 8.1.0: +- Added the ability to pick a custom resolution +- Added email provider for "New Outlook" +- Improved "Keyboard Shortcuts" interface +- Fixed an issue with shared TWAIN scanners +- Fixed missing screen reader text for some buttons +- Fixed excessive Flatpak size + +Changes in 8.0b3: +- Added "Keyboard Shortcuts" settings +- Added "--waitscan" and "--firstnow" console options +- Added support for importing zip files +- Upgraded Tesseract from 5.3.4 to 5.5.0 for OCR +- Fixed an issue with antivirus false positives +- Bug fixes + +Changes in 8.0b2: +- Fixes from 7.5.3 +- Fixed WIA scanning +- Fixed drag and drop on Windows +- Fixed notifications on Linux +- Fixed sidebar settings being shown with "Use native UI" selected +- Linux flatpak runtime has been upgraded to 24.08 + +Changes in 8.0b1: +- [Beta feedback thread](https://github.com/cyanfish/naps2/discussions/467) +- Added a scanning sidebar + - Quickly change basic profile settings + - Click the icon in the bottom-left (top-left on Mac) to open/close + - Admins can set [HideSidebar](https://www.naps2.com/doc/org-use#hide-sidebar) to remove it entirely +- Changed system requirements + - Windows 7, 8 and 8.1 are no longer supported + - Windows 32-bit is no longer supported + - Windows 10 1607+ is required + - macOS 10.15 and 11 are no longer supported + - macOS 12+ is required +- Improved support for high-dpi screens +- Windows: Added support for dark themes +- Twain: Removed "Legacy" twain implementation option +- Changed the "Clear" icon to a broom +- Performance improvements +- Bug fixes + +Changes in 7.5.3: +- Windows: Fixed a bug with maximized windows +- Mac: Fixed bugs with Apple Driver +- Linux: Fixed compatibility with Fedora 41 and others + +Changes in 7.5.2: +- ~~NAPS2 is now available on the Microsoft Store & the Mac App Store~~ + - ~~It costs a small fee to support the developer and provide automatic updates~~ + - ~~NAPS2 will continue to be freely available at www.naps2.com~~ +- Windows: Installers and executables are now EV code-signed +- Fixed NAPS2.Console issues with cancellation +- Fixed ESCL compatibility with AirSane +- Fixed an issue with Apple Driver and out-of-order pages +- Fixed auto save file prompts to cancel correctly + +Changes in 7.5.1: +- Mac: Use more native icons +- Fixed an issue loading profiles + +Changes in 7.5.0: +- Reworked device selection + - Driver selection is now in the "Choose device" window + - Click the top-right button to toggle between icon and list views + - You can no longer create a profile without selecting a device + - To prompt for a device each time you scan, "Always Ask" must be explicitly selected +- Added "Manual IP" option for ESCL +- Available profile options now change based on scanner support +- Improved the error message when the worker process crashes +- Sane: Fixed an issue with selecting the wrong grayscale mode +- Fixed an issue with auto save paths that include Unicode +- Fixed an issue with "Combine" for black and white images + +Changes in 7.4.3: +- Fixed some ESCL connection issues +- Fixed email compatibility with HCL Notes +- Fixed issues with "Outlook Web Access" email provider +- Fixed SANE compability with some HP devices +- Fixed Fraktur-based languages for OCR + +Changes in 7.4.2: +- Bug fixes + +Changes in 7.4.1: +- Improved OCR text alignment +- Added "Open With" support for PDF and image files +- Changed some labels to improve clarity + - "Automatically run OCR after scanning" → "Pre-emptively run OCR after scanning" + - "Flip duplexed pages" → "Flip back sides of duplex pages" +- Added HTTPS support for scanner sharing + - Uses an auto-generated self-signed certificate by default + - Admins can set [EsclServerCertificatePath](https://www.naps2.com/doc/org-use#escl-server-certificate-path) to use a custom certificate + - Admins can set [EsclSecurityPolicy](https://www.naps2.com/doc/org-use#escl-security-policy) to force servers/clients to only use HTTPS + - This affects all ESCL devices, not just shared scanners +- Improved ESCL reliability with network interruptions +- Fixed some issues with Preview window zooming +- Made confirmation dialogs more consistent (OK/Cancel vs. Yes/No) +- Added more default keyboard shortcuts +- Mac: Fixed issues with keyboard shortcuts +- Mac: Added some missing menu items (Zoom In/Out, Move Up/Down, Profiles) +- Linux: Added a signature to .deb/.rpm packages +- Windows: The .msi installer can no longer be used to upgrade over .exe +- Bug fixes + +Changes in 7.4.0: +- Added Undo/Redo (from the right-click menu or Ctrl+Z) + - Deletions can't be undone +- Added Split/Combine (under the Image menu) + - Split can be used for book scanning to separate pages + - Combine can be used to include front/back sides of an ID card in one image +- Added "Multiple Languages" as an option for OCR (in the OCR language dropdown) +- Added a "Fix white balance and remove noise" OCR option + - This can improve OCR with low-quality scans, but will make OCR slower + - This is equivalent to using "Document Correction" before OCR +- Upgraded Tesseract from 5.2.0 to 5.3.4 for OCR +- Added a "Show native TWAIN progress" profile option (under Advanced) +- Bug fixes + +Changes in 7.3.1: +- Improved loading time for "Keep images across sessions" +- PDF encryption settings are now hidden until enabled +- Fixed some SANE devices incorrectly appearing offline +- Fixed some SANE devices not respecting page size +- Fixed OCR issues with non-Latin alphabets +- Bug fixes + +Changes in 7.3.0: +- Added a general "Settings" window with new options (some not available on Mac/Linux): + - Show page numbers + - Show Profiles toolbar + - Scan menu changes default profile + - Scan button default action + - Save button default action + - Clear images after saving + - Keep images across sessions + - Only allow a single NAPS2 instance +- Added corresponding appsettings.xml options + - See https://www.naps2.com/doc/org-use +- Added "mode" attribute to some settings in appsettings.xml: + - mode="default" provides a default value for the user + - mode="lock" prevents the user from changing the value +- Added new console options: + - "--noprofile" to only use CLI options (not GUI profiles) + - "--listdevices" to see available scanning devices + - "--driver", "--device", "--source", "--pagesize", "--dpi", "--bitdepth" scanning options + - "--deskew", "--rotate" post-processing options + - See https://www.naps2.com/doc/command-line +- Windows: Updated .exe installer style +- Windows: Added back "Alt" hotkeys +- Windows: Fixed an issue sending email with Outlook 2010-2016 +- Bug fixes + +Changes in 7.2.2: +- Bug fixes + +Changes in 7.2.1: +- Bug fixes + +Changes in 7.2.0: +- Scanner Sharing + - Share scanners with other computers on the local network, for example: + - Turn a desktop-connected USB scanner into a wireless scanner usable from your laptop or phone + - Allow Windows-only scanners to be used from Mac/Linux using a virtual machine + - Set up a Raspberry Pi to turn a USB scanner into a wireless scanner + - On the host computer, in the Profiles window, click Scanner Sharing and choose the scanners to share + - On the client computer, select "ESCL Driver" in your profile settings and you should be able to select the shared scanner + - NAPS2 currently must be kept open on the host for sharing to work + - Shared scanners can be used from any ESCL-capable client, not just NAPS2 + - Try [Mopria Scan](https://play.google.com/store/apps/details?id=org.mopria.scan.application) for Android + - Use NoScannerSharing in appsettings.xml to disable +- Slightly updated icons in the Profiles window +- Old unrecoverable files are now cleaned up on startup +- Mac/Linux have been upgraded to the .NET 8 runtime +- Linux flatpak runtime has been upgraded to 23.08 +- Bug fixes + +Changes in 7.1.2: +- Mac: Fixed scanning with macOS 14 Sonoma + +Changes in 7.1.1: +- Bug fixes + +Changes in 7.1.0: +- Windows: Added ESCL Driver option + - This allows using most network scanners without needing to install a separate driver +- PDF saving is much faster in some cases +- Imported PDFs now render forms and annotations +- Added Hindi language +- Bug fixes +- NAPS2.Sdk is now available on [Nuget](https://www.nuget.org/packages/NAPS2.Sdk) + +Changes in 7.0b9: +- Improved accuracy of PDF page sizes +- Improved UI responsiveness when OCR is running +- Mac: Improved color accuracy for scans with Apple Driver +- Mac: Added support for dark themes +- Linux: Added support for dark themes +- Linux: Added arm64 .deb/.rpm builds +- Bug fixes + +Changes in 7.0b8: +- Added "Email PDF" support to Mac and Linux + - Mac: Apple Mail, Gmail, and Outlook Web options + - Linux: Thunderbird, Gmail, and Outlook Web options +- Added "Print" support to Mac and Linux +- Added notifications to Mac and Linux + - Also updated notification appearance in general +- Linux: Added drag & drop support +- Linux: Improved compatibility with older Linux (e.g. Ubuntu 18.04) +- Linux: Added dependencies to .deb package +- Sane: Show IP addresses for escl/airscan backends +- Windows: Changed installer publisher to "NAPS2 Software" +- Improved error log formatting +- Added debug logging for scanning diagnostics + - Turn on by checking "Enable debug logging" in the About window + - This will record information about scanning activity on disk + - You can find debuglog.txt in the [same folder](https://www.naps2.com/doc/troubleshooting#error-log) as errorlog.txt + - Use NoDebugLogging in appsettings.xml to hide the option +- Added Bosnian and Indonesian languages +- Bug fixes + +Changes in 7.0b7: +- Bug fixes + +Changes in 7.0b6: +- Bug fixes + +Changes in 7.0b5: +- Added 2400/4800 dpi options +- Linux: Added .deb/.rpm packages +- Sane: Show devices incrementally (only with Mac / Linux flatpak) +- Crop improvements +- Fixed formatting for OCR of non-NAPS2 PDFs +- Bug fixes + +Changes in 7.0b4: +- Twain: Changed default transfer mode + - "Alternative Transfer" has been renamed "Memory Transfer" and is now used when "Default" is selected + - "Native Transfer" can be used to revert to the old transfer mode +- Saved images now use optimized bit depths for smaller file sizes +- Bug fixes + +Changes in 7.0b3: +- Bug fixes + +Changes in 7.0b2: +- Bug fixes + +Changes in 7.0b1: +- Most NAPS2 code has been rewritten. Things should mostly look the same but under the hood there are many differences. + - [Beta feedback thread](https://github.com/cyanfish/naps2/discussions/35) +- Added Mac support + - Supports macOS 10.15 and later + - The Universal download should work for all users. Or you can use the Intel/Apple Silicon downloads for a smaller download/install size if you know which one your Mac has. + - NAPS2 on Mac bundles SANE drivers for USB scanners, allowing supported scanners to be used even on new M1/M2 Macs (which normally wouldn't work without manufacturer-provided drivers) +- Added native Linux support + - Requires Flatpak for installation (https://flatpak.org/setup/) + - Mono is no longer required + - The UI should now feel like a native Linux app + - Much better performance and reliability +- TWAIN support has been reworked + - Some lifecycle-related issues should hopefully be fixed (e.g. only being able to scan once) + - With "Use predefined settings", TWAIN now uses the built-in NAPS2 progress window, which allows multitasking + - TWAIN UI should no longer be visible in console and batch mode + - TWAIN should also now support scanning larger images (e.g. 1200dpi) without out-of-memory issues +- Upgraded Tesseract to 5.2.0 for OCR + - Up to 30% faster OCR performance + - Tesseract is now bundled with the NAPS2 download, so no extra download is required (though you still need to download language data if you don't already have it). +- PDF import and export have been rewritten to leverage Pdfium + - This means better support for importing different kinds of PDFs + - In some cases this means much faster import/export + - Pdfium is bundled with the NAPS2 download so there is no longer an extra download needed to import non-NAPS2 PDFs +- New Crop UI +- Minor tweaks to blank page detection +- Image list tweaks + - Selected images appear with just a blue border + - Spacing has been optimized +- New automatic image correction functionality (work in progress) + - "Document Correction" under the Image menu + - Automatic fixing of color calibration, noise, skew, and other common scanning issues + - Eventually this will be integrated into profiles +- JPEG2000 support for importing/saving images (Mac only for now) +- Dropped support for rarely-used image file formats (.emf, .exif, .gif) + - Please request if you want this back +- NAPS2 on Windows now requires .NET Framework 4.6.2 + - This means no more support for Windows XP + - Windows 7 SP1 is now the minimum requirement +- The 64-bit Windows install location is now "Program Files" instead of "Program Files (x86)" +- The MSI installer now has separate 64-bit and 32-bit downloads +- The AppData format for config.xml and Tesseract files has changed (will be automatically migrated) +- Improved icon quality +- Translations have been moved to Crowdin + - See [translate.naps2.com](https://translate.naps2.com) +- Various performance and reliability improvements +- Bug fixes + +Changes in 6.1.2: +- Added --autosend support for Gmail in NAPS2.Console +- Bug fixes + +Changes in 6.1.1: +- Faster and more accurate deskew +- Bug fixes + +Changes in 6.1.0: +- Added a "Single page files" option in PDF Settings +- Improved accessibility +- Faster cropping +- Event logging now uses an XML format +- Bug fixes + +Changes in 6.0b4: +- Beta feedback thread: https://sourceforge.net/p/naps2/discussion/general/thread/8776c818/ +- Upgraded WIA version from 1.0 to 2.0; can be changed back in your profile under Advanced +- Improved WIA compatibility with feeders and duplex +- Added support for background scanning with WIA + - Does not work with "Use native UI" + - This means you can scan with multiple devices at the same time +- Removed some obsolete WIA compatibility options +- Bug fixes + +Changes in 6.0b3: +- Beta feedback thread: https://sourceforge.net/p/naps2/discussion/general/thread/8776c818/ +- Added optional event logging + - See https://www.naps2.com/doc-org-use.html#event-logging +- Improved console import speed +- Bug fixes + +Changes in 6.0b2: +- Beta feedback thread: https://sourceforge.net/p/naps2/discussion/general/thread/8776c818/ +- OCR users from 6.0b1 will need to click the OCR button and re-download +- Fixed an issue with OCR missing a DLL on some systems +- Fixed an issue with OCR not terminating +- Other minor fixes and improvements + +Changes in 6.0b1: +- Beta feedback thread: https://sourceforge.net/p/naps2/discussion/general/thread/8776c818/ +- Linux support (download one of the portable archives - currently experimental, please give feedback!) + - Requires Mono (5.17+ preferably), see https://www.naps2.com/doc-getting-started.html#system-requirements +- Added an automatic update check + - Opt in from the About window + - Not available if installed from the MSI +- New OCR version, significantly more accurate in many cases + - The OCR button will prompt to update. This can be disabled with the NoUpdatePrompt flag in appsettings.xml + - Not supported on Windows XP (will use the older version instead) + - You can choose between multiple modes: Fast (recommended), Best (slow), and Legacy (to simulate the older version) +- Added the ability to choose an email provider + - When you first click Email PDF, you will be prompted to choose. Afterwards use Email Settings to change + - Switch between installed clients (Outlook, Thunderbird, etc.) + - Webmail integration for Gmail and Outlook Web Access +- Added support for Unicode in email attachment names +- Crop selection will be remembered (in case you're cropping multiple images but need to adjust them individually) +- Added the ability to run most operations in the background for multitasking +- Improved performance with very large images +- Substantially reduced installation footprint and portable zip size +- Minimized TWAIN UI in console and batch mode +- NAPS2 installers are now signed + - This should eventually help remove SmartScreen notifications +- NAPS2 will now run in 64-bit mode on compatible systems + - If you have a 64-bit system, NAPS2 will better handle memory-intensive operations + - If you downloaded the add-on to open any PDF (gsdll32.dll), you may need to re-download the 64-bit version +- Improved documentation and usability for developers (see https://www.naps2.com/doc-dev-onboarding.html) +- Bug fixes + +Changes in 5.8.2: +- Added Japanese language +- Fixed a bug with importing some PDFs +- Fixed a bug with the Alternative Transfer TWAIN option + +Changes in 5.8.1: +- Fixed a bug with PDF/A support + +Changes in 5.8.0: +- PDF/A support + - PDF/A1-b, PDF/A2-b, PDF/A3-b, and PDF/A3-u support + - In the "Save PDF" menu, click "PDF Settings", and select it under "Compatibility" + - Use --pdfcompat in NAPS2.Console. See www.naps2.com/doc-command-line.html#pdf-options + - Use ForcePdfCompat in appsettings.xml. See www.naps2.com/doc-org-use.html#force-pdf-compat +- TIFF changes + - Better compression for black and white TIFF files by default + - Added a "Compression" option under Image Settings + - Added a "Single page files" option under Image Settings that prevents saving multi-page TIFF files + - Use --tiffcomp and --split in NAPS2.Console. See www.naps2.com/doc-command-line.html#image-options +- Donate button + - The About window now has a Donate button + - An unobtrusive donation prompt is shown after a month of use + - Use HideDonateButton in appsettings.xml to disable both. See www.naps2.com/doc-org-use.html#hide-donate-button + - The prompt is disabled by default in the MSI distribution +- Added multi-language support to the EXE installation wizard + +Changes in 5.7.1: +- Added --split, --splitscans, --splitpatcht, and --splitsize options to NAPS2.Console + - See www.naps2.com/doc-command-line.html#split-options +- Added slice support to --import in NAPS2.Console + - See www.naps2.com/doc-command-line.html#slicing-imported-files + +Changes in 5.7.0: +- Fixed downloads for OCR (etc.) +- Improved deskew +- Added a confirmation for batch cancel +- Minor performance improvements +- Bug fixes + +Changes in 5.6.2: +- Bug fixes + +Changes in 5.6.1: +- Fixed a crash + +Changes in 5.6.0: +- Increased the maximum thumbnail size from 256x256 to 1024x1024 +- Improved PDF import to allow many more types of PDFs to be imported +- OCR can now be used on imported PDFs (if they don't already have text) +- Improved PDF file size for some black and white images +- Combined Brightness and Contrast adjustments into a single window +- Added Hue, Saturation, Black+White, and Sharpen image adjustments +- Added more keyboard shortcuts in the preview window (arrow keys to change pages, Ctrl/Alt/Shift + arrow keys to pan) +- Added "HideImportButton", "HideOcrButton", "HideSavePdfButton", and "HideSaveImages" options to appsettings.xml +- Added "OcrState" and "OcrDefaultLanguage" options to appsettings.xml +- Bug fixes + +Changes in 5.5.0: +- Added support for importing any PDF (requires an additional download, can be disabled by NoUpdatePrompt or DisableGenericPdfImport in appsettings.xml) +- Added the ability to install optional components using NAPS2.Console (with the "--install" argument) +- Added "Alternative Transfer" TWAIN compatibility option +- Added .txt extension to license/contributor file names +- Bug fixes + +Changes in 5.4.0: +- Added automatic deskew option (under the Rotate menu or under Advanced in your profile settings) (credit to Peter Hommel) +- Added single-page save buttons to the preview window +- Added "Prompt for file path" option to Auto Save Settings +- Split "Force matching page size" option into "Stretch to page size" and "Crop to page size" options +- Added "Retry on failure" and "Delay between scans" WIA compatibility options +- Added support for environment variables in most paths +- Added LICENSE and CONTRIBUTORS files to the root directory (this replaces most copyright notices elsewhere) +- Added Nynorsk language +- Bug fixes + +Changes in 5.3.3: +- Bug fixes + +Changes in 5.3.2: +- Added Slovenian language +- Fixed AV false positive issue + +Changes in 5.3.1: +- Added Afrikaans and Vietnamese languages + +Changes in 5.3.0: +- Significantly improved OCR speed on multi-core systems +- Improved OCR text alignment +- Patch-T is now supported for all scanners, with both WIA and TWAIN +- Improved and added technical details to some error messages +- Tweaked the spacing between thumbnails for less wasted space +- Added Latvian language +- Fixed OCR on Windows XP (requires an extra download, can be disabled by NoUpdatePrompt in appsettings.xml) +- Fixed Auto Save and Batch to use a default file name when a directory is specified instead of a file path + +Changes in 5.2.1: +- Added an "OcrTimeoutInSeconds" option to appsettings.xml +- Bug fixes + +Changes in 5.2.0: +- Added the ability to copy/paste and drag/drop profiles +- Changed the way "LockSystemProfiles" behaves to allow users to specify a device if not specified by the admin +- Added "NoUserProfiles", "AlwaysRememberDevice", and "LockUnspecifiedDevices" options to appsettings.xml +- Added "HideEmailButton" and "HidePrintButton" options to appsettings.xml +- Added "PromptIfSelected" as a possible value for the "SaveButtonDefaultAction" option in appsettings.xml +- Added Arabic, Serbian (Latin + Cyrillic), and Slovak languages + +Changes in 5.1.1: +- Updated the default appsettings.xml to be easier to edit +- Bug fixes + +Changes in 5.1.0: +- Custom page sizes can now be named and reused across multiple profiles +- Added the ability to draw a line to align the page in Custom Rotation +- Added a "Restore Defaults" button to Advanced Profile Settings +- Added a "ComponentsPath" option to appsettings.xml +- Added a "SingleInstance" option to appsettings.xml +- Placeholders can now be used in --subject and --body arguments in NAPS2.Console +- Bug fixes + +Changes in 5.0b3: +- Added save notifications (use DisableSaveNotifications in appsettings.xml to disable) +- Added a "Skip save prompt" option to PDF and Image settings. Also changed "Default File Name" to "Default File Path" (can be a file name, folder, or full path now) +- Bug fixes + +Changes in 5.0b2: +- Added a "Flip duplexed pages" compatibility option +- Added a "DeleteAfterSaving" option to appsettings.xml +- Bug fixes + +Changes in 5.0b1: +- Updated tesseract-ocr (from 3.02 to 3.04) + - The OCR button will prompt to update. This can be disabled with the NoUpdatePrompt flag in appsettings.xml + - If you have the old version it will continue to function normally +- Updated the default TWAIN implementation + - Choose the "Old DSM" implementation under advanced profile settings to revert +- Changed the default Horizontal Align in profile settings from Left to Right to match most scanners + - If you deploy your own appsettings.xml the specified alignment specified will continue to be used as default +- Added a "LockSystemProfiles" flag to appsettings.xml that allows an administrator better control over user profiles + - See www.naps2.com/doc-org-use.html#lock-system-profiles +- Added an "Offset width based on alignment (WIA)" compatibility option (for ticket #124) +- Added Farsi and Korean languages to installers + +Changes in 4.7.2: +- Fixed a TWAIN issue + +Changes in 4.7.1: +- Improved memory capabilities on 64-bit systems +- Fixed a WIA issue + +Changes in 4.7.0: +- Added option in NAPS2.Console to use auto-save settings (-a/--autosave) +- Added click-and-drag scrolling in the preview window +- Improved cropping (can now click and drag to select an area) +- Added more descriptive error messages for some WIA errors (e.g. device busy) +- Fixed button alignment on left/right toolbar placements +- Added Korean, Lithuanian, and Farsi languages +- Various performance improvements +- Various bug fixes + +Changes in 4.6.1: +- Bug fixes + +Changes in 4.6.0: +- New feature: Exclude blank pages (under "Advanced" in profile settings) +- New options in NAPS2.Console for reordering (e.g. interleave) +- Keyboard shortcuts are now customizable in appsettings.xml (and some more default shortcuts added) +- Optional file type filters when importing +- Importing multiple files at once now sorts the files better +- Fix an issue with the left side of the scanned page being cut off with WIA +- Other bug fixes + +Changes in 4.5.1: +- Improved performance when editing and rearranging thumbnails +- Automatically scroll the thumbnail list when trying to drag thumbnails up or down +- Display an indicator while dragging thumbnails to show where they'll drop +- Fixed Thai/Tagalog OCR language download +- Fixed minor translation issues + +Changes in 4.5.0: +- New feature: Auto Save - Enable it from the profile editor (can be disabled by organizations in appsettings.xml) +- New feature: Drag and Drop support (re-order images within NAPS2, import files into NAPS2, or copy images between different instances of NAPS2) +- New feature: "Advanced" profile options for image quality and scanner compatibility +- New feature: Copy/Paste within NAPS2 (previously could only copy, not paste) +- New progress dialogs for Import, Save, etc. with cancellation +- Better contrast implementation +- Selected images are now kept in view when editing and reordering images +- The default action when clicking on Save PDF, Save Images, and Email PDF can be configured in appsettings.xml (SaveAll, SaveSelected, or AlwaysPrompt) +- New command-line options for NAPS2.exe to enable/disable scanning from a physical "Scan" button in portable versions ("/RegisterSti", "/UnregisterSti", and "/Silent") +- Improved TWAIN error logging +- Bug fixes + +Changes in 4.4.1: +- Tool strip location in the main form is remembered +- Bug fixes + +Changes in 4.4.0: +- New feature: NAPS2 can be started and/or instantly scan when you press the physical "Scan" button on your scanner (requires reboot after installation) +- Added "Delete" to the context menu in the main window +- Fixed file size of black and white images after rotate/crop +- Fixed cancel in OCR download progress window +- Fixed issues with the default profile logic +- Fixed various translation-related issues + +Changes in 4.3.1: +- Bug fixes + +Changes in 4.3.0: +- New feature: Batch scan (under the Scan menu) +- New feature: Bulk image editing (brightness/contrast/crop/custom rotation) +- Added "Alternative Interleave" function for interleaving duplexed pages in a different order +- Added Finnish language +- Bug fixes + +Changes in 4.2.3: +- Added Greek and Estonian languages +- Added support for multiple OCR languages on the command line (e.g. "--ocrlang eng+fra") +- Fixed an issue with importing certain PDFs +- Fixed an issue that caused a black background when rotating and saving as certain formats +- Fixed an issue that caused duplicate close prompts +- Improved responsiveness while importing large PDFs + +Changes in 4.2.2: +- Fixed an issue with OCR for non-English languages +- Fixed an issue with missing translations for Move Up/Down buttons + +Changes in 4.2.1: +- Fixed an issue where focus is lost when scanning from the Profiles window +- Fixed an issue where Native WIA doesn't work properly + +Changes in 4.2.0: +- Added a "Delete" button to the preview window +- Added new keyboard shortcuts to the preview window: Esc (close), Page Up (prev), Page Down (next) +- Added unicode support to PDF metadata and OCR text +- Bug fixes + +Changes in 4.1.1: +- New language: Romanian +- New language: Norwegian (Bokmål) +- Bug fixes + +Changes in 4.1.0: +- Changed the website link in the About window to www.naps2.com +- Changed "Substitutions" to "Placeholders" for consistency with other software +- Bug fixes + +Changes in 4.0b3: +- New feature: Thumbnails can be resized for easier viewing +- New feature: Substitutions can be used in both the GUI and NAPS2.Console when saving (e.g. "$(YYYY)-$(MM)-$(DD) $(nn).pdf" to include the date and an incrementing number) +- New feature: Image settings (default file name, jpeg quality), and default file name setting in PDF settings +- Bug fixes + +Changes in 4.0b2: +- New feature: PDF settings (metadata, encryption) and email settings (can change attachment name) +- Changed format of standalone/portable archives for easier usage +- Scanning multiple pages with WIA no longer steals focus from other applications +- Scanning with WIA in NAPS2.Console no longer displays a separate window +- Bug fixes + +Changes in 4.0b1: +- Merged the Quick Scan functionality into the toolbar +- Merged the previous Scan functionality into the Profiles window +- New feature: Image Editing - Crop, Brightness, Contrast, Custom Rotation +- New feature: Enhanced Preview Window - Can now browse through the images one-by-one and also edit them +- New feature: Print scanned images directly from NAPS2 +- New feature: Prompt when trying to exit with unsaved changes +- New feature: The file type used when saving images is remembered +- Added more keyboard shortcuts (Ctrl+S for save all as PDF, Ctrl+O for import, Ctrl+Enter for scan) + +Changes in 3.3.5: +- Bug fix: Added missing OCR languages + +Changes in 3.3.4: +- New language: Turkish +- Bug fix: Searching PDFs generated with OCR should now work for all readers +- Bug fix: Fixed issue with some TWAIN devices when scanning fails + +Changes in 3.3.3: +- Minor bug fixes + +Changes in 3.3.2: +- Bug fixes + +Changes in 3.3.1: +- Bug fix: Fixed issue with TWAIN + +Changes in 3.3.0: +- New feature: TWAIN with predefined settings +- New feature: OCR options in command-line interface +- New language: Chinese (Taiwan) + +Changes in 3.2.1: +- New language: Albanian +- Bug fix: Increase time allotted for OCR + +Changes in 3.2.0: +- New feature: Custom page sizes +- Added built-in B5 and B4 page size options +- Added 400 and 800 dpi options + +Changes in 3.1.1: +- New language: Swedish +- Bug fix: Dutch language added to installer + +Changes in 3.1.0: +- New feature: One-click scan +- New feature: Can reverse the order of all or some pages with a single click +- New languages: Croatian, Dutch +- Bug fix: Prevent downloading corrupted OCR files +- Bug fix: Resolve some issues when scanning from document feeders + +Changes in 3.0b1: +- New feature: OCR (Optical Character Recognition) to make PDF files searchable +- New feature: Can import PDF and image files (e.g. to resume a previous scanning session) +- New feature: Option to save selected pages only +- New feature: Can re-order (interleave) pages with a single click +- New feature: Added a right-click menu and the ability to copy images to the clipboard +- New feature: Added a 150dpi option to WIA settings +- Bug fix: Incorrect page size for black and white images +- Bug fix: Duplex scanning (for some models) +- Various other changes and bug fixes + +Changes in 2.6.3: +- Added Bulgarian translation +- Added Portuguese translations + +Changes in 2.6.2: +- Added Danish translation + +Changes in 2.6.1: +- Fixed a bug when scanning after clearing previously scanned images +- Fixed an error in NAPS2.Console's help text + +Changes in 2.6: +- Added Czech, French, and Polish translations +- Fixed Catalan translation when using EXE installer + +Changes in 2.5: +- Command-line interface (naps2.console.exe) can send emails +- More windows can be resized, and all windows remember their size and position +- NAPS2 will offer to recover scanned images if it previously closed unexpectedly +- Substantially reduced memory usage +- Added Hebrew and Catalan translations +- Bug fixes + +Changes in 2.4: +- Profiles can now be created without specifying a device (the device will be chosen when scanning) +- Organizations can now configure some application settings in appsettings.xml (see Wiki) +- Updated German translation +- Bug fixes + +Changes in 2.3: +- Added German and Italian translations + +Changes in 2.2: +- Added Russian translation +- Updated Ukrainian translation +- Various bug fixes + +Changes in 2.1: +- Added language dropdown +- Added translations for Spanish and Ukrainian + +Changes in 2.0: +- Major bug fixes for TWAIN on x64 and native WIA +- Added command-line interface (naps2.console.exe) +- Added logging capabilities for error reporting +- Changed .NET dependency from 3.5 Client Profile to 4.0 Client Profile + +Changes in 1.0b2: +- Added Clear button to toolbar +- Added Ctrl+A shortcut to select all thumbnails +- The last-used profile is now remembered and used as the default +- Fix for crash when scanning with the "Black and White" option (credit to Peter De Leeuw) +- Fix for crash when trying to use an offline scanner (WIA) + +Changes in 1.0b1: +- Now requires .NET framework 3.5 (or later) +- New icons +- Better user experience +- Admin no longer required to save profiles +- Various other bug fixes and minor enhancements \ No newline at end of file diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 21ca92c557..6d8674b4d0 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,5 +1,5 @@ Primary NAPS2 developer: -Ben Olden-Cooligan (Copyright 2012-2022) +Ben Olden-Cooligan (Copyright 2012-2025) Original NAPS developer: Pavel Sorejs (Copyright 2009) @@ -10,3 +10,6 @@ Luca De Petrillo (Copyright 2015) Peter De Leeuw Peter Hommel Alexander Rabenstein + +And others: +https://github.com/cyanfish/naps2/graphs/contributors \ No newline at end of file diff --git a/LICENSE b/LICENSE index 95e68d8e5c..d759310349 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ NAPS2 - Not Another PDF Scanner https://www.naps2.com -Copyright 2009-2022 NAPS2 Contributors +Copyright 2009-2025 NAPS2 Contributors This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/NAPS2.App.Console/NAPS2.App.Console.csproj b/NAPS2.App.Console/NAPS2.App.Console.csproj index bf8162006c..4f94379706 100644 --- a/NAPS2.App.Console/NAPS2.App.Console.csproj +++ b/NAPS2.App.Console/NAPS2.App.Console.csproj @@ -1,22 +1,21 @@  - net6-windows;net462 + net9-windows true Exe - app.config - true NAPS2.Console NAPS2.Console - + + true + win-x64;win-arm64 + + false + none + true + NAPS2 - Not Another PDF Scanner NAPS2 - Not Another PDF Scanner - Copyright 2009, 2012-2020 NAPS2 Contributors; Icons from http://www.fatcow.com/free-icons - - - None @@ -24,13 +23,17 @@ - - - + + - + + + Always + appsettings.xml + appsettings.xml + - + \ No newline at end of file diff --git a/NAPS2.App.Console/Program.cs b/NAPS2.App.Console/Program.cs index f9a642981c..5ea10e425b 100644 --- a/NAPS2.App.Console/Program.cs +++ b/NAPS2.App.Console/Program.cs @@ -1,4 +1,5 @@ -using NAPS2.EntryPoints; +using System.Runtime; +using NAPS2.EntryPoints; namespace NAPS2.Console; @@ -10,7 +11,11 @@ static class Program [STAThread] static int Main(string[] args) { - // Use reflection to avoid antivirus false positives (yes, really) - return (int) typeof(WindowsConsoleEntryPoint).GetMethod("Run")!.Invoke(null, new object[] { args })!; + var profilesPath = Path.Combine(Paths.AppData, "jit"); + Directory.CreateDirectory(profilesPath); + ProfileOptimization.SetProfileRoot(profilesPath); + ProfileOptimization.StartProfile("naps2.console.jit"); + + return WindowsConsoleEntryPoint.Run(args); } } \ No newline at end of file diff --git a/NAPS2.App.Console/app.config b/NAPS2.App.Console/app.config deleted file mode 100644 index 1230633e9a..0000000000 --- a/NAPS2.App.Console/app.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/NAPS2.App.Gtk/NAPS2.App.Gtk.csproj b/NAPS2.App.Gtk/NAPS2.App.Gtk.csproj index 1e604f6da3..784a6a27d9 100644 --- a/NAPS2.App.Gtk/NAPS2.App.Gtk.csproj +++ b/NAPS2.App.Gtk/NAPS2.App.Gtk.csproj @@ -1,34 +1,33 @@ - net6 + net9 Exe NAPS2 naps2 - 7.0.1 ../NAPS2.Lib/Icons/favicon.ico true true true + partial - linux-x64 + linux-x64;linux-arm64 NAPS2 - Not Another PDF Scanner NAPS2 - Not Another PDF Scanner - Copyright 2009, 2012-2022 NAPS2 Contributors - - + + - + diff --git a/NAPS2.App.Gtk/Program.cs b/NAPS2.App.Gtk/Program.cs index e2de74b715..26f6df58ae 100644 --- a/NAPS2.App.Gtk/Program.cs +++ b/NAPS2.App.Gtk/Program.cs @@ -1,4 +1,5 @@ -using NAPS2.EntryPoints; +using System.Runtime; +using NAPS2.EntryPoints; namespace NAPS2; @@ -9,6 +10,11 @@ static class Program /// static void Main(string[] args) { + var profilesPath = Path.Combine(Paths.AppData, "jit"); + Directory.CreateDirectory(profilesPath); + ProfileOptimization.SetProfileRoot(profilesPath); + ProfileOptimization.StartProfile("naps2.jit"); + // Use reflection to avoid antivirus false positives (yes, really) typeof(GtkEntryPoint).GetMethod("Run").Invoke(null, new object[] { args }); } diff --git a/NAPS2.App.Mac/Entitlements.plist b/NAPS2.App.Mac/Entitlements.plist index 446fe171da..853adcac37 100644 --- a/NAPS2.App.Mac/Entitlements.plist +++ b/NAPS2.App.Mac/Entitlements.plist @@ -4,5 +4,13 @@ com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-dyld-environment-variables + + + com.apple.security.cs.allow-unsigned-executable-memory + diff --git a/NAPS2.App.Mac/Icon.icns b/NAPS2.App.Mac/Icon.icns index 5e8d6ff8c8..96bb89a763 100644 Binary files a/NAPS2.App.Mac/Icon.icns and b/NAPS2.App.Mac/Icon.icns differ diff --git a/NAPS2.App.Mac/Info.plist b/NAPS2.App.Mac/Info.plist index 0c847a4706..e7498de2df 100644 --- a/NAPS2.App.Mac/Info.plist +++ b/NAPS2.App.Mac/Info.plist @@ -2,14 +2,16 @@ + + CFBundleName NAPS2 CFBundleIdentifier com.naps2.desktop CFBundleShortVersionString - 7.0.1 + 8.2.1 LSMinimumSystemVersion - 10.15 + 12.0 CFBundleDevelopmentRegion en NSHumanReadableCopyright @@ -17,6 +19,77 @@ CFBundleIconFile Icon.icns NSPrincipalClass + + NSApplication + LSBackgroundOnly + + + + CFBundleDocumentTypes + + + CFBundleTypeRole + Editor + CFBundleTypeName + PDF + LSItemContentTypes + + com.adobe.pdf + + + + CFBundleTypeRole + Editor + CFBundleTypeName + JPG + LSItemContentTypes + + public.jpeg + + + + CFBundleTypeRole + Editor + CFBundleTypeName + JP2 + LSItemContentTypes + + public.jpeg-2000 + + + + CFBundleTypeRole + Editor + CFBundleTypeName + PNG + LSItemContentTypes + + public.png + + + + CFBundleTypeRole + Editor + CFBundleTypeName + TIFF + LSItemContentTypes + + public.tiff + + + + CFBundleTypeRole + Editor + CFBundleTypeName + BMP + LSItemContentTypes + + com.microsoft.bmp + + + + diff --git a/NAPS2.App.Mac/NAPS2.App.Mac.csproj b/NAPS2.App.Mac/NAPS2.App.Mac.csproj index 4f5a43f7e3..a692a0d6c6 100644 --- a/NAPS2.App.Mac/NAPS2.App.Mac.csproj +++ b/NAPS2.App.Mac/NAPS2.App.Mac.csproj @@ -1,31 +1,31 @@  - net7-macos10.15 + net9-macos Exe NAPS2 NAPS2 - 7.0.1 ../NAPS2.Lib/Icons/favicon.ico - osx-x64;osx-arm64 + 12.0 + osx-x64;osx-arm64 + partial NAPS2 - Not Another PDF Scanner NAPS2 - Not Another PDF Scanner - Copyright 2009, 2012-2022 NAPS2 Contributors - - - + + + - + diff --git a/NAPS2.App.Mac/Program.cs b/NAPS2.App.Mac/Program.cs index 5ec76fe769..e8c5fc82b5 100644 --- a/NAPS2.App.Mac/Program.cs +++ b/NAPS2.App.Mac/Program.cs @@ -1,3 +1,4 @@ +using System.Runtime; using NAPS2.EntryPoints; namespace NAPS2; @@ -9,6 +10,11 @@ static class Program /// static void Main(string[] args) { + var profilesPath = Path.Combine(Paths.AppData, "jit"); + Directory.CreateDirectory(profilesPath); + ProfileOptimization.SetProfileRoot(profilesPath); + ProfileOptimization.StartProfile("naps2.jit"); + // Use reflection to avoid antivirus false positives (yes, really) typeof(MacEntryPoint).GetMethod("Run").Invoke(null, new object[] { args }); } diff --git a/NAPS2.App.PortableLauncher/NAPS2.App.PortableLauncher.csproj b/NAPS2.App.PortableLauncher/NAPS2.App.PortableLauncher.csproj index e10496ba52..cca05817e1 100644 --- a/NAPS2.App.PortableLauncher/NAPS2.App.PortableLauncher.csproj +++ b/NAPS2.App.PortableLauncher/NAPS2.App.PortableLauncher.csproj @@ -1,14 +1,14 @@  - net6;net462 + net8;net462 WinExe NAPS2.Portable NAPS2.Portable + ../NAPS2.Lib/Icons/favicon.ico NAPS2 - Not Another PDF Scanner NAPS2 - Not Another PDF Scanner - Copyright 2009, 2012-2020 NAPS2 Contributors; Icons from http://www.fatcow.com/free-icons diff --git a/NAPS2.App.PortableLauncher/Program.cs b/NAPS2.App.PortableLauncher/Program.cs index 459723f9b7..8e6e55abf2 100644 --- a/NAPS2.App.PortableLauncher/Program.cs +++ b/NAPS2.App.PortableLauncher/Program.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Threading; namespace NAPS2.Portable; @@ -9,28 +10,55 @@ static void Main(string[] args) var portableExeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); if (portableExeDir != null) { + bool failedUpdate = false; try { if (args.Length == 3 && args[0] == "/Update") { + failedUpdate = true; UpdatePortableApp(portableExeDir, args[1], args[2]); + failedUpdate = false; } } finally { var portableExePath = Path.Combine(portableExeDir, "App", "NAPS2.exe"); - typeof(Process).GetMethod("Start", new[] {typeof(string)}).Invoke(null, new object[] {portableExePath}); + if (failedUpdate) + { + Process.Start(portableExePath, "/FailedUpdate"); + } + else + { + Process.Start(portableExePath); + } } } } private static void UpdatePortableApp(string portableExeDir, string procId, string newAppFolderPath) { - // Wait for the starting process to finish so we don't try to mess with files in use + // Wait for the starting process and workers to finish so we don't try to mess with files in use try { - var proc = Process.GetProcessById(int.Parse(procId)); - proc.WaitForExit(); + var stopwatch = Stopwatch.StartNew(); + + // Wait for the starting process to exit + var mainProc = Process.GetProcessById(int.Parse(procId)); + mainProc.WaitForExit(); + + // Assume any process named NAPS2 or NAPS2.Worker could be a worker, although this isn't necessarily true + // if there are multiple NAPS2 installations. + var processesToWaitOn = + Process.GetProcessesByName("NAPS2") + .Concat(Process.GetProcessesByName("NAPS2.Worker")) + .ToList(); + + // Wait at most 10 seconds for them to exit (which is WorkerEntryPoint.ParentCheckInterval) + const int waitTimeout = 10_000; + while (stopwatch.ElapsedMilliseconds < waitTimeout && processesToWaitOn.Any(x => !x.HasExited)) + { + Thread.Sleep(100); + } } catch (ArgumentException) { diff --git a/NAPS2.App.PortableLauncher/scanner-app.ico b/NAPS2.App.PortableLauncher/scanner-app.ico deleted file mode 100644 index 4ce0e7cc20..0000000000 Binary files a/NAPS2.App.PortableLauncher/scanner-app.ico and /dev/null differ diff --git a/NAPS2.App.Tests/AppTestData.cs b/NAPS2.App.Tests/AppTestData.cs index 1a773f4302..8e45faeb0e 100644 --- a/NAPS2.App.Tests/AppTestData.cs +++ b/NAPS2.App.Tests/AppTestData.cs @@ -7,9 +7,10 @@ public class AppTestData : IEnumerable { public IEnumerator GetEnumerator() { +#if NET6_0_OR_GREATER if (OperatingSystem.IsWindows()) { - yield return new object[] { new WinNet462AppTestTarget() }; + yield return new object[] { new WindowsAppTestTarget() }; } else if (OperatingSystem.IsMacOS()) { @@ -19,6 +20,9 @@ public IEnumerator GetEnumerator() { yield return new object[] { new LinuxAppTestTarget() }; } +#else + yield return new object[] { new WindowsAppTestTarget() }; +#endif } IEnumerator IEnumerable.GetEnumerator() diff --git a/NAPS2.App.Tests/AppTestHelper.cs b/NAPS2.App.Tests/AppTestHelper.cs index 9a65f0c07b..cba7dfec0a 100644 --- a/NAPS2.App.Tests/AppTestHelper.cs +++ b/NAPS2.App.Tests/AppTestHelper.cs @@ -1,6 +1,7 @@ using System.Runtime.InteropServices; using System.Threading; using NAPS2.App.Tests.Targets; +using NAPS2.Scan; using Xunit; namespace NAPS2.App.Tests; @@ -46,7 +47,8 @@ public static string GetBaseDirectory(AppTestExe exe) public static string GetExePath(AppTestExe exe) { var dir = GetBaseDirectory(exe); - if (dir != exe.DefaultRootPath && exe.TestRootSubPath != null) + if (!File.Exists(Path.Combine(dir, exe.ExeSubPath)) + && dir != exe.DefaultRootPath && exe.TestRootSubPath != null) { dir = Path.Combine(dir, exe.TestRootSubPath); } @@ -102,5 +104,21 @@ public static void AssertErrorLog(string appData) Assert.True(File.Exists(path), path); } - public static string SolutionRoot => Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..")); + public static string SolutionRoot => + Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..")); + + public static string GetDeviceName(Driver driver) + { + var deviceFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".naps2", "devices"); + if (!File.Exists(deviceFileName)) return null; + var driverStr = driver.ToString().ToLowerInvariant(); + foreach (var line in File.ReadLines(deviceFileName)) + { + if (line.StartsWith(driverStr + "=")) + { + return line.Trim().Substring(driverStr.Length + 1); + } + } + return null; + } } \ No newline at end of file diff --git a/NAPS2.App.Tests/Appium/AppiumTestData.cs b/NAPS2.App.Tests/Appium/AppiumTestData.cs index ca039d1c3a..8b8a6f3be9 100644 --- a/NAPS2.App.Tests/Appium/AppiumTestData.cs +++ b/NAPS2.App.Tests/Appium/AppiumTestData.cs @@ -7,9 +7,10 @@ public class AppiumTestData : IEnumerable { public IEnumerator GetEnumerator() { +#if NET6_0_OR_GREATER if (OperatingSystem.IsWindows()) { - yield return new object[] { new WinNet462AppTestTarget() }; + yield return new object[] { new WindowsAppTestTarget() }; } else if (OperatingSystem.IsMacOS()) { @@ -19,6 +20,9 @@ public IEnumerator GetEnumerator() { // No Appium impl yet } +#else + yield return new object[] { new WindowsAppTestTarget() }; +#endif } IEnumerator IEnumerable.GetEnumerator() diff --git a/NAPS2.App.Tests/Appium/AppiumTests.cs b/NAPS2.App.Tests/Appium/AppiumTests.cs index 18bbe78e24..be33192b08 100644 --- a/NAPS2.App.Tests/Appium/AppiumTests.cs +++ b/NAPS2.App.Tests/Appium/AppiumTests.cs @@ -1,7 +1,7 @@ +using System.Linq.Expressions; using System.Threading; using NAPS2.App.Tests.Targets; using NAPS2.Sdk.Tests; -using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; @@ -21,41 +21,53 @@ private static WindowsDriver StartSession(AppTestExe exe, string public void Init(IAppTestTarget target) { + Thread.Sleep(5000); _session = StartSession(target.Gui, FolderPath); + ResetMainWindow(); } public override void Dispose() { - _session.Dispose(); + try + { + _session.Dispose(); + } + catch (Exception) + { + // Ignore disposal errors + } base.Dispose(); } - protected void WaitUntilGone(string name, int timeoutInMs) + protected void ResetMainWindow() + { + _session.SwitchTo().Window(WaitFor(() => _session.WindowHandles.Single())); + } + + protected T WaitFor(Expression> expr, int timeoutInMs = 10_000) { + var func = expr.Compile(); var stopwatch = Stopwatch.StartNew(); - try + while (true) { - while (true) + try { - if (_session.FindElementsByName(name).Count == 0) + var value = func(); + if (value is null or false) { - break; + throw new Exception(); } + return value; + } + catch (Exception) + { if (stopwatch.ElapsedMilliseconds > timeoutInMs) { - throw new WebDriverException("Timeout waiting for element to be gone"); + throw new Exception($"Timed out waiting for \"{expr.Body}\""); } Thread.Sleep(100); } } - catch (InvalidOperationException) - { - } - } - - protected void ResetMainWindow() - { - _session.SwitchTo().Window(_session.WindowHandles[0]); } protected void ClickAt(WindowsElement element) @@ -68,7 +80,7 @@ protected void ClickAt(WindowsElement element) protected void ClickAtName(string name) { - ClickAt(WaitAndFindElementByName(name)); + ClickAt(WaitFor(() => _session.FindElementByName(name))); } protected void DoubleClickAt(WindowsElement element) @@ -82,23 +94,11 @@ protected void DoubleClickAt(WindowsElement element) protected void DoubleClickAtName(string name) { - DoubleClickAt(WaitAndFindElementByName(name)); + DoubleClickAt(WaitFor(() => _session.FindElementByName(name))); } - protected WindowsElement WaitAndFindElementByName(string name) + protected bool HasElementWithName(string name) { - int i = 0; - while(true) - { - try - { - return _session.FindElementByName(name); - } - catch (WebDriverException) - { - if (++i > 10) throw; - Thread.Sleep(100); - } - } + return _session.FindElementsByName(name).Count > 0; } } \ No newline at end of file diff --git a/NAPS2.App.Tests/Appium/ImportAndSaveTests.cs b/NAPS2.App.Tests/Appium/ImportAndSaveTests.cs index 06956d3626..73aac0f7b4 100644 --- a/NAPS2.App.Tests/Appium/ImportAndSaveTests.cs +++ b/NAPS2.App.Tests/Appium/ImportAndSaveTests.cs @@ -10,7 +10,7 @@ namespace NAPS2.App.Tests.Appium; [Collection("appium")] public class ImportAndSaveTests : AppiumTests { - [VerifyTheory(AllowDebug = true)] + [VerifyTheory(AllowDebug = true, WindowsAppium = true)] [ClassData(typeof(AppiumTestData))] public void ImportVariousAndSavePdfWithOcr(IAppTestTarget target) { @@ -34,14 +34,13 @@ public void ImportVariousAndSavePdfWithOcr(IAppTestTarget target) ClickAtName("Save PDF"); ResetMainWindow(); - var fileNameElements = _session.FindElementsByName("File name:"); - var fileTextBox = fileNameElements.Last(); + var fileTextBox = WaitFor(() => _session.FindElementsByName("File name:").Last()); ClickAt(fileTextBox); fileTextBox.SendKeys("test.pdf"); ClickAtName("Save"); // Wait for the save to finish Thread.Sleep(100); - WaitUntilGone("Cancel", 10_000); + WaitFor(() => !HasElementWithName("Cancel"), 30_000); var path = Path.Combine(FolderPath, "test.pdf"); PdfAsserts.AssertImages(path, diff --git a/NAPS2.App.Tests/Appium/LanguageSelectionTests.cs b/NAPS2.App.Tests/Appium/LanguageSelectionTests.cs index 05656ef2b9..d2a5574507 100644 --- a/NAPS2.App.Tests/Appium/LanguageSelectionTests.cs +++ b/NAPS2.App.Tests/Appium/LanguageSelectionTests.cs @@ -11,9 +11,9 @@ namespace NAPS2.App.Tests.Appium; [Collection("appium")] public class LanguageSelectionTests : AppiumTests { - private static readonly HashSet ExpectedMissingLanguages = new() { "bn", "hi", "id", "th", "ur" }; + private static readonly HashSet ExpectedMissingLanguages = ["bn", "ur"]; - [VerifyTheory(AllowDebug = true)] + [VerifyTheory(AllowDebug = true, WindowsAppium = true)] [ClassData(typeof(AppiumTestData))] public void OpenLanguageDropdown(IAppTestTarget target) { diff --git a/NAPS2.App.Tests/Appium/ScanAndSaveTests.cs b/NAPS2.App.Tests/Appium/ScanAndSaveTests.cs index 616066afeb..5ed6c21d89 100644 --- a/NAPS2.App.Tests/Appium/ScanAndSaveTests.cs +++ b/NAPS2.App.Tests/Appium/ScanAndSaveTests.cs @@ -1,6 +1,7 @@ using System.Threading; using NAPS2.App.Tests.Targets; using NAPS2.App.Tests.Verification; +using NAPS2.Scan; using NAPS2.Sdk.Tests.Asserts; using Xunit; @@ -10,74 +11,71 @@ namespace NAPS2.App.Tests.Appium; [Collection("appium")] public class ScanAndSaveTests : AppiumTests { - private const string WIA_DEVICE_NAME = ""; - private const string TWAIN_DEVICE_NAME = ""; - - [VerifyTheory(AllowDebug = true)] + [VerifyTheory(AllowDebug = true, WindowsAppium = true)] [ClassData(typeof(AppiumTestData))] public void ScanWiaSavePdf(IAppTestTarget target) { Init(target); // Clicking Scan without a profile opens the profile settings window ClickAtName("Scan"); - // WIA driver is selected by default, so we open the WIA device dialog ClickAtName("Choose device"); - Thread.Sleep(100); - if (WIA_DEVICE_NAME != "") ClickAtName(WIA_DEVICE_NAME); - // Click OK in the wia device dialog (selecting the first available device by default) - // TODO: More consistent way to pick the right OK button - ClickAt(_session.FindElementsByName("OK")[0]); - WaitUntilGone("Properties", 1_000); + WaitFor(() => HasElementWithName("Always Ask")); + var deviceName = AppTestHelper.GetDeviceName(Driver.Wia); + // WIA driver is selected by default, so we just click the device + if (!string.IsNullOrEmpty(deviceName)) ClickAtName(deviceName); + ClickAt(_session.FindElementByName("Select")); + WaitFor(() => !HasElementWithName("Select")); // Click OK in the profile settings window ClickAtName("OK"); + WaitFor(() => HasElementWithName("Cancel")); // Wait for scanning to finish - WaitUntilGone("Cancel", 30_000); + WaitFor(() => !HasElementWithName("Cancel"), 30_000); ResetMainWindow(); // Save "test.pdf" in the default location (which will be the test data path as NAPS2 knows we're in a test)^ ClickAtName("Save PDF"); ResetMainWindow(); - var fileNameElements = _session.FindElementsByName("File name:"); - var fileTextBox = fileNameElements.Last(); + var fileTextBox = WaitFor(() => _session.FindElementsByName("File name:").Last()); ClickAt(fileTextBox); fileTextBox.SendKeys("test.pdf"); ClickAtName("Save"); // Wait for the save to finish, it should be almost instant - Thread.Sleep(200); + Thread.Sleep(1000); PdfAsserts.AssertPageCount(1, Path.Combine(FolderPath, "test.pdf")); AppTestHelper.AssertNoErrorLog(FolderPath); } - [VerifyTheory(AllowDebug = true)] + [VerifyTheory(AllowDebug = true, WindowsAppium = true)] [ClassData(typeof(AppiumTestData))] public void ScanTwainSaveImage(IAppTestTarget target) { Init(target); // Clicking Scan without a profile opens the profile settings window ClickAtName("Scan"); - ClickAtName("TWAIN Driver"); - // Open the TWAIN device dialog ClickAtName("Choose device"); + WaitFor(() => HasElementWithName("Always Ask")); + var deviceName = AppTestHelper.GetDeviceName(Driver.Twain); + ClickAtName("TWAIN Driver"); Thread.Sleep(100); - if (TWAIN_DEVICE_NAME != "") ClickAtName(TWAIN_DEVICE_NAME); - // Click Select in the twain device dialog (selecting the first available device by default) + WaitFor(() => HasElementWithName("Always Ask")); + if (!string.IsNullOrEmpty(deviceName)) ClickAtName(deviceName); ClickAtName("Select"); - WaitUntilGone("Select", 1_000); + WaitFor(() => !HasElementWithName("Select")); // Click OK in the profile settings window ClickAtName("OK"); + WaitFor(() => HasElementWithName("Cancel")); // Wait for scanning to finish - WaitUntilGone("Cancel", 30_000); + WaitFor(() => !HasElementWithName("Cancel"), 30_000); ResetMainWindow(); // Save "test.pdf" in the default location (which will be the test data path as NAPS2 knows we're in a test)^ ClickAtName("Save Images"); ResetMainWindow(); - var fileNameElements = _session.FindElementsByName("File name:"); - var fileTextBox = fileNameElements.Last(); + var fileTextBox = WaitFor(() => _session.FindElementsByName("File name:").Last()); ClickAt(fileTextBox); fileTextBox.SendKeys("test.jpg"); ClickAtName("Save"); // Wait for the save to finish, it should be almost instant - Thread.Sleep(200); + Thread.Sleep(1000); ImageAsserts.Inches(Path.Combine(FolderPath, "test.jpg"), 8.5, 11); AppTestHelper.AssertNoErrorLog(FolderPath); diff --git a/NAPS2.App.Tests/ConsoleAppTests.cs b/NAPS2.App.Tests/ConsoleAppTests.cs index 33b722c4a8..7b30d89323 100644 --- a/NAPS2.App.Tests/ConsoleAppTests.cs +++ b/NAPS2.App.Tests/ConsoleAppTests.cs @@ -6,6 +6,8 @@ namespace NAPS2.App.Tests; public class ConsoleAppTests : ContextualTests { + private const int EXIT_TIMEOUT = 30_000; + [Theory] [ClassData(typeof(AppTestData))] public void ConvertsImportedFile(IAppTestTarget target) @@ -17,7 +19,7 @@ public void ConvertsImportedFile(IAppTestTarget target) var process = AppTestHelper.StartProcess(target.Console, FolderPath, args); try { - Assert.True(process.WaitForExit(5000)); + Assert.True(process.WaitForExit(EXIT_TIMEOUT)); var stdout = process.StandardOutput.ReadToEnd(); Assert.Equal(0, process.ExitCode); Assert.Empty(stdout); @@ -41,9 +43,9 @@ public void NonZeroExitCodeForError(IAppTestTarget target) var process = AppTestHelper.StartProcess(target.Console, FolderPath, args); try { - Assert.True(process.WaitForExit(5000)); + Assert.True(process.WaitForExit(EXIT_TIMEOUT)); var stdout = process.StandardOutput.ReadToEnd(); - if (OperatingSystem.IsWindows()) + if (target.IsWindows) { // TODO: Figure out why ExitCode always appears as 0 on Mac/Linux Assert.NotEqual(0, process.ExitCode); diff --git a/NAPS2.App.Tests/GuiAppTests.cs b/NAPS2.App.Tests/GuiAppTests.cs index 47331afa8d..a2848a3448 100644 --- a/NAPS2.App.Tests/GuiAppTests.cs +++ b/NAPS2.App.Tests/GuiAppTests.cs @@ -1,4 +1,3 @@ -using System.Threading; using NAPS2.App.Tests.Targets; using NAPS2.Remoting; using NAPS2.Sdk.Tests; @@ -8,14 +7,14 @@ namespace NAPS2.App.Tests; public class GuiAppTests : ContextualTests { - [Theory] + [GuiTheory] [ClassData(typeof(AppTestData))] public void CreatesWindow(IAppTestTarget target) { var process = AppTestHelper.StartGuiProcess(target.Gui, FolderPath); try { - if (OperatingSystem.IsWindows()) + if (target.IsWindows) { AppTestHelper.WaitForVisibleWindow(process); Assert.Equal("NAPS2 - Not Another PDF Scanner", process.MainWindowTitle); @@ -23,8 +22,8 @@ public void CreatesWindow(IAppTestTarget target) } else { - Thread.Sleep(1000); - Assert.True(Pipes.SendMessage(process, Pipes.MSG_CLOSE_WINDOW)); + var helper = ProcessCoordinator.CreateDefault(); + Assert.True(helper.CloseWindow(process, 5000)); } Assert.True(process.WaitForExit(5000)); AppTestHelper.AssertNoErrorLog(FolderPath); diff --git a/NAPS2.App.Tests/GuiTheoryAttribute.cs b/NAPS2.App.Tests/GuiTheoryAttribute.cs new file mode 100644 index 0000000000..58d875a013 --- /dev/null +++ b/NAPS2.App.Tests/GuiTheoryAttribute.cs @@ -0,0 +1,14 @@ +using Xunit; + +namespace NAPS2.App.Tests; + +public class GuiTheoryAttribute : TheoryAttribute +{ + public GuiTheoryAttribute() + { + if (Environment.GetEnvironmentVariable("NAPS2_TEST_NOGUI") == "1") + { + Skip = "Running in headless mode, skipping GUI tests"; + } + } +} \ No newline at end of file diff --git a/NAPS2.App.Tests/NAPS2.App.Tests.csproj b/NAPS2.App.Tests/NAPS2.App.Tests.csproj index a0bef07c53..db41c078f7 100644 --- a/NAPS2.App.Tests/NAPS2.App.Tests.csproj +++ b/NAPS2.App.Tests/NAPS2.App.Tests.csproj @@ -1,7 +1,8 @@ - net6 + net9-windows + net9 true None Debug;Release;DebugLang @@ -13,16 +14,16 @@ - - + + - - - - - + + + + + \ No newline at end of file diff --git a/NAPS2.App.Tests/ServerAppTests.cs b/NAPS2.App.Tests/ServerAppTests.cs new file mode 100644 index 0000000000..377f65f646 --- /dev/null +++ b/NAPS2.App.Tests/ServerAppTests.cs @@ -0,0 +1,27 @@ +using NAPS2.App.Tests.Targets; +using NAPS2.Remoting; +using NAPS2.Sdk.Tests; +using Xunit; + +namespace NAPS2.App.Tests; + +public class ServerAppTests : ContextualTests +{ + [GuiTheory] + [ClassData(typeof(AppTestData))] + public void StartAndStopServer(IAppTestTarget target) + { + var process = AppTestHelper.StartProcess(target.Server, FolderPath); + try + { + var helper = ProcessCoordinator.CreateDefault(); + Assert.True(helper.StopSharingServer(process, 5000)); + Assert.True(process.WaitForExit(5000)); + AppTestHelper.AssertNoErrorLog(FolderPath); + } + finally + { + AppTestHelper.Cleanup(process); + } + } +} \ No newline at end of file diff --git a/NAPS2.App.Tests/Targets/IAppTestTarget.cs b/NAPS2.App.Tests/Targets/IAppTestTarget.cs index 454095706c..70ecccd713 100644 --- a/NAPS2.App.Tests/Targets/IAppTestTarget.cs +++ b/NAPS2.App.Tests/Targets/IAppTestTarget.cs @@ -5,4 +5,6 @@ public interface IAppTestTarget AppTestExe Console { get; } AppTestExe Gui { get; } AppTestExe Worker { get; } + AppTestExe Server { get; } + bool IsWindows { get; } } \ No newline at end of file diff --git a/NAPS2.App.Tests/Targets/LinuxAppTestTarget.cs b/NAPS2.App.Tests/Targets/LinuxAppTestTarget.cs index 4e90031aa0..c8f5411567 100644 --- a/NAPS2.App.Tests/Targets/LinuxAppTestTarget.cs +++ b/NAPS2.App.Tests/Targets/LinuxAppTestTarget.cs @@ -7,12 +7,14 @@ public class LinuxAppTestTarget : IAppTestTarget public AppTestExe Console => GetAppTestExe("console"); public AppTestExe Gui => GetAppTestExe(null); public AppTestExe Worker => GetAppTestExe("worker"); + public AppTestExe Server => GetAppTestExe("server"); + public bool IsWindows => false; private AppTestExe GetAppTestExe(string argPrefix) { var runtime = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "linux-arm64" : "linux-x64"; return new AppTestExe( - Path.Combine(AppTestHelper.SolutionRoot, "NAPS2.App.Gtk", "bin", "Debug", "net6", runtime), + Path.Combine(AppTestHelper.SolutionRoot, "NAPS2.App.Gtk", "bin", "Debug", "net9", runtime), "naps2", argPrefix); } diff --git a/NAPS2.App.Tests/Targets/MacAppTestTarget.cs b/NAPS2.App.Tests/Targets/MacAppTestTarget.cs index ccec634be3..87152e1c96 100644 --- a/NAPS2.App.Tests/Targets/MacAppTestTarget.cs +++ b/NAPS2.App.Tests/Targets/MacAppTestTarget.cs @@ -5,11 +5,13 @@ public class MacAppTestTarget : IAppTestTarget public AppTestExe Console => GetAppTestExe("console"); public AppTestExe Gui => GetAppTestExe(null); public AppTestExe Worker => GetAppTestExe("worker"); + public AppTestExe Server => GetAppTestExe("server"); + public bool IsWindows => false; private AppTestExe GetAppTestExe(string argPrefix) { return new AppTestExe( - Path.Combine(AppTestHelper.SolutionRoot, "NAPS2.App.Mac", "bin", "Debug", "net7-macos10.15"), + Path.Combine(AppTestHelper.SolutionRoot, "NAPS2.App.Mac", "bin", "Debug", "net9-macos"), Path.Combine("NAPS2.app", "Contents", "MacOS", "NAPS2"), argPrefix); } diff --git a/NAPS2.App.Tests/Targets/WinNet462AppTestTarget.cs b/NAPS2.App.Tests/Targets/WinNet462AppTestTarget.cs deleted file mode 100644 index 7b5f24ccaf..0000000000 --- a/NAPS2.App.Tests/Targets/WinNet462AppTestTarget.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace NAPS2.App.Tests.Targets; - -public class WinNet462AppTestTarget : IAppTestTarget -{ - public AppTestExe Console => GetAppTestExe("NAPS2.App.Console", "NAPS2.Console.exe", null); - public AppTestExe Gui => GetAppTestExe("NAPS2.App.WinForms", "NAPS2.exe", null); - public AppTestExe Worker => GetAppTestExe("NAPS2.App.Worker", "NAPS2.Worker.exe", "lib"); - - private AppTestExe GetAppTestExe(string project, string exeName, string testRootSubPath) - { - return new AppTestExe( - Path.Combine(AppTestHelper.SolutionRoot, project, "bin", "Debug", "net462"), - exeName, - TestRootSubPath: testRootSubPath); - } - - public override string ToString() => "Windows (net462)"; -} \ No newline at end of file diff --git a/NAPS2.App.Tests/Targets/WindowsAppTestTarget.cs b/NAPS2.App.Tests/Targets/WindowsAppTestTarget.cs new file mode 100644 index 0000000000..98a044e84b --- /dev/null +++ b/NAPS2.App.Tests/Targets/WindowsAppTestTarget.cs @@ -0,0 +1,22 @@ +namespace NAPS2.App.Tests.Targets; + +public class WindowsAppTestTarget : IAppTestTarget +{ + public AppTestExe Console => GetAppTestExe("NAPS2.App.Console", "NAPS2.Console.exe", "win-x64"); + public AppTestExe Gui => GetAppTestExe("NAPS2.App.WinForms", "NAPS2.exe", "win-x64"); + public AppTestExe Worker => GetAppTestExe("NAPS2.App.Worker", "NAPS2.Worker.exe", "win-x86", null, "lib"); + public AppTestExe Server => GetAppTestExe("NAPS2.App.WinForms", "NAPS2.exe", "win-x64", "server"); + public bool IsWindows => true; + + private AppTestExe GetAppTestExe(string project, string exeName, string arch, string argPrefix = null, + string testRootSubPath = null) + { + return new AppTestExe( + Path.Combine(AppTestHelper.SolutionRoot, project, "bin", "Debug", "net9-windows", arch), + exeName, + argPrefix, + testRootSubPath); + } + + public override string ToString() => "Windows"; +} \ No newline at end of file diff --git a/NAPS2.App.Tests/Verification/InstallDirTestData.cs b/NAPS2.App.Tests/Verification/InstallDirTestData.cs index acf8cc2e04..e2e9f5b68b 100644 --- a/NAPS2.App.Tests/Verification/InstallDirTestData.cs +++ b/NAPS2.App.Tests/Verification/InstallDirTestData.cs @@ -6,7 +6,12 @@ public class InstallDirTestData : IEnumerable { public IEnumerator GetEnumerator() { - if (OperatingSystem.IsWindows() && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NAPS2_TEST_VERIFY"))) + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NAPS2_TEST_VERIFY"))) + { + yield break; + } +#if NET6_0_OR_GREATER + if (OperatingSystem.IsWindows()) { yield return new object[] { Environment.GetEnvironmentVariable("NAPS2_TEST_ROOT") }; } @@ -18,6 +23,9 @@ public IEnumerator GetEnumerator() { // No tests yet } +#else + yield return new object[] { Environment.GetEnvironmentVariable("NAPS2_TEST_ROOT") }; +#endif } IEnumerator IEnumerable.GetEnumerator() diff --git a/NAPS2.App.Tests/Verification/VerifyFactAttribute.cs b/NAPS2.App.Tests/Verification/VerifyFactAttribute.cs index eb53405aff..f2b35901a3 100644 --- a/NAPS2.App.Tests/Verification/VerifyFactAttribute.cs +++ b/NAPS2.App.Tests/Verification/VerifyFactAttribute.cs @@ -5,6 +5,7 @@ namespace NAPS2.App.Tests.Verification; public sealed class VerifyTheoryAttribute : TheoryAttribute { private bool _allowDebug; + private bool _windowsAppium; public VerifyTheoryAttribute() { @@ -33,4 +34,19 @@ public bool AllowDebug _allowDebug = value; } } + + public bool WindowsAppium + { + get => _windowsAppium; + set + { +#if NET6_0_OR_GREATER + if (value && Skip == null && !OperatingSystem.IsWindows()) + { + Skip = "Appium tests are only supported on Windows right now."; + } +#endif + _windowsAppium = value; + } + } } \ No newline at end of file diff --git a/NAPS2.App.WinForms/NAPS2.App.WinForms.csproj b/NAPS2.App.WinForms/NAPS2.App.WinForms.csproj index 0182e57abb..c474a455c2 100644 --- a/NAPS2.App.WinForms/NAPS2.App.WinForms.csproj +++ b/NAPS2.App.WinForms/NAPS2.App.WinForms.csproj @@ -1,30 +1,22 @@  - net6-windows;net462 + net9-windows true WinExe - app.config - true NAPS2 NAPS2 - 7.0.1 ../NAPS2.Lib/Icons/favicon.ico - - - - - + true + win-x64;win-arm64 + + false + none + true NAPS2 - Not Another PDF Scanner NAPS2 - Not Another PDF Scanner - Copyright 2009, 2012-2020 NAPS2 Contributors; Icons from http://www.fatcow.com/free-icons - - - None @@ -32,13 +24,10 @@ - - + + - - - diff --git a/NAPS2.App.WinForms/Program.cs b/NAPS2.App.WinForms/Program.cs index d41eb00e33..bbfed04e6f 100644 --- a/NAPS2.App.WinForms/Program.cs +++ b/NAPS2.App.WinForms/Program.cs @@ -1,3 +1,4 @@ +using System.Runtime; using NAPS2.EntryPoints; namespace NAPS2; @@ -10,7 +11,11 @@ static class Program [STAThread] static void Main(string[] args) { - // Use reflection to avoid antivirus false positives (yes, really) - typeof(WinFormsEntryPoint).GetMethod("Run").Invoke(null, new object[] { args }); + var profilesPath = Path.Combine(Paths.AppData, "jit"); + Directory.CreateDirectory(profilesPath); + ProfileOptimization.SetProfileRoot(profilesPath); + ProfileOptimization.StartProfile("naps2.jit"); + + WinFormsEntryPoint.Run(args); } } \ No newline at end of file diff --git a/NAPS2.App.WinForms/app.config b/NAPS2.App.WinForms/app.config deleted file mode 100644 index 1230633e9a..0000000000 --- a/NAPS2.App.WinForms/app.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/NAPS2.App.WinForms/runtimeconfig.template.json b/NAPS2.App.WinForms/runtimeconfig.template.json new file mode 100644 index 0000000000..4f1f5b6f5c --- /dev/null +++ b/NAPS2.App.WinForms/runtimeconfig.template.json @@ -0,0 +1,5 @@ +{ + "configProperties": { + "System.Windows.Forms.ScaleTopLevelFormMinMaxSizeForDpi": false + } +} \ No newline at end of file diff --git a/NAPS2.App.Worker/NAPS2.App.Worker.csproj b/NAPS2.App.Worker/NAPS2.App.Worker.csproj index 24c3e149b9..7ffd26807f 100644 --- a/NAPS2.App.Worker/NAPS2.App.Worker.csproj +++ b/NAPS2.App.Worker/NAPS2.App.Worker.csproj @@ -1,27 +1,42 @@  - net6-windows;net462 + net9-windows true WinExe + enable true NAPS2.Worker NAPS2.Worker - x86 + true + true + win-x86 + true + partial + true NAPS2 - Not Another PDF Scanner NAPS2 - Not Another PDF Scanner - Copyright 2009, 2012-2020 NAPS2 Contributors; Icons from http://www.fatcow.com/free-icons + - - + + + + + + - + + + + + + \ No newline at end of file diff --git a/NAPS2.App.Worker/Program.cs b/NAPS2.App.Worker/Program.cs index 04c73fd732..88d13d82cd 100644 --- a/NAPS2.App.Worker/Program.cs +++ b/NAPS2.App.Worker/Program.cs @@ -1,4 +1,11 @@ -using NAPS2.EntryPoints; +using System.Runtime; +using NAPS2.EntryPoints; +using NAPS2.Images.Gdi; +using NAPS2.ImportExport.Email.Mapi; +using NAPS2.Platform.Windows; +using NAPS2.Remoting.Worker; +using NAPS2.Scan; +using NAPS2.Scan.Internal.Twain; namespace NAPS2.Worker; @@ -8,9 +15,31 @@ static class Program /// The NAPS2.Worker.exe main method. /// [STAThread] - static void Main(string[] args) + static int Main(string[] args) { - // Use reflection to avoid antivirus false positives (yes, really) - typeof(WindowsWorkerEntryPoint).GetMethod("Run").Invoke(null, new object[] { args }); + var profilesPath = Path.Combine(Paths.AppData, "jit"); + Directory.CreateDirectory(profilesPath); + ProfileOptimization.SetProfileRoot(profilesPath); + ProfileOptimization.StartProfile("naps2.worker.jit"); + + // This NAPS2.App.Worker project doesn't follow the conventions of the rest of NAPS2 as far as using EntryPoint + // classes for everything. The reason is that we want to avoid pulling in extra dependencies as NAPS2.Worker.exe + // is 32-bit and therefore requires a second copy of every single dependency we use. + // + // Thus the simplest solution is just to pull in a bit of code from NAPS2.Lib that has what we need + // (pretty much only paths, logging, and the worker setup) and avoid using Autofac. + var logger = NLogConfig.CreateLogger(() => NLogConfig.EnvDebugLogging); + var messagePump = Win32MessagePump.Create(); + messagePump.Logger = logger; + var scanningContext = new ScanningContext(new GdiImageContext()); + scanningContext.Logger = logger; + var serviceImpl = new WorkerServiceImpl(scanningContext, new ThumbnailRenderer(scanningContext.ImageContext), + new MapiWrapper(logger), new LocalTwainController(scanningContext)); + + Trace.Listeners.Add(new NLog.NLogTraceListener()); + Invoker.Current = messagePump; + TwainHandleManager.Factory = () => new Win32TwainHandleManager(messagePump); + + return CoreWorkerEntryPoint.Run(args, logger, serviceImpl, messagePump.RunMessageLoop, messagePump.Dispose); } } \ No newline at end of file diff --git a/NAPS2.Escl.Client/CompilerAttributes.cs b/NAPS2.Escl.Client/CompilerAttributes.cs deleted file mode 100644 index 8230a289b1..0000000000 --- a/NAPS2.Escl.Client/CompilerAttributes.cs +++ /dev/null @@ -1,61 +0,0 @@ -// https://sergiopedri.medium.com/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb -// ReSharper disable once CheckNamespace - -namespace System.Runtime.CompilerServices -{ - internal static class IsExternalInit - { - } - - /// Specifies that a type has required members or that a member is required. - [AttributeUsage( - AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, - AllowMultiple = false, Inherited = false)] - internal sealed class RequiredMemberAttribute : Attribute - { - } - - /// - /// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. - /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] - internal sealed class CompilerFeatureRequiredAttribute : Attribute - { - public CompilerFeatureRequiredAttribute(string featureName) - { - FeatureName = featureName; - } - - /// - /// The name of the compiler feature. - /// - public string FeatureName { get; } - - /// - /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . - /// - public bool IsOptional { get; init; } - - /// - /// The used for the ref structs C# feature. - /// - public const string RefStructs = nameof(RefStructs); - - /// - /// The used for the required members C# feature. - /// - public const string RequiredMembers = nameof(RequiredMembers); - } -} - -namespace System.Diagnostics.CodeAnalysis -{ - /// - /// Specifies that this constructor sets all required members for the current type, and callers - /// do not need to set any required members themselves. - /// - [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] - internal sealed class SetsRequiredMembersAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/NAPS2.Escl.Client/EsclClient.cs b/NAPS2.Escl.Client/EsclClient.cs deleted file mode 100644 index 47170776b7..0000000000 --- a/NAPS2.Escl.Client/EsclClient.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System.Xml.Linq; - -namespace NAPS2.Escl.Client; - -public class EsclClient -{ - private static readonly XNamespace ScanNs = EsclXmlHelper.ScanNs; - private static readonly XNamespace PwgNs = EsclXmlHelper.PwgNs; - - private readonly EsclService _service; - - public EsclClient(EsclService service) - { - _service = service; - } - - public async Task GetCapabilities() - { - var doc = await DoRequest("ScannerCapabilities"); - var root = doc.Root; - if (root?.Name != ScanNs + "ScannerCapabilities") - { - throw new InvalidOperationException("Unexpected root element: " + doc.Root?.Name); - } - var settingProfilesEl = root.Element(ScanNs + "SettingProfiles"); - var settingProfiles = new Dictionary(); - if (settingProfilesEl != null) - { - foreach (var el in settingProfilesEl.Elements(ScanNs + "SettingProfile")) - { - ParseSettingProfile(el, settingProfiles); - } - } - return new EsclCapabilities - { - Version = root.Element(PwgNs + "Version")?.Value, - MakeAndModel = root.Element(PwgNs + "MakeAndModel")?.Value, - SerialNumber = root.Element(PwgNs + "SerialNumber")?.Value, - Uuid = root.Element(ScanNs + "UUID")?.Value, - AdminUri = root.Element(ScanNs + "AdminURI")?.Value, - IconUri = root.Element(ScanNs + "IconURI")?.Value - }; - } - - public async Task GetStatus() - { - var text = await new HttpClient().GetStringAsync(GetUrl("ScannerStatus")); - var doc = XDocument.Parse(text); - return new EsclScannerStatus(); - } - - public async Task CreateScanJob(EsclScanSettings scanSettings) - { - var doc = - EsclXmlHelper.CreateDocAsString( - new XElement(ScanNs + "ScanSettings", - new XElement(PwgNs + "Version", "2.6"), - new XElement(ScanNs + "Intent", "Photo"), - new XElement(PwgNs + "ScanRegions", - new XElement(PwgNs + "ScanRegion", - new XElement(PwgNs + "Height", "1200"), - new XElement(PwgNs + "ContentRegionUnits", "escl:ThreeHundredthsOfInches"), - new XElement(PwgNs + "Width", "1800"), - new XElement(PwgNs + "XOffset"), - new XElement(PwgNs + "YOffset"))), - new XElement(PwgNs + "InputSource", "Platen"), - new XElement(ScanNs + "ColorMode", "Grayscale8"))); - var response = await new HttpClient().PostAsync(GetUrl("ScanJobs"), new StringContent(doc)); - response.EnsureSuccessStatusCode(); - return new EsclJob - { - Uri = response.Headers.Location! - }; - } - - public async Task NextDocument(EsclJob job) - { - var client = new HttpClient(); - client.DefaultRequestHeaders.TransferEncodingChunked = true; - // TODO: Maybe check Content-Location on the response header to ensure no duplicate document? - return await client.GetByteArrayAsync(job.Uri + "/NextDocument"); - } - - private EsclSettingProfile ParseSettingProfile(XElement element, - Dictionary profilesDict) - { - var profileRef = element.Attribute("ref")?.Value; - if (profileRef != null) - { - return profilesDict[profileRef]; - } - var profile = new EsclSettingProfile - { - Name = element.Attribute("name")?.Value, - ColorModes = - ParseEnumValues(element.Element(ScanNs + "ColorModes")?.Elements(ScanNs + "ColorMode")) - }; - if (profile.Name != null) - { - profilesDict[profile.Name] = profile; - } - return profile; - } - - private List ParseEnumValues(IEnumerable? elements) where T : struct - { - var list = new List(); - if (elements == null) - { - return list; - } - foreach (var el in elements) - { - if (Enum.TryParse(el.Value, out var parsed)) - { - list.Add(parsed); - } - } - return list; - } - - private async Task DoRequest(string endpoint) - { - // TODO: We're supposed to reuse HttpClient, right? - var text = await new HttpClient().GetStringAsync(GetUrl(endpoint)); - return XDocument.Parse(text); - } - - private string GetUrl(string endpoint) - { - var protocol = _service.Tls ? "https" : "http"; - return new UriBuilder(protocol, _service.Ip.ToString(), _service.Port, $"{_service.RootUrl}/{endpoint}") - .Uri.ToString(); - } -} \ No newline at end of file diff --git a/NAPS2.Escl.Client/EsclService.cs b/NAPS2.Escl.Client/EsclService.cs deleted file mode 100644 index 7fea336c6b..0000000000 --- a/NAPS2.Escl.Client/EsclService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Net; - -namespace NAPS2.Escl.Client; - -public class EsclService -{ - public required IPAddress Ip { get; init; } - public required int Port { get; init; } - public required bool Tls { get; init; } - public required string RootUrl { get; init; } -} \ No newline at end of file diff --git a/NAPS2.Escl.Client/EsclServiceLocator.cs b/NAPS2.Escl.Client/EsclServiceLocator.cs deleted file mode 100644 index 006faf6aac..0000000000 --- a/NAPS2.Escl.Client/EsclServiceLocator.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Net; -using Makaretu.Dns; - -namespace NAPS2.Escl.Client; - -public class EsclServiceLocator -{ - public async Task> Locate() - { - using var sd = new ServiceDiscovery(); - var locatedServices = new List(); - sd.ServiceInstanceDiscovered += (sender, args) => - { - try - { - var service = ParseService(args); - lock (locatedServices) - { - locatedServices.Add(service); - } - } - catch (Exception) - { - // TODO: Log? - } - }; - sd.QueryServiceInstances("_uscan._tcp"); - sd.QueryServiceInstances("_uscans._tcp"); - await Task.Delay(2000); - // TODO: De-duplicate http/https services? - return locatedServices; - - // var txtVers = props.GetValueOrDefault("txtvers"); // txt record version - // var adminUrl = props.GetValueOrDefault("adminurl"); // url to scanner config page - // var esclVersion = props.GetValueOrDefault("Vers"); // escl version e.g. "2.0" - // var thumbnail = props.GetValueOrDefault("representation"); // url to png or ico - // var urlBasePath = props.GetValueOrDefault("rs"); // no leading (or trailing) slash - // var scannerName = props.GetValueOrDefault("ty"); // human readable - // var note = props.GetValueOrDefault("note"); // supposed to be "scanner location", e.g. "Copy Room" - // // Note jpeg is better in that we can get one image at a time, but pdf does allow png quality potentially - // // Hopefully decent scanners can support png too - // // Also for the server we can definitely provide NAPS2-generated pdfs, which is kind of a cool idea for e.g. using from mobile - // var pdl = props.GetValueOrDefault("pdl"); // comma separated mime types supported "application/pdf,image/jpeg" at minimum - // var uuid = props.GetValueOrDefault("uuid"); // physical device id - // var colorSpace = props.GetValueOrDefault("cs"); // comma separated capabilites, "color,grayscale,binary" - // var source = props.GetValueOrDefault("is"); // "platen,adf,camera" platen = flatbed - // var duplex = props.GetValueOrDefault("duplex"); // "T"rue or "F"alse - // - } - - private EsclService ParseService(ServiceInstanceDiscoveryEventArgs args) - { - string name = args.ServiceInstanceName.Labels[0]; - string protocol = args.ServiceInstanceName.Labels[1]; - IPAddress? ip = null; - int port = -1; - var props = new Dictionary(); - foreach (var record in args.Message.AdditionalRecords) - { - if (record is ARecord a) - { - ip ??= a.Address; - } - if (record is AAAARecord aaaa) - { - ip = aaaa.Address; - } - if (record is SRVRecord srv) - { - port = srv.Port; - } - if (record is TXTRecord txt) - { - foreach (var str in txt.Strings) - { - var eq = str.IndexOf("=", StringComparison.Ordinal); - if (eq != -1) - { - props[str.Substring(0, eq).ToLowerInvariant()] = str.Substring(eq + 1); - } - } - } - } - bool http = protocol == "_uscan"; - bool https = protocol == "_uscans"; - if (ip == null || port == -1 || !http && !https) - { - throw new ArgumentException(); - } - return new EsclService - { - // Uuid = props["uuid"], - // Name = props["ty"], - Ip = ip, - Port = port, - Tls = https, - RootUrl = props["rs"] // TODO: More props, some required, some optional maybe - }; - } - - private string? Get(Dictionary props, string key) - { - return props.TryGetValue(key, out var value) ? value : null; - } -} \ No newline at end of file diff --git a/NAPS2.Escl.Client/NAPS2.Escl.Client.csproj b/NAPS2.Escl.Client/NAPS2.Escl.Client.csproj deleted file mode 100644 index 7e8fb1e063..0000000000 --- a/NAPS2.Escl.Client/NAPS2.Escl.Client.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net6.0;net462;netstandard2.0 - enable - enable - 11 - USB - - - - - - - - - diff --git a/NAPS2.Escl.Server/CertificateHelper.cs b/NAPS2.Escl.Server/CertificateHelper.cs new file mode 100644 index 0000000000..2a96d34e3a --- /dev/null +++ b/NAPS2.Escl.Server/CertificateHelper.cs @@ -0,0 +1,57 @@ +using System.Collections.ObjectModel; +using System.Reflection; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; + +namespace NAPS2.Escl.Server; + +internal static class CertificateHelper +{ + private static readonly Type? CertificateRequestType; + private static readonly ConstructorInfo? CertificateRequestConstructor; + private static readonly PropertyInfo? CertificateExtensionsProperty; + private static readonly MethodInfo? CreateSelfSignedMethod; + + static CertificateHelper() + { + // TODO: On net472+ we can avoid the reflection + CertificateRequestType = typeof(RSACertificateExtensions).Assembly.GetType( + "System.Security.Cryptography.X509Certificates.CertificateRequest"); + CertificateRequestConstructor = CertificateRequestType?.GetConstructor( + new[] { typeof(string), typeof(RSA), typeof(HashAlgorithmName), typeof(RSASignaturePadding) }); + CertificateExtensionsProperty = CertificateRequestType?.GetProperty("CertificateExtensions"); + CreateSelfSignedMethod = CertificateRequestType?.GetMethod("CreateSelfSigned"); + } + + // See https://stackoverflow.com/a/65258808/2112909 + public static X509Certificate2? GenerateSelfSignedCertificate(ILogger logger) + { + try + { + if (CertificateRequestType == null) + { + logger.LogDebug("CertificateRequest type not available"); + return null; + } + + var request = CertificateRequestConstructor!.Invoke(new object[] + { "CN=NAPS2-ESCL-Self-Signed", RSA.Create(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1 }); + + var extensions = (Collection) CertificateExtensionsProperty!.GetValue(request)!; + extensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, true)); + + var now = DateTimeOffset.UtcNow; + var cert = (X509Certificate2) CreateSelfSignedMethod!.Invoke(request, + new object[] { now.AddDays(-1), now.AddYears(10) })!; + var pfxCert = new X509Certificate2(cert.Export(X509ContentType.Pfx)); + + return pfxCert; + } + catch (Exception ex) + { + logger.LogError(ex, "Error generating self-signed certificate"); + return null; + } + } +} \ No newline at end of file diff --git a/NAPS2.Escl.Server/CompilerAttributes.cs b/NAPS2.Escl.Server/CompilerAttributes.cs deleted file mode 100644 index 8230a289b1..0000000000 --- a/NAPS2.Escl.Server/CompilerAttributes.cs +++ /dev/null @@ -1,61 +0,0 @@ -// https://sergiopedri.medium.com/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb -// ReSharper disable once CheckNamespace - -namespace System.Runtime.CompilerServices -{ - internal static class IsExternalInit - { - } - - /// Specifies that a type has required members or that a member is required. - [AttributeUsage( - AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, - AllowMultiple = false, Inherited = false)] - internal sealed class RequiredMemberAttribute : Attribute - { - } - - /// - /// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. - /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] - internal sealed class CompilerFeatureRequiredAttribute : Attribute - { - public CompilerFeatureRequiredAttribute(string featureName) - { - FeatureName = featureName; - } - - /// - /// The name of the compiler feature. - /// - public string FeatureName { get; } - - /// - /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . - /// - public bool IsOptional { get; init; } - - /// - /// The used for the ref structs C# feature. - /// - public const string RefStructs = nameof(RefStructs); - - /// - /// The used for the required members C# feature. - /// - public const string RequiredMembers = nameof(RequiredMembers); - } -} - -namespace System.Diagnostics.CodeAnalysis -{ - /// - /// Specifies that this constructor sets all required members for the current type, and callers - /// do not need to set any required members themselves. - /// - [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] - internal sealed class SetsRequiredMembersAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/NAPS2.Escl.Server/EsclApiController.cs b/NAPS2.Escl.Server/EsclApiController.cs index ee4a313b54..b1286b5082 100644 --- a/NAPS2.Escl.Server/EsclApiController.cs +++ b/NAPS2.Escl.Server/EsclApiController.cs @@ -1,9 +1,10 @@ -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Xml.Linq; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; +using Microsoft.Extensions.Logging; namespace NAPS2.Escl.Server; @@ -12,89 +13,138 @@ internal class EsclApiController : WebApiController private static readonly XNamespace ScanNs = EsclXmlHelper.ScanNs; private static readonly XNamespace PwgNs = EsclXmlHelper.PwgNs; - private readonly EsclServerConfig _serverConfig; + private readonly EsclDeviceConfig _deviceConfig; private readonly EsclServerState _serverState; + private readonly EsclSecurityPolicy _securityPolicy; + private readonly ILogger _logger; - internal EsclApiController(EsclServerConfig serverConfig, EsclServerState serverState) + internal EsclApiController(EsclDeviceConfig deviceConfig, EsclServerState serverState, + EsclSecurityPolicy securityPolicy, ILogger logger) { - _serverConfig = serverConfig; + _deviceConfig = deviceConfig; _serverState = serverState; + _securityPolicy = securityPolicy; + _logger = logger; } [Route(HttpVerbs.Get, "/ScannerCapabilities")] public async Task GetScannerCapabilities() { - var caps = _serverConfig.Capabilities; + var caps = _deviceConfig.Capabilities; + var protocol = _securityPolicy.HasFlag(EsclSecurityPolicy.ServerRequireHttps) ? "https" : "http"; + var iconUri = caps.IconPng != null ? $"{protocol}://naps2-{caps.Uuid}.local.:{_deviceConfig.Port}/eSCL/icon.png" : ""; var doc = EsclXmlHelper.CreateDocAsString( new XElement(ScanNs + "ScannerCapabilities", - new XElement(PwgNs + "Version", caps.Version), // TODO: Probably hard code version or something + new XElement(PwgNs + "Version", caps.Version), new XElement(PwgNs + "MakeAndModel", caps.MakeAndModel), new XElement(PwgNs + "SerialNumber", caps.SerialNumber), - new XElement(ScanNs + "UUID", "0e468f6d-e5dc-4abe-8e9f-ad08d8546b0c"), + new XElement(ScanNs + "Manufacturer", caps.Manufacturer), + new XElement(ScanNs + "UUID", caps.Uuid), new XElement(ScanNs + "AdminURI", ""), - new XElement(ScanNs + "IconURI", ""), - new XElement(ScanNs + "SettingProfiles", - new XElement(ScanNs + "SettingProfile", - new XAttribute("name", "p1"), - new XElement(ScanNs + "ColorModes", - new XElement(ScanNs + "ColorMode", "BlackAndWhite1"), - new XElement(ScanNs + "ColorMode", "Grayscale8"), - new XElement(ScanNs + "ColorMode", "RGB24")), - new XElement(ScanNs + "DocumentFormats", - new XElement(PwgNs + "DocumentFormat", "application/pdf"), - new XElement(PwgNs + "DocumentFormat", "image/jpeg"), - new XElement(PwgNs + "DocumentFormat", "image/png"), - new XElement(ScanNs + "DocumentFormatExt", "application/pdf"), - new XElement(ScanNs + "DocumentFormatExt", "image/jpeg"), - new XElement(ScanNs + "DocumentFormatExt", "image/png") - ), - new XElement(ScanNs + "SupportedResolutions", - new XElement(ScanNs + "DiscreteResolutions", - new XElement(ScanNs + "DiscreteResolution", - new XElement(ScanNs + "XResolution", "100"), - new XElement(ScanNs + "YResolution", "100")))))), + new XElement(ScanNs + "IconURI", iconUri), + new XElement(ScanNs + "Naps2Extensions", "Progress;ErrorDetails;ShortTimeout;AnyDpi"), new XElement(ScanNs + "Platen", - new XElement(ScanNs + "PlatenInputCaps", - new XElement(ScanNs + "MinWidth", "1"), - new XElement(ScanNs + "MaxWidth", "3000"), - new XElement(ScanNs + "MinHeight", "1"), - new XElement(ScanNs + "MaxHeight", "3600"), - new XElement(ScanNs + "MaxScanRegions", "1"), - new XElement(ScanNs + "SettingProfiles", - new XElement(ScanNs + "SettingProfile", - new XAttribute("ref", "p1"))))))); + new XElement(ScanNs + "PlatenInputCaps", GetCommonInputCaps())), + new XElement(ScanNs + "Adf", + new XElement(ScanNs + "AdfSimplexInputCaps", GetCommonInputCaps()), + new XElement(ScanNs + "AdfDuplexInputCaps", GetCommonInputCaps())), + new XElement(ScanNs + "CompressionFactorSupport", + new XElement(ScanNs + "Min", 0), + new XElement(ScanNs + "Max", 100), + new XElement(ScanNs + "Normal", 75), + new XElement(ScanNs + "Step", 1)))); Response.ContentType = "text/xml"; using var writer = new StreamWriter(HttpContext.OpenResponseStream()); await writer.WriteAsync(doc); } + private object[] GetCommonInputCaps() + { + // TODO: After implementing scanner capabilities this should be scanner-specific + return + [ + new XElement(ScanNs + "MinWidth", "1"), + new XElement(ScanNs + "MaxWidth", EsclInputCaps.DEFAULT_MAX_WIDTH), + new XElement(ScanNs + "MinHeight", "1"), + new XElement(ScanNs + "MaxHeight", EsclInputCaps.DEFAULT_MAX_HEIGHT), + new XElement(ScanNs + "MaxScanRegions", "1"), + new XElement(ScanNs + "SettingProfiles", + new XElement(ScanNs + "SettingProfile", + new XElement(ScanNs + "ColorModes", + new XElement(ScanNs + "ColorMode", "BlackAndWhite1"), + new XElement(ScanNs + "ColorMode", "Grayscale8"), + new XElement(ScanNs + "ColorMode", "RGB24")), + new XElement(ScanNs + "DocumentFormats", + new XElement(PwgNs + "DocumentFormat", "application/pdf"), + new XElement(PwgNs + "DocumentFormat", "image/jpeg"), + new XElement(PwgNs + "DocumentFormat", "image/png"), + new XElement(ScanNs + "DocumentFormatExt", "application/pdf"), + new XElement(ScanNs + "DocumentFormatExt", "image/jpeg"), + new XElement(ScanNs + "DocumentFormatExt", "image/png") + ), + new XElement(ScanNs + "SupportedResolutions", + new XElement(ScanNs + "DiscreteResolutions", + CreateResolution(100), + CreateResolution(150), + CreateResolution(200), + CreateResolution(300), + CreateResolution(400), + CreateResolution(600), + CreateResolution(800), + CreateResolution(1200), + CreateResolution(2400), + CreateResolution(4800) + )))) + ]; + } + + private XElement CreateResolution(int res) => + new(ScanNs + "DiscreteResolution", + new XElement(ScanNs + "XResolution", res.ToString()), + new XElement(ScanNs + "YResolution", res.ToString())); + + [Route(HttpVerbs.Get, "/icon.png")] + public async Task GetIcon() + { + if (_deviceConfig.Capabilities.IconPng != null) + { + Response.ContentType = "image/png"; + using var stream = Response.OutputStream; + var buffer = _deviceConfig.Capabilities.IconPng; + await stream.WriteAsync(buffer, 0, buffer.Length); + } + else + { + Response.StatusCode = 404; + } + } + [Route(HttpVerbs.Get, "/ScannerStatus")] public async Task GetScannerStatus() { var jobsElement = new XElement(ScanNs + "Jobs"); - foreach (var jobState in _serverState.Jobs.Values) + foreach (var jobInfo in _serverState.Jobs.OrderBy(x => x.LastUpdated.ElapsedMilliseconds)) { jobsElement.Add(new XElement(ScanNs + "JobInfo", - new XElement(PwgNs + "JobUri", $"/escl/ScanJobs/{jobState.Id}"), - new XElement(PwgNs + "JobUuid", jobState.Id), - new XElement(ScanNs + "Age", Math.Ceiling(jobState.LastUpdated.Elapsed.TotalSeconds)), - new XElement(PwgNs + "ImagesCompleted", - jobState.Status is JobStatus.Pending or JobStatus.Processing ? "0" : "1"), - new XElement(PwgNs + "ImagesToTransfer", "1"), - new XElement(PwgNs + "JobState", jobState.Status.ToString()), + new XElement(PwgNs + "JobUri", $"/eSCL/ScanJobs/{jobInfo.Id}"), + new XElement(PwgNs + "JobUuid", jobInfo.Id), + new XElement(ScanNs + "Age", Math.Ceiling(jobInfo.LastUpdated.Elapsed.TotalSeconds)), + new XElement(PwgNs + "ImagesCompleted", jobInfo.ImagesCompleted), + new XElement(PwgNs + "ImagesToTransfer", jobInfo.ImagesToTransfer), + new XElement(PwgNs + "JobState", jobInfo.State.ToString()), new XElement(PwgNs + "JobStateReasons", new XElement(PwgNs + "JobStateReason", - jobState.Status == JobStatus.Processing ? "JobScanning" : "JobCompletedSuccessfully")))); + jobInfo.State == EsclJobState.Processing ? "JobScanning" : "JobCompletedSuccessfully")))); } + var scannerState = _serverState.IsProcessing ? EsclScannerState.Processing : EsclScannerState.Idle; + var adfState = _serverState.IsProcessing ? EsclAdfState.ScannerAdfProcessing : EsclAdfState.ScannedAdfLoaded; var doc = EsclXmlHelper.CreateDocAsString( new XElement(ScanNs + "ScannerStatus", new XElement(PwgNs + "Version", "2.6"), - new XElement(PwgNs + "State", - _serverState.Jobs.Any(x => x.Value.Status is JobStatus.Pending or JobStatus.Processing) - ? "Processing" - : "Idle"), + new XElement(PwgNs + "State", scannerState), + new XElement(ScanNs + "AdfState", adfState), jobsElement )); Response.ContentType = "text/xml"; @@ -105,20 +155,44 @@ public async Task GetScannerStatus() [Route(HttpVerbs.Post, "/ScanJobs")] public void CreateScanJob() { - var jobState = JobState.CreateNewJob(); - _serverState.Jobs[jobState.Id] = jobState; - Response.Headers.Add("Location", $"{Request.Url}/{jobState.Id}"); - Response.StatusCode = 201; + // TODO: Actually use job input for scan options + EsclScanSettings settings; + try + { + var doc = XDocument.Load(Request.InputStream); + settings = SettingsParser.Parse(doc); + } + catch (Exception) + { + Response.StatusCode = 400; // Bad request + return; + } + if (_serverState.IsProcessing) + { + Response.StatusCode = 503; // Service unavailable + return; + } + _serverState.IsProcessing = true; + var jobInfo = JobInfo.CreateNewJob(_serverState, _deviceConfig.CreateJob(settings)); + _serverState.AddJob(jobInfo); + var uri = Request.Url; + if (Request.IsSecureConnection) + { + // Fix https://github.com/unosquare/embedio/issues/593 + uri = new UriBuilder(uri) { Scheme = "https" }.Uri; + } + Response.Headers.Add("Access-Control-Expose-Headers", "Location"); + Response.Headers.Add("Location", $"{uri}/{jobInfo.Id}"); + Response.StatusCode = 201; // Created } [Route(HttpVerbs.Delete, "/ScanJobs/{jobId}")] public void CancelScanJob(string jobId) { - if (_serverState.Jobs.TryGetValue(jobId, out var jobState) && - jobState.Status is JobStatus.Pending or JobStatus.Processing) + if (_serverState.TryGetJob(jobId, out var jobState) && + jobState.State is EsclJobState.Pending or EsclJobState.Processing) { - jobState.Status = JobStatus.Canceled; - jobState.LastUpdated = Stopwatch.StartNew(); + jobState.Job.Cancel(); } else { @@ -131,29 +205,153 @@ public void GetImageinfo(string jobId) { } - [Route(HttpVerbs.Get, "/ScanJobs/{jobId}/NextDocument")] - public void NextDocument(string jobId) + // This endpoint is a NAPS2-specific extension to the ESCL API. + // It gives a chunked response where each line is a double between 0 and 1 representing the current page progress. + // This endpoint should be called once before each call to NextDocument. + [Route(HttpVerbs.Get, "/ScanJobs/{jobId}/Progress")] + public async Task Progress(string jobId) { - if (_serverState.Jobs.TryGetValue(jobId, out var jobState) && - jobState.Status is JobStatus.Pending or JobStatus.Processing) - { - Response.Headers.Add("Content-Location", $"/escl/ScanJobs/{jobState.Id}/1"); - // Bypass https://github.com/unosquare/embedio/issues/510 - var field = Response.GetType().GetField("k__BackingField", - BindingFlags.Instance | BindingFlags.NonPublic); - field!.SetValue(Response, new Version(1, 1)); - Response.SendChunked = true; - Response.ContentType = "image/jpeg"; - Response.ContentEncoding = null; + if (_serverState.TryGetJob(jobId, out var jobState) && + jobState.State is EsclJobState.Pending or EsclJobState.Processing) + { + SetChunkedResponse(); using var stream = Response.OutputStream; - var bytes = File.ReadAllBytes(@"C:\Devel\VS\NAPS2.Future\NAPS2.Sdk.Tests\Resources\dog.jpg"); - stream.Write(bytes, 0, bytes.Length); - jobState.Status = JobStatus.Completed; - jobState.LastUpdated = Stopwatch.StartNew(); + await jobState.Job.WriteProgressTo(stream); } else { Response.StatusCode = 404; } } + + // This endpoint is a NAPS2-specific extension to the ESCL API. + // The ESCL status-based model (where errors like "no paper in feeder" are encompassed by ScannerStatus that can be + // polled every few seconds) is a good match to a physical scanner but a poor match to NAPS2's model. + // Instead, we have a this ErrorDetails endpoint that we call when NextDocument returns a 500 error which gives us + // XML-based details for the exception that occurred. + [Route(HttpVerbs.Get, "/ScanJobs/{jobId}/ErrorDetails")] + public async Task ErrorDetails(string jobId) + { + if (_serverState.TryGetJob(jobId, out var jobState)) + { + Response.ContentType = "text/xml"; + using var stream = Response.OutputStream; + await jobState.Job.WriteErrorDetailsTo(stream); + } + else + { + Response.StatusCode = 404; + } + } + + [Route(HttpVerbs.Get, "/ScanJobs/{jobId}/NextDocument")] + public async Task NextDocument(string jobId) + { + if (!CheckJobState(jobId, out var jobInfo)) + { + return; + } + + await jobInfo.NextDocumentLock.Take(); + try + { + // Recheck job state in case it's been changed while we were waiting on the lock + if (!CheckJobState(jobId, out _)) + { + return; + } + await WaitForAndWriteNextDocument(jobInfo); + } + finally + { + jobInfo.NextDocumentLock.Release(); + } + } + + private bool CheckJobState(string jobId, [NotNullWhen(true)] out JobInfo? jobInfo) + { + if (!_serverState.TryGetJob(jobId, out jobInfo)) + { + Response.StatusCode = 404; + return false; + } + if (jobInfo.State == EsclJobState.Aborted) + { + Response.StatusCode = 500; + return false; + } + if (jobInfo.State is not (EsclJobState.Pending or EsclJobState.Processing)) + { + Response.StatusCode = 404; + return false; + } + return true; + } + + private async Task WaitForAndWriteNextDocument(JobInfo jobInfo) + { + try + { + // If we already have a document (i.e. if a connection error occured during the previous NextDocument + // request), we stay at that same document and don't advance + var cts = new CancellationTokenSource(); + cts.CancelAfter(1000); + jobInfo.NextDocumentReady = jobInfo.NextDocumentReady || await jobInfo.Job.WaitForNextDocument(cts.Token); + } + catch (TaskCanceledException) + { + _logger.LogDebug("Waiting for document timed out, returning 503"); + // Tell the client to retry after 2s + Response.Headers.Add("Retry-After", "2"); + Response.StatusCode = 503; + return; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "ESCL server error waiting for document"); + jobInfo.TransitionState(EsclJobState.Processing, EsclJobState.Aborted); + Response.StatusCode = 500; + return; + } + + // At this point either we have a document and can respond with it, or we have no documents left and should 404 + if (jobInfo.NextDocumentReady) + { + try + { + Response.Headers.Add("Content-Location", $"/eSCL/ScanJobs/{jobInfo.Id}/1"); + SetChunkedResponse(); + Response.ContentType = jobInfo.Job.ContentType; + Response.ContentEncoding = null; + using var stream = Response.OutputStream; + await jobInfo.Job.WriteDocumentTo(stream); + jobInfo.NextDocumentReady = false; + jobInfo.TransferredDocument(); + } + catch (Exception ex) + { + _logger.LogError(ex, "ESCL server error writing document"); + // We don't transition state here, as the assumption is that the problem was network-related and the + // client will retry + Response.StatusCode = 500; + } + } + else + { + jobInfo.TransitionState(EsclJobState.Processing, EsclJobState.Completed); + Response.StatusCode = 404; + } + } + + private void SetChunkedResponse() + { + // Bypass https://github.com/unosquare/embedio/issues/510 + var field = Response.GetType().GetField("k__BackingField", + BindingFlags.Instance | BindingFlags.NonPublic); + if (field != null) + { + field.SetValue(Response, new Version(1, 1)); + } + Response.SendChunked = true; + } } \ No newline at end of file diff --git a/NAPS2.Escl.Server/EsclServer.cs b/NAPS2.Escl.Server/EsclServer.cs index caf4e73035..dec0d9b1e4 100644 --- a/NAPS2.Escl.Server/EsclServer.cs +++ b/NAPS2.Escl.Server/EsclServer.cs @@ -1,45 +1,177 @@ +using System.Security.Cryptography.X509Certificates; using EmbedIO; using EmbedIO.WebApi; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace NAPS2.Escl.Server; -public class EsclServer : IDisposable +public class EsclServer : IEsclServer { - private readonly EsclServerConfig _serverConfig; - private readonly EsclServerState _serverState = new(); - private readonly CancellationTokenSource _cts = new(); - private WebServer? _server; + static EsclServer() + { + Swan.Logging.Logger.NoLogging(); + } + + private readonly Dictionary _devices = new(); + private bool _started; + private CancellationTokenSource? _cts; + + public EsclSecurityPolicy SecurityPolicy { get; set; } - public EsclServer(EsclServerConfig serverConfig) + public X509Certificate2? Certificate { get; set; } + + public ILogger Logger { get; set; } = NullLogger.Instance; + + public void AddDevice(EsclDeviceConfig deviceConfig) { - _serverConfig = serverConfig; + var deviceCtx = new DeviceContext(deviceConfig); + _devices[deviceConfig] = deviceCtx; + if (_started) + { + Task.Run(() => StartServerAndAdvertise(deviceCtx)); + } } - public int Port { get; set; } = 9898; + public void RemoveDevice(EsclDeviceConfig deviceConfig) + { + var deviceCtx = _devices[deviceConfig]; + if (_started) + { + deviceCtx.StartTask?.ContinueWith(_ => deviceCtx.Advertiser.Dispose()); + } + deviceCtx.Cts.Cancel(); + _devices.Remove(deviceConfig); + } - public void Start() + public async Task Start() { - if (_server != null) + if (_started) { - throw new InvalidOperationException(); + return; } - var url = $"http://+:{Port}/"; - _server = new WebServer(o => o + if (SecurityPolicy.HasFlag(EsclSecurityPolicy.ServerRequireHttps) && + SecurityPolicy.HasFlag(EsclSecurityPolicy.ServerDisableHttps)) + { + throw new EsclSecurityPolicyViolationException( + $"EsclSecurityPolicy of {SecurityPolicy} is inconsistent"); + } + if (SecurityPolicy.HasFlag(EsclSecurityPolicy.ServerRequireTrustedCertificate) && Certificate == null) + { + throw new EsclSecurityPolicyViolationException( + $"EsclSecurityPolicy of {SecurityPolicy} needs a certificate to be specified"); + } + _started = true; + _cts = new CancellationTokenSource(); + + // Try to generate a self-signed certificate if the caller hasn't provided one + if (!SecurityPolicy.HasFlag(EsclSecurityPolicy.ServerDisableHttps) && Certificate == null) + { + await Task.Run(() => Certificate = CertificateHelper.GenerateSelfSignedCertificate(Logger)); + } + if (SecurityPolicy.HasFlag(EsclSecurityPolicy.ServerRequireHttps) && Certificate == null) + { + throw new EsclSecurityPolicyViolationException( + $"EsclSecurityPolicy of {SecurityPolicy} needs a certificate to be specified"); + } + + var tasks = new List(); + foreach (var device in _devices.Keys) + { + var deviceCtx = _devices[device]; + deviceCtx.StartTask = Task.Run(() => StartServerAndAdvertise(deviceCtx)); + tasks.Add(deviceCtx.StartTask); + } + await Task.WhenAll(tasks); + } + + private async Task StartServerAndAdvertise(DeviceContext deviceCtx) + { + var cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cts!.Token, deviceCtx.Cts.Token).Token; + // Try to run the server with the port specified in the EsclDeviceConfig first. If that fails, try random ports + // instead, and store the actually-used port back in EsclDeviceConfig so it can be advertised correctly. + bool hasHttp = !SecurityPolicy.HasFlag(EsclSecurityPolicy.ServerRequireHttps); + bool hasHttps = !SecurityPolicy.HasFlag(EsclSecurityPolicy.ServerDisableHttps) && Certificate != null; + if (hasHttp) + { + await PortFinder.RunWithSpecifiedOrRandomPort(deviceCtx.Config.Port, async port => + { + await StartServer(deviceCtx, port, false, cancelToken); + deviceCtx.Config.Port = port; + }, cancelToken); + } + if (hasHttps) + { + await PortFinder.RunWithSpecifiedOrRandomPort(deviceCtx.Config.TlsPort, async tlsPort => + { + await StartServer(deviceCtx, tlsPort, true, cancelToken); + deviceCtx.Config.TlsPort = tlsPort; + }, cancelToken); + } + deviceCtx.Advertiser.AdvertiseDevice(deviceCtx.Config, hasHttp, hasHttps); + } + + private async Task StartServer(DeviceContext deviceCtx, int port, bool tls, CancellationToken cancelToken) + { + var protocol = tls ? "https" : "http"; + var url = $"{protocol}://+:{port}/"; + deviceCtx.ServerState = new EsclServerState(Logger); + var server = new WebServer(o => o .WithMode(HttpListenerMode.EmbedIO) - .WithUrlPrefix(url)) - .WithWebApi("/escl", m => m.WithController(() => new EsclApiController(_serverConfig, _serverState))); - // _server.HandleHttpException(async (_, _) => { }); - _server.StateChanged += ServerOnStateChanged; - // TODO: This might block on tasks, maybe copy impl but async - _server.Start(_cts.Token); + .WithUrlPrefix(url) + .WithCertificate((tls ? Certificate : null)!)) + .HandleUnhandledException(UnhandledServerException); + if (SecurityPolicy.HasFlag(EsclSecurityPolicy.ServerAllowAnyOrigin)) + { + server.WithCors(); + } + server.WithWebApi("/eSCL", + m => m.WithController(() => + new EsclApiController(deviceCtx.Config, deviceCtx.ServerState, SecurityPolicy, Logger))); + await server.StartAsync(cancelToken); + } + + private Task UnhandledServerException(IHttpContext ctx, Exception ex) + { + Logger.LogError(ex, "Unhandled ESCL server error"); + return Task.CompletedTask; } - private void ServerOnStateChanged(object sender, WebServerStateChangedEventArgs e) + public Task Stop() { + if (!_started) + { + return Task.CompletedTask; + } + _started = false; + + _cts!.Cancel(); + var tasks = new List(); + foreach (var device in _devices.Keys) + { + var deviceCtx = _devices[device]; + if (deviceCtx.StartTask != null) + { + tasks.Add(deviceCtx.StartTask.ContinueWith(_ => deviceCtx.Advertiser.UnadvertiseDevice(device))); + } + } + _cts = null; + return Task.WhenAll(tasks); } public void Dispose() { - _cts.Cancel(); + if (_started) + { + Stop(); + } + } + + private record DeviceContext(EsclDeviceConfig Config) + { + public MdnsAdvertiser Advertiser { get; } = new(); + public CancellationTokenSource Cts { get; } = new(); + public Task? StartTask { get; set; } + public EsclServerState? ServerState { get; set; } } } \ No newline at end of file diff --git a/NAPS2.Escl.Server/EsclServerConfig.cs b/NAPS2.Escl.Server/EsclServerConfig.cs deleted file mode 100644 index 41592e9ba1..0000000000 --- a/NAPS2.Escl.Server/EsclServerConfig.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace NAPS2.Escl.Server; - -public class EsclServerConfig -{ - public required EsclCapabilities Capabilities { get; init; } -} \ No newline at end of file diff --git a/NAPS2.Escl.Server/EsclServerState.cs b/NAPS2.Escl.Server/EsclServerState.cs index 8f7727177f..93dbc4d993 100644 --- a/NAPS2.Escl.Server/EsclServerState.cs +++ b/NAPS2.Escl.Server/EsclServerState.cs @@ -1,6 +1,61 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; + namespace NAPS2.Escl.Server; internal class EsclServerState { - public Dictionary Jobs { get; } = new(); + private const int CLEANUP_INTERVAL = 10_000; + + private readonly ILogger _logger; + + private readonly Dictionary _jobDict = new(); + private Timer? _cleanupTimer; + + public EsclServerState(ILogger logger) + { + _logger = logger; + } + + public bool IsProcessing { get; set; } + + public IEnumerable Jobs => _jobDict.Values.ToList(); + + public void AddJob(JobInfo jobInfo) + { + lock (this) + { + _jobDict.Add(jobInfo.Id, jobInfo); + _cleanupTimer ??= new Timer(Cleanup, null, CLEANUP_INTERVAL, CLEANUP_INTERVAL); + } + } + + public bool TryGetJob(string id, [MaybeNullWhen(false)] out JobInfo jobInfo) + { + lock (this) + { + return _jobDict.TryGetValue(id, out jobInfo); + } + } + + private void Cleanup(object? state) + { + lock (this) + { + foreach (var jobInfo in Jobs) + { + if (jobInfo.IsEligibleForCleanup) + { + _logger.LogDebug($"Cleaning up job {jobInfo.Id} (state {jobInfo.State}, last updated {jobInfo.LastUpdated.Elapsed.TotalSeconds:F1} seconds ago)"); + _jobDict.Remove(jobInfo.Id); + jobInfo.Job.Dispose(); + if (_jobDict.Count == 0) + { + _cleanupTimer?.Dispose(); + _cleanupTimer = null; + } + } + } + } + } } \ No newline at end of file diff --git a/NAPS2.Escl.Server/FakeEsclScanJob.cs b/NAPS2.Escl.Server/FakeEsclScanJob.cs new file mode 100644 index 0000000000..c7dbcdb018 --- /dev/null +++ b/NAPS2.Escl.Server/FakeEsclScanJob.cs @@ -0,0 +1,35 @@ +namespace NAPS2.Escl.Server; + +internal class FakeEsclScanJob : IEsclScanJob +{ + private Action? _callback; + + public string ContentType => "image/jpeg"; + + public void Cancel() + { + _callback?.Invoke(StatusTransition.CancelJob); + } + + public void RegisterStatusTransitionCallback(Action callback) + { + _callback = callback; + } + + public Task WaitForNextDocument(CancellationToken cancelToken) => Task.FromResult(true); + + public async Task WriteDocumentTo(Stream stream) + { + var bytes = File.ReadAllBytes(@"C:\Devel\VS\NAPS2\NAPS2.Sdk.Tests\Resources\dog.jpg"); + await stream.WriteAsync(bytes, 0, bytes.Length); + _callback?.Invoke(StatusTransition.ScanComplete); + } + + public Task WriteProgressTo(Stream stream) => Task.CompletedTask; + + public Task WriteErrorDetailsTo(Stream stream) => Task.CompletedTask; + + public void Dispose() + { + } +} \ No newline at end of file diff --git a/NAPS2.Escl.Server/JobInfo.cs b/NAPS2.Escl.Server/JobInfo.cs new file mode 100644 index 0000000000..6bd06cf7cb --- /dev/null +++ b/NAPS2.Escl.Server/JobInfo.cs @@ -0,0 +1,105 @@ +using System.Diagnostics; + +namespace NAPS2.Escl.Server; + +internal class JobInfo +{ + public static JobInfo CreateNewJob(EsclServerState serverState, IEsclScanJob job) + { + var jobInfo = new JobInfo + { + Id = Guid.NewGuid().ToString("D"), + State = EsclJobState.Processing, + LastUpdated = Stopwatch.StartNew(), + Job = job + }; + job.RegisterStatusTransitionCallback(transition => + { + if (transition == StatusTransition.CancelJob) + { + jobInfo.TransitionState(EsclJobState.Processing, EsclJobState.Canceled); + } + if (transition == StatusTransition.AbortJob) + { + jobInfo.TransitionState(EsclJobState.Processing, EsclJobState.Aborted); + } + if (transition == StatusTransition.PageComplete) + { + jobInfo.NewImageToTransfer(); + } + if (transition == StatusTransition.ScanComplete) + { + serverState.IsProcessing = false; + jobInfo.IsScanComplete = true; + } + }); + return jobInfo; + } + + public required string Id { get; init; } + + public required EsclJobState State { get; set; } + + public int ImagesCompleted { get; set; } + + public int ImagesToTransfer { get; set; } + + public required Stopwatch LastUpdated { get; set; } + + public required IEsclScanJob Job { get; set; } + + public SimpleAsyncLock NextDocumentLock { get; } = new(); + + public bool NextDocumentReady { get; set; } + + // This is different than EsclJobState.Completed; the ESCL state only transitions to completed once the client has + // finished querying for documents. IsScanComplete is set to true immediately after the physical scan operation is + // done. + public bool IsScanComplete { get; private set; } + + private bool StateIsTerminal => State is EsclJobState.Completed or EsclJobState.Aborted or EsclJobState.Canceled; + + public bool IsEligibleForCleanup => IsScanComplete && + (StateIsTerminal && LastUpdated.Elapsed > TimeSpan.FromMinutes(1) || + LastUpdated.Elapsed > TimeSpan.FromMinutes(5)); + + public void TransitionState(EsclJobState precondition, EsclJobState newState) + { + lock (this) + { + if (State == precondition) + { + State = newState; + LastUpdated = Stopwatch.StartNew(); + } + } + } + + public void NewImageToTransfer() + { + lock (this) + { + ImagesToTransfer++; + LastUpdated = Stopwatch.StartNew(); + } + } + + public void TransferredDocument() + { + lock (this) + { + if (Job.ContentType == "application/pdf") + { + // Assume all images transferred at once + ImagesCompleted += ImagesToTransfer; + ImagesToTransfer = 0; + } + else + { + ImagesCompleted++; + ImagesToTransfer--; + } + LastUpdated = Stopwatch.StartNew(); + } + } +} \ No newline at end of file diff --git a/NAPS2.Escl.Server/JobState.cs b/NAPS2.Escl.Server/JobState.cs deleted file mode 100644 index 4184d8b592..0000000000 --- a/NAPS2.Escl.Server/JobState.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Diagnostics; - -namespace NAPS2.Escl.Server; - -internal class JobState -{ - public static JobState CreateNewJob() - { - return new JobState - { - Id = Guid.NewGuid().ToString("D"), - Status = JobStatus.Processing, - LastUpdated = Stopwatch.StartNew() - }; - } - - public required string Id { get; init; } - - public required JobStatus Status { get; set; } - - public required Stopwatch LastUpdated { get; set; } -} - -internal enum JobStatus -{ - Pending, - Processing, - Completed, - Canceled, - Aborted -} \ No newline at end of file diff --git a/NAPS2.Escl.Server/LICENSE b/NAPS2.Escl.Server/LICENSE new file mode 100644 index 0000000000..099a807a65 --- /dev/null +++ b/NAPS2.Escl.Server/LICENSE @@ -0,0 +1,518 @@ +NAPS2.Escl +https://www.github.com/cyanfish/naps2/ + +Copyright 2009-2025 NAPS2 Contributors + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/NAPS2.Escl.Server/MdnsAdvertiser.cs b/NAPS2.Escl.Server/MdnsAdvertiser.cs index 8bae6fc842..17958f7767 100644 --- a/NAPS2.Escl.Server/MdnsAdvertiser.cs +++ b/NAPS2.Escl.Server/MdnsAdvertiser.cs @@ -1,31 +1,151 @@ using Makaretu.Dns; +using Makaretu.Dns.Resolving; namespace NAPS2.Escl.Server; public class MdnsAdvertiser : IDisposable { - private ServiceDiscovery? _sd; + private readonly Dictionary _serviceProfiles = new(); + private readonly Dictionary _serviceProfiles2 = new(); - public void Advertise() + // Initializing ServiceDiscovery is slow (it queries network interfaces) so use lazily + private readonly Lazy _sd = new(() => new ServiceDiscovery()); + + private ServiceDiscovery ServiceDiscovery => _sd.Value; + + public void AdvertiseDevice(EsclDeviceConfig deviceConfig, bool hasHttp, bool hasHttps) + { + var caps = deviceConfig.Capabilities; + if (caps.Uuid == null) + { + throw new ArgumentException("UUID must be specified"); + } + if (!hasHttp && !hasHttps) + { + return; + } + var name = caps.MakeAndModel; + + // HTTP+HTTPS should be handled by responding with the relevant records for both _uscan and _uscans when either + // is queried. This isn't handled out-of-the-box by the MDNS library so we need to do some extra work. + var httpProfile = new ServiceProfile(name, "_uscan._tcp", (ushort) deviceConfig.Port); + var httpsProfile = new ServiceProfile(name, "_uscans._tcp", (ushort) deviceConfig.TlsPort); + // If only one of HTTP or HTTPS is enabled, then we use that as the service. If both are enabled, we use the + // HTTP service as a baseline and then hack in the HTTPS records later. + var service = hasHttp ? httpProfile : httpsProfile; + + var domain = $"naps2-{caps.Uuid}"; + var hostName = DomainName.Join(domain, service.Domain); + + // Replace the default TXT record with the first TXT record (HTTP if used, HTTPS otherwise) + service.Resources.RemoveAll(x => x is TXTRecord); + service.Resources.Add(CreateTxtRecord(deviceConfig, hasHttp, service, caps, name)); + + // NSEC records are recommended by RFC6762 to annotate that there's no more info for this host + service.Resources.Add(new NSECRecord + { Name = hostName, NextOwnerName = hostName, Types = [DnsType.A, DnsType.AAAA] }); + + if (hasHttp && hasHttps) + { + // If both HTTP and HTTPS are enabled, we add the extra HTTPS records here + service.Resources.Add(new PTRRecord + { + Name = httpsProfile.QualifiedServiceName, + DomainName = httpsProfile.FullyQualifiedName + }); + service.Resources.Add(new SRVRecord + { + Name = httpsProfile.FullyQualifiedName, + Port = (ushort) deviceConfig.TlsPort + }); + service.Resources.Add(CreateTxtRecord(deviceConfig, false, httpsProfile, caps, name)); + } + + // The default HostName isn't correct, it should be "naps2-uuid.local" (the actual host) instead of + // "name._uscan.local" (the service name) + service.HostName = hostName; + + // Send the full set of HTTP/HTTPS records to anyone currently listening + ServiceDiscovery.Announce(service); + + // Set up to respond to _uscan/_uscans queries with our records. + ServiceDiscovery.Advertise(service); + if (hasHttp && hasHttps) + { + // Add _uscans to the available services (_uscan was already mapped in Advertise()) + ServiceDiscovery.NameServer.Catalog[ServiceDiscovery.ServiceName].Resources.Add(new PTRRecord + { Name = ServiceDiscovery.ServiceName, DomainName = httpsProfile.QualifiedServiceName }); + // Cross-reference _uscan to the HTTPS records + ServiceDiscovery.NameServer.Catalog[httpProfile.QualifiedServiceName].Resources.Add(new PTRRecord + { Name = httpsProfile.QualifiedServiceName, DomainName = httpsProfile.FullyQualifiedName }); + // Add a _uscans reference with both HTTP and HTTPS records + ServiceDiscovery.NameServer.Catalog[httpsProfile.QualifiedServiceName] = new Node + { + Name = httpsProfile.QualifiedServiceName, Authoritative = true, Resources = + { + new PTRRecord + { Name = httpProfile.QualifiedServiceName, DomainName = httpProfile.FullyQualifiedName }, + new PTRRecord + { Name = httpsProfile.QualifiedServiceName, DomainName = httpsProfile.FullyQualifiedName } + } + }; + } + + // Persist the profiles so they can be unadvertised later + _serviceProfiles.Add(caps.Uuid, service); + if (hasHttp && hasHttps) + { + _serviceProfiles2.Add(caps.Uuid, httpsProfile); + } + } + + private static TXTRecord CreateTxtRecord(EsclDeviceConfig deviceConfig, bool http, ServiceProfile service, + EsclCapabilities caps, string? name) + { + var record = new TXTRecord(); + record.Name = service.FullyQualifiedName; + record.Strings.Add("txtvers=1"); + record.Strings.Add("Vers=2.0"); // TODO: verify + if (deviceConfig.Capabilities.IconPng != null) + { + record.Strings.Add( + http + ? $"representation=http://naps2-{caps.Uuid}.local.:{deviceConfig.Port}/eSCL/icon.png" + : $"representation=https://naps2-{caps.Uuid}.local.:{deviceConfig.TlsPort}/eSCL/icon.png"); + } + record.Strings.Add("rs=eSCL"); + record.Strings.Add($"ty={name}"); + record.Strings.Add("pdl=application/pdf,image/jpeg,image/png"); + // TODO: Actual adf/duplex, etc. + record.Strings.Add($"uuid={caps.Uuid}"); + record.Strings.Add("cs=color,grayscale,binary"); + record.Strings.Add("is=platen"); // and ,adf + record.Strings.Add("duplex=F"); + return record; + } + + public void UnadvertiseDevice(EsclDeviceConfig deviceConfig) { - var service = new ServiceProfile("NAPS2-Canon MP495", "_uscan._tcp", 9898); - service.AddProperty("txtvers", "1"); - service.AddProperty("Vers", "2.0"); // TODO: verify - service.AddProperty("rs", "escl"); - service.AddProperty("ty", "NAPS2-Canon MP495"); - service.AddProperty("pdl", "application/pdf,image/jpeg,image/png"); - service.AddProperty("uuid", "0e468f6d-e5dc-4abe-8e9f-ad08d8546b0c"); - service.AddProperty("cs", "color,grayscale,binary"); - service.AddProperty("is", "platen"); // and ,adf - service.AddProperty("duplex", "F"); - _sd = new ServiceDiscovery(); - _sd.Announce(service); - _sd.Advertise(service); + var uuid = deviceConfig.Capabilities.Uuid; + if (uuid == null) + { + throw new ArgumentException("UUID must be specified"); + } + if (_serviceProfiles.ContainsKey(uuid)) + { + ServiceDiscovery.Unadvertise(_serviceProfiles[uuid]); + _serviceProfiles.Remove(uuid); + } + if (_serviceProfiles2.ContainsKey(uuid)) + { + ServiceDiscovery.Unadvertise(_serviceProfiles2[uuid]); + _serviceProfiles2.Remove(uuid); + } } public void Dispose() { - _sd?.Unadvertise(); - _sd?.Dispose(); + ServiceDiscovery.Unadvertise(); + ServiceDiscovery.Dispose(); } } \ No newline at end of file diff --git a/NAPS2.Escl.Server/NAPS2.Escl.Server.csproj b/NAPS2.Escl.Server/NAPS2.Escl.Server.csproj index 2bbedf87cb..fb0483f059 100644 --- a/NAPS2.Escl.Server/NAPS2.Escl.Server.csproj +++ b/NAPS2.Escl.Server/NAPS2.Escl.Server.csproj @@ -1,14 +1,21 @@ - net6.0;net462;netstandard2.0 + net6;net8;net462;netstandard2.0 enable enable - 11 + 12 + + NAPS2.Escl.Server + NAPS2.Escl.Server + ESCL server for NAPS2.Sdk. + naps2 escl + + - + diff --git a/NAPS2.Escl.Server/PortFinder.cs b/NAPS2.Escl.Server/PortFinder.cs new file mode 100644 index 0000000000..59256936e6 --- /dev/null +++ b/NAPS2.Escl.Server/PortFinder.cs @@ -0,0 +1,43 @@ +namespace NAPS2.Escl.Server; + +internal static class PortFinder +{ + private const int MAX_PORT_TRIES = 5; + private const int RANDOM_PORT_MIN = 10001; + private const int RANDOM_PORT_MAX = 19999; + + public static async Task RunWithSpecifiedOrRandomPort(int defaultPort, Func portTaskFunc, + CancellationToken cancelToken) + { + int port = defaultPort; + int retries = 0; + var random = new Random(); + if (port == 0) + { + port = RandomPort(random); + } + while (true) + { + try + { + await portTaskFunc(port); + break; + } + catch (Exception) + { + if (cancelToken.IsCancellationRequested) + { + break; + } + retries++; + port = RandomPort(random); + if (retries > MAX_PORT_TRIES) + { + throw; + } + } + } + } + + private static int RandomPort(Random random) => random.Next(RANDOM_PORT_MIN, RANDOM_PORT_MAX + 1); +} \ No newline at end of file diff --git a/NAPS2.Escl.Server/SettingsParser.cs b/NAPS2.Escl.Server/SettingsParser.cs new file mode 100644 index 0000000000..8d2a8a249b --- /dev/null +++ b/NAPS2.Escl.Server/SettingsParser.cs @@ -0,0 +1,35 @@ +using System.Xml.Linq; + +namespace NAPS2.Escl.Server; + +internal static class SettingsParser +{ + private static readonly XNamespace ScanNs = EsclXmlHelper.ScanNs; + private static readonly XNamespace PwgNs = EsclXmlHelper.PwgNs; + + public static EsclScanSettings Parse(XDocument doc) + { + var root = doc.Root; + if (root?.Name != ScanNs + "ScanSettings") + { + throw new InvalidOperationException("Unexpected root element: " + doc.Root?.Name); + } + var scanRegion = root!.Element(PwgNs + "ScanRegions")?.Elements(PwgNs + "ScanRegion").FirstOrDefault(); + return new EsclScanSettings + { + // TODO: Handle intents? + InputSource = ParseHelper.MaybeParseEnum(root.Element(PwgNs + "InputSource"), EsclInputSource.Platen), + ColorMode = ParseHelper.MaybeParseEnum(root.Element(ScanNs + "ColorMode"), EsclColorMode.RGB24), + DocumentFormat = root.Element(ScanNs + "DocumentFormatExt")?.Value ?? + root.Element(PwgNs + "DocumentFormat")?.Value, + Duplex = root.Element(ScanNs + "Duplex")?.Value == "true", + XResolution = ParseHelper.MaybeParseInt(root.Element(ScanNs + "XResolution")) ?? 0, + YResolution = ParseHelper.MaybeParseInt(root.Element(ScanNs + "YResolution")) ?? 0, + Width = ParseHelper.MaybeParseInt(scanRegion?.Element(PwgNs + "Width")) ?? 0, + Height = ParseHelper.MaybeParseInt(scanRegion?.Element(PwgNs + "Height")) ?? 0, + XOffset = ParseHelper.MaybeParseInt(scanRegion?.Element(PwgNs + "XOffset")) ?? 0, + YOffset = ParseHelper.MaybeParseInt(scanRegion?.Element(PwgNs + "YOffset")) ?? 0, + CompressionFactor = ParseHelper.MaybeParseInt(root.Element(ScanNs + "CompressionFactor")) + }; + } +} \ No newline at end of file diff --git a/NAPS2.Escl.Server/SimpleAsyncLock.cs b/NAPS2.Escl.Server/SimpleAsyncLock.cs new file mode 100644 index 0000000000..0e54712652 --- /dev/null +++ b/NAPS2.Escl.Server/SimpleAsyncLock.cs @@ -0,0 +1,37 @@ +namespace NAPS2.Escl.Server; + +internal class SimpleAsyncLock +{ + private readonly Queue> _listeners = new(); + private bool _isTaken; + + public Task Take() + { + lock (this) + { + if (!_isTaken) + { + _isTaken = true; + return Task.CompletedTask; + } + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _listeners.Enqueue(tcs); + return tcs.Task; + } + } + + public void Release() + { + lock (this) + { + if (_listeners.Count > 0) + { + _listeners.Dequeue().SetResult(true); + } + else + { + _isTaken = false; + } + } + } +} \ No newline at end of file diff --git a/NAPS2.Escl.Server/WebServerExtensions.cs b/NAPS2.Escl.Server/WebServerExtensions.cs new file mode 100644 index 0000000000..fdb300b3e7 --- /dev/null +++ b/NAPS2.Escl.Server/WebServerExtensions.cs @@ -0,0 +1,30 @@ +using EmbedIO; + +namespace NAPS2.Escl.Server; + +internal static class WebServerExtensions +{ + public static async Task StartAsync(this WebServer server, CancellationToken cancelToken = default) + { + var startedTcs = new TaskCompletionSource(); + server.StateChanged += (_, args) => + { + if (args.NewState == WebServerState.Listening) + { + startedTcs.TrySetResult(true); + } + }; + _ = server.RunAsync(cancelToken).ContinueWith(t => + { + if (t.IsFaulted) + { + startedTcs.TrySetException(t.Exception!); + } + else + { + startedTcs.TrySetCanceled(); + } + }); + await startedTcs.Task; + } +} \ No newline at end of file diff --git a/NAPS2.Escl.Tests/AdvertiseTests.cs b/NAPS2.Escl.Tests/AdvertiseTests.cs index 8e78582656..5883d6e3f1 100644 --- a/NAPS2.Escl.Tests/AdvertiseTests.cs +++ b/NAPS2.Escl.Tests/AdvertiseTests.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using NAPS2.Escl.Server; +using NSubstitute; using Xunit; namespace NAPS2.Escl.Tests; @@ -9,21 +10,23 @@ public class AdvertiseTests [Fact] public async Task Advertise() { - using var server = new EsclServer(new EsclServerConfig + var job = Substitute.For(); + using var server = new EsclServer(); + server.AddDevice(new EsclDeviceConfig { - Capabilities = new EsclCapabilities() + Capabilities = new EsclCapabilities { Version = "2.6", MakeAndModel = "HP Blah", - SerialNumber = "123abc" - } + SerialNumber = "123abc", + Uuid = Guid.NewGuid().ToString("D") + }, + CreateJob = _ => job }); - server.Start(); - using var advertiser = new MdnsAdvertiser(); - advertiser.Advertise(); + await server.Start(); if (Debugger.IsAttached) { - for (int i = 0; i < 10; i++) + for (int i = 0; i < 100; i++) { await Task.Delay(5000); } diff --git a/NAPS2.Escl.Tests/CapabilitiesParserTests.cs b/NAPS2.Escl.Tests/CapabilitiesParserTests.cs new file mode 100644 index 0000000000..cb867b0262 --- /dev/null +++ b/NAPS2.Escl.Tests/CapabilitiesParserTests.cs @@ -0,0 +1,132 @@ +using System.Xml.Linq; +using NAPS2.Escl.Client; +using Xunit; + +namespace NAPS2.Escl.Tests; + +public class CapabilitiesParserTests +{ + [Fact] + public void TestBasicCaps() + { + string input = + """ + + + 2.6 + Hewlett Packard Photosmart C4760 + CN01 7971874378PJ + 96a4b400 2a9e 012f 6165 0025559efbc6f + http://192.168.1.2/index.html + http://192.168.1.2/scanner.png + + + + BlackAndWhite1 + Grayscale8 + + + application/pdf + image/jpeg + application/pdf + image/jpeg + + + + + 100 + 100 + + + 200 + 200 + + + 300 + 300 + + + + + + + + 1 + 3000 + 1 + 3600 + 2 + + + + BlackAndWhite1 + Grayscale8 + + + application/pdf + image/jpeg + application/pdf + image/jpeg + + + + + 75 + 1200 + 300 + 10 + + + 75 + 1200 + 300 + 10 + + + + + + + + + + 1 + 2600 + 1 + 3400 + + + + + + DetectPaperLoaded + SelectSinglePage + + + + """; + var caps = CapabilitiesParser.Parse(XDocument.Parse(input)); + Assert.Equal("2.6", caps.Version); + Assert.Equal("Hewlett Packard Photosmart C4760", caps.MakeAndModel); + Assert.Equal("CN01 7971874378PJ", caps.SerialNumber); + Assert.Equal("96a4b400 2a9e 012f 6165 0025559efbc6f", caps.Uuid); + Assert.Equal("http://192.168.1.2/index.html", caps.AdminUri); + Assert.Equal("http://192.168.1.2/scanner.png", caps.IconUri); + } + + [Fact] + public void TestWithDifferentSchemaVersion() + { + string input = + """ + + + Hewlett Packard Photosmart C4760 + + """; + var caps = CapabilitiesParser.Parse(XDocument.Parse(input)); + Assert.Equal("Hewlett Packard Photosmart C4760", caps.MakeAndModel); + } +} diff --git a/NAPS2.Escl.Tests/ClientServerTests.cs b/NAPS2.Escl.Tests/ClientServerTests.cs index b2464bb9f3..58731a31eb 100644 --- a/NAPS2.Escl.Tests/ClientServerTests.cs +++ b/NAPS2.Escl.Tests/ClientServerTests.cs @@ -1,35 +1,63 @@ using System.Net; using NAPS2.Escl.Client; using NAPS2.Escl.Server; +using NSubstitute; using Xunit; namespace NAPS2.Escl.Tests; public class ClientServerTests { - [Fact] + [Fact(Timeout = 60_000)] public async Task ClientServer() { - using var server = new EsclServer(new EsclServerConfig + var job = Substitute.For(); + using var server = new EsclServer(); + var uuid = Guid.NewGuid().ToString("D"); + var deviceConfig = new EsclDeviceConfig { Capabilities = new EsclCapabilities { Version = "2.0", MakeAndModel = "HP Blah", - SerialNumber = "123abc" - } - }) { Port = 9801 }; - server.Start(); + SerialNumber = "123abc", + Uuid = uuid + }, + CreateJob = _ => job + }; + server.AddDevice(deviceConfig); + await server.Start(); var client = new EsclClient(new EsclService { - Ip = IPAddress.IPv6Loopback, - Port = 9801, - RootUrl = "escl", - Tls = false + IpV4 = IPAddress.Loopback, + IpV6 = IPAddress.IPv6Loopback, + Host = $"[{IPAddress.IPv6Loopback}]", + RemoteEndpoint = IPAddress.IPv6Loopback, + Port = deviceConfig.Port, + TlsPort = deviceConfig.TlsPort, + RootUrl = "eSCL", + Tls = false, + Uuid = uuid }); var caps = await client.GetCapabilities(); Assert.Equal("2.0", caps.Version); Assert.Equal("HP Blah", caps.MakeAndModel); Assert.Equal("123abc", caps.SerialNumber); } + + [Fact] + public async Task StartTlsServerWithoutTrustedCertificate() + { + using var server = new EsclServer(); + server.SecurityPolicy = EsclSecurityPolicy.RequireTrustedCertificate; + await Assert.ThrowsAsync(() => server.Start()); + } + + [Fact] + public async Task StartTlsServerWithInconsistentFlags() + { + using var server = new EsclServer(); + server.SecurityPolicy = EsclSecurityPolicy.RequireHttps | EsclSecurityPolicy.ServerDisableHttps; + await Assert.ThrowsAsync(() => server.Start()); + } } \ No newline at end of file diff --git a/NAPS2.Escl.Tests/DeviceServiceLocatorTests.cs b/NAPS2.Escl.Tests/DeviceServiceLocatorTests.cs index bf5d7792e0..7e0e6d3fbf 100644 --- a/NAPS2.Escl.Tests/DeviceServiceLocatorTests.cs +++ b/NAPS2.Escl.Tests/DeviceServiceLocatorTests.cs @@ -1,13 +1,13 @@ -using NAPS2.Escl.Client; -using Xunit; - -namespace NAPS2.Escl.Tests; - -public class DeviceServiceLocatorTests -{ - [Fact] - public async Task Locate() - { - await new EsclServiceLocator().Locate(); - } -} \ No newline at end of file +// using NAPS2.Escl.Client; +// using Xunit; +// +// namespace NAPS2.Escl.Tests; +// +// public class DeviceServiceLocatorTests +// { +// [Fact] +// public async Task Locate() +// { +// await new EsclServiceLocator().Locate(); +// } +// } \ No newline at end of file diff --git a/NAPS2.Escl.Tests/NAPS2.Escl.Tests.csproj b/NAPS2.Escl.Tests/NAPS2.Escl.Tests.csproj index 1061c10ddf..88fcf081b4 100644 --- a/NAPS2.Escl.Tests/NAPS2.Escl.Tests.csproj +++ b/NAPS2.Escl.Tests/NAPS2.Escl.Tests.csproj @@ -1,25 +1,23 @@ - net6.0;net462 + net8;net462 enable enable - 10 - USB + 12 - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + - - + + + - + \ No newline at end of file diff --git a/NAPS2.Escl.Tests/UsbTests.cs b/NAPS2.Escl.Tests/UsbTests.cs index 0ed892aad5..a719d3e75b 100644 --- a/NAPS2.Escl.Tests/UsbTests.cs +++ b/NAPS2.Escl.Tests/UsbTests.cs @@ -1,5 +1,4 @@ -#if USB -using NAPS2.Escl.Client; +using NAPS2.Escl.Usb; using Xunit; namespace NAPS2.Escl.Tests; @@ -20,4 +19,3 @@ public async Task Usb() Assert.NotNull(caps); } } -#endif \ No newline at end of file diff --git a/NAPS2.Escl.Client/.gitignore b/NAPS2.Escl.Usb/.gitignore similarity index 100% rename from NAPS2.Escl.Client/.gitignore rename to NAPS2.Escl.Usb/.gitignore diff --git a/NAPS2.Escl.Client/EsclUsbContext.cs b/NAPS2.Escl.Usb/EsclUsbContext.cs similarity index 94% rename from NAPS2.Escl.Client/EsclUsbContext.cs rename to NAPS2.Escl.Usb/EsclUsbContext.cs index 23e35318ce..6067c1e0fb 100644 --- a/NAPS2.Escl.Client/EsclUsbContext.cs +++ b/NAPS2.Escl.Usb/EsclUsbContext.cs @@ -1,4 +1,3 @@ -#if USB using System.Net; using System.Net.Sockets; using System.Text; @@ -6,8 +5,9 @@ using LibUsbDotNet.Info; using LibUsbDotNet.LibUsb; using LibUsbDotNet.Main; +using NAPS2.Escl.Client; -namespace NAPS2.Escl.Client; +namespace NAPS2.Escl.Usb; public class EsclUsbContext : IDisposable { @@ -47,10 +47,15 @@ public void ConnectToDevice() var port = ((IPEndPoint) _proxyListener.LocalEndpoint).Port; _client = new EsclClient(new EsclService { - Ip = IPAddress.Loopback, + IpV4 = IPAddress.Loopback, + IpV6 = null, + Host = IPAddress.Loopback.ToString(), + RemoteEndpoint = IPAddress.Loopback, Port = port, + TlsPort = 0, RootUrl = "eSCL", - Tls = false + Tls = false, + Uuid = Guid.Empty.ToString("D") }); Task.Run(ProxyLoop); } @@ -153,4 +158,3 @@ public void Dispose() _usbContext.Dispose(); } } -#endif \ No newline at end of file diff --git a/NAPS2.Escl.Client/EsclUsbDescriptor.cs b/NAPS2.Escl.Usb/EsclUsbDescriptor.cs similarity index 73% rename from NAPS2.Escl.Client/EsclUsbDescriptor.cs rename to NAPS2.Escl.Usb/EsclUsbDescriptor.cs index d21383a474..3931bdd7c1 100644 --- a/NAPS2.Escl.Client/EsclUsbDescriptor.cs +++ b/NAPS2.Escl.Usb/EsclUsbDescriptor.cs @@ -1,5 +1,3 @@ -#if USB -namespace NAPS2.Escl.Client; +namespace NAPS2.Escl.Usb; public record EsclUsbDescriptor(int VendorId, int ProductId, string SerialNumber, string Manufacturer, string Product); -#endif \ No newline at end of file diff --git a/NAPS2.Escl.Client/EsclUsbPoller.cs b/NAPS2.Escl.Usb/EsclUsbPoller.cs similarity index 96% rename from NAPS2.Escl.Client/EsclUsbPoller.cs rename to NAPS2.Escl.Usb/EsclUsbPoller.cs index 1ad9eff73a..db9ecf7933 100644 --- a/NAPS2.Escl.Client/EsclUsbPoller.cs +++ b/NAPS2.Escl.Usb/EsclUsbPoller.cs @@ -1,9 +1,8 @@ -#if USB -using LibUsbDotNet; +using LibUsbDotNet; using LibUsbDotNet.Info; using LibUsbDotNet.LibUsb; -namespace NAPS2.Escl.Client; +namespace NAPS2.Escl.Usb; public class EsclUsbPoller { @@ -50,4 +49,3 @@ public Task> Poll() }); } } -#endif \ No newline at end of file diff --git a/NAPS2.Escl.Usb/LICENSE b/NAPS2.Escl.Usb/LICENSE new file mode 100644 index 0000000000..099a807a65 --- /dev/null +++ b/NAPS2.Escl.Usb/LICENSE @@ -0,0 +1,518 @@ +NAPS2.Escl +https://www.github.com/cyanfish/naps2/ + +Copyright 2009-2025 NAPS2 Contributors + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/NAPS2.Escl.Usb/NAPS2.Escl.Usb.csproj b/NAPS2.Escl.Usb/NAPS2.Escl.Usb.csproj new file mode 100644 index 0000000000..6ccce2ee65 --- /dev/null +++ b/NAPS2.Escl.Usb/NAPS2.Escl.Usb.csproj @@ -0,0 +1,17 @@ + + + + net6;net8;net462 + enable + enable + 12 + + + + + + + + + + \ No newline at end of file diff --git a/NAPS2.Escl/Client/CapabilitiesParser.cs b/NAPS2.Escl/Client/CapabilitiesParser.cs new file mode 100644 index 0000000000..1d734d91ee --- /dev/null +++ b/NAPS2.Escl/Client/CapabilitiesParser.cs @@ -0,0 +1,154 @@ +using System.Xml.Linq; + +namespace NAPS2.Escl.Client; + +internal static class CapabilitiesParser +{ + private static readonly XNamespace ScanNs = EsclXmlHelper.ScanNs; + private static readonly XNamespace PwgNs = EsclXmlHelper.PwgNs; + + public static EsclCapabilities Parse(XDocument doc) + { + var root = doc.Root; + if (root?.Name.LocalName != "ScannerCapabilities" || + !root.Name.NamespaceName.StartsWith("http://schemas.hp.com/imaging/escl/")) + { + throw new InvalidOperationException("Unexpected root element: " + doc.Root?.Name); + } + var settingProfilesEl = root!.Element(ScanNs + "SettingProfiles"); + var settingProfiles = new Dictionary(); + if (settingProfilesEl != null) + { + foreach (var el in settingProfilesEl.Elements(ScanNs + "SettingProfile")) + { + ParseSettingProfile(el, settingProfiles); + } + } + var platenCapsEl = root.Element(ScanNs + "Platen")?.Element(ScanNs + "PlatenInputCaps"); + var adfSimplexCapsEl = root.Element(ScanNs + "Adf")?.Element(ScanNs + "AdfSimplexInputCaps"); + var adfDuplexCapsEl = root.Element(ScanNs + "Adf")?.Element(ScanNs + "AdfDuplexInputCaps"); + return new EsclCapabilities + { + Version = root.Element(PwgNs + "Version")?.Value ?? EsclCapabilities.DEFAULT_VERSION, + MakeAndModel = root.Element(PwgNs + "MakeAndModel")?.Value, + SerialNumber = root.Element(PwgNs + "SerialNumber")?.Value, + Manufacturer = root.Element(ScanNs + "Manufacturer")?.Value, + Uuid = root.Element(ScanNs + "UUID")?.Value, + AdminUri = root.Element(ScanNs + "AdminURI")?.Value, + IconUri = root.Element(ScanNs + "IconURI")?.Value, + Naps2Extensions = root.Element(ScanNs + "Naps2Extensions")?.Value, + PlatenCaps = ParseInputCaps(platenCapsEl, settingProfiles), + AdfSimplexCaps = ParseInputCaps(adfSimplexCapsEl, settingProfiles), + AdfDuplexCaps = ParseInputCaps(adfDuplexCapsEl, settingProfiles), + CompressionFactorSupport = ParseRange(root.Element(ScanNs + "CompressionFactorSupport")) + }; + } + + private static EsclSettingProfile ParseSettingProfile(XElement element, + Dictionary profilesDict) + { + var profileRef = element.Attribute("ref")?.Value; + if (profileRef != null) + { + return profilesDict[profileRef]; + } + var profile = new EsclSettingProfile + { + Name = element.Attribute("name")?.Value, + ColorModes = + ParseEnumValues(element.Element(ScanNs + "ColorModes")?.Elements(ScanNs + "ColorMode")), + DocumentFormats = element.Element(ScanNs + "DocumentFormats")?.Elements(PwgNs + "DocumentFormat") + .Select(x => x.Value).ToList() ?? new List(), + DocumentFormatsExt = element.Element(ScanNs + "DocumentFormats")?.Elements(PwgNs + "DocumentFormatExt") + .Select(x => x.Value).ToList() ?? new List(), + DiscreteResolutions = ParseDiscreteResolutions(element.Element(ScanNs + "SupportedResolutions") + ?.Element(ScanNs + "DiscreteResolutions")?.Elements(ScanNs + "DiscreteResolution")), + XResolutionRange = ParseRange(element.Element(ScanNs + "SupportedResolutions") + ?.Element(ScanNs + "XResolutionRange")), + YResolutionRange = ParseRange(element.Element(ScanNs + "SupportedResolutions") + ?.Element(ScanNs + "YResolutionRange")), + }; + if (profile.Name != null) + { + profilesDict[profile.Name] = profile; + } + return profile; + } + + private static EsclRange? ParseRange(XElement? element) + { + if (element == null) return null; + + var min = ParseHelper.MaybeParseInt(element.Element(ScanNs + "Min")); + var max = ParseHelper.MaybeParseInt(element.Element(ScanNs + "Max")); + var normal = ParseHelper.MaybeParseInt(element.Element(ScanNs + "Normal")); + var step = ParseHelper.MaybeParseInt(element.Element(ScanNs + "Step")); + if (min != null && max != null && normal != null) + { + return new EsclRange(min.Value, max.Value, normal.Value, step ?? 1); + } + return null; + } + + private static List ParseDiscreteResolutions(IEnumerable? elements) + { + var list = new List(); + if (elements == null) + { + return list; + } + foreach (var el in elements) + { + var xRes = el.Element(ScanNs + "XResolution"); + var yRes = el.Element(ScanNs + "YResolution"); + if (xRes != null && yRes != null) + { + list.Add(new DiscreteResolution(int.Parse(xRes.Value), int.Parse(yRes.Value))); + } + } + return list; + } + + private static List ParseEnumValues(IEnumerable? elements) where T : struct + { + var list = new List(); + if (elements == null) + { + return list; + } + foreach (var el in elements) + { + if (Enum.TryParse(el.Value, out var parsed)) + { + list.Add(parsed); + } + } + return list; + } + + private static EsclInputCaps? ParseInputCaps(XElement? element, + Dictionary settingProfilesMap) + { + if (element == null) + { + return null; + } + var settingProfiles = new List(); + var settingProfilesEl = element.Element(ScanNs + "SettingProfiles"); + if (settingProfilesEl != null) + { + foreach (var el in settingProfilesEl.Elements(ScanNs + "SettingProfile")) + { + settingProfiles.Add(ParseSettingProfile(el, settingProfilesMap)); + } + } + return new EsclInputCaps + { + SettingProfiles = settingProfiles, + MinWidth = ParseHelper.MaybeParseInt(element.Element(ScanNs + "MinWidth")), + MaxWidth = ParseHelper.MaybeParseInt(element.Element(ScanNs + "MaxWidth")), + MinHeight = ParseHelper.MaybeParseInt(element.Element(ScanNs + "MinHeight")), + MaxHeight = ParseHelper.MaybeParseInt(element.Element(ScanNs + "MaxHeight")), + }; + } +} \ No newline at end of file diff --git a/NAPS2.Escl/Client/EsclClient.cs b/NAPS2.Escl/Client/EsclClient.cs new file mode 100644 index 0000000000..6a374aec6c --- /dev/null +++ b/NAPS2.Escl/Client/EsclClient.cs @@ -0,0 +1,398 @@ +using System.Globalization; +using System.Net; +using System.Net.Http; +using System.Security.Authentication; +using System.Text; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace NAPS2.Escl.Client; + +public class EsclClient +{ + private static readonly XNamespace ScanNs = EsclXmlHelper.ScanNs; + private static readonly XNamespace PwgNs = EsclXmlHelper.PwgNs; + + // Client that verifies HTTPS certificates + private static readonly HttpMessageHandler VerifiedHttpClientHandler = new StandardSocketsHttpHandler + { + MaxConnectionsPerServer = 256, + ConnectTimeout = TimeSpan.FromSeconds(5) + }; + private static readonly HttpClient VerifiedHttpClient = new(VerifiedHttpClientHandler) + { + Timeout = TimeSpan.FromSeconds(10) + }; + private static readonly HttpClient LongTimeoutVerifiedHttpClient = new(VerifiedHttpClientHandler) + { + Timeout = TimeSpan.FromSeconds(120) + }; + + // Client that doesn't verify HTTPS certificates + private static readonly HttpMessageHandler UnverifiedHttpClientHandler = new StandardSocketsHttpHandler + { + MaxConnectionsPerServer = 256, + ConnectTimeout = TimeSpan.FromSeconds(5), + SslOptions = + { + // ESCL certificates are generally self-signed - we aren't trying to verify server authenticity, just ensure + // that the connection is encrypted and protect against passive interception. + RemoteCertificateValidationCallback = (_, _, _, _) => true + } + }; + private static readonly HttpClient UnverifiedHttpClient = new(UnverifiedHttpClientHandler) + { + Timeout = TimeSpan.FromSeconds(10) + }; + private static readonly HttpClient LongTimeoutUnverifiedHttpClient = new(UnverifiedHttpClientHandler) + { + Timeout = TimeSpan.FromSeconds(120) + }; + + public static HttpClient GetHttpClient(EsclSecurityPolicy securityPolicy, bool longTimeout = false) + { + if (longTimeout) + { + return securityPolicy.HasFlag(EsclSecurityPolicy.ClientRequireTrustedCertificate) + ? LongTimeoutVerifiedHttpClient + : LongTimeoutUnverifiedHttpClient; + } + return securityPolicy.HasFlag(EsclSecurityPolicy.ClientRequireTrustedCertificate) + ? VerifiedHttpClient + : UnverifiedHttpClient; + } + + private readonly EsclService? _service; + private readonly Uri? _uriBase; + private readonly string _rootUrl; + private bool _httpFallback; + + public EsclClient(EsclService service) + { + _service = service; + _rootUrl = _service.RootUrl; + } + + public EsclClient(Uri uriBase) + { + _uriBase = uriBase; + _rootUrl = _uriBase.AbsolutePath; + if (_rootUrl.StartsWith("/")) _rootUrl = _rootUrl.Substring(1); + } + + public EsclSecurityPolicy SecurityPolicy { get; set; } + + public ILogger Logger { get; set; } = NullLogger.Instance; + + public CancellationToken CancelToken { get; set; } + + private HttpClient HttpClient => GetHttpClient(SecurityPolicy, false); + + private HttpClient LongTimeoutHttpClient => GetHttpClient(SecurityPolicy, true); + + public async Task GetCapabilities() + { + var doc = await DoRequest("ScannerCapabilities"); + return CapabilitiesParser.Parse(doc); + } + + public async Task GetStatus() + { + var doc = await DoRequest("ScannerStatus"); + var root = doc.Root; + if (root?.Name != ScanNs + "ScannerStatus") + { + throw new InvalidOperationException("Unexpected root element: " + doc.Root?.Name); + } + var jobStates = new Dictionary(); + foreach (var jobInfoEl in root.Element(ScanNs + "Jobs")?.Elements(ScanNs + "JobInfo") ?? []) + { + var jobUri = jobInfoEl.Element(PwgNs + "JobUri")?.Value; + var jobState = ParseHelper.MaybeParseEnum(jobInfoEl.Element(PwgNs + "JobState"), EsclJobState.Unknown); + if (jobUri != null && jobState != EsclJobState.Unknown) + { + jobStates.Add(jobUri, jobState); + } + } + return new EsclScannerStatus + { + State = ParseHelper.MaybeParseEnum(root.Element(PwgNs + "State"), EsclScannerState.Unknown), + AdfState = ParseHelper.MaybeParseEnum(root.Element(ScanNs + "AdfState"), EsclAdfState.Unknown), + JobStates = jobStates + }; + } + + public async Task CreateScanJob(EsclScanSettings settings) + { + var doc = + EsclXmlHelper.CreateDocAsString( + new XElement(ScanNs + "ScanSettings", + new XElement(PwgNs + "Version", "2.0"), + new XElement(ScanNs + "Intent", "TextAndGraphic"), + new XElement(PwgNs + "ScanRegions", + new XAttribute(PwgNs + "MustHonor", "true"), + new XElement(PwgNs + "ScanRegion", + new XElement(PwgNs + "Height", settings.Height), + new XElement(PwgNs + "ContentRegionUnits", "escl:ThreeHundredthsOfInches"), + new XElement(PwgNs + "Width", settings.Width), + new XElement(PwgNs + "XOffset", settings.XOffset), + new XElement(PwgNs + "YOffset", settings.YOffset))), + new XElement(PwgNs + "InputSource", settings.InputSource), + new XElement(ScanNs + "Duplex", settings.Duplex), + new XElement(ScanNs + "ColorMode", settings.ColorMode), + new XElement(ScanNs + "XResolution", settings.XResolution), + new XElement(ScanNs + "YResolution", settings.YResolution), + // TODO: Brightness/contrast/threshold + // new XElement(ScanNs + "Brightness", settings.Brightness), + // new XElement(ScanNs + "Contrast", settings.Contrast), + // new XElement(ScanNs + "Threshold", settings.Threshold), + OptionalElement(ScanNs + "CompressionFactor", settings.CompressionFactor), + new XElement(PwgNs + "DocumentFormat", settings.DocumentFormat))); + var content = new StringContent(doc, Encoding.UTF8, "text/xml"); + var response = await WithHttpFallback( + () => GetUrl($"/{_rootUrl}/ScanJobs"), + url => + { + Logger.LogDebug("ESCL POST {Url}", url); + return HttpClient.PostAsync(url, content); + }); + response.EnsureSuccessStatusCode(); + Logger.LogDebug("POST OK"); + + var uri = response.Headers.Location!; + + return new EsclJob + { + UriPath = uri.IsAbsoluteUri ? uri.AbsolutePath : uri.OriginalString + }; + } + + private XElement? OptionalElement(XName elementName, int? value) + { + if (value == null) return null; + return new XElement(elementName, value); + } + + public async Task NextDocument(EsclJob job, Action? pageProgress = null, + bool shortTimeout = false) + { + var progressCts = new CancellationTokenSource(); + if (pageProgress != null) + { + var progressUrl = GetUrl($"{job.UriPath}/Progress"); + var progressResponse = await LongTimeoutHttpClient.GetStreamAsync(progressUrl); + var streamReader = new StreamReader(progressResponse); + _ = Task.Run(async () => + { + using var streamReaderForDisposal = streamReader; + while (await streamReader.ReadLineAsync() is { } line) + { + if (progressCts.IsCancellationRequested) + { + return; + } + if (double.TryParse(line, NumberStyles.Any, CultureInfo.InvariantCulture, out var progress)) + { + pageProgress(progress); + } + } + }); + } + try + { + // TODO: Maybe check Content-Location on the response header to ensure no duplicate document? + HttpResponseMessage response; + while (true) + { + response = await WithHttpFallback( + () => GetUrl($"{job.UriPath}/NextDocument"), + url => + { + Logger.LogDebug("ESCL GET {Url}", url); + var client = shortTimeout ? HttpClient : LongTimeoutHttpClient; + return client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + }); + if (response.StatusCode == HttpStatusCode.ServiceUnavailable) + { + // ServiceUnavailable = retry after a delay + Logger.LogDebug("GET returned 503, waiting to retry"); + await Task.Delay(2000); + continue; + } + if (response.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Gone) + { + // NotFound = end of scan, Gone = canceled + Logger.LogDebug("GET failed: {Status}", response.StatusCode); + return null; + } + response.EnsureSuccessStatusCode(); + break; + } + // TODO: Define a NAPS2 protocol extension to shorten this timeout to 10s (once we do the rollout of server-side 503s) + var data = await ReadStreamWithPerReadTimeout(await response.Content.ReadAsStreamAsync(), 60_000); + var doc = new RawDocument + { + Data = data, + ContentType = response.Content.Headers.ContentType?.MediaType, + ContentLocation = response.Content.Headers.ContentLocation?.ToString() + }; + if (doc.Data.Length == 0) + { + throw new Exception("ESCL response had no data, the connection may have been interrupted"); + } + Logger.LogDebug("GET OK: {Type} ({Bytes} bytes) {Location}", doc.ContentType, doc.Data.Length, + doc.ContentLocation); + return doc; + } + finally + { + progressCts.Cancel(); + } + } + + private async Task ReadStreamWithPerReadTimeout(Stream stream, int timeout) + { + // We expect the server to be continuously sending some kind of data - if reads take longer than the timeout, + // we assume that the connection has been disrupted. + MemoryStream tempStream = new MemoryStream(); + byte[] buffer = new byte[65536]; + while (true) + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(timeout); + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cts.Token); + if (bytesRead == 0) break; + tempStream.Write(buffer, 0, bytesRead); + } + return tempStream.ToArray(); + } + + public async Task ErrorDetails(EsclJob job) + { + var response = await WithHttpFallback( + () => GetUrl($"{job.UriPath}/ErrorDetails"), + url => + { + Logger.LogDebug("ESCL GET {Url}", url); + return HttpClient.GetAsync(url); + }); + response.EnsureSuccessStatusCode(); + Logger.LogDebug("GET OK"); + return await response.Content.ReadAsStringAsync(); + } + + public async Task CancelJob(EsclJob job) + { + var response = await WithHttpFallback( + () => GetUrl(job.UriPath), + url => + { + Logger.LogDebug("ESCL DELETE {Url}", url); + return HttpClient.DeleteAsync(url); + }); + if (!response.IsSuccessStatusCode) + { + Logger.LogDebug("DELETE failed: {Status}", response.StatusCode); + return; + } + response.EnsureSuccessStatusCode(); + Logger.LogDebug("DELETE OK"); + } + + public string? IconUri + { + get + { + // TODO: Consider replacing the hostname with the remote endpoint? + var iconUri = _service?.Thumbnail; + if (string.IsNullOrWhiteSpace(iconUri)) + { + return null; + } + if (iconUri!.StartsWith("http://") || iconUri.StartsWith("https://")) + { + return iconUri; + } + if (!iconUri.StartsWith("/")) + { + iconUri = "/" + iconUri; + } + return GetUrl(iconUri); + } + } + + public string ConnectionUri => GetUrl($"/{_rootUrl}"); + + private async Task DoRequest(string endpoint) + { + // TODO: Retry logic + var response = await WithHttpFallback( + () => GetUrl($"/{_rootUrl}/{endpoint}"), + url => + { + Logger.LogDebug("ESCL GET {Url}", url); + return HttpClient.GetAsync(url, CancelToken); + }); + response.EnsureSuccessStatusCode(); + Logger.LogDebug("GET OK"); + var text = await response.Content.ReadAsStringAsync(); + // Fix for invalid doctype declarations + text = text.Replace(" WithHttpFallback(Func urlFunc, Func> func) + { + string url = urlFunc(); + try + { + return await func(url); + } + catch (HttpRequestException ex) when (!SecurityPolicy.HasFlag(EsclSecurityPolicy.ClientRequireHttps) && + !_httpFallback && + url.StartsWith("https://") && ( + ex.InnerException is AuthenticationException || + ex.InnerException?.InnerException is AuthenticationException)) + { + Logger.LogDebug(ex, "TLS authentication error; falling back to HTTP"); + _httpFallback = true; + url = urlFunc(); + return await func(url); + } + } + + private string GetUrl(string endpoint) + { + bool tls = _service == null + ? _uriBase!.Scheme == "https" + : (_service.Tls || _service.Port == 443) && !_httpFallback && + !SecurityPolicy.HasFlag(EsclSecurityPolicy.ClientDisableHttps); + if (SecurityPolicy.HasFlag(EsclSecurityPolicy.ClientRequireHttps) && !tls) + { + throw new EsclSecurityPolicyViolationException( + $"EsclSecurityPolicy of {SecurityPolicy} doesn't allow HTTP connections"); + } + if (_service == null) + { + return $"{_uriBase!.Scheme}://{_uriBase.Host}:{_uriBase.Port}{endpoint}"; + } + var protocol = tls ? "https" : "http"; + return $"{protocol}://{GetHostAndPort(_service.Tls && !_httpFallback)}{endpoint}"; + } + + private string GetHostAndPort(bool tls) + { + var port = tls ? _service!.TlsPort : _service!.Port; + var host = new IPEndPoint(_service.RemoteEndpoint, port).ToString(); +#if NET6_0_OR_GREATER + if (OperatingSystem.IsMacOS()) + { + // Using the mDNS hostname is more reliable on Mac (but doesn't work at all on Windows) + host = $"{_service.Host}:{port}"; + } +#endif + return host; + } +} \ No newline at end of file diff --git a/NAPS2.Escl/Client/EsclService.cs b/NAPS2.Escl/Client/EsclService.cs new file mode 100644 index 0000000000..6f277fd907 --- /dev/null +++ b/NAPS2.Escl/Client/EsclService.cs @@ -0,0 +1,102 @@ +using System.Net; + +namespace NAPS2.Escl.Client; + +public class EsclService +{ + /// + /// The IP (v4) address of the scanner. At least one of IpV4 and IpV6 will be non-null. + /// + public required IPAddress? IpV4 { get; init; } + + /// + /// The IP (v6) address of the scanner. At least one of IpV4 and IpV6 will be non-null. + /// + public required IPAddress? IpV6 { get; init; } + + /// + /// The mDNS host name of the scanner. + /// + public required string Host { get; init; } + + /// + /// The IP address of the DNS response. + /// + public required IPAddress RemoteEndpoint { get; init; } + + /// + /// The HTTP port of the ESCL service. + /// + public required int Port { get; init; } + + /// + /// The HTTPS port of the ESCL service. + /// + public required int TlsPort { get; init; } + + /// + /// Whether to use HTTPS for the connection. + /// + public required bool Tls { get; init; } + + /// + /// The root path of the ESCL URLs with no leading or trailing slash. For example, "eSCL" means we would use a URL + /// like "http://192.168.1.111:80/eSCL/ScannerCapabilities". + /// + public required string RootUrl { get; init; } + + /// + /// A unique identifier for the physical scanner device. + /// + public required string Uuid { get; init; } + + /// + /// The make and model of the scanner. + /// + public string? ScannerName { get; init; } + + /// + /// The version of the TXT record the information in this class came from. + /// + public string? TxtVersion { get; init; } + + /// + /// The configuration URL for the scanner. + /// + public string? AdminUrl { get; init; } + + /// + /// The ESCL protocol version. Should be 2.0. + /// + public string? EsclVersion { get; init; } + + /// + /// A URL to an image representing the scanner. + /// + public string? Thumbnail { get; init; } + + /// + /// Extra information about the scanner's location (e.g. "3rd Floor Copy Room"). + /// + public string? Note { get; init; } + + /// + /// The MIME types supported by the scanner (e.g. "application/pdf", "image/jpeg"). + /// + public string[]? MimeTypes { get; init; } + + /// + /// Supported color options (e.g. "color", "grayscale", "binary"). + /// + public string[]? ColorOptions { get; init; } + + /// + /// Supported input source options (e.g. "platen", "adf", "camera"). + /// + public string? SourceOptions { get; init; } + + /// + /// Whether duplex is supported with the "adf" source. + /// + public bool? DuplexSupported { get; init; } +} \ No newline at end of file diff --git a/NAPS2.Escl/Client/EsclServiceLocator.cs b/NAPS2.Escl/Client/EsclServiceLocator.cs new file mode 100644 index 0000000000..6b52c57d86 --- /dev/null +++ b/NAPS2.Escl/Client/EsclServiceLocator.cs @@ -0,0 +1,176 @@ +using System.Net; +using Makaretu.Dns; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace NAPS2.Escl.Client; + +public class EsclServiceLocator : IDisposable +{ + private readonly ServiceDiscovery _discovery; + private readonly HashSet _locatedServices = new(); + private bool _started; + private int _nextQueryInterval = 1000; + + public EsclServiceLocator(Action serviceCallback) + { + _discovery = new ServiceDiscovery(); + _discovery.ServiceInstanceDiscovered += (_, args) => + { + try + { + if (args.ServiceInstanceName.Labels[1] is not ("_uscan" or "_uscans")) + { + return; + } + var service = ParseService(args); + // TODO: Does the IP really make the device distinct? Not that it should matter in practice, but still. + // TODO: We definitely want to de-duplicate HTTP/HTTPS, but I'm not sure how to do that. Remind me how + var serviceKey = new ServiceKey(service.ScannerName, service.Uuid, service.Port, service.IpV4, service.IpV6); + lock (_locatedServices) + { + if (!_locatedServices.Add(serviceKey)) + { + // Don't callback for duplicates + return; + } + } + Logger.LogDebug("Discovered ESCL Service: {Name}, instance {Instance}, endpoint {Endpoint}, ipv4 {Ipv4}, ipv6 {IpV6}, host {Host}, port {Port}, tlsPort {Port}, uuid {Uuid}", + service.ScannerName, args.ServiceInstanceName, args.RemoteEndPoint, service.IpV4, service.IpV6, service.Host, service.Port, service.TlsPort, service.Uuid); + serviceCallback(service); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error parsing ESCL service"); + } + }; + } + + public ILogger Logger { get; set; } = NullLogger.Instance; + + public void Start() + { + if (_started) throw new InvalidOperationException("Already started"); + _started = true; + + Query(); + } + + private void Query() + { + if (_discovery.Mdns == null) + { + return; + } + // TODO: De-duplicate http/https services? + _discovery.QueryServiceInstances("_uscan._tcp"); + _discovery.QueryServiceInstances("_uscans._tcp"); + + // We query once when we start, then again after 1s, 2s, etc. to account for race conditions where there was a + // previous query/answer on the network just before we started listening, which would prevent us from receiving + // a response. See the following: + // + // "When retransmitting Multicast DNS queries to implement continuous monitoring, the interval between the first + // two queries MUST be at least one second, and the intervals between successive queries MUST increase by at + // least a factor of two." + // https://datatracker.ietf.org/doc/html/rfc6762#section-5.2 + // + // "A Multicast DNS responder MUST NOT multicast a record on a given interface until at least one second has + // elapsed since the last time that record was multicast on that particular interface." + // https://datatracker.ietf.org/doc/html/rfc6762#section-6 + Task.Delay(_nextQueryInterval).ContinueWith(_ => Query()); + _nextQueryInterval *= 2; + } + + private EsclService ParseService(ServiceInstanceDiscoveryEventArgs args) + { + string name = args.ServiceInstanceName.Labels[0]; + bool isTls = false; + IPAddress? ipv4 = null, ipv6 = null; + int port = -1; + int tlsPort = -1; + string? host = null; + var props = new Dictionary(); + foreach (var record in args.Message.Answers.Concat(args.Message.AdditionalRecords)) + { + Logger.LogTrace("{Type} {Record}", record.GetType().Name, record); + if (record is ARecord a) + { + ipv4 = a.Address; + } + if (record is AAAARecord aaaa) + { + ipv6 = aaaa.Address; + } + if (record is SRVRecord srv) + { + bool recordIsTls = srv.Name.IsSubdomainOf(DomainName.Join("_uscans", "_tcp", "local")); + if (recordIsTls) + { + tlsPort = srv.Port; + } + else + { + port = srv.Port; + } + if (host == null || recordIsTls) + { + // HTTPS overrides HTTP but not the other way around + host = srv.Target.ToString(); + isTls = recordIsTls; + } + } + if (record is TXTRecord txt) + { + foreach (var str in txt.Strings) + { + var eq = str.IndexOf("=", StringComparison.Ordinal); + if (eq != -1) + { + props[str.Substring(0, eq).ToLowerInvariant()] = str.Substring(eq + 1); + } + } + } + } + string? uuid = Get(props, "uuid"); + if ((ipv4 == null && ipv6 == null) || (port == -1 && tlsPort == -1) || host == null || uuid == null) + { + throw new ArgumentException("Missing host/IP/port/uuid"); + } + + return new EsclService + { + IpV4 = ipv4, + IpV6 = ipv6, + Host = host, + RemoteEndpoint = args.RemoteEndPoint.Address, + Port = port, + TlsPort = tlsPort, + Tls = isTls, + Uuid = uuid, + ScannerName = props["ty"], + RootUrl = props["rs"], + TxtVersion = Get(props, "txtvers"), + AdminUrl = Get(props, "adminurl"), + EsclVersion = Get(props, "Vers"), + Thumbnail = Get(props, "representation"), + Note = Get(props, "note"), + MimeTypes = Get(props, "pdl")?.Split(','), + ColorOptions = Get(props, "cs")?.Split(','), + SourceOptions = Get(props, "is"), + DuplexSupported = Get(props, "duplex")?.ToUpperInvariant() == "T" + }; + } + + private string? Get(Dictionary props, string key) + { + return props.TryGetValue(key, out var value) ? value : null; + } + + public void Dispose() + { + _discovery.Dispose(); + } + + private record ServiceKey(string? ScannerName, string? Uuid, int Port, IPAddress? IpV4, IPAddress? IpV6); +} diff --git a/NAPS2.Escl/Client/RawDocument.cs b/NAPS2.Escl/Client/RawDocument.cs new file mode 100644 index 0000000000..3336c2b7e6 --- /dev/null +++ b/NAPS2.Escl/Client/RawDocument.cs @@ -0,0 +1,10 @@ +namespace NAPS2.Escl.Client; + +public class RawDocument +{ + public required byte[] Data { get; init; } + + public required string? ContentType { get; init; } + + public string? ContentLocation { get; init; } +} \ No newline at end of file diff --git a/NAPS2.Escl/CompilerAttributes.cs b/NAPS2.Escl/CompilerAttributes.cs deleted file mode 100644 index 8230a289b1..0000000000 --- a/NAPS2.Escl/CompilerAttributes.cs +++ /dev/null @@ -1,61 +0,0 @@ -// https://sergiopedri.medium.com/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb -// ReSharper disable once CheckNamespace - -namespace System.Runtime.CompilerServices -{ - internal static class IsExternalInit - { - } - - /// Specifies that a type has required members or that a member is required. - [AttributeUsage( - AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, - AllowMultiple = false, Inherited = false)] - internal sealed class RequiredMemberAttribute : Attribute - { - } - - /// - /// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. - /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] - internal sealed class CompilerFeatureRequiredAttribute : Attribute - { - public CompilerFeatureRequiredAttribute(string featureName) - { - FeatureName = featureName; - } - - /// - /// The name of the compiler feature. - /// - public string FeatureName { get; } - - /// - /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . - /// - public bool IsOptional { get; init; } - - /// - /// The used for the ref structs C# feature. - /// - public const string RefStructs = nameof(RefStructs); - - /// - /// The used for the required members C# feature. - /// - public const string RequiredMembers = nameof(RequiredMembers); - } -} - -namespace System.Diagnostics.CodeAnalysis -{ - /// - /// Specifies that this constructor sets all required members for the current type, and callers - /// do not need to set any required members themselves. - /// - [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] - internal sealed class SetsRequiredMembersAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/NAPS2.Escl/DiscreteResolution.cs b/NAPS2.Escl/DiscreteResolution.cs new file mode 100644 index 0000000000..65b7fdf1b6 --- /dev/null +++ b/NAPS2.Escl/DiscreteResolution.cs @@ -0,0 +1,3 @@ +namespace NAPS2.Escl; + +public record DiscreteResolution(int XResolution, int YResolution); \ No newline at end of file diff --git a/NAPS2.Escl/EsclAdfState.cs b/NAPS2.Escl/EsclAdfState.cs new file mode 100644 index 0000000000..7acc38421b --- /dev/null +++ b/NAPS2.Escl/EsclAdfState.cs @@ -0,0 +1,17 @@ +namespace NAPS2.Escl; + +public enum EsclAdfState +{ + Unknown, + ScannerAdfProcessing, + ScannerAdfEmpty, + ScannerAdfJam, + ScannedAdfLoaded, + ScannerAdfMispick, + ScannerAdfHatchOpen, + ScannerAdfDuplexPageTooShort, + ScannerAdfDuplexPageTooLong, + ScannerAdfMultipickDetected, + ScannerAdfInputTrayFailed, + ScannerAdfInputTrayOverloaded +} \ No newline at end of file diff --git a/NAPS2.Escl/EsclCapabilities.cs b/NAPS2.Escl/EsclCapabilities.cs index 770c9aae4b..b2e9255f24 100644 --- a/NAPS2.Escl/EsclCapabilities.cs +++ b/NAPS2.Escl/EsclCapabilities.cs @@ -2,10 +2,19 @@ namespace NAPS2.Escl; public class EsclCapabilities { - public string? Version { get; init; } + public const string DEFAULT_VERSION = "2.6"; + + public string Version { get; init; } = DEFAULT_VERSION; public string? MakeAndModel { get; init; } public string? SerialNumber { get; init; } + public string? Manufacturer { get; init; } public string? Uuid { get; init; } public string? AdminUri { get; init; } public string? IconUri { get; init; } + public byte[]? IconPng { get; init; } + public string? Naps2Extensions { get; init; } + public EsclInputCaps? PlatenCaps { get; init; } + public EsclInputCaps? AdfSimplexCaps { get; init; } + public EsclInputCaps? AdfDuplexCaps { get; init; } + public EsclRange? CompressionFactorSupport { get; init; } } \ No newline at end of file diff --git a/NAPS2.Escl/EsclInputCaps.cs b/NAPS2.Escl/EsclInputCaps.cs new file mode 100644 index 0000000000..5ba7d869d3 --- /dev/null +++ b/NAPS2.Escl/EsclInputCaps.cs @@ -0,0 +1,14 @@ +namespace NAPS2.Escl; + +public class EsclInputCaps +{ + // Units of 1/300 inch (per ESCL spec); supports A3 in both orientations + public const int DEFAULT_MAX_WIDTH = 5000; + public const int DEFAULT_MAX_HEIGHT = 5000; + + public List SettingProfiles { get; init; } = new(); + public int? MinWidth { get; set; } + public int? MaxWidth { get; set; } + public int? MinHeight { get; set; } + public int? MaxHeight { get; set; } +} \ No newline at end of file diff --git a/NAPS2.Escl/EsclInputSource.cs b/NAPS2.Escl/EsclInputSource.cs new file mode 100644 index 0000000000..f839043f28 --- /dev/null +++ b/NAPS2.Escl/EsclInputSource.cs @@ -0,0 +1,7 @@ +namespace NAPS2.Escl; + +public enum EsclInputSource +{ + Platen, + Feeder +} \ No newline at end of file diff --git a/NAPS2.Escl/EsclJob.cs b/NAPS2.Escl/EsclJob.cs index cecc3c5cfc..13743300a6 100644 --- a/NAPS2.Escl/EsclJob.cs +++ b/NAPS2.Escl/EsclJob.cs @@ -2,5 +2,5 @@ namespace NAPS2.Escl; public class EsclJob { - public required Uri Uri { get; init; } + public required string UriPath { get; init; } } \ No newline at end of file diff --git a/NAPS2.Escl/EsclJobState.cs b/NAPS2.Escl/EsclJobState.cs new file mode 100644 index 0000000000..c3afd0d816 --- /dev/null +++ b/NAPS2.Escl/EsclJobState.cs @@ -0,0 +1,11 @@ +namespace NAPS2.Escl; + +public enum EsclJobState +{ + Unknown, + Pending, + Processing, + Completed, + Canceled, + Aborted +} \ No newline at end of file diff --git a/NAPS2.Escl/EsclRange.cs b/NAPS2.Escl/EsclRange.cs new file mode 100644 index 0000000000..f07988dd3e --- /dev/null +++ b/NAPS2.Escl/EsclRange.cs @@ -0,0 +1,3 @@ +namespace NAPS2.Escl; + +public record EsclRange(int Min, int Max, int Normal, int Step); \ No newline at end of file diff --git a/NAPS2.Escl/EsclScanSettings.cs b/NAPS2.Escl/EsclScanSettings.cs index e725012080..937bacf540 100644 --- a/NAPS2.Escl/EsclScanSettings.cs +++ b/NAPS2.Escl/EsclScanSettings.cs @@ -1,5 +1,32 @@ namespace NAPS2.Escl; -public class EsclScanSettings +public record EsclScanSettings { + public int Width { get; init; } + + public int Height { get; init; } + + public int XOffset { get; init; } + + public int YOffset { get; init; } + + public string? DocumentFormat { get; init; } + + public EsclInputSource InputSource { get; init; } + + public int XResolution { get; init; } + + public int YResolution { get; init; } + + public EsclColorMode ColorMode { get; init; } + + public bool Duplex { get; init; } + + public int Brightness { get; init; } + + public int Contrast { get; init; } + + public int Threshold { get; init; } + + public int? CompressionFactor { get; set; } } \ No newline at end of file diff --git a/NAPS2.Escl/EsclScannerState.cs b/NAPS2.Escl/EsclScannerState.cs new file mode 100644 index 0000000000..0b483cc352 --- /dev/null +++ b/NAPS2.Escl/EsclScannerState.cs @@ -0,0 +1,11 @@ +namespace NAPS2.Escl; + +public enum EsclScannerState +{ + Unknown, + Idle, + Processing, + Testing, + Stopped, + Down +} \ No newline at end of file diff --git a/NAPS2.Escl/EsclScannerStatus.cs b/NAPS2.Escl/EsclScannerStatus.cs index d3f6b35255..27bcf2a3dc 100644 --- a/NAPS2.Escl/EsclScannerStatus.cs +++ b/NAPS2.Escl/EsclScannerStatus.cs @@ -2,4 +2,7 @@ namespace NAPS2.Escl; public class EsclScannerStatus { + public EsclScannerState State { get; init; } + public EsclAdfState AdfState { get; init; } + public Dictionary JobStates { get; set; } = new(); } \ No newline at end of file diff --git a/NAPS2.Escl/EsclSecurityPolicy.cs b/NAPS2.Escl/EsclSecurityPolicy.cs new file mode 100644 index 0000000000..fdb532355c --- /dev/null +++ b/NAPS2.Escl/EsclSecurityPolicy.cs @@ -0,0 +1,32 @@ +namespace NAPS2.Escl; + +[Flags] +public enum EsclSecurityPolicy +{ + /// + /// Allow both HTTP and HTTPS connections. + /// + None = 0, + + ServerDisableHttps = 1, + ServerRequireHttps = 2, + ServerRequireTrustedCertificate = 4, + ClientDisableHttps = 8, + ClientRequireHttps = 16, + ClientRequireTrustedCertificate = 32, + + /// + /// Only allow HTTPS connections, but clients will accept self-signed certificates. + /// + RequireHttps = ServerRequireHttps | ClientRequireHttps, + + /// + /// Only allow HTTPS connections, and clients will only accept trusted certificates. + /// + RequireTrustedCertificate = RequireHttps | ServerRequireTrustedCertificate | ClientRequireTrustedCertificate, + + /// + /// Set the header "Access-Control-Allow-Origin: *" on all server responses. + /// + ServerAllowAnyOrigin = 64 +} \ No newline at end of file diff --git a/NAPS2.Escl/EsclSecurityPolicyViolationException.cs b/NAPS2.Escl/EsclSecurityPolicyViolationException.cs new file mode 100644 index 0000000000..43bdc58af1 --- /dev/null +++ b/NAPS2.Escl/EsclSecurityPolicyViolationException.cs @@ -0,0 +1,3 @@ +namespace NAPS2.Escl; + +public class EsclSecurityPolicyViolationException(string message) : Exception(message); \ No newline at end of file diff --git a/NAPS2.Escl/EsclSettingProfile.cs b/NAPS2.Escl/EsclSettingProfile.cs index d6ebf2dec3..7247d19ffa 100644 --- a/NAPS2.Escl/EsclSettingProfile.cs +++ b/NAPS2.Escl/EsclSettingProfile.cs @@ -1,7 +1,14 @@ +using NAPS2.Escl.Client; + namespace NAPS2.Escl; public class EsclSettingProfile { public string? Name { get; init; } public List ColorModes { get; init; } = new(); + public List DocumentFormats { get; init; } = new(); + public List DocumentFormatsExt { get; init; } = new(); + public List DiscreteResolutions { get; init; } = new(); + public EsclRange? XResolutionRange { get; init; } + public EsclRange? YResolutionRange { get; init; } } \ No newline at end of file diff --git a/NAPS2.Escl/EsclXmlHelper.cs b/NAPS2.Escl/EsclXmlHelper.cs index 570e415e21..1b262e9c1f 100644 --- a/NAPS2.Escl/EsclXmlHelper.cs +++ b/NAPS2.Escl/EsclXmlHelper.cs @@ -16,6 +16,6 @@ public static string CreateDocAsString(XElement root) root.Add(ScanNsAttr); root.Add(PwgNsAttr); var content = new XDocument(root); - return $"{Decl}{Environment.NewLine}{content}"; + return $"{Decl}{content}".Replace("\n", ""); } } \ No newline at end of file diff --git a/NAPS2.Escl/LICENSE b/NAPS2.Escl/LICENSE new file mode 100644 index 0000000000..099a807a65 --- /dev/null +++ b/NAPS2.Escl/LICENSE @@ -0,0 +1,518 @@ +NAPS2.Escl +https://www.github.com/cyanfish/naps2/ + +Copyright 2009-2025 NAPS2 Contributors + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/NAPS2.Escl/NAPS2.Escl.csproj b/NAPS2.Escl/NAPS2.Escl.csproj index ba8827e4ce..3201eeedca 100644 --- a/NAPS2.Escl/NAPS2.Escl.csproj +++ b/NAPS2.Escl/NAPS2.Escl.csproj @@ -1,15 +1,36 @@ - net6.0;net462;netstandard2.0 + net6;net8;net462;netstandard2.0 enable enable - 11 + 12 + + NAPS2.Escl + NAPS2.Escl + ESCL client for NAPS2.Sdk. + naps2 escl + + - + + + + + + + + + + + <_Parameter1>NAPS2.Escl.Server + + + <_Parameter1>NAPS2.Escl.Tests + diff --git a/NAPS2.Escl/ParseHelper.cs b/NAPS2.Escl/ParseHelper.cs new file mode 100644 index 0000000000..eac775b98c --- /dev/null +++ b/NAPS2.Escl/ParseHelper.cs @@ -0,0 +1,13 @@ +using System.Globalization; +using System.Xml.Linq; + +namespace NAPS2.Escl; + +internal class ParseHelper +{ + public static T MaybeParseEnum(XElement? element, T defaultValue) where T : struct => + Enum.TryParse(element?.Value, out var value) ? value : defaultValue; + + public static int? MaybeParseInt(XElement? element) => + int.TryParse(element?.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : null; +} \ No newline at end of file diff --git a/NAPS2.Escl/Server/EsclDeviceConfig.cs b/NAPS2.Escl/Server/EsclDeviceConfig.cs new file mode 100644 index 0000000000..87a54ed7b7 --- /dev/null +++ b/NAPS2.Escl/Server/EsclDeviceConfig.cs @@ -0,0 +1,12 @@ +namespace NAPS2.Escl.Server; + +public class EsclDeviceConfig +{ + public required EsclCapabilities Capabilities { get; init; } + + public required Func CreateJob { get; init; } + + public int Port { get; set; } + + public int TlsPort { get; set; } +} \ No newline at end of file diff --git a/NAPS2.Escl/Server/IEsclScanJob.cs b/NAPS2.Escl/Server/IEsclScanJob.cs new file mode 100644 index 0000000000..1b7cf80398 --- /dev/null +++ b/NAPS2.Escl/Server/IEsclScanJob.cs @@ -0,0 +1,12 @@ +namespace NAPS2.Escl.Server; + +public interface IEsclScanJob : IDisposable +{ + string ContentType { get; } + void Cancel(); + void RegisterStatusTransitionCallback(Action callback); + Task WaitForNextDocument(CancellationToken cancelToken); + Task WriteDocumentTo(Stream stream); + Task WriteProgressTo(Stream stream); + Task WriteErrorDetailsTo(Stream stream); +} \ No newline at end of file diff --git a/NAPS2.Escl/Server/IEsclServer.cs b/NAPS2.Escl/Server/IEsclServer.cs new file mode 100644 index 0000000000..9d9c915a57 --- /dev/null +++ b/NAPS2.Escl/Server/IEsclServer.cs @@ -0,0 +1,15 @@ +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; + +namespace NAPS2.Escl.Server; + +public interface IEsclServer : IDisposable +{ + void AddDevice(EsclDeviceConfig deviceConfig); + void RemoveDevice(EsclDeviceConfig deviceConfig); + Task Start(); + Task Stop(); + public EsclSecurityPolicy SecurityPolicy { get; set; } + public X509Certificate2? Certificate { get; set; } + ILogger Logger { get; set; } +} \ No newline at end of file diff --git a/NAPS2.Escl/Server/StatusTransition.cs b/NAPS2.Escl/Server/StatusTransition.cs new file mode 100644 index 0000000000..c069b8a97b --- /dev/null +++ b/NAPS2.Escl/Server/StatusTransition.cs @@ -0,0 +1,9 @@ +namespace NAPS2.Escl.Server; + +public enum StatusTransition +{ + CancelJob, + AbortJob, + ScanComplete, + PageComplete +} \ No newline at end of file diff --git a/NAPS2.Images.Gdi/GdiConverters.cs b/NAPS2.Images.Gdi/GdiConverters.cs deleted file mode 100644 index 5952ce28e7..0000000000 --- a/NAPS2.Images.Gdi/GdiConverters.cs +++ /dev/null @@ -1,44 +0,0 @@ -// using System.Drawing; -// using System.Drawing.Imaging; -// -// namespace NAPS2.Images.Gdi; -// -// public class GdiConverters -// { -// [StorageConverter] -// public FileStorage ConvertToFile(GdiImage input, StorageConvertParams convertParams) -// { -// if (convertParams.Temporary) -// { -// var path = Path.Combine(Paths.Temp, Path.GetRandomFileName()); -// input.Bitmap.Save(path); -// return new FileStorage(path); -// } -// else -// { -// var tempPath = ScannedImageHelper.SaveSmallestBitmap(input.Bitmap, convertParams.BitDepth, convertParams.Lossless, convertParams.LossyQuality, out ImageFormat fileFormat); -// string ext = Equals(fileFormat, ImageFormat.Png) ? ".png" : ".jpg"; -// var path = _imageContext.FileStorageManager.NextFilePath() + ext; -// File.Move(tempPath, path); -// return new FileStorage(path); -// } -// } -// -// [StorageConverter] -// public GdiImage ConvertToGdi(FileStorage input, StorageConvertParams convertParams) -// { -// // TODO: Allow multiple converters (with priority?) and fall back to the next if it returns null -// // Then we can have a PDF->Image converter that returns null if it's not a pdf file. -// if (IsPdfFile(input)) -// { -// return (GdiImage)_imageContext.PdfRenderer.Render(input.FullPath, 300).Single(); -// } -// else -// { -// return new GdiImage(new Bitmap(input.FullPath)); -// } -// } -// -// private static bool IsPdfFile(FileStorage fileStorage) => Path.GetExtension(fileStorage.FullPath)?.Equals(".pdf", StringComparison.InvariantCultureIgnoreCase) ?? false; -// -// } \ No newline at end of file diff --git a/NAPS2.Images.Gdi/GdiExtensions.cs b/NAPS2.Images.Gdi/GdiExtensions.cs index a7cd8a6925..7049817c55 100644 --- a/NAPS2.Images.Gdi/GdiExtensions.cs +++ b/NAPS2.Images.Gdi/GdiExtensions.cs @@ -3,9 +3,7 @@ namespace NAPS2.Images.Gdi; -#if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif public static class GdiExtensions { public static Bitmap RenderToBitmap(this IRenderableImage image) @@ -63,7 +61,7 @@ public static ImageFileFormat AsImageFileFormat(this ImageFormat imageFormat) { return ImageFileFormat.Tiff; } - return ImageFileFormat.Unspecified; + return ImageFileFormat.Unknown; } public static PixelFormat AsPixelFormat(this ImagePixelFormat pixelFormat) @@ -95,7 +93,7 @@ public static ImagePixelFormat AsImagePixelFormat(this PixelFormat pixelFormat) case PixelFormat.Format1bppIndexed: return ImagePixelFormat.BW1; default: - return ImagePixelFormat.Unsupported; + return ImagePixelFormat.Unknown; } } diff --git a/NAPS2.Images.Gdi/GdiImage.cs b/NAPS2.Images.Gdi/GdiImage.cs index 5005166694..b0d500c1d4 100644 --- a/NAPS2.Images.Gdi/GdiImage.cs +++ b/NAPS2.Images.Gdi/GdiImage.cs @@ -7,15 +7,11 @@ namespace NAPS2.Images.Gdi; /// /// An implementation of IMemoryImage that wraps a GDI+ image (System.Drawing.Bitmap). /// -#if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif public class GdiImage : IMemoryImage { - public GdiImage(ImageContext imageContext, Bitmap bitmap) + public GdiImage(Bitmap bitmap) { - if (imageContext is not GdiImageContext) throw new ArgumentException("Expected GdiImageContext"); - ImageContext = imageContext; if (bitmap == null) { throw new ArgumentNullException(nameof(bitmap)); @@ -23,10 +19,9 @@ public GdiImage(ImageContext imageContext, Bitmap bitmap) FixedPixelFormat = GdiPixelFormatFixer.MaybeFixPixelFormat(ref bitmap); Bitmap = bitmap; OriginalFileFormat = bitmap.RawFormat.AsImageFileFormat(); - LogicalPixelFormat = PixelFormat; } - public ImageContext ImageContext { get; } + public ImageContext ImageContext { get; } = new GdiImageContext(); /// /// Gets the underlying System.Drawing.Bitmap object for this image. @@ -49,6 +44,10 @@ public GdiImage(ImageContext imageContext, Bitmap bitmap) public ImageLockState Lock(LockMode lockMode, out BitwiseImageData imageData) { + if (lockMode != LockMode.ReadOnly) + { + LogicalPixelFormat = ImagePixelFormat.Unknown; + } return GdiImageLockState.Create(Bitmap, lockMode, out imageData); } @@ -57,39 +56,43 @@ public ImageLockState Lock(LockMode lockMode, out BitwiseImageData imageData) public ImagePixelFormat LogicalPixelFormat { get; set; } - public void Save(string path, ImageFileFormat imageFormat = ImageFileFormat.Unspecified, int quality = -1) + public void Save(string path, ImageFileFormat imageFormat = ImageFileFormat.Unknown, ImageSaveOptions? options = null) { - if (imageFormat == ImageFileFormat.Unspecified) + if (imageFormat == ImageFileFormat.Unknown) { imageFormat = ImageContext.GetFileFormatFromExtension(path); } ImageContext.CheckSupportsFormat(imageFormat); - if (imageFormat == ImageFileFormat.Jpeg && quality != -1) + options ??= new ImageSaveOptions(); + using var helper = PixelFormatHelper.Create(this, options.PixelFormatHint); + if (imageFormat == ImageFileFormat.Jpeg && options.Quality != -1) { - var (encoder, encoderParams) = GetJpegSaveArgs(quality); - Bitmap.Save(path, encoder, encoderParams); + var (encoder, encoderParams) = GetJpegSaveArgs(options.Quality); + helper.Image.Bitmap.Save(path, encoder, encoderParams); } else { - Bitmap.Save(path, imageFormat.AsImageFormat()); + helper.Image.Bitmap.Save(path, imageFormat.AsImageFormat()); } } - public void Save(Stream stream, ImageFileFormat imageFormat, int quality = -1) + public void Save(Stream stream, ImageFileFormat imageFormat, ImageSaveOptions? options = null) { - if (imageFormat == ImageFileFormat.Unspecified) + if (imageFormat == ImageFileFormat.Unknown) { throw new ArgumentException("Format required to save to a stream", nameof(imageFormat)); } ImageContext.CheckSupportsFormat(imageFormat); - if (imageFormat == ImageFileFormat.Jpeg && quality != -1) + options ??= new ImageSaveOptions(); + using var helper = PixelFormatHelper.Create(this, options.PixelFormatHint); + if (imageFormat == ImageFileFormat.Jpeg && options.Quality != -1) { - var (encoder, encoderParams) = GetJpegSaveArgs(quality); - Bitmap.Save(stream, encoder, encoderParams); + var (encoder, encoderParams) = GetJpegSaveArgs(options.Quality); + helper.Image.Bitmap.Save(stream, encoder, encoderParams); } else { - Bitmap.Save(stream, imageFormat.AsImageFormat()); + helper.Image.Bitmap.Save(stream, imageFormat.AsImageFormat()); } } @@ -104,11 +107,9 @@ private static (ImageCodecInfo, EncoderParameters) GetJpegSaveArgs(int quality) public IMemoryImage Clone() { - var newImage = new GdiImage(ImageContext, (Bitmap) Bitmap.Clone()); - // TODO: We want to make these more consistent when copying around and transforming images - newImage.OriginalFileFormat = OriginalFileFormat; - newImage.LogicalPixelFormat = LogicalPixelFormat; - return newImage; + // TODO: Ideally we'd like to make use of copy-on-write. But GDI copy-on-write (Clone) is not thread safe. + // Maybe we can implement something like CopyOnWrite to use instead of just Bitmap. + return this.Copy(); } public void Dispose() diff --git a/NAPS2.Images.Gdi/GdiImageContext.cs b/NAPS2.Images.Gdi/GdiImageContext.cs index 066b0d8557..d2d25ce922 100644 --- a/NAPS2.Images.Gdi/GdiImageContext.cs +++ b/NAPS2.Images.Gdi/GdiImageContext.cs @@ -4,18 +4,12 @@ namespace NAPS2.Images.Gdi; -#if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif public class GdiImageContext : ImageContext { private readonly GdiImageTransformer _imageTransformer; - public GdiImageContext() : this(null) - { - } - - public GdiImageContext(IPdfRenderer? pdfRenderer) : base(typeof(GdiImage), pdfRenderer) + public GdiImageContext() : base(typeof(GdiImage)) { _imageTransformer = new GdiImageTransformer(this); } @@ -30,8 +24,9 @@ public override IMemoryImage PerformTransform(IMemoryImage image, Transform tran protected override IMemoryImage LoadCore(Stream stream, ImageFileFormat format) { - stream = EnsureMemoryStream(stream); - return new GdiImage(this, new Bitmap(stream)); + var memoryStream = EnsureMemoryStream(stream); + using var bitmap = new Bitmap(memoryStream); + return new GdiImage(bitmap).Copy(); } protected override void LoadFramesCore(Action produceImage, Stream stream, @@ -45,7 +40,7 @@ protected override void LoadFramesCore(Action produceImage, Stream progress.Report(i, count); if (progress.IsCancellationRequested) break; bitmap.SelectActiveFrame(FrameDimension.Page, i); - produceImage(new GdiImage(this, bitmap).Copy()); + produceImage(new GdiImage(bitmap).Copy()); } progress.Report(count, count); } @@ -88,6 +83,6 @@ public override IMemoryImage Create(int width, int height, ImagePixelFormat pixe } bitmap.Palette = p; } - return new GdiImage(this, bitmap); + return new GdiImage(bitmap); } } \ No newline at end of file diff --git a/NAPS2.Images.Gdi/GdiImageLockState.cs b/NAPS2.Images.Gdi/GdiImageLockState.cs index 3ee9f78b03..b948ade638 100644 --- a/NAPS2.Images.Gdi/GdiImageLockState.cs +++ b/NAPS2.Images.Gdi/GdiImageLockState.cs @@ -4,10 +4,8 @@ namespace NAPS2.Images.Gdi; -#if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif -public class GdiImageLockState : ImageLockState +internal class GdiImageLockState : ImageLockState { public static GdiImageLockState Create(Bitmap bitmap, LockMode lockMode, out BitwiseImageData imageData) { diff --git a/NAPS2.Images.Gdi/GdiImageTransformer.cs b/NAPS2.Images.Gdi/GdiImageTransformer.cs index 9987b5231a..5f71b7489c 100644 --- a/NAPS2.Images.Gdi/GdiImageTransformer.cs +++ b/NAPS2.Images.Gdi/GdiImageTransformer.cs @@ -4,10 +4,8 @@ namespace NAPS2.Images.Gdi; -#if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif -public class GdiImageTransformer : AbstractImageTransformer +internal class GdiImageTransformer : AbstractImageTransformer { public GdiImageTransformer(ImageContext imageContext) : base(imageContext) { @@ -53,7 +51,7 @@ protected override GdiImage PerformTransform(GdiImage image, RotationTransform t g.TranslateTransform(-image.Width / 2.0f, -image.Height / 2.0f); g.DrawImage(image.Bitmap, new Rectangle(0, 0, image.Width, image.Height)); } - var resultImage = new GdiImage(ImageContext, result); + var resultImage = new GdiImage(result); OptimizePixelFormat(image, ref resultImage); image.Dispose(); return resultImage; @@ -61,7 +59,10 @@ protected override GdiImage PerformTransform(GdiImage image, RotationTransform t protected override GdiImage PerformTransform(GdiImage image, ResizeTransform transform) { - var result = new Bitmap(transform.Width, transform.Height, PixelFormat.Format24bppRgb); + var result = new Bitmap(transform.Width, transform.Height, + image.PixelFormat == ImagePixelFormat.ARGB32 + ? PixelFormat.Format32bppArgb + : PixelFormat.Format24bppRgb); using Graphics g = Graphics.FromImage(result); g.InterpolationMode = InterpolationMode.HighQualityBicubic; // We set WrapMode to avoid artifacts @@ -82,6 +83,6 @@ protected override GdiImage PerformTransform(GdiImage image, ResizeTransform tra image.HorizontalResolution * image.Width / transform.Width, image.VerticalResolution * image.Height / transform.Height); image.Dispose(); - return new GdiImage(ImageContext, result); + return new GdiImage(result); } } \ No newline at end of file diff --git a/NAPS2.Images.Gdi/GdiPixelFormatFixer.cs b/NAPS2.Images.Gdi/GdiPixelFormatFixer.cs index 75bbd84a47..190f9e3e9d 100644 --- a/NAPS2.Images.Gdi/GdiPixelFormatFixer.cs +++ b/NAPS2.Images.Gdi/GdiPixelFormatFixer.cs @@ -7,9 +7,7 @@ namespace NAPS2.Images.Gdi; /// /// Ensures that bitmaps use a standard pixel format/palette. /// -#if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif internal static class GdiPixelFormatFixer { public static bool MaybeFixPixelFormat(ref Bitmap bitmap) diff --git a/NAPS2.Images.Gdi/GdiTiffWriter.cs b/NAPS2.Images.Gdi/GdiTiffWriter.cs index 96bc1be0d6..34846a5c67 100644 --- a/NAPS2.Images.Gdi/GdiTiffWriter.cs +++ b/NAPS2.Images.Gdi/GdiTiffWriter.cs @@ -4,9 +4,7 @@ namespace NAPS2.Images.Gdi; -#if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif internal class GdiTiffWriter : ITiffWriter { public bool SaveTiff(IList images, string path, diff --git a/NAPS2.Images.Gdi/IsExternalInit.cs b/NAPS2.Images.Gdi/IsExternalInit.cs deleted file mode 100644 index ba0435b8b5..0000000000 --- a/NAPS2.Images.Gdi/IsExternalInit.cs +++ /dev/null @@ -1,5 +0,0 @@ -// https://sergiopedri.medium.com/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb -// ReSharper disable once CheckNamespace -namespace System.Runtime.CompilerServices; - -public static class IsExternalInit {} \ No newline at end of file diff --git a/NAPS2.Images.Gdi/LICENSE b/NAPS2.Images.Gdi/LICENSE index 3f89270f9d..f62d4e9cab 100644 --- a/NAPS2.Images.Gdi/LICENSE +++ b/NAPS2.Images.Gdi/LICENSE @@ -1,7 +1,7 @@ NAPS2.Images https://www.github.com/cyanfish/naps2/ -Copyright 2009-2022 NAPS2 Contributors +Copyright 2009-2025 NAPS2 Contributors This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public diff --git a/NAPS2.Images.Gdi/NAPS2.Images.Gdi.csproj b/NAPS2.Images.Gdi/NAPS2.Images.Gdi.csproj index 50eaff4b02..591dbfd1f6 100644 --- a/NAPS2.Images.Gdi/NAPS2.Images.Gdi.csproj +++ b/NAPS2.Images.Gdi/NAPS2.Images.Gdi.csproj @@ -1,19 +1,24 @@ - net6;net462;netstandard2.0 + net6;net8;net462;netstandard2.0 enable true false NAPS2.Images.Gdi NAPS2.Images.Gdi - Copyright 2022 Ben Olden-Cooligan + NAPS2.Images.Gdi + Images based on System.Drawing.Bitmap for NAPS2.Sdk. + naps2 + + - + + diff --git a/NAPS2.Images.Gtk/GtkExtensions.cs b/NAPS2.Images.Gtk/GtkExtensions.cs new file mode 100644 index 0000000000..ec978c01c1 --- /dev/null +++ b/NAPS2.Images.Gtk/GtkExtensions.cs @@ -0,0 +1,19 @@ +using Gdk; + +namespace NAPS2.Images.Gtk; + +public static class GtkExtensions +{ + public static Pixbuf RenderToPixbuf(this IRenderableImage image) + { + var gtkImageContext = image.ImageContext as GtkImageContext ?? + throw new ArgumentException("The provided image does not have a GtkImageContext"); + return gtkImageContext.RenderToPixbuf(image); + } + + public static Pixbuf AsPixbuf(this IMemoryImage image) + { + var gtkImage = image as GtkImage ?? throw new ArgumentException("Expected a GtkImage", nameof(image)); + return gtkImage.Pixbuf; + } +} \ No newline at end of file diff --git a/NAPS2.Images.Gtk/GtkImage.cs b/NAPS2.Images.Gtk/GtkImage.cs index ab6e57989e..0fa29df0c7 100644 --- a/NAPS2.Images.Gtk/GtkImage.cs +++ b/NAPS2.Images.Gtk/GtkImage.cs @@ -7,18 +7,15 @@ namespace NAPS2.Images.Gtk; public class GtkImage : IMemoryImage { - public GtkImage(ImageContext imageContext, Pixbuf pixbuf) + public GtkImage(Pixbuf pixbuf) { - if (imageContext is not GtkImageContext) throw new ArgumentException("Expected GtkImageContext"); LeakTracer.StartTracking(this); - ImageContext = imageContext; Pixbuf = pixbuf; - LogicalPixelFormat = PixelFormat; HorizontalResolution = float.TryParse(pixbuf.GetOption("x-dpi"), out var xDpi) ? xDpi : 0; VerticalResolution = float.TryParse(pixbuf.GetOption("y-dpi"), out var yDpi) ? yDpi : 0; } - public ImageContext ImageContext { get; } + public ImageContext ImageContext { get; } = new GtkImageContext(); public Pixbuf Pixbuf { get; } @@ -45,6 +42,10 @@ public void SetResolution(float xDpi, float yDpi) public ImageLockState Lock(LockMode lockMode, out BitwiseImageData imageData) { + if (lockMode != LockMode.ReadOnly) + { + LogicalPixelFormat = ImagePixelFormat.Unknown; + } var ptr = Pixbuf.Pixels; var stride = Pixbuf.Rowstride; var subPixelType = PixelFormat switch @@ -58,7 +59,7 @@ public ImageLockState Lock(LockMode lockMode, out BitwiseImageData imageData) } // TODO: Should we implement some kind of actual locking? - public class GtkImageLockState : ImageLockState + internal class GtkImageLockState : ImageLockState { public override void Dispose() { @@ -69,39 +70,46 @@ public override void Dispose() public ImagePixelFormat LogicalPixelFormat { get; set; } - public void Save(string path, ImageFileFormat imageFormat = ImageFileFormat.Unspecified, int quality = -1) + public void Save(string path, ImageFileFormat imageFormat = ImageFileFormat.Unknown, + ImageSaveOptions? options = null) { - if (imageFormat == ImageFileFormat.Unspecified) + if (imageFormat == ImageFileFormat.Unknown) { imageFormat = ImageContext.GetFileFormatFromExtension(path); } if (imageFormat == ImageFileFormat.Tiff) { - ((GtkImageContext) ImageContext).TiffIo.SaveTiff(new List { this }, path); + ((GtkImageContext) ImageContext).TiffIo.SaveTiff([this], path); return; } ImageContext.CheckSupportsFormat(imageFormat); + options ??= new ImageSaveOptions(); var type = GetType(imageFormat); - var (keys, values) = GetSaveOptions(imageFormat, quality); - Pixbuf.Savev(path, type, keys, values); + var (keys, values) = GetSaveOptions(imageFormat, options.Quality); + using var helper = PixelFormatHelper.Create(this, options.PixelFormatHint, minFormat: ImagePixelFormat.RGB24); + helper.Image.Pixbuf.Savev(path, type, keys, values); } - public void Save(Stream stream, ImageFileFormat imageFormat, int quality = -1) + public void Save(Stream stream, ImageFileFormat imageFormat, ImageSaveOptions? options = null) { - if (imageFormat == ImageFileFormat.Unspecified) + if (imageFormat == ImageFileFormat.Unknown) { throw new ArgumentException("Format required to save to a stream", nameof(imageFormat)); } if (imageFormat == ImageFileFormat.Tiff) { - ((GtkImageContext) ImageContext).TiffIo.SaveTiff(new List { this }, stream); + ((GtkImageContext) ImageContext).TiffIo.SaveTiff([this], stream); return; } ImageContext.CheckSupportsFormat(imageFormat); + options ??= new ImageSaveOptions(); var type = GetType(imageFormat); - var (keys, values) = GetSaveOptions(imageFormat, quality); + var (keys, values) = GetSaveOptions(imageFormat, options.Quality); + // TODO: GDK doesn't support optimizing bit depth (e.g. 1bit/8bit instead of 24bit/32bit) for BMP/PNG/JPEG. + // We'd probably need to use libpng/libjpeg etc. directly to fix that. + using var helper = PixelFormatHelper.Create(this, options.PixelFormatHint, minFormat: ImagePixelFormat.RGB24); // TODO: Map to OutputStream directly? - stream.Write(Pixbuf.SaveToBuffer(type, keys, values)); + stream.Write(helper.Image.Pixbuf.SaveToBuffer(type, keys, values)); } private string GetType(ImageFileFormat fileFormat) => fileFormat switch @@ -132,7 +140,7 @@ public void Save(Stream stream, ImageFileFormat imageFormat, int quality = -1) return (keys.ToArray(), values.ToArray()); } - public IMemoryImage Clone() => new GtkImage(ImageContext, (Pixbuf) Pixbuf.Clone()) + public IMemoryImage Clone() => new GtkImage((Pixbuf) Pixbuf.Clone()) { OriginalFileFormat = OriginalFileFormat, LogicalPixelFormat = LogicalPixelFormat, diff --git a/NAPS2.Images.Gtk/GtkImageContext.cs b/NAPS2.Images.Gtk/GtkImageContext.cs index d30edb19f8..102a54aee7 100644 --- a/NAPS2.Images.Gtk/GtkImageContext.cs +++ b/NAPS2.Images.Gtk/GtkImageContext.cs @@ -9,7 +9,7 @@ public class GtkImageContext : ImageContext private readonly GtkImageTransformer _imageTransformer; private readonly LibTiffIo _tiffIo; - public GtkImageContext(IPdfRenderer? pdfRenderer = null) : base(typeof(GtkImage), pdfRenderer) + public GtkImageContext() : base(typeof(GtkImage)) { _imageTransformer = new GtkImageTransformer(this); _tiffIo = new LibTiffIo(this); @@ -32,7 +32,7 @@ protected override IMemoryImage LoadCore(Stream stream, ImageFileFormat format) _tiffIo.LoadTiff(img => { image = img; cts.Cancel(); }, stream, cts.Token); return image; } - return new GtkImage(this, new Pixbuf(stream)); + return new GtkImage(new Pixbuf(stream)); } protected override void LoadFramesCore(Action produceImage, Stream stream, @@ -53,13 +53,18 @@ protected override void LoadFramesCore(Action produceImage, Stream internal LibTiffIo TiffIo => _tiffIo; + public Pixbuf RenderToPixbuf(IRenderableImage image) + { + return ((GtkImage) Render(image)).Pixbuf; + } + public override IMemoryImage Create(int width, int height, ImagePixelFormat pixelFormat) { - if (pixelFormat == ImagePixelFormat.Unsupported) + if (pixelFormat == ImagePixelFormat.Unknown) { throw new ArgumentException("Unsupported pixel format"); } var pixbuf = new Pixbuf(Colorspace.Rgb, pixelFormat == ImagePixelFormat.ARGB32, 8, width, height); - return new GtkImage(this, pixbuf); + return new GtkImage(pixbuf); } } \ No newline at end of file diff --git a/NAPS2.Images.Gtk/GtkImageTransformer.cs b/NAPS2.Images.Gtk/GtkImageTransformer.cs index 7c786c9a45..417f5dcfae 100644 --- a/NAPS2.Images.Gtk/GtkImageTransformer.cs +++ b/NAPS2.Images.Gtk/GtkImageTransformer.cs @@ -4,7 +4,7 @@ namespace NAPS2.Images.Gtk; -public class GtkImageTransformer : AbstractImageTransformer +internal class GtkImageTransformer : AbstractImageTransformer { public GtkImageTransformer(ImageContext imageContext) : base(imageContext) { @@ -31,11 +31,9 @@ protected override GtkImage PerformTransform(GtkImage image, RotationTransform t context.Translate(-image.Width / 2.0, -image.Height / 2.0); CairoHelper.SetSourcePixbuf(context, image.Pixbuf, 0, 0); context.Paint(); - var newImage = new GtkImage(ImageContext, new Pixbuf(surface, 0, 0, width, height)); - // TODO: In Gdi, we convert this back to BW1. Should we do the same? - newImage.LogicalPixelFormat = image.LogicalPixelFormat == ImagePixelFormat.BW1 - ? ImagePixelFormat.Gray8 - : image.LogicalPixelFormat; + var newImage = new GtkImage(new Pixbuf(surface, 0, 0, width, height)); + OptimizePixelFormat(image, ref newImage); + newImage.LogicalPixelFormat = image.LogicalPixelFormat; newImage.SetResolution(xres, yres); image.Dispose(); return newImage; @@ -43,13 +41,15 @@ protected override GtkImage PerformTransform(GtkImage image, RotationTransform t protected override GtkImage PerformTransform(GtkImage image, ResizeTransform transform) { + // TODO: Can we improve interpolation? Somehow integrate Cairo.Filter.Bilinear or Cairo.Filter.Best, though + // it's not clear how to reconcile that with SetSourcePixbuf. var format = image.PixelFormat == ImagePixelFormat.ARGB32 ? Format.Argb32 : Format.Rgb24; using var surface = new ImageSurface(format, transform.Width, transform.Height); using var context = new Context(surface); context.Scale(transform.Width / (double) image.Width, transform.Height / (double) image.Height); CairoHelper.SetSourcePixbuf(context, image.Pixbuf, 0, 0); context.Paint(); - var newImage = new GtkImage(ImageContext, new Pixbuf(surface, 0, 0, transform.Width, transform.Height)); + var newImage = new GtkImage(new Pixbuf(surface, 0, 0, transform.Width, transform.Height)); newImage.LogicalPixelFormat = image.LogicalPixelFormat == ImagePixelFormat.BW1 ? ImagePixelFormat.Gray8 : image.LogicalPixelFormat; @@ -69,4 +69,14 @@ protected override GtkImage PerformTransform(GtkImage image, BlackWhiteTransform image.LogicalPixelFormat = ImagePixelFormat.BW1; return image; } + + protected override GtkImage PerformTransform(GtkImage image, GrayscaleTransform transform) + { + new DecolorBitwiseImageOp(false).Perform(image); + if (image.LogicalPixelFormat != ImagePixelFormat.Unknown) + { + image.LogicalPixelFormat = ImagePixelFormat.Gray8; + } + return image; + } } \ No newline at end of file diff --git a/NAPS2.Images.Gtk/LICENSE b/NAPS2.Images.Gtk/LICENSE index 3f89270f9d..f62d4e9cab 100644 --- a/NAPS2.Images.Gtk/LICENSE +++ b/NAPS2.Images.Gtk/LICENSE @@ -1,7 +1,7 @@ NAPS2.Images https://www.github.com/cyanfish/naps2/ -Copyright 2009-2022 NAPS2 Contributors +Copyright 2009-2025 NAPS2 Contributors This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public diff --git a/NAPS2.Images.Gtk/LibTiff.cs b/NAPS2.Images.Gtk/LibTiff.cs index 700e63b5ba..5919af8375 100644 --- a/NAPS2.Images.Gtk/LibTiff.cs +++ b/NAPS2.Images.Gtk/LibTiff.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using NAPS2.Util; using toff_t = System.IntPtr; using tsize_t = System.IntPtr; using thandle_t = System.IntPtr; @@ -8,24 +9,64 @@ namespace NAPS2.Images.Gtk; internal static class LibTiff { - // TODO: String marshalling? - [DllImport("libtiff.so.5")] - public static extern IntPtr TIFFOpen(string filename, string mode); - - [DllImport("libtiff.so.5")] - public static extern IntPtr TIFFSetErrorHandler(TIFFErrorHandler handler); - - [DllImport("libtiff.so.5")] - public static extern IntPtr TIFFSetWarningHandler(TIFFErrorHandler handler); + private const int RTLD_LAZY = 1; + private const int RTLD_GLOBAL = 8; + + private static readonly Dictionary FuncCache = new(); + + private static readonly Lazy LibraryHandle = new(() => + { + var handle = dlopen("libtiff.so.5", RTLD_LAZY | RTLD_GLOBAL); + if (handle == IntPtr.Zero) + { + handle = dlopen("libtiff.so.6", RTLD_LAZY | RTLD_GLOBAL); + } + if (handle == IntPtr.Zero) + { + var error = dlerror(); + throw new InvalidOperationException($"Could not load library: \"libtiff\". Error: {error}"); + } + return handle; + }); + + public static T Load() + { + return (T) FuncCache.Get(typeof(T), () => Marshal.GetDelegateForFunctionPointer(LoadFunc())!); + } + + private static IntPtr LoadFunc() + { + var symbol = typeof(T).Name.Split("_")[0]; + var ptr = dlsym(LibraryHandle.Value, symbol); + if (ptr == IntPtr.Zero) + { + var error = dlerror(); + throw new InvalidOperationException($"Could not load symbol: \"{symbol}\". Error: {error}"); + } + return ptr; + } + + public delegate IntPtr TIFFOpen_d(string filename, string mode); + + public static TIFFOpen_d TIFFOpen => Load(); + + public delegate IntPtr TIFFSetErrorHandler_d(TIFFErrorHandler handler); + + public static TIFFSetErrorHandler_d TIFFSetErrorHandler => Load(); + + public delegate IntPtr TIFFSetWarningHandler_d(TIFFErrorHandler handler); + + public static TIFFSetWarningHandler_d TIFFSetWarningHandler => Load(); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void TIFFErrorHandler(string x, string y, IntPtr va_args); - [DllImport("libtiff.so.5")] - public static extern IntPtr TIFFClientOpen(string filename, string mode, IntPtr clientdata, + public delegate IntPtr TIFFClientOpen_d(string filename, string mode, IntPtr clientdata, TIFFReadWriteProc readproc, TIFFReadWriteProc writeproc, TIFFSeekProc seekproc, TIFFCloseProc closeproc, TIFFSizeProc sizeproc, TIFFMapFileProc mapproc, TIFFUnmapFileProc unmapproc); + public static TIFFClientOpen_d TIFFClientOpen => Load(); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate tsize_t TIFFReadWriteProc(thandle_t clientdata, tdata_t data, tsize_t size); @@ -44,52 +85,71 @@ public static extern IntPtr TIFFClientOpen(string filename, string mode, IntPtr [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void TIFFUnmapFileProc(thandle_t clientdata, tdata_t a, toff_t b); - [DllImport("libtiff.so.5")] - public static extern IntPtr TIFFClose(IntPtr tiff); + public delegate IntPtr TIFFClose_d(IntPtr tiff); + + public static TIFFClose_d TIFFClose => Load(); + + public delegate short TIFFNumberOfDirectories_d(IntPtr tiff); + + public static TIFFNumberOfDirectories_d TIFFNumberOfDirectories => Load(); + + public delegate int TIFFReadDirectory_d(IntPtr tiff); + + public static TIFFReadDirectory_d TIFFReadDirectory => Load(); + + public delegate int TIFFWriteDirectory_d(IntPtr tiff); + + public static TIFFWriteDirectory_d TIFFWriteDirectory => Load(); - [DllImport("libtiff.so.5")] - public static extern short TIFFNumberOfDirectories(IntPtr tiff); + public delegate int TIFFGetField_d1(IntPtr tiff, TiffTag tag, out int field); - [DllImport("libtiff.so.5")] - public static extern int TIFFReadDirectory(IntPtr tiff); + public static TIFFGetField_d1 TIFFGetFieldInt => Load(); - [DllImport("libtiff.so.5")] - public static extern int TIFFWriteDirectory(IntPtr tiff); + public delegate int TIFFGetField_d2(IntPtr tiff, TiffTag tag, out float field); - // TODO: Clean these overloads up - [DllImport("libtiff.so.5")] - public static extern int TIFFGetField(IntPtr tiff, TiffTag tag, out int field); + public static TIFFGetField_d2 TIFFGetFieldFloat => Load(); - [DllImport("libtiff.so.5")] - public static extern int TIFFGetField(IntPtr tiff, TiffTag tag, out float field); + public delegate int TIFFGetField_d3(IntPtr tiff, TiffTag tag, out double field); - [DllImport("libtiff.so.5")] - public static extern int TIFFGetField(IntPtr tiff, TiffTag tag, out double field); + public static TIFFGetField_d3 TIFFGetFieldDouble => Load(); - [DllImport("libtiff.so.5")] - public static extern int TIFFSetField(IntPtr tiff, TiffTag tag, int field); + public delegate int TIFFSetField_d1(IntPtr tiff, TiffTag tag, int field); - [DllImport("libtiff.so.5")] - public static extern int TIFFSetField(IntPtr tiff, TiffTag tag, float field); + public static TIFFSetField_d1 TIFFSetFieldInt => Load(); - [DllImport("libtiff.so.5")] - public static extern int TIFFSetField(IntPtr tiff, TiffTag tag, double field); + public delegate int TIFFSetField_d2(IntPtr tiff, TiffTag tag, float field); - [DllImport("libtiff.so.5")] - public static extern int TIFFSetField(IntPtr tiff, TiffTag tag, short field, short[] array); + public static TIFFSetField_d2 TIFFSetFieldFloat => Load(); - [DllImport("libtiff.so.5")] - public static extern int TIFFWriteScanline( + public delegate int TIFFSetField_d3(IntPtr tiff, TiffTag tag, double field); + + public static TIFFSetField_d3 TIFFSetFieldDouble => Load(); + + public delegate int TIFFSetField_d4(IntPtr tiff, TiffTag tag, short field, short[] array); + + public static TIFFSetField_d4 TIFFSetFieldShortArray => Load(); + + public delegate int TIFFWriteScanline_d( IntPtr tiff, tdata_t buf, int row, short sample); - [DllImport("libtiff.so.5")] - public static extern int TIFFReadRGBAImage( + public static TIFFWriteScanline_d TIFFWriteScanline => Load(); + + public delegate int TIFFReadRGBAImage_d( IntPtr tiff, int w, int h, IntPtr raster, int stopOnError); - [DllImport("libtiff.so.5")] - public static extern int TIFFReadRGBAImageOriented( + public static TIFFReadRGBAImage_d TIFFReadRGBAImage => Load(); + + public delegate int TIFFReadRGBAImageOriented_d( IntPtr tiff, int w, int h, IntPtr raster, int orientation, int stopOnError); - // TODO: For streams - // https://linux.die.net/man/3/tiffclientopen + public static TIFFReadRGBAImageOriented_d TIFFReadRGBAImageOriented => Load(); + + [DllImport("libdl.so.2")] + public static extern IntPtr dlopen(string filename, int flags); + + [DllImport("libdl.so.2")] + public static extern string dlerror(); + + [DllImport("libdl.so.2")] + public static extern IntPtr dlsym(IntPtr handle, string symbol); } \ No newline at end of file diff --git a/NAPS2.Images.Gtk/LibTiffIo.cs b/NAPS2.Images.Gtk/LibTiffIo.cs index abd98bdb1f..346027ab5c 100644 --- a/NAPS2.Images.Gtk/LibTiffIo.cs +++ b/NAPS2.Images.Gtk/LibTiffIo.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using NAPS2.Images.Bitwise; using NAPS2.Util; @@ -37,6 +38,7 @@ private bool WriteTiff(IntPtr tiff, LibTiffStreamClient? client, IList TiffCompression.G4, TiffCompressionType.Lzw => TiffCompression.Lzw, @@ -96,29 +97,29 @@ private static void WriteTiffMetadata(IntPtr tiff, ImagePixelFormat pixelFormat, ? TiffCompression.G4 : TiffCompression.Lzw })); - LibTiff.TIFFSetField(tiff, TiffTag.Orientation, 1); - LibTiff.TIFFSetField(tiff, TiffTag.BitsPerSample, pixelFormat == ImagePixelFormat.BW1 ? 1 : 8); - LibTiff.TIFFSetField(tiff, TiffTag.SamplesPerPixel, pixelFormat switch + LibTiff.TIFFSetFieldInt(tiff, TiffTag.Orientation, 1); + LibTiff.TIFFSetFieldInt(tiff, TiffTag.BitsPerSample, pixelFormat == ImagePixelFormat.BW1 ? 1 : 8); + LibTiff.TIFFSetFieldInt(tiff, TiffTag.SamplesPerPixel, pixelFormat switch { ImagePixelFormat.RGB24 => 3, ImagePixelFormat.ARGB32 => 4, _ => 1 }); - LibTiff.TIFFSetField(tiff, TiffTag.Photometric, (int) (pixelFormat switch + LibTiff.TIFFSetFieldInt(tiff, TiffTag.Photometric, (int) (pixelFormat switch { ImagePixelFormat.RGB24 or ImagePixelFormat.ARGB32 => TiffPhotometric.Rgb, _ => TiffPhotometric.MinIsBlack })); if (pixelFormat == ImagePixelFormat.ARGB32) { - LibTiff.TIFFSetField(tiff, TiffTag.ExtraSamples, 1, new short[] { 2 }); + LibTiff.TIFFSetFieldShortArray(tiff, TiffTag.ExtraSamples, 1, new short[] { 2 }); } if (image.HorizontalResolution != 0 && image.VerticalResolution != 0) { - LibTiff.TIFFSetField(tiff, TiffTag.ResolutionUnit, 2); + LibTiff.TIFFSetFieldInt(tiff, TiffTag.ResolutionUnit, 2); // TODO: Why do we need to write as a double? It's supposed to be a float. - LibTiff.TIFFSetField(tiff, TiffTag.XResolution, (double) image.HorizontalResolution); - LibTiff.TIFFSetField(tiff, TiffTag.YResolution, (double) image.VerticalResolution); + LibTiff.TIFFSetFieldDouble(tiff, TiffTag.XResolution, image.HorizontalResolution); + LibTiff.TIFFSetFieldDouble(tiff, TiffTag.YResolution, image.VerticalResolution); } } @@ -129,16 +130,11 @@ public void LoadTiff(Action produceImage, Stream stream, ProgressH EnumerateTiffFrames(produceImage, tiff, progress, client); } - public void LoadTiff(Action produceImage, string path, ProgressHandler progress) - { - var tiff = LibTiff.TIFFOpen(path, "r"); - EnumerateTiffFrames(produceImage, tiff, progress); - } - private void EnumerateTiffFrames(Action produceImage, IntPtr tiff, - ProgressHandler progress, LibTiffStreamClient? client = null) + ProgressHandler progress, LibTiffStreamClient client) { // We keep a reference to the client to avoid garbage collection + var handle = GCHandle.Alloc(client); try { var count = LibTiff.TIFFNumberOfDirectories(tiff); @@ -147,11 +143,11 @@ private void EnumerateTiffFrames(Action produceImage, IntPtr tiff, do { if (progress.IsCancellationRequested) break; - LibTiff.TIFFGetField(tiff, TiffTag.ImageWidth, out int w); - LibTiff.TIFFGetField(tiff, TiffTag.ImageHeight, out int h); + LibTiff.TIFFGetFieldInt(tiff, TiffTag.ImageWidth, out int w); + LibTiff.TIFFGetFieldInt(tiff, TiffTag.ImageHeight, out int h); // TODO: Check return values - LibTiff.TIFFGetField(tiff, TiffTag.XResolution, out float xres); - LibTiff.TIFFGetField(tiff, TiffTag.YResolution, out float yres); + LibTiff.TIFFGetFieldFloat(tiff, TiffTag.XResolution, out float xres); + LibTiff.TIFFGetFieldFloat(tiff, TiffTag.YResolution, out float yres); var img = _imageContext.Create(w, h, ImagePixelFormat.ARGB32); img.SetResolution(xres, yres); img.OriginalFileFormat = ImageFileFormat.Tiff; @@ -160,13 +156,14 @@ private void EnumerateTiffFrames(Action produceImage, IntPtr tiff, imageLock.Dispose(); // LibTiff always produces pre-multiplied alpha, which we don't want new UnmultiplyAlphaOp().Perform(img); - produceImage(img); progress.Report(++i, count); + produceImage(img); } while (LibTiff.TIFFReadDirectory(tiff) == 1); } finally { LibTiff.TIFFClose(tiff); + handle.Free(); } } diff --git a/NAPS2.Images.Gtk/NAPS2.Images.Gtk.csproj b/NAPS2.Images.Gtk/NAPS2.Images.Gtk.csproj index 85500f4a4b..e88050f7bf 100644 --- a/NAPS2.Images.Gtk/NAPS2.Images.Gtk.csproj +++ b/NAPS2.Images.Gtk/NAPS2.Images.Gtk.csproj @@ -1,18 +1,22 @@ - net6 + net6;net8 enable true NAPS2.Images.Gtk NAPS2.Images.Gtk - Copyright 2022 Ben Olden-Cooligan + NAPS2.Images.Gtk + Images based on Gdk.Pixbuf for NAPS2.Sdk. + naps2 + + - + diff --git a/NAPS2.Images.ImageSharp/.gitignore b/NAPS2.Images.ImageSharp/.gitignore new file mode 100644 index 0000000000..866b3a4299 --- /dev/null +++ b/NAPS2.Images.ImageSharp/.gitignore @@ -0,0 +1,34 @@ +Thumbs.db +*.obj +*.exe +*.pdb +*.user +*.aps +*.pch +*.vspscc +*_i.c +*_p.c +*.ncb +*.suo +*.sln.docstates +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +[Bb]in +[Dd]ebug*/ +*.lib +*.sbr +obj/ +[Rr]elease*/ +_ReSharper*/ +[Tt]est[Rr]esult* +*.vssscc +$tf*/ +publish/ +bin/ +temp/ +google.credentials.json +microsoft.credentials.json \ No newline at end of file diff --git a/NAPS2.Images.ImageSharp/ImageSharpImage.cs b/NAPS2.Images.ImageSharp/ImageSharpImage.cs new file mode 100644 index 0000000000..9e9438c24f --- /dev/null +++ b/NAPS2.Images.ImageSharp/ImageSharpImage.cs @@ -0,0 +1,168 @@ +using System.Buffers; +using NAPS2.Images.Bitwise; +using NAPS2.Util; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace NAPS2.Images.ImageSharp; + +public class ImageSharpImage : IMemoryImage +{ + public ImageSharpImage(Image image) + { + LeakTracer.StartTracking(this); + // TODO: Something similar to MacImage where if it's not a supported pixel type we convert + // TODO: Though we might also want to add support where reasonable, e.g. we can probably support argb or bgr pretty easily? + Image = image; + } + + public ImageContext ImageContext { get; } = new ImageSharpImageContext(); + + public Image Image { get; } + + public int Width => Image.Width; + + public int Height => Image.Height; + + public float HorizontalResolution => (float) (Image.Metadata.HorizontalResolution * ResolutionMultiplier); + + public float VerticalResolution => (float) (Image.Metadata.HorizontalResolution * ResolutionMultiplier); + + private double ResolutionMultiplier => Image.Metadata.ResolutionUnits switch + { + PixelResolutionUnit.PixelsPerCentimeter => 2.54, + PixelResolutionUnit.PixelsPerMeter => 2.54 / 100, + _ => 1 + }; + + public void SetResolution(float xDpi, float yDpi) + { + Image.Metadata.ResolutionUnits = PixelResolutionUnit.PixelsPerInch; + Image.Metadata.HorizontalResolution = xDpi; + Image.Metadata.VerticalResolution = yDpi; + } + + public ImagePixelFormat PixelFormat => Image.PixelType switch + { + { BitsPerPixel: 8 } => ImagePixelFormat.Gray8, + { BitsPerPixel: 24 } => ImagePixelFormat.RGB24, + { BitsPerPixel: 32 } => ImagePixelFormat.ARGB32, + _ => throw new InvalidOperationException("Unsupported pixel format") + }; + + public unsafe ImageLockState Lock(LockMode lockMode, out BitwiseImageData imageData) + { + if (lockMode != LockMode.ReadOnly) + { + LogicalPixelFormat = ImagePixelFormat.Unknown; + } + var memoryHandle = PixelFormat switch + { + ImagePixelFormat.RGB24 => ((Image) Image).DangerousTryGetSinglePixelMemory(out var mem) + ? mem.Pin() + : throw new InvalidOperationException("Could not get contiguous memory for ImageSharp image"), + ImagePixelFormat.ARGB32 => ((Image) Image).DangerousTryGetSinglePixelMemory(out var mem) + ? mem.Pin() + : throw new InvalidOperationException("Could not get contiguous memory for ImageSharp image"), + ImagePixelFormat.Gray8 => ((Image) Image).DangerousTryGetSinglePixelMemory(out var mem) + ? mem.Pin() + : throw new InvalidOperationException("Could not get contiguous memory for ImageSharp image"), + _ => throw new InvalidOperationException("Unsupported pixel format") + }; + var subPixelType = PixelFormat switch + { + ImagePixelFormat.RGB24 => SubPixelType.Rgb, + ImagePixelFormat.ARGB32 => SubPixelType.Rgba, + ImagePixelFormat.Gray8 => SubPixelType.Gray, + _ => throw new InvalidOperationException("Unsupported pixel format") + }; + imageData = new BitwiseImageData((byte*) memoryHandle.Pointer, new PixelInfo(Width, Height, subPixelType)); + return new ImageSharpImageLockState(memoryHandle); + } + + internal class ImageSharpImageLockState : ImageLockState + { + private readonly MemoryHandle _memoryHandle; + + public ImageSharpImageLockState(MemoryHandle memoryHandle) + { + _memoryHandle = memoryHandle; + } + + public override void Dispose() + { + _memoryHandle.Dispose(); + } + } + + public ImageFileFormat OriginalFileFormat { get; set; } + + public ImagePixelFormat LogicalPixelFormat { get; set; } + + public void Save(string path, ImageFileFormat imageFormat = ImageFileFormat.Unknown, + ImageSaveOptions? options = null) + { + if (imageFormat == ImageFileFormat.Unknown) + { + imageFormat = ImageContext.GetFileFormatFromExtension(path); + } + ImageContext.CheckSupportsFormat(imageFormat); + + options ??= new ImageSaveOptions(); + using var helper = PixelFormatHelper.Create(this, options.PixelFormatHint, minFormat: ImagePixelFormat.Gray8); + var encoder = GetImageEncoder(imageFormat, options); + helper.Image.Image.Save(path, encoder); + } + + public void Save(Stream stream, ImageFileFormat imageFormat, ImageSaveOptions? options = null) + { + if (imageFormat == ImageFileFormat.Unknown) + { + throw new ArgumentException("Format required to save to a stream", nameof(imageFormat)); + } + ImageContext.CheckSupportsFormat(imageFormat); + + options ??= new ImageSaveOptions(); + using var helper = PixelFormatHelper.Create(this, options.PixelFormatHint, minFormat: ImagePixelFormat.Gray8); + var encoder = GetImageEncoder(imageFormat, options); + helper.Image.Image.Save(stream, encoder); + } + + private static ImageEncoder GetImageEncoder(ImageFileFormat imageFormat, ImageSaveOptions options) + { + var encoder = imageFormat switch + { + ImageFileFormat.Bmp => (ImageEncoder) new BmpEncoder(), + ImageFileFormat.Png => new PngEncoder(), + ImageFileFormat.Jpeg => new JpegEncoder + { + Quality = options.Quality == -1 ? 75 : options.Quality, + // ImageSharp will automatically save an RGB24 image as Grayscale if the actual image colors are gray. + // We prevent that here if the caller specified an RGB PixelFormatHint. + ColorType = options.PixelFormatHint >= ImagePixelFormat.RGB24 ? JpegEncodingColor.Rgb : null + }, + ImageFileFormat.Tiff => new TiffEncoder(), + _ => throw new InvalidOperationException() + }; + return encoder; + } + + public IMemoryImage Clone() => new ImageSharpImage(Image.Clone(_ => { })) + { + OriginalFileFormat = OriginalFileFormat, + LogicalPixelFormat = LogicalPixelFormat + }; + + public void Dispose() + { + Image.Dispose(); + LeakTracer.StopTracking(this); + } +} \ No newline at end of file diff --git a/NAPS2.Images.ImageSharp/ImageSharpImageContext.cs b/NAPS2.Images.ImageSharp/ImageSharpImageContext.cs new file mode 100644 index 0000000000..31d6d213f8 --- /dev/null +++ b/NAPS2.Images.ImageSharp/ImageSharpImageContext.cs @@ -0,0 +1,71 @@ +using NAPS2.Util; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.PixelFormats; + +namespace NAPS2.Images.ImageSharp; + +public class ImageSharpImageContext : ImageContext +{ + private readonly ImageSharpImageTransformer _imageTransformer; + + public static Configuration GetConfiguration() + { + var config = Configuration.Default.Clone(); + config.PreferContiguousImageBuffers = true; + return config; + } + + public static DecoderOptions GetDecoderOptions() => new() + { + Configuration = GetConfiguration() + }; + + public ImageSharpImageContext() : base(typeof(ImageSharpImage)) + { + _imageTransformer = new ImageSharpImageTransformer(this); + } + + protected override bool SupportsTiff => true; + + public override IMemoryImage PerformTransform(IMemoryImage image, Transform transform) + { + var imageSharpImage = image as ImageSharpImage ?? throw new ArgumentException("Expected ImageSharpImage object"); + return _imageTransformer.Apply(imageSharpImage, transform); + } + + protected override IMemoryImage LoadCore(Stream stream, ImageFileFormat format) + { + return new ImageSharpImage(Image.Load(GetDecoderOptions(), stream)); + } + + protected override void LoadFramesCore(Action produceImage, Stream stream, + ImageFileFormat format, ProgressHandler progress) + { + progress.Report(0, 1); + if (progress.IsCancellationRequested) return; + produceImage(LoadCore(stream, format)); + progress.Report(1, 1); + } + + public Image RenderToImage(IRenderableImage image) + { + return ((ImageSharpImage) Render(image)).Image; + } + + public override IMemoryImage Create(int width, int height, ImagePixelFormat pixelFormat) + { + if (pixelFormat == ImagePixelFormat.Unknown) + { + throw new ArgumentException("Unsupported pixel format"); + } + var image = pixelFormat switch + { + ImagePixelFormat.ARGB32 => (Image) new Image(GetConfiguration(), width, height), + ImagePixelFormat.RGB24 => new Image(GetConfiguration(), width, height), + ImagePixelFormat.Gray8 or ImagePixelFormat.BW1 => new Image(GetConfiguration(), width, height), + _ => throw new InvalidOperationException("Unsupported pixel format") + }; + return new ImageSharpImage(image); + } +} \ No newline at end of file diff --git a/NAPS2.Images.ImageSharp/ImageSharpImageTransformer.cs b/NAPS2.Images.ImageSharp/ImageSharpImageTransformer.cs new file mode 100644 index 0000000000..ecd4580c95 --- /dev/null +++ b/NAPS2.Images.ImageSharp/ImageSharpImageTransformer.cs @@ -0,0 +1,69 @@ +using NAPS2.Images.Bitwise; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace NAPS2.Images.ImageSharp; + +internal class ImageSharpImageTransformer : AbstractImageTransformer +{ + public ImageSharpImageTransformer(ImageContext imageContext) : base(imageContext) + { + } + + protected override ImageSharpImage PerformTransform(ImageSharpImage image, RotationTransform transform) + { + // TODO: We can maybe optimize this in some ways, e.g. skip a clone if we're already Rgba32, or convert the + // final pixel format back to whatever the original was. + + var width = image.Width; + var height = image.Height; + float xres = image.HorizontalResolution, yres = image.VerticalResolution; + if (transform.Angle is > 45.0 and < 135.0 or > 225.0 and < 315.0) + { + (width, height) = (height, width); + (xres, yres) = (yres, xres); + } + + // Get an image with an alpha channel so we can set the background color to white + var copy = image.Image.CloneAs(); + + copy.Mutate(x => x.Rotate((float) transform.Angle).BackgroundColor(Color.White)); + + var cropRect = new Rectangle((copy.Width - width) / 2, (copy.Height - height) / 2, width, height); + copy.Mutate(x => x.Crop(cropRect)); + + var newImage = new ImageSharpImage(copy); + // TODO: In Gdi, we convert this back to BW1. Should we do the same? + newImage.LogicalPixelFormat = image.LogicalPixelFormat == ImagePixelFormat.BW1 + ? ImagePixelFormat.Gray8 + : image.LogicalPixelFormat; + newImage.SetResolution(xres, yres); + image.Dispose(); + return newImage; + } + + protected override ImageSharpImage PerformTransform(ImageSharpImage image, ResizeTransform transform) + { + image.Image.Mutate(x => x.Resize(transform.Width, transform.Height)); + image.LogicalPixelFormat = image.LogicalPixelFormat == ImagePixelFormat.BW1 + ? ImagePixelFormat.Gray8 + : image.LogicalPixelFormat; + image.SetResolution( + image.HorizontalResolution * image.Width / transform.Width, + image.VerticalResolution * image.Height / transform.Height); + return image; + } + + protected override ImageSharpImage PerformTransform(ImageSharpImage image, BlackWhiteTransform transform) + { + new DecolorBitwiseImageOp(true) + { + BlackWhiteThreshold = (transform.Threshold + 1000) / 2000f + }.Perform(image); + var newImage = (ImageSharpImage) image.CopyWithPixelFormat(ImagePixelFormat.Gray8); + newImage.LogicalPixelFormat = ImagePixelFormat.BW1; + image.Dispose(); + return newImage; + } +} \ No newline at end of file diff --git a/NAPS2.Images.ImageSharp/LICENSE b/NAPS2.Images.ImageSharp/LICENSE new file mode 100644 index 0000000000..f62d4e9cab --- /dev/null +++ b/NAPS2.Images.ImageSharp/LICENSE @@ -0,0 +1,518 @@ +NAPS2.Images +https://www.github.com/cyanfish/naps2/ + +Copyright 2009-2025 NAPS2 Contributors + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/NAPS2.Images.ImageSharp/NAPS2.Images.ImageSharp.csproj b/NAPS2.Images.ImageSharp/NAPS2.Images.ImageSharp.csproj new file mode 100644 index 0000000000..c73669f263 --- /dev/null +++ b/NAPS2.Images.ImageSharp/NAPS2.Images.ImageSharp.csproj @@ -0,0 +1,34 @@ + + + + net6;net8 + enable + true + NAPS2.Images.ImageSharp + + NAPS2.Images.ImageSharp + NAPS2.Images.ImageSharp + Images based on ImageSharp for NAPS2.Sdk. + naps2 + + + + + + + + + + + <_Parameter1>NAPS2.Sdk.Tests + + + + + + + + + + + diff --git a/NAPS2.Images.Mac/FloatHelper.cs b/NAPS2.Images.Mac/FloatHelper.cs index d15c05588e..df3ed9dc2c 100644 --- a/NAPS2.Images.Mac/FloatHelper.cs +++ b/NAPS2.Images.Mac/FloatHelper.cs @@ -4,7 +4,7 @@ namespace NAPS2.Images.Mac; /// Building xamarin-mac and monomac on different platforms can mean dealing with different floating point types. /// This class allows minimizing conditional compilation at the target site. /// -public static class FloatHelper +internal static class FloatHelper { #if MONOMAC public static float ToFloat(this float value) diff --git a/NAPS2.Images.Mac/LICENSE b/NAPS2.Images.Mac/LICENSE index 3f89270f9d..f62d4e9cab 100644 --- a/NAPS2.Images.Mac/LICENSE +++ b/NAPS2.Images.Mac/LICENSE @@ -1,7 +1,7 @@ NAPS2.Images https://www.github.com/cyanfish/naps2/ -Copyright 2009-2022 NAPS2 Contributors +Copyright 2009-2025 NAPS2 Contributors This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public diff --git a/NAPS2.Images.Mac/MacBitmapHelper.cs b/NAPS2.Images.Mac/MacBitmapHelper.cs index bf7cefa9a8..408bab8b26 100644 --- a/NAPS2.Images.Mac/MacBitmapHelper.cs +++ b/NAPS2.Images.Mac/MacBitmapHelper.cs @@ -11,7 +11,8 @@ public static NSBitmapImageRep CopyRep(NSBitmapImageRep original) var copy = CreateRepForDrawing(w, h); using var c = CreateContext(copy, false, true); CGRect rect = new CGRect(0, 0, w, h); - c.DrawImage(rect, original.AsCGImage(ref rect, null, null)); + using var cgImage = original.AsCGImage(ref rect, null, null); + c.DrawImage(rect, cgImage); return copy; } diff --git a/NAPS2.Images.Mac/MacImage.cs b/NAPS2.Images.Mac/MacImage.cs index 9897102d0f..3d164585bd 100644 --- a/NAPS2.Images.Mac/MacImage.cs +++ b/NAPS2.Images.Mac/MacImage.cs @@ -2,17 +2,18 @@ namespace NAPS2.Images.Mac; -// TODO: We might need to dispose things more aggressively public class MacImage : IMemoryImage { - public MacImage(ImageContext imageContext, NSImage image) + public MacImage(NSImage image) { - if (imageContext is not MacImageContext) throw new ArgumentException("Expected MacImageContext"); - ImageContext = imageContext; NsImage = image ?? throw new ArgumentNullException(nameof(image)); var reps = NsImage.Representations(); if (reps.Length != 1) { + foreach (var rep in reps) + { + rep.Dispose(); + } throw new ArgumentException("Expected NSImage with exactly one representation"); } lock (MacImageContext.ConstructorLock) @@ -24,10 +25,12 @@ public MacImage(ImageContext imageContext, NSImage image) #endif } PixelFormat = GetPixelFormat(Rep); - // TODO: Why do we get an unsupported warning for ColorSpaceName? + // TODO: Any replacement for deprecated ColorSpaceName? +#pragma warning disable CA1416,CA1422 bool isDeviceColorSpace = Rep.ColorSpaceName == NSColorSpace.DeviceRGB || Rep.ColorSpaceName == NSColorSpace.DeviceWhite; - if (PixelFormat == ImagePixelFormat.Unsupported) +#pragma warning restore CA1416,CA1422 + if (PixelFormat == ImagePixelFormat.Unknown) { var rep = MacBitmapHelper.CopyRep(Rep); ReplaceRep(rep); @@ -38,9 +41,9 @@ public MacImage(ImageContext imageContext, NSImage image) ? NSColorSpace.DeviceGrayColorSpace : NSColorSpace.DeviceRGBColorSpace; var rep = Rep.ConvertingToColorSpace(newColorSpace, NSColorRenderingIntent.Default); + rep.Size = Rep.Size; ReplaceRep(rep); } - LogicalPixelFormat = PixelFormat; } private void ReplaceRep(NSBitmapImageRep rep) @@ -60,33 +63,26 @@ private static ImagePixelFormat GetPixelFormat(NSBitmapImageRep rep) { BitsPerPixel: 32, BitsPerSample: 8, SamplesPerPixel: 3 } => ImagePixelFormat.RGB24, { BitsPerPixel: 8, BitsPerSample: 8, SamplesPerPixel: 1 } => ImagePixelFormat.Gray8, { BitsPerPixel: 1, BitsPerSample: 1, SamplesPerPixel: 1 } => ImagePixelFormat.BW1, - _ => ImagePixelFormat.Unsupported + _ => ImagePixelFormat.Unknown }; } - public ImageContext ImageContext { get; } + public ImageContext ImageContext { get; } = new MacImageContext(); public NSImage NsImage { get; } internal NSBitmapImageRep Rep { get; private set; } - public void Dispose() - { - NsImage.Dispose(); - // TODO: Does this need to dispose the imageRep? - } - public int Width => (int) Rep.PixelsWide; public int Height => (int) Rep.PixelsHigh; - public float HorizontalResolution => (float) NsImage.Size.Width.ToDouble() / Width * 72; - public float VerticalResolution => (float) NsImage.Size.Height.ToDouble() / Height * 72; + public float HorizontalResolution => (float) (Width / NsImage.Size.Width * 72).ToDouble(); + public float VerticalResolution => (float) (Height / NsImage.Size.Height * 72).ToDouble(); public void SetResolution(float xDpi, float yDpi) { - // TODO: Image size or imagerep size? if (xDpi > 0 && yDpi > 0) { - NsImage.Size = new CGSize(xDpi / 72 * Width, yDpi / 72 * Height); + NsImage.Size = Rep.Size = new CGSize(Width / xDpi * 72, Height / yDpi * 72); } } @@ -94,6 +90,10 @@ public void SetResolution(float xDpi, float yDpi) public ImageLockState Lock(LockMode lockMode, out BitwiseImageData imageData) { + if (lockMode != LockMode.ReadOnly) + { + LogicalPixelFormat = ImagePixelFormat.Unknown; + } var ptr = Rep.BitmapData; var stride = (int) Rep.BytesPerRow; var subPixelType = PixelFormat switch @@ -109,7 +109,7 @@ public ImageLockState Lock(LockMode lockMode, out BitwiseImageData imageData) } // TODO: Should we implement some kind of actual locking? - public class MacImageLockState : ImageLockState + internal class MacImageLockState : ImageLockState { public override void Dispose() { @@ -120,61 +120,74 @@ public override void Dispose() public ImagePixelFormat LogicalPixelFormat { get; set; } - public void Save(string path, ImageFileFormat imageFormat = ImageFileFormat.Unspecified, int quality = -1) + public void Save(string path, ImageFileFormat imageFormat = ImageFileFormat.Unknown, + ImageSaveOptions? options = null) { - if (imageFormat == ImageFileFormat.Unspecified) + if (imageFormat == ImageFileFormat.Unknown) { imageFormat = ImageContext.GetFileFormatFromExtension(path); } ImageContext.CheckSupportsFormat(imageFormat); - var rep = GetRepForSaving(imageFormat, quality); + var rep = GetRepForSaving(imageFormat, options); if (!rep.Save(path, false, out var error)) { throw new IOException(error!.Description); } } - public void Save(Stream stream, ImageFileFormat imageFormat, int quality = -1) + public void Save(Stream stream, ImageFileFormat imageFormat, ImageSaveOptions? options = null) { - if (imageFormat == ImageFileFormat.Unspecified) + if (imageFormat == ImageFileFormat.Unknown) { throw new ArgumentException("Format required to save to a stream", nameof(imageFormat)); } ImageContext.CheckSupportsFormat(imageFormat); - var rep = GetRepForSaving(imageFormat, quality); + var rep = GetRepForSaving(imageFormat, options); rep.AsStream().CopyTo(stream); } - private NSData GetRepForSaving(ImageFileFormat imageFormat, int quality) + private NSData GetRepForSaving(ImageFileFormat imageFormat, ImageSaveOptions? options) { + options ??= new ImageSaveOptions(); lock (MacImageContext.ConstructorLock) { - var props = quality != -1 && imageFormat is ImageFileFormat.Jpeg or ImageFileFormat.Jpeg2000 - ? NSDictionary.FromObjectAndKey(NSNumber.FromDouble(quality / 100.0), - NSBitmapImageRep.CompressionFactor) - : null; var fileType = imageFormat switch { - ImageFileFormat.Jpeg => NSBitmapImageFileType.Jpeg, - ImageFileFormat.Png => NSBitmapImageFileType.Png, - ImageFileFormat.Bmp => NSBitmapImageFileType.Bmp, - ImageFileFormat.Tiff => NSBitmapImageFileType.Tiff, - ImageFileFormat.Jpeg2000 => NSBitmapImageFileType.Jpeg2000, + // TODO: Any replacement for deprecated UTType? +#pragma warning disable CA1416,CA1422 + ImageFileFormat.Jpeg => UTType.JPEG, + ImageFileFormat.Png => UTType.PNG, + ImageFileFormat.Bmp => UTType.BMP, + ImageFileFormat.Tiff => UTType.TIFF, + ImageFileFormat.Jpeg2000 => UTType.JPEG2000, +#pragma warning restore CA1416,CA1422 _ => throw new InvalidOperationException("Unsupported image format") }; - var targetFormat = LogicalPixelFormat; - if (imageFormat == ImageFileFormat.Bmp && targetFormat == ImagePixelFormat.Gray8) + var targetFormat = options.PixelFormatHint; + if (imageFormat == ImageFileFormat.Bmp && targetFormat == ImagePixelFormat.Unknown && + PixelFormat == ImagePixelFormat.Gray8) { - // Workaround for NSImage issue saving 8bit BMPs + // Workaround for issue in some macOS versions with 8bit BMPs targetFormat = ImagePixelFormat.RGB24; } - if (targetFormat != PixelFormat) + using var helper = PixelFormatHelper.Create(this, targetFormat); + var cgImage = helper.Image.Rep.CGImage; //RepresentationUsingTypeProperties(fileType, props); + var data = new NSMutableData(); + var props = new NSMutableDictionary(); + props.Add((NSString) "DPIWidth", NSObject.FromObject(HorizontalResolution)); + props.Add((NSString) "DPIHeight", NSObject.FromObject(VerticalResolution)); + if (options.Quality != -1 && imageFormat is ImageFileFormat.Jpeg or ImageFileFormat.Jpeg2000) { - // We only want to save with the needed color info to minimize file sizes - using var copy = (MacImage) this.CopyWithPixelFormat(targetFormat); - return copy.Rep.RepresentationUsingTypeProperties(fileType, props); + props.Add((NSString) "kCGImageDestinationLossyCompressionQuality", NSNumber.FromFloat(options.Quality / 100.0f)); } - return Rep.RepresentationUsingTypeProperties(fileType, props); +#if MONOMAC + using var dest = CGImageDestination.FromData(data, fileType, 1); +#else + using var dest = CGImageDestination.Create(data, fileType.ToString(), 1)!; +#endif + dest.AddImage(cgImage, props); + dest.Close(); + return data; } } @@ -193,11 +206,17 @@ public IMemoryImage Clone() #else var nsImage = (NSImage) NsImage.Copy(); #endif - return new MacImage(ImageContext, nsImage) + return new MacImage(nsImage) { OriginalFileFormat = OriginalFileFormat, LogicalPixelFormat = LogicalPixelFormat }; } } + + public void Dispose() + { + Rep.Dispose(); + NsImage.Dispose(); + } } \ No newline at end of file diff --git a/NAPS2.Images.Mac/MacImageContext.cs b/NAPS2.Images.Mac/MacImageContext.cs index 2d9190adfd..583a1ee9a2 100644 --- a/NAPS2.Images.Mac/MacImageContext.cs +++ b/NAPS2.Images.Mac/MacImageContext.cs @@ -9,9 +9,8 @@ public class MacImageContext : ImageContext private readonly MacImageTransformer _imageTransformer; - public MacImageContext(IPdfRenderer? pdfRenderer = null) : base(typeof(MacImage), pdfRenderer) + public MacImageContext() : base(typeof(MacImage)) { - // TODO: Not sure if this is truly thread safe. NSApplication.CheckForIllegalCrossThreadCalls = false; _imageTransformer = new MacImageTransformer(this); } @@ -29,13 +28,24 @@ protected override IMemoryImage LoadCore(Stream stream, ImageFileFormat format) { lock (ConstructorLock) { - var image = new NSImage(NSData.FromStream(stream) ?? throw new ArgumentException(nameof(stream))); + var image = NSImage.FromStream(stream)!; var reps = image.Representations(); - if (reps.Length > 1) + try { - return CreateImage(reps[0]); + if (reps.Length > 1) + { + image.Dispose(); + return CreateImage(reps[0]); + } + return new MacImage(image); + } + finally + { + foreach (var rep in reps) + { + rep.Dispose(); + } } - return new MacImage(this, image); } } @@ -48,17 +58,33 @@ protected override void LoadFramesCore(Action produceImage, Stream image = new NSImage(NSData.FromStream(stream) ?? throw new ArgumentException(nameof(stream))); } var reps = image.Representations(); - for (int i = 0; i < reps.Length; i++) + try { - progress.Report(i, reps.Length); - if (progress.IsCancellationRequested) break; - produceImage(CreateImage(reps[i])); + for (int i = 0; i < reps.Length; i++) + { + progress.Report(i, reps.Length); + if (progress.IsCancellationRequested) break; + produceImage(CreateImage(reps[i])); + } + progress.Report(reps.Length, reps.Length); + } + finally + { + image.Dispose(); + foreach (var rep in reps) + { + rep.Dispose(); + } } - progress.Report(reps.Length, reps.Length); } public override ITiffWriter TiffWriter { get; } = new MacTiffWriter(); + public NSImage RenderToNsImage(IRenderableImage image) + { + return ((MacImage) Render(image)).NsImage; + } + private IMemoryImage CreateImage(NSImageRep rep) { NSImage frame; @@ -67,7 +93,7 @@ private IMemoryImage CreateImage(NSImageRep rep) frame = new NSImage(rep.Size); } frame.AddRepresentation(rep); - return new MacImage(this, frame); + return new MacImage(frame); } public override IMemoryImage Create(int width, int height, ImagePixelFormat pixelFormat) @@ -77,7 +103,8 @@ public override IMemoryImage Create(int width, int height, ImagePixelFormat pixe var rep = MacBitmapHelper.CreateRep(width, height, pixelFormat); var image = new NSImage(rep.Size); image.AddRepresentation(rep); - return new MacImage(this, image); + rep.Dispose(); + return new MacImage(image); } } } \ No newline at end of file diff --git a/NAPS2.Images.Mac/MacImageExtensions.cs b/NAPS2.Images.Mac/MacImageExtensions.cs new file mode 100644 index 0000000000..304a470824 --- /dev/null +++ b/NAPS2.Images.Mac/MacImageExtensions.cs @@ -0,0 +1,18 @@ + +namespace NAPS2.Images.Mac; + +public static class MacImageExtensions +{ + public static NSImage RenderToNsImage(this IRenderableImage image) + { + var macImageContext = image.ImageContext as MacImageContext ?? + throw new ArgumentException("The provided image does not have a MacImageContext"); + return macImageContext.RenderToNsImage(image); + } + + public static NSImage AsNsImage(this IMemoryImage image) + { + var macImage = image as MacImage ?? throw new ArgumentException("Expected a MacImage", nameof(image)); + return macImage.NsImage; + } +} \ No newline at end of file diff --git a/NAPS2.Images.Mac/MacImageTransformer.cs b/NAPS2.Images.Mac/MacImageTransformer.cs index 58963c26c8..4730b92288 100644 --- a/NAPS2.Images.Mac/MacImageTransformer.cs +++ b/NAPS2.Images.Mac/MacImageTransformer.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Mac; -public class MacImageTransformer : AbstractImageTransformer +internal class MacImageTransformer : AbstractImageTransformer { public MacImageTransformer(ImageContext imageContext) : base(imageContext) { @@ -32,7 +32,9 @@ protected override MacImage PerformTransform(MacImage image, RotationTransform t c.ConcatCTM(CGAffineTransform.Multiply(CGAffineTransform.Multiply(t1, t2), t3)); CGRect rect = new CGRect(0, 0, image.Width, image.Height); - c.DrawImage(rect, image.Rep.AsCGImage(ref rect, null, null)); + using var cgImage = image.Rep.AsCGImage(ref rect, null, null); + c.DrawImage(rect, cgImage); + OptimizePixelFormat(image, ref newImage); image.Dispose(); return newImage; } @@ -46,7 +48,8 @@ protected override MacImage PerformTransform(MacImage image, ResizeTransform tra using CGBitmapContext c = MacBitmapHelper.CreateContext(newImage); CGRect rect = new CGRect(0, 0, transform.Width, transform.Height); // TODO: This changes the image size to match the original which we probably don't want. - c.DrawImage(rect, image.Rep.AsCGImage(ref rect, null, null)); + using var cgImage = image.Rep.AsCGImage(ref rect, null, null); + c.DrawImage(rect, cgImage); image.Dispose(); return newImage; } diff --git a/NAPS2.Images.Mac/MacTiffWriter.cs b/NAPS2.Images.Mac/MacTiffWriter.cs index 19516abb89..e7c0fe3714 100644 --- a/NAPS2.Images.Mac/MacTiffWriter.cs +++ b/NAPS2.Images.Mac/MacTiffWriter.cs @@ -2,7 +2,7 @@ namespace NAPS2.Images.Mac; -public class MacTiffWriter : ITiffWriter +internal class MacTiffWriter : ITiffWriter { public bool SaveTiff(IList images, string path, TiffCompressionType compression = TiffCompressionType.Auto, ProgressHandler progress = default) @@ -30,7 +30,8 @@ private static NSData GetTiffData(IList images, TiffCompressionTyp lock (MacImageContext.ConstructorLock) { data = new NSMutableData(); - // TODO: Fix unsupported warning + // TODO: We get a warning for UTType +#pragma warning disable CA1416,CA1422 #if MONOMAC dest = CGImageDestination.FromData( #else diff --git a/NAPS2.Images.Mac/NAPS2.Images.Mac.csproj b/NAPS2.Images.Mac/NAPS2.Images.Mac.csproj index 6c847d2974..2cb119960d 100644 --- a/NAPS2.Images.Mac/NAPS2.Images.Mac.csproj +++ b/NAPS2.Images.Mac/NAPS2.Images.Mac.csproj @@ -1,26 +1,30 @@ - net6;net6-macos10.15 + net6;net8;net8-macos enable true false NAPS2.Images.Mac NAPS2.Images.Mac - Copyright 2022 Ben Olden-Cooligan + NAPS2.Images.Mac + Images based on AppKit.NSImage for NAPS2.Sdk. + naps2 + + - + - + MONOMAC - + @@ -28,7 +32,7 @@ - + diff --git a/NAPS2.Images.Wpf/.gitignore b/NAPS2.Images.Wpf/.gitignore new file mode 100644 index 0000000000..866b3a4299 --- /dev/null +++ b/NAPS2.Images.Wpf/.gitignore @@ -0,0 +1,34 @@ +Thumbs.db +*.obj +*.exe +*.pdb +*.user +*.aps +*.pch +*.vspscc +*_i.c +*_p.c +*.ncb +*.suo +*.sln.docstates +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +[Bb]in +[Dd]ebug*/ +*.lib +*.sbr +obj/ +[Rr]elease*/ +_ReSharper*/ +[Tt]est[Rr]esult* +*.vssscc +$tf*/ +publish/ +bin/ +temp/ +google.credentials.json +microsoft.credentials.json \ No newline at end of file diff --git a/NAPS2.Images.Wpf/LICENSE b/NAPS2.Images.Wpf/LICENSE new file mode 100644 index 0000000000..f62d4e9cab --- /dev/null +++ b/NAPS2.Images.Wpf/LICENSE @@ -0,0 +1,518 @@ +NAPS2.Images +https://www.github.com/cyanfish/naps2/ + +Copyright 2009-2025 NAPS2 Contributors + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/NAPS2.Images.Wpf/NAPS2.Images.Wpf.csproj b/NAPS2.Images.Wpf/NAPS2.Images.Wpf.csproj new file mode 100644 index 0000000000..0c41f21d44 --- /dev/null +++ b/NAPS2.Images.Wpf/NAPS2.Images.Wpf.csproj @@ -0,0 +1,32 @@ + + + + net462;net6-windows;net8-windows + enable + true + NAPS2.Images.Wpf + true + true + + NAPS2.Images.Wpf + NAPS2.Images.Wpf + Images based on WPF for NAPS2.Sdk. + naps2 + + + + + + + + + + + <_Parameter1>NAPS2.Sdk.Tests + + + + + + + diff --git a/NAPS2.Images.Wpf/WpfExtensions.cs b/NAPS2.Images.Wpf/WpfExtensions.cs new file mode 100644 index 0000000000..8dbcf78714 --- /dev/null +++ b/NAPS2.Images.Wpf/WpfExtensions.cs @@ -0,0 +1,19 @@ +using System.Windows.Media.Imaging; + +namespace NAPS2.Images.Wpf; + +public static class WpfExtensions +{ + public static BitmapSource RenderToBitmapSource(this IRenderableImage image) + { + var wpfImageContext = image.ImageContext as WpfImageContext ?? + throw new ArgumentException("The provided image does not have a WpfImageContext"); + return wpfImageContext.RenderToBitmapSource(image); + } + + public static BitmapSource AsBitmapSource(this IMemoryImage image) + { + var wpfImage = image as WpfImage ?? throw new ArgumentException("Expected a WpfImage", nameof(image)); + return wpfImage.Bitmap; + } +} \ No newline at end of file diff --git a/NAPS2.Images.Wpf/WpfImage.cs b/NAPS2.Images.Wpf/WpfImage.cs new file mode 100644 index 0000000000..1b90395c55 --- /dev/null +++ b/NAPS2.Images.Wpf/WpfImage.cs @@ -0,0 +1,179 @@ +using System.Reflection; +using System.Windows.Media.Imaging; +using System.Windows.Threading; +using NAPS2.Images.Bitwise; +using NAPS2.Util; + +namespace NAPS2.Images.Wpf; + +public class WpfImage : IMemoryImage +{ + private static MethodInfo? _detachFromDispatcher; + + private static void DetachFromDispatcher(DispatcherObject dispatcherObject) + { + _detachFromDispatcher ??= + typeof(DispatcherObject).GetMethod("DetachFromDispatcher", BindingFlags.Instance | BindingFlags.NonPublic); + _detachFromDispatcher!.Invoke(dispatcherObject, Array.Empty()); + } + + private bool _disposed; + + public WpfImage(WriteableBitmap bitmap) + { + LeakTracer.StartTracking(this); + // TODO: Something similar to MacImage where if it's not a supported pixel type we convert + WpfPixelFormatFixer.MaybeFixPixelFormat(ref bitmap); + Bitmap = bitmap; + DetachFromDispatcher(Bitmap); + } + + public ImageContext ImageContext { get; } = new WpfImageContext(); + + public WriteableBitmap Bitmap { get; private set; } + + public int Width => Bitmap.PixelWidth; + + public int Height => Bitmap.PixelHeight; + + public float HorizontalResolution => (float) Bitmap.DpiX; + + public float VerticalResolution => (float) Bitmap.DpiY; + + public void SetResolution(float xDpi, float yDpi) + { + var src = Bitmap.BackBuffer; + var srcInfo = new PixelInfo(Width, Height, GetSubPixelType(), Bitmap.BackBufferStride); + + var newImage = new WriteableBitmap(Width, Height, xDpi, yDpi, Bitmap.Format, null); + var dst = newImage.BackBuffer; + var dstInfo = new PixelInfo(Width, Height, GetSubPixelType(), newImage.BackBufferStride); + + new CopyBitwiseImageOp().Perform(src, srcInfo, dst, dstInfo); + DetachFromDispatcher(newImage); + Bitmap = newImage; + } + + public ImagePixelFormat PixelFormat => Bitmap.Format switch + { + { BitsPerPixel: 1 } => ImagePixelFormat.BW1, + { BitsPerPixel: 8 } => ImagePixelFormat.Gray8, + { BitsPerPixel: 24 } => ImagePixelFormat.RGB24, + { BitsPerPixel: 32 } => ImagePixelFormat.ARGB32, + _ => throw new InvalidOperationException("Unsupported pixel format") + }; + + public unsafe ImageLockState Lock(LockMode lockMode, out BitwiseImageData imageData) + { + if (_disposed) throw new InvalidOperationException(); + if (lockMode != LockMode.ReadOnly) + { + LogicalPixelFormat = ImagePixelFormat.Unknown; + } + var subPixelType = GetSubPixelType(); + imageData = new BitwiseImageData((byte*) Bitmap.BackBuffer, + new PixelInfo(Width, Height, subPixelType, Bitmap.BackBufferStride)); + return new WpfImageLockState(); + } + + private class WpfImageLockState : ImageLockState + { + public override void Dispose() + { + } + } + + private SubPixelType GetSubPixelType() => PixelFormat switch + { + ImagePixelFormat.RGB24 => SubPixelType.Bgr, + ImagePixelFormat.ARGB32 => SubPixelType.Bgra, + ImagePixelFormat.Gray8 => SubPixelType.Gray, + ImagePixelFormat.BW1 => SubPixelType.Bit, + _ => throw new InvalidOperationException("Unsupported pixel format") + }; + + public ImageFileFormat OriginalFileFormat { get; set; } + + public ImagePixelFormat LogicalPixelFormat { get; set; } + + public void Save(string path, ImageFileFormat imageFormat = ImageFileFormat.Unknown, + ImageSaveOptions? options = null) + { + if (_disposed) throw new InvalidOperationException(); + if (imageFormat == ImageFileFormat.Unknown) + { + imageFormat = ImageContext.GetFileFormatFromExtension(path); + } + ImageContext.CheckSupportsFormat(imageFormat); + + options ??= new ImageSaveOptions(); + using var helper = PixelFormatHelper.Create(this, options.PixelFormatHint, minFormat: ImagePixelFormat.Gray8); + var encoder = GetImageEncoder(imageFormat, options); + encoder.Frames.Add(BitmapFrame.Create(helper.Image.Bitmap)); + using var stream = new FileStream(path, FileMode.Create, FileAccess.Write); + encoder.Save(stream); + } + + public void Save(Stream stream, ImageFileFormat imageFormat, ImageSaveOptions? options = null) + { + if (_disposed) throw new InvalidOperationException(); + if (imageFormat == ImageFileFormat.Unknown) + { + throw new ArgumentException("Format required to save to a stream", nameof(imageFormat)); + } + ImageContext.CheckSupportsFormat(imageFormat); + + options ??= new ImageSaveOptions(); + using var helper = PixelFormatHelper.Create(this, options.PixelFormatHint, minFormat: ImagePixelFormat.Gray8); + var encoder = GetImageEncoder(imageFormat, options); + encoder.Frames.Add(BitmapFrame.Create(helper.Image.Bitmap)); + SaveMaybeWithoutSeeking(stream, encoder); + } + + private static void SaveMaybeWithoutSeeking(Stream stream, BitmapEncoder encoder) + { + if (stream.CanSeek) + { + encoder.Save(stream); + } + else + { + var memoryStream = new MemoryStream(); + encoder.Save(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + memoryStream.CopyTo(stream); + } + } + + private static BitmapEncoder GetImageEncoder(ImageFileFormat imageFormat, ImageSaveOptions options) + { + var encoder = imageFormat switch + { + ImageFileFormat.Bmp => (BitmapEncoder) new BmpBitmapEncoder(), + ImageFileFormat.Png => new PngBitmapEncoder(), + ImageFileFormat.Jpeg => new JpegBitmapEncoder + { + QualityLevel = options.Quality == -1 ? 75 : options.Quality + }, + ImageFileFormat.Tiff => new TiffBitmapEncoder(), + _ => throw new InvalidOperationException() + }; + return encoder; + } + + public IMemoryImage Clone() + { + if (_disposed) throw new InvalidOperationException(); + return new WpfImage(Bitmap.Clone()) + { + OriginalFileFormat = OriginalFileFormat, + LogicalPixelFormat = LogicalPixelFormat + }; + } + + public void Dispose() + { + _disposed = true; + LeakTracer.StopTracking(this); + } +} \ No newline at end of file diff --git a/NAPS2.Images.Wpf/WpfImageContext.cs b/NAPS2.Images.Wpf/WpfImageContext.cs new file mode 100644 index 0000000000..94b4554987 --- /dev/null +++ b/NAPS2.Images.Wpf/WpfImageContext.cs @@ -0,0 +1,82 @@ +using System.Windows.Media; +using System.Windows.Media.Imaging; +using NAPS2.Util; +using Transform = NAPS2.Images.Transforms.Transform; + +namespace NAPS2.Images.Wpf; + +public class WpfImageContext : ImageContext +{ + private readonly WpfImageTransformer _imageTransformer; + + public WpfImageContext() : base(typeof(WpfImage)) + { + _imageTransformer = new WpfImageTransformer(this); + } + + protected override bool SupportsTiff => true; + + public override ITiffWriter TiffWriter => new WpfTiffWriter(); + + public override IMemoryImage PerformTransform(IMemoryImage image, Transform transform) + { + var wpfImage = image as WpfImage ?? throw new ArgumentException("Expected WpfImage object"); + return _imageTransformer.Apply(wpfImage, transform); + } + + protected override IMemoryImage LoadCore(Stream stream, ImageFileFormat format) + { + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.StreamSource = stream; + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + bitmap.Freeze(); + return new WpfImage(new WriteableBitmap(bitmap)); + } + + protected override void LoadFramesCore(Action produceImage, Stream stream, + ImageFileFormat format, ProgressHandler progress) + { + if (format == ImageFileFormat.Tiff) + { + var decoder = new TiffBitmapDecoder(stream, BitmapCreateOptions.DelayCreation, BitmapCacheOption.OnLoad); + progress.Report(0, decoder.Frames.Count); + int i = 0; + foreach (var frame in decoder.Frames) + { + if (progress.IsCancellationRequested) return; + produceImage(new WpfImage(new WriteableBitmap(frame))); + progress.Report(++i, decoder.Frames.Count); + } + return; + } + progress.Report(0, 1); + if (progress.IsCancellationRequested) return; + produceImage(LoadCore(stream, format)); + progress.Report(1, 1); + } + + public BitmapSource RenderToBitmapSource(IRenderableImage image) + { + return ((WpfImage) Render(image)).Bitmap; + } + + public override IMemoryImage Create(int width, int height, ImagePixelFormat pixelFormat) + { + if (pixelFormat == ImagePixelFormat.Unknown) + { + throw new ArgumentException("Unsupported pixel format"); + } + var wpfPixelFormat = pixelFormat switch + { + ImagePixelFormat.ARGB32 => PixelFormats.Bgr32, + ImagePixelFormat.RGB24 => PixelFormats.Bgr24, + ImagePixelFormat.Gray8 => PixelFormats.Gray8, + ImagePixelFormat.BW1 => PixelFormats.BlackWhite, + _ => throw new InvalidOperationException("Unsupported pixel format") + }; + var image = new WriteableBitmap(width, height, 0, 0, wpfPixelFormat, null); + return new WpfImage(image); + } +} \ No newline at end of file diff --git a/NAPS2.Images.Wpf/WpfImageTransformer.cs b/NAPS2.Images.Wpf/WpfImageTransformer.cs new file mode 100644 index 0000000000..82d22b2e21 --- /dev/null +++ b/NAPS2.Images.Wpf/WpfImageTransformer.cs @@ -0,0 +1,62 @@ +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace NAPS2.Images.Wpf; + +internal class WpfImageTransformer : AbstractImageTransformer +{ + public WpfImageTransformer(ImageContext imageContext) : base(imageContext) + { + } + + protected override WpfImage PerformTransform(WpfImage image, RotationTransform transform) + { + var width = image.Width; + var height = image.Height; + float xres = image.HorizontalResolution, yres = image.VerticalResolution; + if (transform.Angle is > 45.0 and < 135.0 or > 225.0 and < 315.0) + { + (width, height) = (height, width); + (xres, yres) = (yres, xres); + } + + var visual = new DrawingVisual(); + using (var dc = visual.RenderOpen()) + { + dc.DrawRectangle(Brushes.White, null, new Rect(0, 0, width, height)); + dc.PushTransform(new TranslateTransform(width / 2.0, height / 2.0)); + dc.PushTransform(new RotateTransform(transform.Angle)); + dc.PushTransform(new TranslateTransform(-image.Width / 2.0, -image.Height / 2.0)); + dc.DrawImage(image.Bitmap, new Rect(0, 0, image.Width, image.Height)); + } + + var rtb = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Default); + rtb.Render(visual); + + var newImage = new WpfImage(new WriteableBitmap(rtb)); + // TODO: In Gdi, we convert this back to BW1 (or the original pixel format). Should we do the same? + newImage.LogicalPixelFormat = image.LogicalPixelFormat == ImagePixelFormat.BW1 + ? ImagePixelFormat.Gray8 + : image.LogicalPixelFormat; + newImage.SetResolution(xres, yres); + image.Dispose(); + return newImage; + } + + protected override WpfImage PerformTransform(WpfImage image, ResizeTransform transform) + { + var copy = new TransformedBitmap(image.Bitmap, + new System.Windows.Media.ScaleTransform(transform.Width / (double) image.Width, + transform.Height / (double) image.Height)); + var newImage = new WpfImage(new WriteableBitmap(copy)); + newImage.LogicalPixelFormat = image.LogicalPixelFormat == ImagePixelFormat.BW1 + ? ImagePixelFormat.Gray8 + : image.LogicalPixelFormat; + newImage.SetResolution( + image.HorizontalResolution * image.Width / transform.Width, + image.VerticalResolution * image.Height / transform.Height); + image.Dispose(); + return newImage; + } +} \ No newline at end of file diff --git a/NAPS2.Images.Wpf/WpfPixelFormatFixer.cs b/NAPS2.Images.Wpf/WpfPixelFormatFixer.cs new file mode 100644 index 0000000000..8a0b69babd --- /dev/null +++ b/NAPS2.Images.Wpf/WpfPixelFormatFixer.cs @@ -0,0 +1,41 @@ +using System.Windows.Media; +using System.Windows.Media.Imaging; +using NAPS2.Images.Bitwise; + +namespace NAPS2.Images.Wpf; + +/// +/// Ensures that bitmaps use a standard pixel format/palette. +/// +internal static class WpfPixelFormatFixer +{ + public static bool MaybeFixPixelFormat(ref WriteableBitmap bitmap) + { + if (bitmap.Format.BitsPerPixel == 1) + { + if (bitmap.Palette?.Colors[0] == Colors.White && bitmap.Palette?.Colors[1] == Colors.Black) + { + InvertPalette(ref bitmap); + return true; + } + } + return false; + } + + private static void InvertPalette(ref WriteableBitmap bitmap) + { + int w = bitmap.PixelWidth; + int h = bitmap.PixelHeight; + + var src = bitmap.BackBuffer; + var srcInfo = new PixelInfo(w, h, SubPixelType.InvertedBit, bitmap.BackBufferStride); + + var newBitmap = new WriteableBitmap(w, h, bitmap.DpiX, bitmap.DpiY, PixelFormats.BlackWhite, null); + var dst = newBitmap.BackBuffer; + var dstInfo = new PixelInfo(w, h, SubPixelType.Bit, newBitmap.BackBufferStride); + + new CopyBitwiseImageOp().Perform(src, srcInfo, dst, dstInfo); + newBitmap.Freeze(); + bitmap = newBitmap; + } +} \ No newline at end of file diff --git a/NAPS2.Images.Wpf/WpfTiffWriter.cs b/NAPS2.Images.Wpf/WpfTiffWriter.cs new file mode 100644 index 0000000000..be664178ce --- /dev/null +++ b/NAPS2.Images.Wpf/WpfTiffWriter.cs @@ -0,0 +1,49 @@ +using System.Windows.Media.Imaging; +using NAPS2.Util; + +namespace NAPS2.Images.Wpf; + +internal class WpfTiffWriter : ITiffWriter +{ + public bool SaveTiff(IList images, string path, + TiffCompressionType compression = TiffCompressionType.Auto, ProgressHandler progress = default) + { + using var fileStream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite); + return SaveTiff(images, fileStream, compression, progress); + } + + public bool SaveTiff(IList images, Stream stream, + TiffCompressionType compression = TiffCompressionType.Auto, ProgressHandler progress = default) + { + if (progress.IsCancellationRequested) return false; + var tiffEncoder = new TiffBitmapEncoder + { + Compression = compression switch + { + TiffCompressionType.Ccitt4 => TiffCompressOption.Ccitt4, + TiffCompressionType.Lzw => TiffCompressOption.Lzw, + TiffCompressionType.None => TiffCompressOption.None, + _ => TiffCompressOption.Default + } + }; + int i = 0; + progress.Report(i, images.Count); + foreach (var image in images) + { + image.UpdateLogicalPixelFormat(); + if (compression == TiffCompressionType.Ccitt4 && image.LogicalPixelFormat != ImagePixelFormat.BW1) + { + using var bwCopy = image.Clone().PerformTransform(new BlackWhiteTransform()); + tiffEncoder.Frames.Add(BitmapFrame.Create(((WpfImage) bwCopy).Bitmap)); + } + else + { + tiffEncoder.Frames.Add(BitmapFrame.Create(((WpfImage) image).Bitmap)); + } + progress.Report(++i, images.Count); + if (progress.IsCancellationRequested) return false; + } + tiffEncoder.Save(stream); + return true; + } +} \ No newline at end of file diff --git a/NAPS2.Images/Bitwise/BilateralFilterOp.cs b/NAPS2.Images/Bitwise/BilateralFilterOp.cs index 28cd1d2846..866752bc39 100644 --- a/NAPS2.Images/Bitwise/BilateralFilterOp.cs +++ b/NAPS2.Images/Bitwise/BilateralFilterOp.cs @@ -4,7 +4,7 @@ namespace NAPS2.Images.Bitwise; /// Runs a bilateral filter operation, which reduces noise without losing edges or fine details. /// https://en.wikipedia.org/wiki/Bilateral_filter /// -public class BilateralFilterOp : BinaryBitwiseImageOp +internal class BilateralFilterOp : BinaryBitwiseImageOp { // The color distance (in the 0-255 range) at which pixels are weighted to 0. // The weight linearly scales up as the color distance approaches 0. @@ -20,11 +20,14 @@ public class BilateralFilterOp : BinaryBitwiseImageOp protected override void PerformCore(BitwiseImageData src, BitwiseImageData dst, int partStart, int partEnd) { - // TODO: Implement grayscale? if (src.bytesPerPixel is 3 or 4 && dst.bytesPerPixel is 3 or 4) { PerformRgba(src, dst, partStart, partEnd); } + else if (src.bytesPerPixel == 1 && dst.bytesPerPixel == 1) + { + PerformGray(src, dst, partStart, partEnd); + } else { throw new InvalidOperationException("Unsupported pixel format"); @@ -36,25 +39,8 @@ private unsafe void PerformRgba(BitwiseImageData src, BitwiseImageData dst, int bool copyAlpha = src.hasAlpha && dst.hasAlpha; const int s = FILTER_SIZE / 2; - var filter = new int[FILTER_SIZE, FILTER_SIZE]; - for (int filterX = 0; filterX < FILTER_SIZE; filterX++) - { - for (int filterY = 0; filterY < FILTER_SIZE; filterY++) - { - int dx = filterX - s; - int dy = filterY - s; - var dmax = Math.Sqrt(2 * s * s); - var d = Math.Sqrt(dx * dx + dy * dy) / dmax; - filter[filterX, filterY] = (int)((1 - d) * 256); - } - } - - var diffWeights = new int[256 * 3 * 2]; - for (int i = 0; i < COLOR_DIST_MAX * 3; i++) - { - diffWeights[256 * 3 + i] = COLOR_DIST_MAX - i / 3; - diffWeights[256 * 3 - i] = COLOR_DIST_MAX - i / 3; - } + var filter = BuildFilter(); + var diffWeights = BuildRgbaDiffWeights(); for (int i = partStart; i < partEnd; i++) { @@ -81,7 +67,8 @@ private unsafe void PerformRgba(BitwiseImageData src, BitwiseImageData dst, int byte nextR = *(nextPixel + src.rOff); byte nextG = *(nextPixel + src.gOff); byte nextB = *(nextPixel + src.bOff); - if (prevR == 255 && prevG == 255 && prevB == 255 && nextR == 255 && nextG == 255 && nextB == 255) + if (prevR == 255 && prevG == 255 && prevB == 255 && nextR == 255 && nextG == 255 && + nextB == 255) { skipPixel = true; } @@ -105,7 +92,7 @@ private unsafe void PerformRgba(BitwiseImageData src, BitwiseImageData dst, int // TODO: Better color distance var diff = (r + g + b) - (r2 + g2 + b2) + 256 * 3; - var weight = filter[filterX, filterY] * diffWeights[diff]; + var weight = filter[filterX + filterY * FILTER_SIZE] * diffWeights[diff]; weightTotal += weight; rTotal += r2 * weight; gTotal += g2 * weight; @@ -127,4 +114,104 @@ private unsafe void PerformRgba(BitwiseImageData src, BitwiseImageData dst, int } } } + + private unsafe void PerformGray(BitwiseImageData src, BitwiseImageData dst, int partStart, int partEnd) + { + const int s = FILTER_SIZE / 2; + + var filter = BuildFilter(); + var diffWeights = BuildGrayDiffWeights(); + + for (int i = partStart; i < partEnd; i++) + { + var srcRow = src.ptr + src.stride * i; + var dstRow = dst.ptr + dst.stride * i; + for (int j = 0; j < src.w; j++) + { + var srcPixel = srcRow + j * src.bytesPerPixel; + var dstPixel = dstRow + j * dst.bytesPerPixel; + int lum = *srcPixel; + + if (j > s && j < src.w - s && i > s && i < src.h - s) + { + bool skipPixel = false; + if (lum == 255) + { + var prevPixel = src.ptr + src.stride * i + src.bytesPerPixel * j - 1; + var nextPixel = src.ptr + src.stride * i + src.bytesPerPixel * j + 1; + byte prev = *prevPixel; + byte next = *nextPixel; + if (prev == 255 && next == 255) + { + skipPixel = true; + } + } + if (!skipPixel) + { + int lumTotal = 0; + int weightTotal = 0; + for (int filterX = 0; filterX < FILTER_SIZE; filterX++) + { + for (int filterY = 0; filterY < FILTER_SIZE; filterY++) + { + int imageX = j - s + filterX; + int imageY = i - s + filterY; + + var pixel = src.ptr + src.stride * imageY + src.bytesPerPixel * imageX; + + var lum2 = *pixel; + + var diff = lum - lum2 + 256; + var weight = filter[filterX + filterY * FILTER_SIZE] * diffWeights[diff]; + weightTotal += weight; + lumTotal += lum2 * weight; + } + } + lum = lumTotal / weightTotal; + } + } + *dstPixel = (byte) lum; + } + } + } + + private static int[] BuildFilter() + { + const int s = FILTER_SIZE / 2; + var filter = new int[FILTER_SIZE * FILTER_SIZE]; + for (int filterX = 0; filterX < FILTER_SIZE; filterX++) + { + for (int filterY = 0; filterY < FILTER_SIZE; filterY++) + { + int dx = filterX - s; + int dy = filterY - s; + var dmax = Math.Sqrt(2 * s * s); + var d = Math.Sqrt(dx * dx + dy * dy) / dmax; + filter[filterX + filterY * FILTER_SIZE] = (int) ((1 - d) * 256); + } + } + return filter; + } + + private static int[] BuildRgbaDiffWeights() + { + var diffWeights = new int[256 * 3 * 2]; + for (int i = 0; i < COLOR_DIST_MAX * 3; i++) + { + diffWeights[256 * 3 + i] = COLOR_DIST_MAX - i / 3; + diffWeights[256 * 3 - i] = COLOR_DIST_MAX - i / 3; + } + return diffWeights; + } + + private static int[] BuildGrayDiffWeights() + { + var diffWeights = new int[256 * 2]; + for (int i = 0; i < COLOR_DIST_MAX; i++) + { + diffWeights[256 + i] = COLOR_DIST_MAX - i; + diffWeights[256 - i] = COLOR_DIST_MAX - i; + } + return diffWeights; + } } \ No newline at end of file diff --git a/NAPS2.Images/Bitwise/BinaryBitwiseImageOp.cs b/NAPS2.Images/Bitwise/BinaryBitwiseImageOp.cs index 12f649efc3..868037c19b 100644 --- a/NAPS2.Images/Bitwise/BinaryBitwiseImageOp.cs +++ b/NAPS2.Images/Bitwise/BinaryBitwiseImageOp.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public abstract class BinaryBitwiseImageOp : BitwiseImageOp +internal abstract class BinaryBitwiseImageOp : BitwiseImageOp { public void Perform(IMemoryImage src, IMemoryImage dst) { @@ -38,6 +38,26 @@ public unsafe void Perform(byte[] src, PixelInfo srcPixelInfo, IMemoryImage dst) } } + public unsafe void Perform(byte[] src, PixelInfo srcPixelInfo, byte[] dst, PixelInfo dstPixelInfo) + { + if (src.Length < srcPixelInfo.Length) + { + throw new ArgumentException("Source byte array length is less than expected"); + } + if (dst.Length < dstPixelInfo.Length) + { + throw new ArgumentException( + $"Destination byte array length {dst.Length} is less than expected for height {dstPixelInfo.Height} and stride {dstPixelInfo.Stride}"); + } + fixed (byte* srcPtr = src) + fixed (byte* dstPtr = dst) + { + var srcData = new BitwiseImageData(srcPtr, srcPixelInfo); + var dstData = new BitwiseImageData(dstPtr, dstPixelInfo); + ValidateAndPerform(srcData, dstData); + } + } + public void Perform(IntPtr src, PixelInfo srcPixelInfo, IMemoryImage dst) { using var dstLock = dst.Lock(DstLockMode, out var dstData); @@ -52,6 +72,13 @@ public void Perform(IMemoryImage src, IntPtr dst, PixelInfo dstPixelInfo) ValidateAndPerform(srcData, dstData); } + public void Perform(IntPtr src, PixelInfo srcPixelInfo, IntPtr dst, PixelInfo dstPixelInfo) + { + var srcData = new BitwiseImageData(src, srcPixelInfo); + var dstData = new BitwiseImageData(dst, dstPixelInfo); + ValidateAndPerform(srcData, dstData); + } + private void ValidateAndPerform(BitwiseImageData src, BitwiseImageData dst) { ValidateConsistency(src); diff --git a/NAPS2.Images/Bitwise/BitPixelReader.cs b/NAPS2.Images/Bitwise/BitPixelReader.cs index 43b54d76e8..76c94ded19 100644 --- a/NAPS2.Images/Bitwise/BitPixelReader.cs +++ b/NAPS2.Images/Bitwise/BitPixelReader.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public class BitPixelReader : IDisposable +internal class BitPixelReader : IDisposable { private const int THRESHOLD = 140 * 1000; diff --git a/NAPS2.Images/Bitwise/BitwiseImageOp.cs b/NAPS2.Images/Bitwise/BitwiseImageOp.cs index e327129951..10819a46c2 100644 --- a/NAPS2.Images/Bitwise/BitwiseImageOp.cs +++ b/NAPS2.Images/Bitwise/BitwiseImageOp.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public class BitwiseImageOp +internal class BitwiseImageOp { public const int R_MULT = 299; public const int G_MULT = 587; diff --git a/NAPS2.Images/Bitwise/BitwisePrimitives.cs b/NAPS2.Images/Bitwise/BitwisePrimitives.cs index a763b80575..f4e35bf24f 100644 --- a/NAPS2.Images/Bitwise/BitwisePrimitives.cs +++ b/NAPS2.Images/Bitwise/BitwisePrimitives.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public static class BitwisePrimitives +internal static class BitwisePrimitives { public static unsafe void Invert(BitwiseImageData data, int partStart = -1, int partEnd = -1) { @@ -34,6 +34,7 @@ public static unsafe void Fill(BitwiseImageData data, byte value, int partStart { if (partStart == -1) partStart = 0; if (partEnd == -1) partEnd = data.h; + if (data.invertColorSpace) value = (byte) ~value; var longCount = data.stride / 8; var remainingStart = longCount * 8; diff --git a/NAPS2.Images/Bitwise/BlankDetectionImageOp.cs b/NAPS2.Images/Bitwise/BlankDetectionImageOp.cs new file mode 100644 index 0000000000..d6bdb5c399 --- /dev/null +++ b/NAPS2.Images/Bitwise/BlankDetectionImageOp.cs @@ -0,0 +1,160 @@ +namespace NAPS2.Images.Bitwise; + +internal class BlankDetectionImageOp : UnaryBitwiseImageOp +{ + // If the pixel value (0-255) >= white_threshold, then it counts as a white pixel. + private const int WHITE_THRESHOLD_MIN = 1; + private const int WHITE_THRESHOLD_MAX = 255; + // If the fraction of non-white pixels > coverage_threshold, then it counts as a non-blank page. + private const double COVERAGE_THRESHOLD_MIN = 0.00; + private const double COVERAGE_THRESHOLD_MAX = 0.01; + private const double IGNORE_EDGE_FRACTION = 0.01; + + private readonly int _whiteThresholdAdjusted; + private readonly double _coverageThresholdAdjusted; + + private int _startX; + private int _startY; + private int _endX; + private int _endY; + private long _totalMatch; + private long _totalPixels; + + public BlankDetectionImageOp(int whiteThreshold, int coverageThreshold) + { + _whiteThresholdAdjusted = (int) Math.Round(WHITE_THRESHOLD_MIN + + (whiteThreshold / 100.0) * + (WHITE_THRESHOLD_MAX - WHITE_THRESHOLD_MIN)); + _coverageThresholdAdjusted = COVERAGE_THRESHOLD_MIN + + (coverageThreshold / 100.0) * (COVERAGE_THRESHOLD_MAX - COVERAGE_THRESHOLD_MIN); + } + + protected override LockMode LockMode => LockMode.ReadOnly; + + public double Coverage { get; private set; } + + public bool IsBlank { get; private set; } + + protected override void ValidateCore(BitwiseImageData data) + { + } + + protected override void StartCore(BitwiseImageData data) + { + _totalPixels = data.w * data.h; + _startX = (int) (data.w * IGNORE_EDGE_FRACTION); + _startY = (int) (data.h * IGNORE_EDGE_FRACTION); + _endX = (int) (data.w * (1 - IGNORE_EDGE_FRACTION)); + _endY = (int) (data.h * (1 - IGNORE_EDGE_FRACTION)); + } + + protected override void PerformCore(BitwiseImageData data, int partStart, int partEnd) + { + if (data.bytesPerPixel is 3 or 4) + { + PerformRgba(data, partStart, partEnd); + } + else if (data.bytesPerPixel == 1) + { + PerformGray(data, partStart, partEnd); + } + else if (data.bitsPerPixel == 1) + { + PerformBit(data, partStart, partEnd); + } + else + { + throw new InvalidOperationException("Unsupported pixel format"); + } + } + + private unsafe void PerformRgba(BitwiseImageData data, int partStart, int partEnd) + { + int match = 0; + for (int i = partStart; i < partEnd; i++) + { + if (i < _startY || i > _endY) continue; + var row = data.ptr + data.stride * i; + for (int j = 0; j < data.w; j++) + { + if (j < _startX || j > _endX) continue; + var pixel = row + j * data.bytesPerPixel; + var r = *(pixel + data.rOff); + var g = *(pixel + data.gOff); + var b = *(pixel + data.bOff); + + int luma = r * 299 + g * 587 + b * 114; + if (luma < _whiteThresholdAdjusted * 1000) + { + match++; + } + } + } + lock (this) + { + _totalMatch += match; + } + } + + private unsafe void PerformGray(BitwiseImageData data, int partStart, int partEnd) + { + int match = 0; + for (int i = partStart; i < partEnd; i++) + { + if (i < _startY || i > _endY) continue; + var row = data.ptr + data.stride * i; + for (int j = 0; j < data.w; j++) + { + if (j < _startX || j > _endX) continue; + var pixel = row + j * data.bytesPerPixel; + var luma = *pixel; + + if (luma < _whiteThresholdAdjusted) + { + match++; + } + } + } + lock (this) + { + _totalMatch += match; + } + } + + private unsafe void PerformBit(BitwiseImageData data, int partStart, int partEnd) + { + int match = 0; + for (int i = partStart; i < partEnd; i++) + { + if (i < _startY || i > _endY) continue; + var row = data.ptr + data.stride * i; + for (int j = 0; j < data.w; j += 8) + { + if (j < _startX || j > _endX) continue; + byte fullByte = *(row + j / 8); + for (int k = 7; k >= 0; k--) + { + var bit = fullByte & 1; + fullByte >>= 1; + if (j + k < data.w) + { + if (bit == 0) + { + match++; + } + } + } + } + } + lock (this) + { + _totalMatch += match; + } + } + + protected override void FinishCore() + { + Coverage = _totalMatch / (double) _totalPixels; + IsBlank = Coverage < _coverageThresholdAdjusted; + } +} \ No newline at end of file diff --git a/NAPS2.Images/Bitwise/BrightnessBitwiseImageOp.cs b/NAPS2.Images/Bitwise/BrightnessBitwiseImageOp.cs index eb3d3f371f..f64f8f0ab8 100644 --- a/NAPS2.Images/Bitwise/BrightnessBitwiseImageOp.cs +++ b/NAPS2.Images/Bitwise/BrightnessBitwiseImageOp.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public class BrightnessBitwiseImageOp : UnaryBitwiseImageOp +internal class BrightnessBitwiseImageOp : UnaryBitwiseImageOp { private readonly float _brightnessAdjusted; diff --git a/NAPS2.Images/Bitwise/ColorChannel.cs b/NAPS2.Images/Bitwise/ColorChannel.cs index 67b1e4bb0a..f4ce043de2 100644 --- a/NAPS2.Images/Bitwise/ColorChannel.cs +++ b/NAPS2.Images/Bitwise/ColorChannel.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public enum ColorChannel +internal enum ColorChannel { All, Red, diff --git a/NAPS2.Images/Bitwise/ColumnColorOp.cs b/NAPS2.Images/Bitwise/ColumnColorOp.cs index bb6ebfcd21..9e378803e1 100644 --- a/NAPS2.Images/Bitwise/ColumnColorOp.cs +++ b/NAPS2.Images/Bitwise/ColumnColorOp.cs @@ -9,7 +9,7 @@ namespace NAPS2.Images.Bitwise; /// be calibrated independently. Of course that means this correction must happen before deskew or anything else that /// can combine values across columns. /// -public class ColumnColorOp : UnaryBitwiseImageOp +internal class ColumnColorOp : UnaryBitwiseImageOp { /// /// Performs this operation including pre-processing steps. diff --git a/NAPS2.Images/Bitwise/ColumnColorPreOp.cs b/NAPS2.Images/Bitwise/ColumnColorPreOp.cs index bdd337c1f1..647b955f6f 100644 --- a/NAPS2.Images/Bitwise/ColumnColorPreOp.cs +++ b/NAPS2.Images/Bitwise/ColumnColorPreOp.cs @@ -3,7 +3,7 @@ namespace NAPS2.Images.Bitwise; /// /// Performs pre-processing for the ColumnColorOp. /// -public class ColumnColorPreOp : UnaryBitwiseImageOp +internal class ColumnColorPreOp : UnaryBitwiseImageOp { private const double COL_IGNORE_TOP_AND_BOTTOM = 0.02; diff --git a/NAPS2.Images/Bitwise/ContrastBitwiseImageOp.cs b/NAPS2.Images/Bitwise/ContrastBitwiseImageOp.cs index 5f80e98a16..5b4f3af78d 100644 --- a/NAPS2.Images/Bitwise/ContrastBitwiseImageOp.cs +++ b/NAPS2.Images/Bitwise/ContrastBitwiseImageOp.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public class ContrastBitwiseImageOp : UnaryBitwiseImageOp +internal class ContrastBitwiseImageOp : UnaryBitwiseImageOp { private readonly float _contrastAdjusted; private readonly float _offset; diff --git a/NAPS2.Images/Bitwise/CopyBitwiseImageOp.cs b/NAPS2.Images/Bitwise/CopyBitwiseImageOp.cs index a518ec904c..bdbed0a960 100644 --- a/NAPS2.Images/Bitwise/CopyBitwiseImageOp.cs +++ b/NAPS2.Images/Bitwise/CopyBitwiseImageOp.cs @@ -1,7 +1,7 @@ namespace NAPS2.Images.Bitwise; // TODO: Need to double check callers set resolution when needed -public class CopyBitwiseImageOp : BinaryBitwiseImageOp +internal class CopyBitwiseImageOp : BinaryBitwiseImageOp { // TODO: Consider requiring an explicit DiscardAlpha parameter @@ -48,10 +48,10 @@ protected override void ValidateCore(BitwiseImageData src, BitwiseImageData dst) throw new ArgumentException( "DestChannel is only supported when the source is grayscale/color and the destination is color."); } - if ((src.invertColorSpace || dst.invertColorSpace) && (src.bitsPerPixel != 1 || dst.bitsPerPixel != 1)) + if (src.invertColorSpace & dst.invertColorSpace && (src.hasAlpha || dst.hasAlpha)) { throw new ArgumentException( - "SubPixelType.InvertedBit is only supported when both source and destination are 1 bit per pixel"); + "SubPixelType.InvertedBit is not supported with alpha channels."); } } @@ -59,8 +59,14 @@ protected override void ValidateCore(BitwiseImageData src, BitwiseImageData dst) protected override void PerformCore(BitwiseImageData src, BitwiseImageData dst, int partStart, int partEnd) { if (src.BitLayout == dst.BitLayout && - (src.bytesPerPixel > 0 || (SourceXOffset % 8 == 0 && DestXOffset % 8 == 0)) && - DestChannel == ColorChannel.All) + DestChannel == ColorChannel.All && + (src.bytesPerPixel > 0 || + // For Black & White images, to use the fast copy path, we must have that: + // 1. The offsets are to whole bytes + // 2a. Either we copy whole bytes, or + // 2b. We end at the far-right side of the destination (so any excess bits copied will be ignored) + (SourceXOffset % 8 == 0 && DestXOffset % 8 == 0 && + (src.w % 8 == 0 || src.w + DestXOffset == dst.w)))) { FastCopy(src, dst, partStart, partEnd); } @@ -254,6 +260,7 @@ private unsafe void UnalignedBitCopy(BitwiseImageData src, BitwiseImageData dst, var dstPixelIndex = j + DestXOffset; var dstPtr = dstRow + dstPixelIndex / 8; var dstByte = *dstPtr; + dstByte &= (byte) ~(1 << (7 - dstPixelIndex % 8)); dstByte |= (byte) (bit << (7 - dstPixelIndex % 8)); *dstPtr = dstByte; } diff --git a/NAPS2.Images/Bitwise/DecolorBitwiseImageOp.cs b/NAPS2.Images/Bitwise/DecolorBitwiseImageOp.cs index 83b998bb76..dfe4be8584 100644 --- a/NAPS2.Images/Bitwise/DecolorBitwiseImageOp.cs +++ b/NAPS2.Images/Bitwise/DecolorBitwiseImageOp.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public class DecolorBitwiseImageOp : UnaryBitwiseImageOp +internal class DecolorBitwiseImageOp : UnaryBitwiseImageOp { private readonly bool _blackAndWhite; diff --git a/NAPS2.Images/Bitwise/FillColorImageOp.cs b/NAPS2.Images/Bitwise/FillColorImageOp.cs new file mode 100644 index 0000000000..027402e469 --- /dev/null +++ b/NAPS2.Images/Bitwise/FillColorImageOp.cs @@ -0,0 +1,71 @@ +namespace NAPS2.Images.Bitwise; + +internal class FillColorImageOp : UnaryBitwiseImageOp +{ + public static FillColorImageOp Black => new(0, 0, 0, 255); + public static FillColorImageOp White => new(255, 255, 255, 255); + + private readonly byte _r, _g, _b, _a; + + public FillColorImageOp(byte r, byte g, byte b, byte a) + { + _r = r; + _g = g; + _b = b; + _a = a; + } + + protected override void ValidateCore(BitwiseImageData data) + { + } + + protected override void PerformCore(BitwiseImageData data, int partStart, int partEnd) + { + if (data.bytesPerPixel is 1 or 3 or 4) + { + PerformRgba(data, partStart, partEnd); + } + else if (data.bitsPerPixel == 1 && (_r, _g, _b, _a) is (0, 0, 0, 255) or (255, 255, 255, 255)) + { + PerformBw(data, partStart, partEnd); + } + else + { + throw new InvalidOperationException("Unsupported pixel format"); + } + } + + private unsafe void PerformRgba(BitwiseImageData data, int partStart, int partEnd) + { + bool gray = data.bytesPerPixel == 1; + byte luma = (byte) ((_r * R_MULT + _g * G_MULT + _b * B_MULT) / 1000); + for (int i = partStart; i < partEnd; i++) + { + var row = data.ptr + data.stride * i; + for (int j = 0; j < data.w; j++) + { + var pixel = row + j * data.bytesPerPixel; + if (gray) + { + *pixel = luma; + } + else + { + *(pixel + data.rOff) = _r; + *(pixel + data.gOff) = _g; + *(pixel + data.bOff) = _b; + } + if (data.hasAlpha) + { + *(pixel + data.aOff) = _a; + } + } + } + } + + private void PerformBw(BitwiseImageData data, int partStart, int partEnd) + { + byte fill = (byte) (_r == 255 ? 0xFF : 0x00); + BitwisePrimitives.Fill(data, fill, partStart, partEnd); + } +} \ No newline at end of file diff --git a/NAPS2.Images/Bitwise/GammaTables.cs b/NAPS2.Images/Bitwise/GammaTables.cs index 1677669bf8..ea9fbf77fc 100644 --- a/NAPS2.Images/Bitwise/GammaTables.cs +++ b/NAPS2.Images/Bitwise/GammaTables.cs @@ -1,20 +1,35 @@ namespace NAPS2.Images.Bitwise; +/// +/// The normal 0-255 RGB values we use are in the "intensity" space. We can use a gamma conversion +/// to convert these to the "luminance" space, which is better for certain kinds of image processing. +/// Then convert back to the "intensity" space once we're done. This class provides pre-calculated tables +/// for these conversions. +/// https://www.benq.com/en-ca/knowledge-center/knowledge/gamma-monitor.html +/// internal static class GammaTables { - public static byte[] IntensityToLum { get; } + // Doing math in the 0-255 integral space means there's a significant loss of precision. + // Instead we can work in the 0-MAX_LUM space to give more granularity without needing to use slower floating point. + public const int MULTIPLIER = 16; + public const int MAX_LUM = 255 * MULTIPLIER; + + public static short[] IntensityToLum { get; } public static byte[] LumToIntensity { get; } static GammaTables() { const double gamma = 2.2; - IntensityToLum = new byte[256]; - LumToIntensity = new byte[256]; + IntensityToLum = new short[256]; + LumToIntensity = new byte[MAX_LUM + 1]; for (int x = 0; x < 256; x++) { var i = Math.Pow(x / 255.0, 1 / gamma); - IntensityToLum[x] = (byte) Math.Round(i * 255); - var l = Math.Pow(x / 255.0, gamma); + IntensityToLum[x] = (short) Math.Round(i * MAX_LUM); + } + for (int x = 0; x <= MAX_LUM; x++) + { + var l = Math.Pow(x / (double) MAX_LUM, gamma); LumToIntensity[x] = (byte) Math.Round(l * 255); } } diff --git a/NAPS2.Images/Bitwise/HueShiftBitwiseImageOp.cs b/NAPS2.Images/Bitwise/HueShiftBitwiseImageOp.cs index c1fe367221..c0beff58a4 100644 --- a/NAPS2.Images/Bitwise/HueShiftBitwiseImageOp.cs +++ b/NAPS2.Images/Bitwise/HueShiftBitwiseImageOp.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public class HueShiftBitwiseImageOp : UnaryBitwiseImageOp +internal class HueShiftBitwiseImageOp : UnaryBitwiseImageOp { private readonly float _shiftAdjusted; diff --git a/NAPS2.Images/Bitwise/LogicalPixelFormatOp.cs b/NAPS2.Images/Bitwise/LogicalPixelFormatOp.cs index 9d779313cc..c45762304f 100644 --- a/NAPS2.Images/Bitwise/LogicalPixelFormatOp.cs +++ b/NAPS2.Images/Bitwise/LogicalPixelFormatOp.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public class LogicalPixelFormatOp : UnaryBitwiseImageOp +internal class LogicalPixelFormatOp : UnaryBitwiseImageOp { public ImagePixelFormat LogicalPixelFormat { get; private set; } diff --git a/NAPS2.Images/Bitwise/RgbPixelReader.cs b/NAPS2.Images/Bitwise/RgbPixelReader.cs index bcb477e9e4..b607c8d86a 100644 --- a/NAPS2.Images/Bitwise/RgbPixelReader.cs +++ b/NAPS2.Images/Bitwise/RgbPixelReader.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public class RgbPixelReader : IDisposable +internal class RgbPixelReader : IDisposable { private readonly ImageLockState _lock; private readonly BitwiseImageData _data; diff --git a/NAPS2.Images/Bitwise/RmseBitwiseImageOp.cs b/NAPS2.Images/Bitwise/RmseBitwiseImageOp.cs index a8de16fff3..aa320d9066 100644 --- a/NAPS2.Images/Bitwise/RmseBitwiseImageOp.cs +++ b/NAPS2.Images/Bitwise/RmseBitwiseImageOp.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public class RmseBitwiseImageOp : BinaryBitwiseImageOp +internal class RmseBitwiseImageOp : BinaryBitwiseImageOp { protected override LockMode SrcLockMode => LockMode.ReadOnly; protected override LockMode DstLockMode => LockMode.ReadOnly; diff --git a/NAPS2.Images/Bitwise/SaturationBitwiseImageOp.cs b/NAPS2.Images/Bitwise/SaturationBitwiseImageOp.cs index fe47aeec0c..a258eba3dd 100644 --- a/NAPS2.Images/Bitwise/SaturationBitwiseImageOp.cs +++ b/NAPS2.Images/Bitwise/SaturationBitwiseImageOp.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public class SaturationBitwiseImageOp : UnaryBitwiseImageOp +internal class SaturationBitwiseImageOp : UnaryBitwiseImageOp { private readonly float _saturationAdjusted; diff --git a/NAPS2.Images/Bitwise/SharpenBitwiseImageOp.cs b/NAPS2.Images/Bitwise/SharpenBitwiseImageOp.cs index ec2cc6b21a..0bacfb4944 100644 --- a/NAPS2.Images/Bitwise/SharpenBitwiseImageOp.cs +++ b/NAPS2.Images/Bitwise/SharpenBitwiseImageOp.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public class SharpenBitwiseImageOp : BinaryBitwiseImageOp +internal class SharpenBitwiseImageOp : BinaryBitwiseImageOp { private readonly float _sharpness; diff --git a/NAPS2.Images/Bitwise/UnaryBitwiseImageOp.cs b/NAPS2.Images/Bitwise/UnaryBitwiseImageOp.cs index f8153e09b9..c0a06ca28f 100644 --- a/NAPS2.Images/Bitwise/UnaryBitwiseImageOp.cs +++ b/NAPS2.Images/Bitwise/UnaryBitwiseImageOp.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public abstract class UnaryBitwiseImageOp : BitwiseImageOp +internal abstract class UnaryBitwiseImageOp : BitwiseImageOp { public void Perform(IMemoryImage image) { diff --git a/NAPS2.Images/Bitwise/UnmultiplyAlphaOp.cs b/NAPS2.Images/Bitwise/UnmultiplyAlphaOp.cs index 5625f778e3..00bd9a4d2b 100644 --- a/NAPS2.Images/Bitwise/UnmultiplyAlphaOp.cs +++ b/NAPS2.Images/Bitwise/UnmultiplyAlphaOp.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images.Bitwise; -public class UnmultiplyAlphaOp : UnaryBitwiseImageOp +internal class UnmultiplyAlphaOp : UnaryBitwiseImageOp { protected override void PerformCore(BitwiseImageData data, int partStart, int partEnd) { diff --git a/NAPS2.Images/Bitwise/WhiteBlackPointOp.cs b/NAPS2.Images/Bitwise/WhiteBlackPointOp.cs index 68d5cf3842..0fb5949389 100644 --- a/NAPS2.Images/Bitwise/WhiteBlackPointOp.cs +++ b/NAPS2.Images/Bitwise/WhiteBlackPointOp.cs @@ -3,7 +3,7 @@ namespace NAPS2.Images.Bitwise; /// /// Corrects images with poor calibration for white/black values. /// -public class WhiteBlackPointOp : UnaryBitwiseImageOp +internal class WhiteBlackPointOp : UnaryBitwiseImageOp { // When we've identified the block of pixel values that we consider white (or black), // this is the percentile (counting from the mid levels) at which we set the @@ -49,7 +49,7 @@ public WhiteBlackPointOp(WhiteBlackPointPreOp preOp) _blackPoint = GetBlackPoint(counts, blackPeak); _valid = true; - Console.WriteLine($"Correcting with whitepoint {_whitePoint} blackpoint {_blackPoint}"); + // Console.WriteLine($"Correcting with whitepoint {_whitePoint} blackpoint {_blackPoint}"); } private static int GetWhitePoint(int[] counts, Peak whitePeak) @@ -201,9 +201,9 @@ protected override unsafe void PerformCore(BitwiseImageData data, int partStart, int gL = iToL[g]; int bL = iToL[b]; // Scale the color values in the luminescence space - rL = (rL - blackL) * 255 / (whiteL - blackL); - gL = (gL - blackL) * 255 / (whiteL - blackL); - bL = (bL - blackL) * 255 / (whiteL - blackL); + rL = (rL - blackL) * GammaTables.MAX_LUM / (whiteL - blackL); + gL = (gL - blackL) * GammaTables.MAX_LUM / (whiteL - blackL); + bL = (bL - blackL) * GammaTables.MAX_LUM / (whiteL - blackL); // Convert back to the intensity space r = lToI[rL]; g = lToI[gL]; diff --git a/NAPS2.Images/Bitwise/WhiteBlackPointPreOp.cs b/NAPS2.Images/Bitwise/WhiteBlackPointPreOp.cs index d0601f72db..0f904251c3 100644 --- a/NAPS2.Images/Bitwise/WhiteBlackPointPreOp.cs +++ b/NAPS2.Images/Bitwise/WhiteBlackPointPreOp.cs @@ -3,7 +3,7 @@ namespace NAPS2.Images.Bitwise; /// /// Performs pre-processing for the WhiteBlackPointOp. /// -public class WhiteBlackPointPreOp : UnaryBitwiseImageOp +internal class WhiteBlackPointPreOp : UnaryBitwiseImageOp { public WhiteBlackPointPreOp(CorrectionMode mode) { diff --git a/NAPS2.Images/Bitwise/BitwiseImageData.cs b/NAPS2.Images/BitwiseImageData.cs similarity index 97% rename from NAPS2.Images/Bitwise/BitwiseImageData.cs rename to NAPS2.Images/BitwiseImageData.cs index b944910608..b5c65f7447 100644 --- a/NAPS2.Images/Bitwise/BitwiseImageData.cs +++ b/NAPS2.Images/BitwiseImageData.cs @@ -1,4 +1,4 @@ -namespace NAPS2.Images.Bitwise; +namespace NAPS2.Images; public struct BitwiseImageData { diff --git a/NAPS2.Images/Storage/FileStorageManager.cs b/NAPS2.Images/FileStorageManager.cs similarity index 96% rename from NAPS2.Images/Storage/FileStorageManager.cs rename to NAPS2.Images/FileStorageManager.cs index 89d644ab22..0817de1b49 100644 --- a/NAPS2.Images/Storage/FileStorageManager.cs +++ b/NAPS2.Images/FileStorageManager.cs @@ -1,6 +1,6 @@ using System.Globalization; -namespace NAPS2.Images.Storage; +namespace NAPS2.Images; public class FileStorageManager : IDisposable { diff --git a/NAPS2.Images/Storage/IImageStorage.cs b/NAPS2.Images/IImageStorage.cs similarity index 52% rename from NAPS2.Images/Storage/IImageStorage.cs rename to NAPS2.Images/IImageStorage.cs index 2760a6cb9b..d5ba11b7e1 100644 --- a/NAPS2.Images/Storage/IImageStorage.cs +++ b/NAPS2.Images/IImageStorage.cs @@ -1,8 +1,7 @@ -namespace NAPS2.Images.Storage; +namespace NAPS2.Images; /// -/// Base type for image storage, which can be a normal in-memory image (see IMemoryImage) or an image stored on the -/// filesystem (see ImageFileStorage). +/// Base type for image storage, which can be a normal in-memory image or an image stored on the filesystem. /// public interface IImageStorage : IDisposable { diff --git a/NAPS2.Images/IMemoryImage.cs b/NAPS2.Images/IMemoryImage.cs index b3e6c87f27..4b4ea01a0c 100644 --- a/NAPS2.Images/IMemoryImage.cs +++ b/NAPS2.Images/IMemoryImage.cs @@ -67,7 +67,9 @@ public interface IMemoryImage : IImageStorage /// /// Gets the color content of the image. For example, an image might be stored in memory with PixelFormat = ARGB32, - /// but if it's a grayscale image with no transparency, then LogicalPixelFormat = Gray8. + /// but if it's a grayscale image with no transparency, then LogicalPixelFormat = Gray8. By default this is not + /// calculated and is set to ImagePixelFormat.Unsupported. Call IMemoryImage.UpdateLogicalPixelFormat() to ensure + /// this is calculated. /// ImagePixelFormat LogicalPixelFormat { get; set; } @@ -77,16 +79,16 @@ public interface IMemoryImage : IImageStorage /// /// The path to save the image file to. /// The file format to use. - /// The quality parameter for JPEG compression, if applicable. -1 for default. - void Save(string path, ImageFileFormat imageFormat = ImageFileFormat.Unspecified, int quality = -1); + /// Options for saving, e.g. JPEG quality. + void Save(string path, ImageFileFormat imageFormat = ImageFileFormat.Unknown, ImageSaveOptions? options = null); /// /// Saves the image to the given stream. The file format must be specified. /// /// The stream to save the image to. /// The file format to use. - /// The quality parameter for JPEG compression, if applicable. -1 for default. - void Save(Stream stream, ImageFileFormat imageFormat, int quality = -1); + /// Options for saving, e.g. JPEG quality. + void Save(Stream stream, ImageFileFormat imageFormat, ImageSaveOptions? options = null); /// /// Creates a copy of the image so that one can be edited or disposed without affecting the other. diff --git a/NAPS2.Images/IPdfRenderer.cs b/NAPS2.Images/IPdfRenderer.cs index 3ac58daa18..702e1a4755 100644 --- a/NAPS2.Images/IPdfRenderer.cs +++ b/NAPS2.Images/IPdfRenderer.cs @@ -1,9 +1,16 @@ namespace NAPS2.Images; -public interface IPdfRenderer +internal interface IPdfRenderer { - IEnumerable Render(ImageContext imageContext, string path, PdfRenderSize renderSize, string? password = null); + IEnumerable Render(ImageContext imageContext, string path, PdfRenderSize renderSize, + string? password = null); IEnumerable Render(ImageContext imageContext, byte[] buffer, int length, PdfRenderSize renderSize, string? password = null); + + IMemoryImage RenderPage(ImageContext imageContext, string path, PdfRenderSize renderSize, int pageIndex = 0, + string? password = null); + + IMemoryImage RenderPage(ImageContext imageContext, byte[] buffer, int length, PdfRenderSize renderSize, + int pageIndex = 0, string? password = null); } \ No newline at end of file diff --git a/NAPS2.Images/IPdfRendererProvider.cs b/NAPS2.Images/IPdfRendererProvider.cs new file mode 100644 index 0000000000..26c1a704da --- /dev/null +++ b/NAPS2.Images/IPdfRendererProvider.cs @@ -0,0 +1,6 @@ +namespace NAPS2.Images; + +internal interface IPdfRendererProvider +{ + IPdfRenderer PdfRenderer { get; } +} \ No newline at end of file diff --git a/NAPS2.Images/ImageContext.cs b/NAPS2.Images/ImageContext.cs index f7f95e5033..cb20468e85 100644 --- a/NAPS2.Images/ImageContext.cs +++ b/NAPS2.Images/ImageContext.cs @@ -1,12 +1,11 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using NAPS2.Util; namespace NAPS2.Images; public abstract class ImageContext { - private readonly IPdfRenderer? _pdfRenderer; - public static ImageFileFormat GetFileFormatFromExtension(string path) { return Path.GetExtension(path).ToLowerInvariant() switch @@ -16,7 +15,7 @@ public static ImageFileFormat GetFileFormatFromExtension(string path) ".jpg" or ".jpeg" => ImageFileFormat.Jpeg, ".tif" or ".tiff" => ImageFileFormat.Tiff, ".jp2" or ".jpx" => ImageFileFormat.Jpeg2000, - _ => throw new ArgumentException($"Could not infer file format from extension: {path}") + _ => ImageFileFormat.Unknown }; } @@ -24,74 +23,64 @@ private static ImageFileFormat GetFileFormatFromFirstBytes(Stream stream) { if (!stream.CanSeek) { - return ImageFileFormat.Unspecified; + return ImageFileFormat.Unknown; } var firstBytes = new byte[8]; stream.Seek(0, SeekOrigin.Begin); stream.Read(firstBytes, 0, 8); stream.Seek(0, SeekOrigin.Begin); - if (firstBytes[0] == 0x89 && firstBytes[1] == 0x50 && firstBytes[2] == 0x4E && firstBytes[3] == 0x47) - { - return ImageFileFormat.Png; - } - if (firstBytes[0] == 0xFF && firstBytes[1] == 0xD8) - { - return ImageFileFormat.Jpeg; - } - if (firstBytes[0] == 0x42 && firstBytes[1] == 0x4D) - { - return ImageFileFormat.Bmp; - } - if (firstBytes[0] == 0x49 && firstBytes[1] == 0x49 && firstBytes[2] == 0x2A && firstBytes[3] == 0x00) - { - return ImageFileFormat.Tiff; - } - if (firstBytes[0] == 0x4D && firstBytes[1] == 0x4D && firstBytes[2] == 0x00 && firstBytes[3] == 0x2A) - { - return ImageFileFormat.Tiff; - } - if (firstBytes[4] == 0x6A && firstBytes[5] == 0x50 && firstBytes[6] == 0x20 && firstBytes[7] == 0x20) + + return GetFileFormatFromFirstBytes(firstBytes); + } + + public static ImageFileFormat GetFileFormatFromFirstBytes(byte[] firstBytes) + { + return firstBytes switch { - return ImageFileFormat.Jpeg2000; - } - return ImageFileFormat.Unspecified; + [0x89, 0x50, 0x4E, 0x47, ..] => ImageFileFormat.Png, + [0xFF, 0xD8, ..] => ImageFileFormat.Jpeg, + [0x42, 0x4D, ..] => ImageFileFormat.Bmp, + [0x49, 0x49, 0x2A, 0x00, ..] => ImageFileFormat.Tiff, + [0x4D, 0x4D, 0x00, 0x2A, ..] => ImageFileFormat.Tiff, + [_, _, _, _, 0x6A, 0x50, 0x20, 0x20, ..] => ImageFileFormat.Jpeg2000, + _ => ImageFileFormat.Unknown + }; } - protected ImageContext(Type imageType, IPdfRenderer? pdfRenderer = null) + protected ImageContext(Type imageType) { ImageType = imageType; - _pdfRenderer = pdfRenderer; } - // TODO: Add NotNullWhen attribute? - private bool MaybeRenderPdf(ImageFileStorage fileStorage, out IMemoryImage? renderedPdf) + private bool MaybeRenderPdf(ImageFileStorage fileStorage, IPdfRenderer? pdfRenderer, + [NotNullWhen(true)] out IMemoryImage? renderedPdf) { if (Path.GetExtension(fileStorage.FullPath).ToLowerInvariant() == ".pdf") { - if (_pdfRenderer == null) + if (pdfRenderer == null) { throw new InvalidOperationException( - "Unable to render pdf page as the ImageContext wasn't created with an IPdfRenderer."); + "Unable to render pdf page as the IRenderableImage didn't implement IPdfRendererProvider."); } - renderedPdf = _pdfRenderer.Render(this, fileStorage.FullPath, PdfRenderSize.Default).Single(); + renderedPdf = pdfRenderer.RenderPage(this, fileStorage.FullPath, PdfRenderSize.Default); return true; } renderedPdf = null; return false; } - private bool MaybeRenderPdf(ImageMemoryStorage memoryStorage, out IMemoryImage? renderedPdf) + private bool MaybeRenderPdf(ImageMemoryStorage memoryStorage, IPdfRenderer? pdfRenderer, + [NotNullWhen(true)] out IMemoryImage? renderedPdf) { if (memoryStorage.TypeHint == ".pdf") { - if (_pdfRenderer == null) + if (pdfRenderer == null) { throw new InvalidOperationException( - "Unable to render pdf page as the ImageContext wasn't created with an IPdfRenderer."); + "Unable to render pdf page as the IRenderableImage didn't implement IPdfRendererProvider."); } var stream = memoryStorage.Stream; - renderedPdf = _pdfRenderer.Render(this, stream.GetBuffer(), (int) stream.Length, PdfRenderSize.Default) - .Single(); + renderedPdf = pdfRenderer.RenderPage(this, stream.GetBuffer(), (int) stream.Length, PdfRenderSize.Default); return true; } renderedPdf = null; @@ -133,8 +122,6 @@ format is ImageFileFormat.Bmp or ImageFileFormat.Jpeg or ImageFileFormat.Png || protected virtual bool SupportsTiff => false; protected virtual bool SupportsJpeg2000 => false; - // TODO: Implement these 4 load methods here, calling protected abstract internal methods. - // TODO: That will let us implement common behavior (reading file formats, setting originalfileformat/logicalpixelformat) consistently. /// /// Loads an image from the given file path. /// @@ -156,11 +143,10 @@ public IMemoryImage Load(Stream stream) var format = GetFileFormatFromFirstBytes(stream); CheckSupportsFormat(format); var image = LoadCore(stream, format); - if (image.OriginalFileFormat == ImageFileFormat.Unspecified) + if (image.OriginalFileFormat == ImageFileFormat.Unknown) { image.OriginalFileFormat = format; } - image.UpdateLogicalPixelFormat(); return image; } @@ -234,11 +220,10 @@ private async IAsyncEnumerable WrapSource(IAsyncEnumerable + /// Checks if we can copy the source JPEG directly rather than re-encoding and suffering JPEG degradation. + /// + /// + /// + /// + internal static bool IsUntransformedJpegFile(this IRenderableImage image, out string jpegPath) + { + if (image is { Storage: ImageFileStorage fileStorage, TransformState.IsEmpty: true } && + ImageContext.GetFileFormatFromExtension(fileStorage.FullPath) == ImageFileFormat.Jpeg) + { + jpegPath = fileStorage.FullPath; + return true; + } + jpegPath = null!; + return false; + } + + /// + /// Saves the image to the given file path. If the file format is unspecified, it will be inferred from the + /// file extension if possible. + /// + /// The image to save. + /// The path to save the image file to. + /// The file format to use. + /// Options for saving, e.g. JPEG quality. + public static void Save(this IRenderableImage image, string path, + ImageFileFormat imageFormat = ImageFileFormat.Unknown, ImageSaveOptions? options = null) + { + if (imageFormat == ImageFileFormat.Unknown) + { + imageFormat = ImageContext.GetFileFormatFromExtension(path); + } + if (imageFormat == ImageFileFormat.Jpeg && image.IsUntransformedJpegFile(out var jpegPath)) + { + File.Copy(jpegPath, path); + return; + } + using var renderedImage = image.Render(); + renderedImage.Save(path, imageFormat, options); + } + + /// + /// Saves the image to the given stream. The file format must be specified. + /// + /// The image to save. + /// The stream to save the image to. + /// The file format to use. + /// Options for saving, e.g. JPEG quality. + public static void Save(this IRenderableImage image, Stream stream, + ImageFileFormat imageFormat = ImageFileFormat.Unknown, ImageSaveOptions? options = null) + { + if (imageFormat == ImageFileFormat.Unknown) + { + throw new ArgumentException("Format required to save to a stream", nameof(imageFormat)); + } + if (imageFormat == ImageFileFormat.Jpeg && image.IsUntransformedJpegFile(out var jpegPath)) + { + using var fileStream = File.OpenRead(jpegPath); + fileStream.CopyTo(stream); + return; + } + using var renderedImage = image.Render(); + renderedImage.Save(stream, imageFormat, options); + } + + /// + /// Saves the image to a new MemoryStream object. The file format must be specified. + /// + /// The image to save. + /// The file format to use. + /// Options for saving, e.g. JPEG quality. + public static MemoryStream SaveToMemoryStream(this IMemoryImage image, ImageFileFormat imageFormat, + ImageSaveOptions? options = null) + { + var stream = new MemoryStream(); + image.Save(stream, imageFormat, options); + stream.Seek(0, SeekOrigin.Begin); + return stream; + } + + /// + /// Saves the image to a new MemoryStream object. The file format must be specified. + /// + /// The image to save. + /// The file format to use. + /// Options for saving, e.g. JPEG quality. + public static MemoryStream SaveToMemoryStream(this IRenderableImage image, ImageFileFormat imageFormat, + ImageSaveOptions? options = null) + { + var stream = new MemoryStream(); + image.Save(stream, imageFormat, options); + return stream; + } + public static IMemoryImage PerformTransform(this IMemoryImage image, Transform transform) { return image.ImageContext.PerformTransform(image, transform); @@ -19,11 +115,16 @@ public static IMemoryImage PerformAllTransforms(this IMemoryImage image, IEnumer return image.ImageContext.PerformAllTransforms(image, transforms); } - public static void UpdateLogicalPixelFormat(this IMemoryImage image) + public static ImagePixelFormat UpdateLogicalPixelFormat(this IMemoryImage image) { + if (image.LogicalPixelFormat != ImagePixelFormat.Unknown) + { + return image.LogicalPixelFormat; + } var op = new LogicalPixelFormatOp(); op.Perform(image); image.LogicalPixelFormat = op.LogicalPixelFormat; + return image.LogicalPixelFormat; } /// @@ -47,6 +148,17 @@ public static IMemoryImage Copy(this IMemoryImage source) return source.CopyWithPixelFormat(source.PixelFormat); } + /// + /// Creates a new image with the same content, dimensions, and resolution as this image. + /// + /// + /// + /// + public static IMemoryImage Copy(this IMemoryImage source, ImageContext imageContext) + { + return source.CopyWithPixelFormat(imageContext, source.PixelFormat); + } + /// /// Creates a new image with the same content, dimensions, and resolution as this image, but possibly with a different pixel format. /// This can result in some loss of information (e.g. when converting color to gray or black/white). @@ -56,13 +168,28 @@ public static IMemoryImage Copy(this IMemoryImage source) /// public static IMemoryImage CopyWithPixelFormat(this IMemoryImage source, ImagePixelFormat pixelFormat) { - if (pixelFormat == ImagePixelFormat.Unsupported) throw new ArgumentException(); - var newImage = source.CopyBlankWithPixelFormat(pixelFormat); + return source.CopyWithPixelFormat(source.ImageContext, pixelFormat); + } + + /// + /// Creates a new image with the same content, dimensions, and resolution as this image, but possibly with a different pixel format. + /// This can result in some loss of information (e.g. when converting color to gray or black/white). + /// + /// + /// + /// + /// + public static IMemoryImage CopyWithPixelFormat(this IMemoryImage source, ImageContext imageContext, + ImagePixelFormat pixelFormat) + { + if (pixelFormat == ImagePixelFormat.Unknown) throw new ArgumentException(); + var newImage = source.CopyBlankWithPixelFormat(imageContext, pixelFormat); new CopyBitwiseImageOp().Perform(source, newImage); newImage.OriginalFileFormat = source.OriginalFileFormat; - if (source.LogicalPixelFormat < pixelFormat) + if (source.LogicalPixelFormat != ImagePixelFormat.Unknown) { - newImage.LogicalPixelFormat = source.LogicalPixelFormat; + newImage.LogicalPixelFormat = + source.LogicalPixelFormat < pixelFormat ? source.LogicalPixelFormat : pixelFormat; } return newImage; } @@ -85,19 +212,23 @@ public static IMemoryImage CopyBlank(this IMemoryImage source) /// public static IMemoryImage CopyBlankWithPixelFormat(this IMemoryImage source, ImagePixelFormat pixelFormat) { - if (pixelFormat == ImagePixelFormat.Unsupported) throw new ArgumentException(); - var newImage = source.ImageContext.Create(source.Width, source.Height, pixelFormat); - newImage.SetResolution(source.HorizontalResolution, source.VerticalResolution); - return newImage; + return CopyBlankWithPixelFormat(source, source.ImageContext, pixelFormat); } - public static MemoryStream SaveToMemoryStream(this IMemoryImage image, ImageFileFormat imageFormat, - int quality = -1) + /// + /// Creates a new (empty) image with the same dimensions and resolution as this image, and the specified pixel format. + /// + /// + /// + /// + /// + public static IMemoryImage CopyBlankWithPixelFormat(this IMemoryImage source, ImageContext imageContext, + ImagePixelFormat pixelFormat) { - var stream = new MemoryStream(); - image.Save(stream, imageFormat, quality); - stream.Seek(0, SeekOrigin.Begin); - return stream; + if (pixelFormat == ImagePixelFormat.Unknown) throw new ArgumentException(); + var newImage = imageContext.Create(source.Width, source.Height, pixelFormat); + newImage.SetResolution(source.HorizontalResolution, source.VerticalResolution); + return newImage; } public static string AsTypeHint(this ImageFileFormat imageFormat) diff --git a/NAPS2.Images/ImageFileFormat.cs b/NAPS2.Images/ImageFileFormat.cs index 996f291f20..416861993a 100644 --- a/NAPS2.Images/ImageFileFormat.cs +++ b/NAPS2.Images/ImageFileFormat.cs @@ -2,7 +2,7 @@ namespace NAPS2.Images; public enum ImageFileFormat { - Unspecified, + Unknown, Bmp, Jpeg, Jpeg2000, diff --git a/NAPS2.Images/Storage/ImageFileStorage.cs b/NAPS2.Images/ImageFileStorage.cs similarity index 90% rename from NAPS2.Images/Storage/ImageFileStorage.cs rename to NAPS2.Images/ImageFileStorage.cs index c841fae71b..2c4c2541aa 100644 --- a/NAPS2.Images/Storage/ImageFileStorage.cs +++ b/NAPS2.Images/ImageFileStorage.cs @@ -1,6 +1,6 @@ -namespace NAPS2.Images.Storage; +namespace NAPS2.Images; -public class ImageFileStorage : IImageStorage +internal class ImageFileStorage : IImageStorage { private bool _disposed; diff --git a/NAPS2.Images/Storage/ImageMemoryStorage.cs b/NAPS2.Images/ImageMemoryStorage.cs similarity index 91% rename from NAPS2.Images/Storage/ImageMemoryStorage.cs rename to NAPS2.Images/ImageMemoryStorage.cs index 5859c0332d..9ddcccc345 100644 --- a/NAPS2.Images/Storage/ImageMemoryStorage.cs +++ b/NAPS2.Images/ImageMemoryStorage.cs @@ -1,4 +1,4 @@ -namespace NAPS2.Images.Storage; +namespace NAPS2.Images; /// /// A special type of image storage that stores an image encoded as an in-memory PNG/JPEG/PDF stream. Normally in-memory @@ -6,7 +6,7 @@ /// serialization use cases where we don't know yet if the image will be stored in-memory or on disk. And for PDFs /// this is the only option for in-memory storage. /// -public class ImageMemoryStorage : IImageStorage +internal class ImageMemoryStorage : IImageStorage { public ImageMemoryStorage(MemoryStream stream, string typeHint) { diff --git a/NAPS2.Images/ImagePixelFormat.cs b/NAPS2.Images/ImagePixelFormat.cs index 3f7c379cce..5349b3ed63 100644 --- a/NAPS2.Images/ImagePixelFormat.cs +++ b/NAPS2.Images/ImagePixelFormat.cs @@ -2,9 +2,9 @@ public enum ImagePixelFormat { - Unsupported, + Unknown, BW1, Gray8, - RGB24, // This is actually BGR in the binary representation + RGB24, ARGB32 } \ No newline at end of file diff --git a/NAPS2.Images/ImageSaveOptions.cs b/NAPS2.Images/ImageSaveOptions.cs new file mode 100644 index 0000000000..4ea35eeb0c --- /dev/null +++ b/NAPS2.Images/ImageSaveOptions.cs @@ -0,0 +1,18 @@ +namespace NAPS2.Images; + +public record ImageSaveOptions +{ + /// + /// The quality parameter for JPEG compression, if applicable. -1 for default. + /// + public int Quality { get; init; } = -1; + + /// + /// The preferred pixel format that should be used for saving. If not specified, the image's LogicalPixelFormat + /// will be preferred to minimize disk space used. + /// + /// This will not result in a loss of information, e.g. if you set this to BW1 but your image has color in it, it + /// will have no effect. If you want to change the color information, use CopyWithPixelFormat before saving. + /// + public ImagePixelFormat PixelFormatHint { get; init; } +} \ No newline at end of file diff --git a/NAPS2.Images/LICENSE b/NAPS2.Images/LICENSE index 3f89270f9d..f62d4e9cab 100644 --- a/NAPS2.Images/LICENSE +++ b/NAPS2.Images/LICENSE @@ -1,7 +1,7 @@ NAPS2.Images https://www.github.com/cyanfish/naps2/ -Copyright 2009-2022 NAPS2 Contributors +Copyright 2009-2025 NAPS2 Contributors This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public diff --git a/NAPS2.Images/NAPS2.Images.csproj b/NAPS2.Images/NAPS2.Images.csproj index 7056ad8f25..4d6750708c 100644 --- a/NAPS2.Images/NAPS2.Images.csproj +++ b/NAPS2.Images/NAPS2.Images.csproj @@ -1,7 +1,7 @@ - net6;net462;netstandard2.0 + net6;net8;net462;netstandard2.0 enable true false @@ -9,14 +9,20 @@ true NAPS2.Images - Copyright 2022 Ben Olden-Cooligan + NAPS2.Images + Base image abstraction for NAPS2.Sdk. Don't reference this project directly. + naps2 + + - - - - + + + + + + @@ -26,9 +32,27 @@ <_Parameter1>NAPS2.Sdk.Tests + + <_Parameter1>NAPS2.Lib.Tests + <_Parameter1>NAPS2.Lib + + <_Parameter1>NAPS2.Images.Gdi + + + <_Parameter1>NAPS2.Images.Wpf + + + <_Parameter1>NAPS2.Images.Gtk + + + <_Parameter1>NAPS2.Images.Mac + + + <_Parameter1>NAPS2.Images.ImageSharp + diff --git a/NAPS2.Images/PdfRenderSize.cs b/NAPS2.Images/PdfRenderSize.cs index 5fd9df5948..4e43029173 100644 --- a/NAPS2.Images/PdfRenderSize.cs +++ b/NAPS2.Images/PdfRenderSize.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images; -public class PdfRenderSize +internal class PdfRenderSize { public static readonly PdfRenderSize Default = FromDpi(300); diff --git a/NAPS2.Images/PixelFormatHelper.cs b/NAPS2.Images/PixelFormatHelper.cs new file mode 100644 index 0000000000..9fbe7cbb05 --- /dev/null +++ b/NAPS2.Images/PixelFormatHelper.cs @@ -0,0 +1,60 @@ +namespace NAPS2.Images; + +// TODO: Use this for TIFF saving too, maybe +internal static class PixelFormatHelper +{ + public static PixelFormatHelper Create(T image, + ImagePixelFormat targetFormat = ImagePixelFormat.Unknown, + ImagePixelFormat minFormat = ImagePixelFormat.Unknown) where T : IMemoryImage + { + return new PixelFormatHelper(image, targetFormat, minFormat); + } +} + +internal class PixelFormatHelper : IDisposable where T : IMemoryImage +{ + public PixelFormatHelper(T image, ImagePixelFormat targetFormat, ImagePixelFormat minFormat) + { + image.UpdateLogicalPixelFormat(); + // TODO: Maybe we can be aware of the target filetype, e.g. JPEG doesn't have 1bpp. Although the specifics + // are going to be platform-dependent. + if (targetFormat == ImagePixelFormat.Unknown) + { + // If targetFormat is not specified, we'll use the logical format to minimize on-disk size. + targetFormat = image.LogicalPixelFormat; + } + if (targetFormat < image.LogicalPixelFormat) + { + // We never want to lose color information. + targetFormat = image.LogicalPixelFormat; + } + if (targetFormat < minFormat) + { + // GTK only supports RGB24/ARGB32 so it's pointless to target BW1/Gray8 as it will end up as RGB24 anyway. + targetFormat = minFormat; + } + + if (targetFormat != image.PixelFormat) + { + Image = (T) image.CopyWithPixelFormat(targetFormat); + IsCopy = true; + } + else + { + Image = image; + IsCopy = false; + } + } + + public T Image { get; } + + public bool IsCopy { get; } + + public void Dispose() + { + if (IsCopy) + { + Image.Dispose(); + } + } +} \ No newline at end of file diff --git a/NAPS2.Images/Bitwise/PixelInfo.cs b/NAPS2.Images/PixelInfo.cs similarity index 78% rename from NAPS2.Images/Bitwise/PixelInfo.cs rename to NAPS2.Images/PixelInfo.cs index f9d37378a4..ea21af4819 100644 --- a/NAPS2.Images/Bitwise/PixelInfo.cs +++ b/NAPS2.Images/PixelInfo.cs @@ -1,8 +1,8 @@ -namespace NAPS2.Images.Bitwise; +namespace NAPS2.Images; public class PixelInfo { - public PixelInfo(int width, int height, SubPixelType subPixelType, int stride = -1) + public PixelInfo(int width, int height, SubPixelType subPixelType, int stride = -1, int strideAlign = -1) { var minStride = (width * subPixelType.BitsPerPixel + 7) / 8; if (stride == -1) @@ -13,6 +13,10 @@ public PixelInfo(int width, int height, SubPixelType subPixelType, int stride = { throw new ArgumentException("Invalid stride"); } + if (strideAlign > 0) + { + stride = (stride + (strideAlign - 1)) / strideAlign * strideAlign; + } Width = width; Height = height; SubPixelType = subPixelType; diff --git a/NAPS2.Images/Bitwise/SubPixelType.cs b/NAPS2.Images/SubPixelType.cs similarity index 98% rename from NAPS2.Images/Bitwise/SubPixelType.cs rename to NAPS2.Images/SubPixelType.cs index 602792a3f9..5007cea6ad 100644 --- a/NAPS2.Images/Bitwise/SubPixelType.cs +++ b/NAPS2.Images/SubPixelType.cs @@ -1,4 +1,4 @@ -namespace NAPS2.Images.Bitwise; +namespace NAPS2.Images; public class SubPixelType { diff --git a/NAPS2.Images/TransformState.cs b/NAPS2.Images/TransformState.cs index 52f759e7a8..6ecdc84f88 100644 --- a/NAPS2.Images/TransformState.cs +++ b/NAPS2.Images/TransformState.cs @@ -3,7 +3,6 @@ namespace NAPS2.Images; -// TODO: Make sure transform equality works public record TransformState(ImmutableList Transforms) { public static readonly TransformState Empty = new(ImmutableList.Empty); diff --git a/NAPS2.Images/Storage/AbstractImageTransformer.cs b/NAPS2.Images/Transforms/AbstractImageTransformer.cs similarity index 83% rename from NAPS2.Images/Storage/AbstractImageTransformer.cs rename to NAPS2.Images/Transforms/AbstractImageTransformer.cs index 15a981648f..c37c04fe14 100644 --- a/NAPS2.Images/Storage/AbstractImageTransformer.cs +++ b/NAPS2.Images/Transforms/AbstractImageTransformer.cs @@ -1,8 +1,9 @@ using NAPS2.Images.Bitwise; +using NAPS2.Util; -namespace NAPS2.Images.Storage; +namespace NAPS2.Images.Transforms; -public abstract class AbstractImageTransformer where TImage : IMemoryImage +internal abstract class AbstractImageTransformer where TImage : IMemoryImage { protected AbstractImageTransformer(ImageContext imageContext) { @@ -13,7 +14,7 @@ protected AbstractImageTransformer(ImageContext imageContext) public TImage Apply(TImage image, Transform transform) { - if (image.PixelFormat == ImagePixelFormat.Unsupported) + if (image.PixelFormat == ImagePixelFormat.Unknown) { throw new ArgumentException("Unsupported pixel format for transforms"); } @@ -41,6 +42,8 @@ public TImage Apply(TImage image, Transform transform) return PerformTransform(image, thumbnailTransform); case BlackWhiteTransform blackWhiteTransform: return PerformTransform(image, blackWhiteTransform); + case GrayscaleTransform grayscaleTransform: + return PerformTransform(image, grayscaleTransform); case ColorBitDepthTransform colorBitDepthTransform: return PerformTransform(image, colorBitDepthTransform); case CorrectionTransform correctionTransform: @@ -52,16 +55,21 @@ public TImage Apply(TImage image, Transform transform) private TImage PerformTransform(TImage image, CorrectionTransform transform) { + image.UpdateLogicalPixelFormat(); + if (image.LogicalPixelFormat == ImagePixelFormat.BW1) + { + return image; + } // TODO: Include deskew? // TODO: Add border detection/removal? After deskew. var stopwatch = Stopwatch.StartNew(); - ColumnColorOp.PerformFullOp(image); - Console.WriteLine($"Column color op time: {stopwatch.ElapsedMilliseconds}"); + // ColumnColorOp.PerformFullOp(image); + // Console.WriteLine($"Column color op time: {stopwatch.ElapsedMilliseconds}"); stopwatch.Restart(); if (transform.Mode == CorrectionMode.Document) { WhiteBlackPointOp.PerformFullOp(image, transform.Mode); - Console.WriteLine($"White/black point op time: {stopwatch.ElapsedMilliseconds}"); + // Console.WriteLine($"White/black point op time: {stopwatch.ElapsedMilliseconds}"); stopwatch.Restart(); // A previous version ran a filter pass before white/black point correction with the theory that it could // help the accuracy of that correction. @@ -70,7 +78,7 @@ private TImage PerformTransform(TImage image, CorrectionTransform transform) // not yet corrected) it could potentially remove fine details. var image2 = (TImage) image.CopyBlank(); new BilateralFilterOp().Perform(image, image2); - Console.WriteLine($"Bilateral filter op time: {stopwatch.ElapsedMilliseconds}"); + // Console.WriteLine($"Bilateral filter op time: {stopwatch.ElapsedMilliseconds}"); stopwatch.Restart(); image.Dispose(); return image2; @@ -78,7 +86,7 @@ private TImage PerformTransform(TImage image, CorrectionTransform transform) else { WhiteBlackPointOp.PerformFullOp(image, transform.Mode); - Console.WriteLine($"White/black point op time: {stopwatch.ElapsedMilliseconds}"); + // Console.WriteLine($"White/black point op time: {stopwatch.ElapsedMilliseconds}"); stopwatch.Restart(); return image; } @@ -86,12 +94,6 @@ private TImage PerformTransform(TImage image, CorrectionTransform transform) protected virtual TImage PerformTransform(TImage image, BrightnessTransform transform) { - if (image.PixelFormat is ImagePixelFormat.BW1) - { - // No need to handle black & white since brightness is a null transform - return image; - } - float brightnessNormalized = transform.Brightness / 1000f; EnsurePixelFormat(ref image); new BrightnessBitwiseImageOp(brightnessNormalized).Perform(image); @@ -162,11 +164,11 @@ protected virtual TImage PerformTransform(TImage image, CropTransform transform) double xScale = image.Width / (double) (transform.OriginalWidth ?? image.Width), yScale = image.Height / (double) (transform.OriginalHeight ?? image.Height); - int x = Clamp((int) Math.Round(transform.Left * xScale), 0, image.Width - 1); - int y = Clamp((int) Math.Round(transform.Top * yScale), 0, image.Height - 1); - int width = Clamp(image.Width - (int) Math.Round((transform.Left + transform.Right) * xScale), 1, + int x = ((int) Math.Round(transform.Left * xScale)).Clamp(0, image.Width - 1); + int y = ((int) Math.Round(transform.Top * yScale)).Clamp(0, image.Height - 1); + int width = (image.Width - (int) Math.Round((transform.Left + transform.Right) * xScale)).Clamp(1, image.Width - x); - int height = Clamp(image.Height - (int) Math.Round((transform.Top + transform.Bottom) * yScale), 1, + int height = (image.Height - (int) Math.Round((transform.Top + transform.Bottom) * yScale)).Clamp(1, image.Height - y); var result = ImageContext.Create(width, height, image.PixelFormat); @@ -183,23 +185,10 @@ protected virtual TImage PerformTransform(TImage image, CropTransform transform) return (TImage) result; } - private int Clamp(int val, int min, int max) - { - if (val.CompareTo(min) < 0) - { - return min; - } - if (val.CompareTo(max) > 0) - { - return max; - } - return val; - } - protected virtual TImage PerformTransform(TImage image, ScaleTransform transform) { - var width = (int) Math.Round(image.Width * transform.ScaleFactor); - var height = (int) Math.Round(image.Height * transform.ScaleFactor); + var width = (int) Math.Max(Math.Round(image.Width * transform.ScaleFactor), 1); + var height = (int) Math.Max(Math.Round(image.Height * transform.ScaleFactor), 1); return PerformTransform(image, new ResizeTransform(width, height)); } @@ -234,6 +223,18 @@ protected virtual TImage PerformTransform(TImage image, BlackWhiteTransform tran return (TImage) monoBitmap; } + protected virtual TImage PerformTransform(TImage image, GrayscaleTransform transform) + { + if (image.PixelFormat is ImagePixelFormat.BW1 or ImagePixelFormat.Gray8) + { + return image; + } + + var grayscaleBitmap = image.CopyWithPixelFormat(ImagePixelFormat.Gray8); + image.Dispose(); + return (TImage) grayscaleBitmap; + } + protected virtual TImage PerformTransform(TImage image, ColorBitDepthTransform transform) { if (image.PixelFormat is ImagePixelFormat.RGB24 or ImagePixelFormat.ARGB32) @@ -265,7 +266,7 @@ protected void EnsurePixelFormat(ref TImage image) /// The result that may be replaced. protected void OptimizePixelFormat(TImage original, ref TImage result) { - if (original.PixelFormat == ImagePixelFormat.BW1) + if (original.UpdateLogicalPixelFormat() == ImagePixelFormat.BW1) { result = PerformTransform(result, new BlackWhiteTransform()); } diff --git a/NAPS2.Images/Transforms/BlackWhiteTransform.cs b/NAPS2.Images/Transforms/BlackWhiteTransform.cs index 07290355c9..e0400fcbed 100644 --- a/NAPS2.Images/Transforms/BlackWhiteTransform.cs +++ b/NAPS2.Images/Transforms/BlackWhiteTransform.cs @@ -3,7 +3,7 @@ namespace NAPS2.Images.Transforms; -public class BlackWhiteTransform : Transform +public record BlackWhiteTransform : Transform { public BlackWhiteTransform() { diff --git a/NAPS2.Images/Transforms/BrightnessTransform.cs b/NAPS2.Images/Transforms/BrightnessTransform.cs index e705808df2..9462021d71 100644 --- a/NAPS2.Images/Transforms/BrightnessTransform.cs +++ b/NAPS2.Images/Transforms/BrightnessTransform.cs @@ -3,7 +3,7 @@ namespace NAPS2.Images.Transforms; -public class BrightnessTransform : Transform +public record BrightnessTransform : Transform { public BrightnessTransform() { diff --git a/NAPS2.Images/Transforms/ColorBitDepthTransform.cs b/NAPS2.Images/Transforms/ColorBitDepthTransform.cs index a187fe39e2..732264d727 100644 --- a/NAPS2.Images/Transforms/ColorBitDepthTransform.cs +++ b/NAPS2.Images/Transforms/ColorBitDepthTransform.cs @@ -1,6 +1,5 @@ namespace NAPS2.Images.Transforms; -// TODO: Maybe replace with CopyWithPixelFormat? -public class ColorBitDepthTransform : Transform +public record ColorBitDepthTransform : Transform { } \ No newline at end of file diff --git a/NAPS2.Images/Transforms/CombineOrientation.cs b/NAPS2.Images/Transforms/CombineOrientation.cs new file mode 100644 index 0000000000..a7ffe620bb --- /dev/null +++ b/NAPS2.Images/Transforms/CombineOrientation.cs @@ -0,0 +1,7 @@ +namespace NAPS2.Images.Transforms; + +public enum CombineOrientation +{ + Horizontal, + Vertical +} \ No newline at end of file diff --git a/NAPS2.Images/Transforms/CorrectionTransform.cs b/NAPS2.Images/Transforms/CorrectionTransform.cs index 6aa941dba6..cadce1134f 100644 --- a/NAPS2.Images/Transforms/CorrectionTransform.cs +++ b/NAPS2.Images/Transforms/CorrectionTransform.cs @@ -1,7 +1,6 @@ namespace NAPS2.Images.Transforms; -// TODO: experimental -public class CorrectionTransform : Transform +public record CorrectionTransform : Transform { public CorrectionTransform() { @@ -15,4 +14,17 @@ public CorrectionTransform(CorrectionMode mode) public CorrectionMode Mode { get; private set; } public override bool IsNull => Mode == CorrectionMode.None; + + public override bool CanSimplify(Transform other) => (other as CorrectionTransform)?.Mode == Mode; + + public override Transform Simplify(Transform other) + { + if ((other as CorrectionTransform)?.Mode != Mode) + { + throw new InvalidOperationException(); + } + // It's not technically correct to say that this transform is idempotent, but in practice if you run it twice in + // a row we probably only want it to be applied once. + return this; + } } \ No newline at end of file diff --git a/NAPS2.Images/Transforms/CropTransform.cs b/NAPS2.Images/Transforms/CropTransform.cs index b994109c8b..b6d96fcc77 100644 --- a/NAPS2.Images/Transforms/CropTransform.cs +++ b/NAPS2.Images/Transforms/CropTransform.cs @@ -4,7 +4,7 @@ namespace NAPS2.Images.Transforms; -public class CropTransform : Transform +public record CropTransform : Transform { public CropTransform() { diff --git a/NAPS2.Images/Transforms/GrayscaleTransform.cs b/NAPS2.Images/Transforms/GrayscaleTransform.cs new file mode 100644 index 0000000000..4e3b598c4c --- /dev/null +++ b/NAPS2.Images/Transforms/GrayscaleTransform.cs @@ -0,0 +1,5 @@ +namespace NAPS2.Images.Transforms; + +public record GrayscaleTransform : Transform +{ +} \ No newline at end of file diff --git a/NAPS2.Images/Transforms/HueTransform.cs b/NAPS2.Images/Transforms/HueTransform.cs index 91e50aecd5..03f91de9ec 100644 --- a/NAPS2.Images/Transforms/HueTransform.cs +++ b/NAPS2.Images/Transforms/HueTransform.cs @@ -3,7 +3,7 @@ namespace NAPS2.Images.Transforms; -public class HueTransform : Transform +public record HueTransform : Transform { public HueTransform() { diff --git a/NAPS2.Images/Transforms/MoreImageTransforms.cs b/NAPS2.Images/Transforms/MoreImageTransforms.cs new file mode 100644 index 0000000000..32fa0e9bab --- /dev/null +++ b/NAPS2.Images/Transforms/MoreImageTransforms.cs @@ -0,0 +1,50 @@ +using NAPS2.Images.Bitwise; + +namespace NAPS2.Images.Transforms; + +public static class MoreImageTransforms +{ + public static IMemoryImage Combine(IMemoryImage first, IMemoryImage second, CombineOrientation orientation, + double offset = 0.5) + { + var imageContext = first.ImageContext; + var pixelFormat = first.PixelFormat > second.PixelFormat + ? first.PixelFormat + : second.PixelFormat; + int width = orientation == CombineOrientation.Horizontal + ? first.Width + second.Width + : Math.Max(first.Width, second.Width); + int height = orientation == CombineOrientation.Vertical + ? first.Height + second.Height + : Math.Max(first.Height, second.Height); + + var combinedImage = imageContext.Create(width, height, pixelFormat); + combinedImage.SetResolution( + Math.Max(first.HorizontalResolution, second.HorizontalResolution), + Math.Max(first.VerticalResolution, second.VerticalResolution)); + + FillColorImageOp.White.Perform(combinedImage); + + new CopyBitwiseImageOp + { + DestXOffset = orientation == CombineOrientation.Horizontal ? 0 : + first.Width > second.Width ? 0 : + (int) (offset * (second.Width - first.Width)), + DestYOffset = orientation == CombineOrientation.Vertical ? 0 : + first.Height > second.Height ? 0 : + (int) (offset * (second.Height - first.Height)) + }.Perform(first, combinedImage); + + new CopyBitwiseImageOp + { + DestXOffset = orientation == CombineOrientation.Horizontal ? first.Width : + second.Width > first.Width ? 0 : + (int) (offset * (first.Width - second.Width)), + DestYOffset = orientation == CombineOrientation.Vertical ? first.Height : + second.Height > first.Height ? 0 : + (int) (offset * (first.Height - second.Height)) + }.Perform(second, combinedImage); + + return combinedImage; + } +} \ No newline at end of file diff --git a/NAPS2.Images/Transforms/ResizeTransform.cs b/NAPS2.Images/Transforms/ResizeTransform.cs index 9c5cd6b2f2..bace9cb803 100644 --- a/NAPS2.Images/Transforms/ResizeTransform.cs +++ b/NAPS2.Images/Transforms/ResizeTransform.cs @@ -3,7 +3,7 @@ namespace NAPS2.Images.Transforms; -public class ResizeTransform : Transform +public record ResizeTransform : Transform { public ResizeTransform() { diff --git a/NAPS2.Images/Transforms/RotationTransform.cs b/NAPS2.Images/Transforms/RotationTransform.cs index c1c6cb8e81..def7a50ba6 100644 --- a/NAPS2.Images/Transforms/RotationTransform.cs +++ b/NAPS2.Images/Transforms/RotationTransform.cs @@ -4,7 +4,7 @@ namespace NAPS2.Images.Transforms; -public class RotationTransform : Transform +public record RotationTransform : Transform { public const double TOLERANCE = 0.001; diff --git a/NAPS2.Images/Transforms/SaturationTransform.cs b/NAPS2.Images/Transforms/SaturationTransform.cs index e0a5259b9d..88a6010afe 100644 --- a/NAPS2.Images/Transforms/SaturationTransform.cs +++ b/NAPS2.Images/Transforms/SaturationTransform.cs @@ -3,7 +3,7 @@ namespace NAPS2.Images.Transforms; -public class SaturationTransform : Transform +public record SaturationTransform : Transform { public SaturationTransform() { diff --git a/NAPS2.Images/Transforms/ScaleTransform.cs b/NAPS2.Images/Transforms/ScaleTransform.cs index de2b6244d7..2f523ab16f 100644 --- a/NAPS2.Images/Transforms/ScaleTransform.cs +++ b/NAPS2.Images/Transforms/ScaleTransform.cs @@ -3,7 +3,7 @@ namespace NAPS2.Images.Transforms; -public class ScaleTransform : Transform +public record ScaleTransform : Transform { public ScaleTransform() { diff --git a/NAPS2.Images/Transforms/SharpenTransform.cs b/NAPS2.Images/Transforms/SharpenTransform.cs index 3140d33ee3..0128b96d33 100644 --- a/NAPS2.Images/Transforms/SharpenTransform.cs +++ b/NAPS2.Images/Transforms/SharpenTransform.cs @@ -3,7 +3,7 @@ namespace NAPS2.Images.Transforms; -public class SharpenTransform : Transform +public record SharpenTransform : Transform { public SharpenTransform() { diff --git a/NAPS2.Images/Transforms/ThumbnailTransform.cs b/NAPS2.Images/Transforms/ThumbnailTransform.cs index 7e0a94b02e..96d84ec34f 100644 --- a/NAPS2.Images/Transforms/ThumbnailTransform.cs +++ b/NAPS2.Images/Transforms/ThumbnailTransform.cs @@ -1,9 +1,8 @@ - -// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local namespace NAPS2.Images.Transforms; -public class ThumbnailTransform : Transform +public record ThumbnailTransform : Transform { public const int DEFAULT_SIZE = 256; @@ -30,7 +29,7 @@ public ThumbnailTransform(int size) width = Size; left = 0; // Scale the drawing height to match the original bitmap's aspect ratio - height = (int)(originalHeight * (Size / (double)originalWidth)); + height = (int) Math.Max(originalHeight * (Size / (double) originalWidth), 1); // Center the drawing vertically top = (Size - height) / 2; } @@ -40,7 +39,7 @@ public ThumbnailTransform(int size) height = Size; top = 0; // Scale the drawing width to match the original bitmap's aspect ratio - width = (int)(originalWidth * (Size / (double)originalHeight)); + width = (int) Math.Max(originalWidth * (Size / (double) originalHeight), 1); // Center the drawing horizontally left = (Size - width) / 2; } diff --git a/NAPS2.Images/Transforms/Transform.cs b/NAPS2.Images/Transforms/Transform.cs index e4285f298f..74b18c40a3 100644 --- a/NAPS2.Images/Transforms/Transform.cs +++ b/NAPS2.Images/Transforms/Transform.cs @@ -1,9 +1,25 @@ using System.Collections.Immutable; +using System.Reflection; +using NAPS2.Serialization; namespace NAPS2.Images.Transforms; -public abstract class Transform +public abstract record Transform { + static Transform() + { + XmlSerializer.RegisterCustomTypes(new TransformTypes()); + } + + private class TransformTypes : CustomXmlTypes + { + protected override Type[] GetKnownTypes() => Assembly + .GetExecutingAssembly() + .GetTypes() + .Where(t => typeof(Transform).IsAssignableFrom(t)) + .ToArray(); + } + /// /// Appends the specified transform to the list, merging with the previous transform on the list if simplication is possible. /// diff --git a/NAPS2.Images/Transforms/TrueContrastTransform.cs b/NAPS2.Images/Transforms/TrueContrastTransform.cs index 72decf44cd..483e793361 100644 --- a/NAPS2.Images/Transforms/TrueContrastTransform.cs +++ b/NAPS2.Images/Transforms/TrueContrastTransform.cs @@ -3,7 +3,7 @@ namespace NAPS2.Images.Transforms; -public class TrueContrastTransform : Transform +public record TrueContrastTransform : Transform { public TrueContrastTransform() { diff --git a/NAPS2.Images/Util/IsExternalInit.cs b/NAPS2.Images/Util/IsExternalInit.cs deleted file mode 100644 index ba0435b8b5..0000000000 --- a/NAPS2.Images/Util/IsExternalInit.cs +++ /dev/null @@ -1,5 +0,0 @@ -// https://sergiopedri.medium.com/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb -// ReSharper disable once CheckNamespace -namespace System.Runtime.CompilerServices; - -public static class IsExternalInit {} \ No newline at end of file diff --git a/NAPS2.Internals/.gitignore b/NAPS2.Internals/.gitignore new file mode 100644 index 0000000000..50815a5c0e --- /dev/null +++ b/NAPS2.Internals/.gitignore @@ -0,0 +1,32 @@ +Thumbs.db +*.obj +*.exe +*.pdb +*.user +*.aps +*.pch +*.vspscc +*_i.c +*_p.c +*.ncb +*.suo +*.sln.docstates +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +[Bb]in +[Dd]ebug*/ +*.lib +*.sbr +obj/ +[Rr]elease*/ +_ReSharper*/ +[Tt]est[Rr]esult* +*.vssscc +$tf*/ +publish/ +bin/ +temp/ \ No newline at end of file diff --git a/NAPS2.Internals/LICENSE b/NAPS2.Internals/LICENSE new file mode 100644 index 0000000000..9920ca593b --- /dev/null +++ b/NAPS2.Internals/LICENSE @@ -0,0 +1,518 @@ +NAPS2.Internals +https://www.github.com/cyanfish/naps2/ + +Copyright 2009-2025 NAPS2 Contributors + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/NAPS2.Internals/NAPS2.Internals.csproj b/NAPS2.Internals/NAPS2.Internals.csproj new file mode 100644 index 0000000000..bbb4993c1e --- /dev/null +++ b/NAPS2.Internals/NAPS2.Internals.csproj @@ -0,0 +1,24 @@ + + + + net6;net8;net462;netstandard2.0 + enable + NAPS2 + + NAPS2.Internals + NAPS2.Internals + Internal code for NAPS2.Sdk. Don't reference this project directly. + naps2 + + + + + + + + + + + + + \ No newline at end of file diff --git a/NAPS2.Sdk/Serialization/CustomXmlSerializer.cs b/NAPS2.Internals/Serialization/CustomXmlSerializer.cs similarity index 100% rename from NAPS2.Sdk/Serialization/CustomXmlSerializer.cs rename to NAPS2.Internals/Serialization/CustomXmlSerializer.cs diff --git a/NAPS2.Sdk/Serialization/CustomXmlTypes.cs b/NAPS2.Internals/Serialization/CustomXmlTypes.cs similarity index 100% rename from NAPS2.Sdk/Serialization/CustomXmlTypes.cs rename to NAPS2.Internals/Serialization/CustomXmlTypes.cs diff --git a/NAPS2.Sdk/Serialization/ISerializer.cs b/NAPS2.Internals/Serialization/ISerializer.cs similarity index 100% rename from NAPS2.Sdk/Serialization/ISerializer.cs rename to NAPS2.Internals/Serialization/ISerializer.cs diff --git a/NAPS2.Sdk/Serialization/SerializerExtensions.cs b/NAPS2.Internals/Serialization/SerializerExtensions.cs similarity index 100% rename from NAPS2.Sdk/Serialization/SerializerExtensions.cs rename to NAPS2.Internals/Serialization/SerializerExtensions.cs diff --git a/NAPS2.Sdk/Serialization/XmlObjectSerializer.cs b/NAPS2.Internals/Serialization/XmlObjectSerializer.cs similarity index 100% rename from NAPS2.Sdk/Serialization/XmlObjectSerializer.cs rename to NAPS2.Internals/Serialization/XmlObjectSerializer.cs diff --git a/NAPS2.Sdk/Serialization/XmlSerializer.cs b/NAPS2.Internals/Serialization/XmlSerializer.cs similarity index 89% rename from NAPS2.Sdk/Serialization/XmlSerializer.cs rename to NAPS2.Internals/Serialization/XmlSerializer.cs index 7d6f24e1b8..5149dbe800 100644 --- a/NAPS2.Sdk/Serialization/XmlSerializer.cs +++ b/NAPS2.Internals/Serialization/XmlSerializer.cs @@ -6,6 +6,7 @@ using System.Text; using System.Xml; using System.Xml.Serialization; +using NAPS2.Util; namespace NAPS2.Serialization; @@ -17,13 +18,13 @@ public abstract class XmlSerializer protected static readonly Dictionary> CustomTypesCache = new(); - protected static readonly List ArrayLikeTypes = new() - { + protected static readonly List ArrayLikeTypes = + [ typeof(List<>), typeof(HashSet<>), typeof(ImmutableList<>), - typeof(ImmutableHashSet<>), - }; + typeof(ImmutableHashSet<>) + ]; protected static readonly Dictionary TypeInfoCache = new() { @@ -49,17 +50,8 @@ public abstract class XmlSerializer { typeof(ImmutableList<>), new XmlTypeInfo { CustomSerializer = new ImmutableListSerializer() } }, { typeof(ImmutableHashSet<>), new XmlTypeInfo { CustomSerializer = new ImmutableHashSetSerializer() } }, { typeof(DateTime), new XmlTypeInfo { CustomSerializer = new DateTimeSerializer() } }, + { typeof(Guid), new XmlTypeInfo { CustomSerializer = new GuidSerializer() } }, { typeof(Nullable<>), new XmlTypeInfo { CustomSerializer = new NullableSerializer() } }, - { - typeof(Transform), new XmlTypeInfo - { - KnownTypesByElementName = Assembly - .GetAssembly(typeof(Transform))! - .GetTypes() - .Where(t => typeof(Transform).IsAssignableFrom(t)) - .ToDictionary(GetElementNameForType, t => t) - } - }, }; private static readonly Dictionary PrimitiveTypesByElementName = @@ -88,7 +80,7 @@ public static void RegisterCustomTypes(Type type, CustomXmlTypes customTypes) { lock (TypeInfoCache) { - CustomTypesCache.GetOrSet(type, new List()).Add(customTypes); + CustomTypesCache.GetOrSet(type, []).Add(customTypes); } } @@ -151,7 +143,16 @@ protected static object DeserializeInternalNonNull(XElement element, Type type) { return typeInfo.CustomSerializer.DeserializeObject(element, actualType); } - var obj = Activator.CreateInstance(actualType, true)!; + object obj; + try + { + obj = Activator.CreateInstance(actualType, true)!; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Could not create type for deserialization: {actualType.FullName}", + ex); + } foreach (var propInfo in typeInfo.Properties!) { // TODO: Detect unmapped elements @@ -240,6 +241,9 @@ protected static XmlTypeInfo GetTypeInfo(Type type) if (typeInfo.CustomSerializer == null) { + // Prevent infinite recursion by populating the partial type info + // Cycles in type info are ok (e.g. Node.Parent is a Node), just not in the actual objects + TypeInfoCache[type] = typeInfo; var knownTypesFromPropertyTypes = props.SelectMany(x => GetTypeInfo(x.PropertyType).KnownTypesByElementName); var knownTypesFromActualType = GetKnownTypes(type); @@ -296,7 +300,7 @@ protected class CharSerializer : CustomXmlSerializer { protected override void Serialize(char obj, XElement element) { - element.Value = obj.ToString(); + element.Value = obj.ToString(CultureInfo.InvariantCulture); } protected override char Deserialize(XElement element) @@ -335,12 +339,12 @@ protected class ByteSerializer : CustomXmlSerializer { protected override void Serialize(byte obj, XElement element) { - element.Value = obj.ToString(); + element.Value = obj.ToString(CultureInfo.InvariantCulture); } protected override byte Deserialize(XElement element) { - return byte.Parse(element.Value); + return byte.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -348,12 +352,12 @@ protected class SByteSerializer : CustomXmlSerializer { protected override void Serialize(sbyte obj, XElement element) { - element.Value = obj.ToString(); + element.Value = obj.ToString(CultureInfo.InvariantCulture); } protected override sbyte Deserialize(XElement element) { - return sbyte.Parse(element.Value); + return sbyte.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -361,12 +365,12 @@ protected class Int16Serializer : CustomXmlSerializer { protected override void Serialize(short obj, XElement element) { - element.Value = obj.ToString(); + element.Value = obj.ToString(CultureInfo.InvariantCulture); } protected override short Deserialize(XElement element) { - return short.Parse(element.Value); + return short.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -374,12 +378,12 @@ protected class UInt16Serializer : CustomXmlSerializer { protected override void Serialize(ushort obj, XElement element) { - element.Value = obj.ToString(); + element.Value = obj.ToString(CultureInfo.InvariantCulture); } protected override ushort Deserialize(XElement element) { - return ushort.Parse(element.Value); + return ushort.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -387,12 +391,12 @@ protected class Int32Serializer : CustomXmlSerializer { protected override void Serialize(int obj, XElement element) { - element.Value = obj.ToString(); + element.Value = obj.ToString(CultureInfo.InvariantCulture); } protected override int Deserialize(XElement element) { - return int.Parse(element.Value); + return int.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -400,12 +404,12 @@ protected class UInt32Serializer : CustomXmlSerializer { protected override void Serialize(uint obj, XElement element) { - element.Value = obj.ToString(); + element.Value = obj.ToString(CultureInfo.InvariantCulture); } protected override uint Deserialize(XElement element) { - return uint.Parse(element.Value); + return uint.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -413,12 +417,12 @@ protected class Int64Serializer : CustomXmlSerializer { protected override void Serialize(long obj, XElement element) { - element.Value = obj.ToString(); + element.Value = obj.ToString(CultureInfo.InvariantCulture); } protected override long Deserialize(XElement element) { - return long.Parse(element.Value); + return long.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -426,12 +430,12 @@ protected class UInt64Serializer : CustomXmlSerializer { protected override void Serialize(ulong obj, XElement element) { - element.Value = obj.ToString(); + element.Value = obj.ToString(CultureInfo.InvariantCulture); } protected override ulong Deserialize(XElement element) { - return ulong.Parse(element.Value); + return ulong.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -444,7 +448,7 @@ protected override void Serialize(float obj, XElement element) protected override float Deserialize(XElement element) { - return float.Parse(element.Value); + return float.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -457,7 +461,7 @@ protected override void Serialize(double obj, XElement element) protected override double Deserialize(XElement element) { - return double.Parse(element.Value); + return double.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -470,7 +474,7 @@ protected override void Serialize(decimal obj, XElement element) protected override decimal Deserialize(XElement element) { - return decimal.Parse(element.Value); + return decimal.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -478,12 +482,12 @@ protected class IntPtrSerializer : CustomXmlSerializer { protected override void Serialize(IntPtr obj, XElement element) { - element.Value = obj.ToString(); + element.Value = ((long) obj).ToString(CultureInfo.InvariantCulture); } protected override IntPtr Deserialize(XElement element) { - return (IntPtr) long.Parse(element.Value); + return (IntPtr) long.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -491,12 +495,12 @@ protected class UIntPtrSerializer : CustomXmlSerializer { protected override void Serialize(UIntPtr obj, XElement element) { - element.Value = obj.ToString(); + element.Value = ((ulong) obj).ToString(CultureInfo.InvariantCulture); } protected override UIntPtr Deserialize(XElement element) { - return (UIntPtr) ulong.Parse(element.Value); + return (UIntPtr) ulong.Parse(element.Value, CultureInfo.InvariantCulture); } } @@ -652,6 +656,19 @@ protected override DateTime Deserialize(XElement element) } } + protected class GuidSerializer : CustomXmlSerializer + { + protected override void Serialize(Guid obj, XElement element) + { + element.Value = obj.ToString(); + } + + protected override Guid Deserialize(XElement element) + { + return Guid.Parse(element.Value); + } + } + protected class NullableSerializer : CustomXmlSerializer { public override void SerializeObject(object obj, XElement element, Type type) diff --git a/NAPS2.Images/Util/AsyncProducers.cs b/NAPS2.Internals/Threading/AsyncProducers.cs similarity index 86% rename from NAPS2.Images/Util/AsyncProducers.cs rename to NAPS2.Internals/Threading/AsyncProducers.cs index e164a29d5c..c7a58bf248 100644 --- a/NAPS2.Images/Util/AsyncProducers.cs +++ b/NAPS2.Internals/Threading/AsyncProducers.cs @@ -1,8 +1,14 @@ // ReSharper disable once CheckNamespace namespace NAPS2.Util; -internal static class AsyncProducers +public static class AsyncProducers { +#pragma warning disable CS1998 + public static async IAsyncEnumerable Empty() + { + yield break; + } + public static IAsyncEnumerable RunProducer(ItemProducer producer) where T : class { return RunProducer(new AsyncItemProducer(produce => diff --git a/NAPS2.Images/Util/AsyncSink.cs b/NAPS2.Internals/Threading/AsyncSink.cs similarity index 91% rename from NAPS2.Images/Util/AsyncSink.cs rename to NAPS2.Internals/Threading/AsyncSink.cs index 4ac5928303..fa2e0b1984 100644 --- a/NAPS2.Images/Util/AsyncSink.cs +++ b/NAPS2.Internals/Threading/AsyncSink.cs @@ -1,14 +1,11 @@ // ReSharper disable once CheckNamespace namespace NAPS2.Util; -internal class AsyncSink where T : class +public class AsyncSink where T : class { private static TaskCompletionSource CreateTcs() => new(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly List> _items = new() - { - CreateTcs() - }; + private readonly List> _items = [CreateTcs()]; private bool _completed; public async IAsyncEnumerable AsAsyncEnumerable() diff --git a/NAPS2.Sdk/Threading/RefreshThrottle.cs b/NAPS2.Internals/Threading/RefreshThrottle.cs similarity index 100% rename from NAPS2.Sdk/Threading/RefreshThrottle.cs rename to NAPS2.Internals/Threading/RefreshThrottle.cs diff --git a/NAPS2.Sdk/Threading/SmoothProgress.cs b/NAPS2.Internals/Threading/SmoothProgress.cs similarity index 96% rename from NAPS2.Sdk/Threading/SmoothProgress.cs rename to NAPS2.Internals/Threading/SmoothProgress.cs index cc6dc17cf9..7b994a1f57 100644 --- a/NAPS2.Sdk/Threading/SmoothProgress.cs +++ b/NAPS2.Internals/Threading/SmoothProgress.cs @@ -39,9 +39,9 @@ public void Reset() _stopwatch = Stopwatch.StartNew(); - _previousInputPos = new LinkedList(); + _previousInputPos = []; _previousInputPos.AddLast(0); - _previousInputTimes = new LinkedList(); + _previousInputTimes = []; _previousInputTimes.AddLast(0); } } diff --git a/NAPS2.Sdk/Threading/TaskExtensions.cs b/NAPS2.Internals/Threading/TaskExtensions.cs similarity index 100% rename from NAPS2.Sdk/Threading/TaskExtensions.cs rename to NAPS2.Internals/Threading/TaskExtensions.cs diff --git a/NAPS2.Sdk/Threading/TimedThrottle.cs b/NAPS2.Internals/Threading/TimedThrottle.cs similarity index 100% rename from NAPS2.Sdk/Threading/TimedThrottle.cs rename to NAPS2.Internals/Threading/TimedThrottle.cs diff --git a/NAPS2.Sdk/Unmanaged/UnmanagedArray.cs b/NAPS2.Internals/Unmanaged/UnmanagedArray.cs similarity index 100% rename from NAPS2.Sdk/Unmanaged/UnmanagedArray.cs rename to NAPS2.Internals/Unmanaged/UnmanagedArray.cs diff --git a/NAPS2.Sdk/Unmanaged/UnmanagedBase.cs b/NAPS2.Internals/Unmanaged/UnmanagedBase.cs similarity index 100% rename from NAPS2.Sdk/Unmanaged/UnmanagedBase.cs rename to NAPS2.Internals/Unmanaged/UnmanagedBase.cs diff --git a/NAPS2.Sdk/Unmanaged/UnmanagedObject.cs b/NAPS2.Internals/Unmanaged/UnmanagedObject.cs similarity index 100% rename from NAPS2.Sdk/Unmanaged/UnmanagedObject.cs rename to NAPS2.Internals/Unmanaged/UnmanagedObject.cs diff --git a/NAPS2.Sdk/Unmanaged/UnmanagedTypes.cs b/NAPS2.Internals/Unmanaged/UnmanagedTypes.cs similarity index 100% rename from NAPS2.Sdk/Unmanaged/UnmanagedTypes.cs rename to NAPS2.Internals/Unmanaged/UnmanagedTypes.cs diff --git a/NAPS2.Sdk/Util/CollectionExtensions.cs b/NAPS2.Internals/Util/CollectionExtensions.cs similarity index 96% rename from NAPS2.Sdk/Util/CollectionExtensions.cs rename to NAPS2.Internals/Util/CollectionExtensions.cs index 4e08bd05f9..63e4e4a4e6 100644 --- a/NAPS2.Sdk/Util/CollectionExtensions.cs +++ b/NAPS2.Internals/Util/CollectionExtensions.cs @@ -13,7 +13,7 @@ public static class CollectionExtensions /// public static HashSet ToHashSet(this IEnumerable enumerable) { - return new HashSet(enumerable); + return [..enumerable]; } #endif @@ -134,7 +134,7 @@ public static void AddMulti(this Dictionary> { if (!dict.ContainsKey(key)) { - dict[key] = new HashSet(); + dict[key] = []; } dict[key].Add(value); } @@ -152,7 +152,7 @@ public static void AddMulti(this Dictionary> { if (!dict.ContainsKey(key)) { - dict[key] = new HashSet(); + dict[key] = []; } foreach (var value in values) { @@ -259,4 +259,12 @@ public static DisposableList ToDisposableList(this IEnumerable enumerab { return new DisposableList(enumerable.ToImmutableList()); } + + public static void DisposeAll(this IEnumerable enumerable) + { + foreach (var item in enumerable) + { + item.Dispose(); + } + } } \ No newline at end of file diff --git a/NAPS2.Sdk/Util/DisposableList.cs b/NAPS2.Internals/Util/DisposableList.cs similarity index 100% rename from NAPS2.Sdk/Util/DisposableList.cs rename to NAPS2.Internals/Util/DisposableList.cs diff --git a/NAPS2.Sdk/Util/DisposableSet.cs b/NAPS2.Internals/Util/DisposableSet.cs similarity index 91% rename from NAPS2.Sdk/Util/DisposableSet.cs rename to NAPS2.Internals/Util/DisposableSet.cs index 714a93aaa1..95d0a69382 100644 --- a/NAPS2.Sdk/Util/DisposableSet.cs +++ b/NAPS2.Internals/Util/DisposableSet.cs @@ -2,7 +2,7 @@ namespace NAPS2.Util; public class DisposableSet : IDisposable where T : IDisposable { - private readonly HashSet _set = new(); + private readonly HashSet _set = []; public void Add(T obj) { diff --git a/NAPS2.Images/Util/ExceptionExtensions.cs b/NAPS2.Internals/Util/ExceptionExtensions.cs similarity index 94% rename from NAPS2.Images/Util/ExceptionExtensions.cs rename to NAPS2.Internals/Util/ExceptionExtensions.cs index 96112d7d05..be48fe86ce 100644 --- a/NAPS2.Images/Util/ExceptionExtensions.cs +++ b/NAPS2.Internals/Util/ExceptionExtensions.cs @@ -3,7 +3,7 @@ // ReSharper disable once CheckNamespace namespace NAPS2.Util; -internal static class ExceptionExtensions +public static class ExceptionExtensions { private static MethodInfo? _internalPreserveStackTrace; diff --git a/NAPS2.Images/Util/LeakTracer.cs b/NAPS2.Internals/Util/LeakTracer.cs similarity index 83% rename from NAPS2.Images/Util/LeakTracer.cs rename to NAPS2.Internals/Util/LeakTracer.cs index e82a9feb5b..21b299488b 100644 --- a/NAPS2.Images/Util/LeakTracer.cs +++ b/NAPS2.Internals/Util/LeakTracer.cs @@ -28,6 +28,11 @@ public static void StopTracking(object obj) public static void PrintTraces() { + if (!ENABLE_TRACING) + { + Console.WriteLine("Leak tracing not enabled."); + return; + } var traceCounts = new Dictionary(); foreach (var trace in _traces.Values) { @@ -42,5 +47,9 @@ public static void PrintTraces() Console.WriteLine($"Potential leak (count: {kvp.Value}):"); Console.WriteLine(kvp.Key); } + if (!traceCounts.Any()) + { + Console.WriteLine("No leaks."); + } } } \ No newline at end of file diff --git a/NAPS2.Internals/Util/NumberExtensions.cs b/NAPS2.Internals/Util/NumberExtensions.cs new file mode 100644 index 0000000000..6784ca7687 --- /dev/null +++ b/NAPS2.Internals/Util/NumberExtensions.cs @@ -0,0 +1,57 @@ +namespace NAPS2.Util; + +public static class NumberExtensions +{ + /// + /// Ensures the provided value is within the provided range (inclusive). + /// + /// + /// + /// + /// + /// + public static T Clamp(this T val, T min, T max) where T : IComparable + { + if (val.CompareTo(min) < 0) + { + return min; + } + if (val.CompareTo(max) > 0) + { + return max; + } + return val; + } + + /// + /// Ensures the provided value is at least the provided minimum value (inclusive). + /// + /// + /// + /// + /// + public static T AtLeast(this T val, T min) where T : IComparable + { + if (val.CompareTo(min) < 0) + { + return min; + } + return val; + } + + /// + /// Ensures the provided value is at most the provided maximum value (inclusive). + /// + /// + /// + /// + /// + public static T AtMost(this T val, T max) where T : IComparable + { + if (val.CompareTo(max) > 0) + { + return max; + } + return val; + } +} \ No newline at end of file diff --git a/NAPS2.Images/Util/ObjectHelpers.cs b/NAPS2.Internals/Util/ObjectHelpers.cs similarity index 94% rename from NAPS2.Images/Util/ObjectHelpers.cs rename to NAPS2.Internals/Util/ObjectHelpers.cs index 469bfd62bf..f268fbaedc 100644 --- a/NAPS2.Images/Util/ObjectHelpers.cs +++ b/NAPS2.Internals/Util/ObjectHelpers.cs @@ -1,7 +1,7 @@ // ReSharper disable once CheckNamespace namespace NAPS2.Util; -internal static class ObjectHelpers +public static class ObjectHelpers { public static bool ListEquals(IList first, IList second) { diff --git a/NAPS2.Internals/Util/Once.cs b/NAPS2.Internals/Util/Once.cs new file mode 100644 index 0000000000..7f79ac23d3 --- /dev/null +++ b/NAPS2.Internals/Util/Once.cs @@ -0,0 +1,26 @@ +namespace NAPS2.Util; + +/// +/// Encapsulates an action that should be run once lazily. +/// +public class Once +{ + private readonly Lazy _lazy; + + public Once(Action action) + { + _lazy = new Lazy(() => + { + action(); + return new object(); + }); + } + + /// + /// Runs the action if it hasn't already been run. If the action is already running, waits for completion. + /// + public void Run() + { + var _ = _lazy.Value; + } +} \ No newline at end of file diff --git a/NAPS2.Images/Util/ProgressCallback.cs b/NAPS2.Internals/Util/ProgressCallback.cs similarity index 100% rename from NAPS2.Images/Util/ProgressCallback.cs rename to NAPS2.Internals/Util/ProgressCallback.cs diff --git a/NAPS2.Images/Util/ProgressHandler.cs b/NAPS2.Internals/Util/ProgressHandler.cs similarity index 100% rename from NAPS2.Images/Util/ProgressHandler.cs rename to NAPS2.Internals/Util/ProgressHandler.cs diff --git a/NAPS2.Images/Util/RefCount.cs b/NAPS2.Internals/Util/RefCount.cs similarity index 98% rename from NAPS2.Images/Util/RefCount.cs rename to NAPS2.Internals/Util/RefCount.cs index f90d43fe49..cafacaab68 100644 --- a/NAPS2.Images/Util/RefCount.cs +++ b/NAPS2.Internals/Util/RefCount.cs @@ -1,7 +1,7 @@ // ReSharper disable once CheckNamespace namespace NAPS2.Util; -internal class RefCount +public class RefCount { private readonly IDisposable _disposable; private int _count; diff --git a/NAPS2.Internals/Util/StringExtensions.cs b/NAPS2.Internals/Util/StringExtensions.cs new file mode 100644 index 0000000000..f94989cc51 --- /dev/null +++ b/NAPS2.Internals/Util/StringExtensions.cs @@ -0,0 +1,9 @@ +namespace NAPS2.Util; + +public static class StringExtensions +{ + public static bool ContainsInvariantIgnoreCase(this string source, string value) + { + return source.IndexOf(value, StringComparison.InvariantCultureIgnoreCase) != -1; + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Gtk/EntryPoints/GtkEntryPoint.cs b/NAPS2.Lib.Gtk/EntryPoints/GtkEntryPoint.cs index 204fb44420..b28cda6f63 100644 --- a/NAPS2.Lib.Gtk/EntryPoints/GtkEntryPoint.cs +++ b/NAPS2.Lib.Gtk/EntryPoints/GtkEntryPoint.cs @@ -1,52 +1,28 @@ -using Autofac; -using NAPS2.EtoForms; -using NAPS2.EtoForms.Ui; +using NAPS2.EtoForms; +using NAPS2.EtoForms.Gtk; using NAPS2.Modules; -using NAPS2.Remoting.Worker; -using UnhandledExceptionEventArgs = Eto.UnhandledExceptionEventArgs; namespace NAPS2.EntryPoints; /// -/// The entry point logic for NAPS2.exe, the NAPS2 GUI. +/// The entry point logic for the Gtk NAPS2 executable. /// public static class GtkEntryPoint { public static int Run(string[] args) { - if (args.Length > 0 && args[0] is "cli" or "console") - { - return ConsoleEntryPoint.Run(args.Skip(1).ToArray(), new GtkModule()); - } - if (args.Length > 0 && args[0] == "worker") - { - return WorkerEntryPoint.Run(args.Skip(1).ToArray(), new GtkModule()); - } - - // Initialize Autofac (the DI framework) - var container = AutoFacHelper.FromModules( - new CommonModule(), new GtkModule(), new RecoveryModule(), new ContextModule()); - - Paths.ClearTemp(); - - // Set up basic application configuration - container.Resolve().SetCulturesFromConfig(); - TaskScheduler.UnobservedTaskException += UnhandledTaskException; GLib.ExceptionManager.UnhandledException += UnhandledGtkException; - Trace.Listeners.Add(new ConsoleTraceListener()); - - // Start a pending worker process - container.Resolve().Init(); + GLibLogInterceptor.WriteToDebugLog(); + EtoPlatform.Current = new GtkEtoPlatform(); - // Show the main form - var application = EtoPlatform.Current.CreateApplication(); - application.UnhandledException += UnhandledException; - var formFactory = container.Resolve(); - var desktop = formFactory.Create(); - // TODO: Clean up invoker setting - // Invoker.Current = new WinFormsInvoker(desktop.ToNative()); - application.Run(desktop); - return 0; + var subArgs = args.Skip(1).ToArray(); + return args switch + { + ["cli" or "console", ..] => ConsoleEntryPoint.Run(subArgs, new GtkImagesModule(), new GtkModule()), + ["worker", ..] => WorkerEntryPoint.Run(subArgs, new GtkImagesModule()), + ["server", ..] => ServerEntryPoint.Run(subArgs, new GtkImagesModule(), new GtkModule()), + _ => GuiEntryPoint.Run(args, new GtkImagesModule(), new GtkModule()) + }; } private static void UnhandledGtkException(GLib.UnhandledExceptionArgs e) @@ -60,15 +36,4 @@ private static void UnhandledGtkException(GLib.UnhandledExceptionArgs e) Log.ErrorException("An unhandled error occurred.", e.ExceptionObject as Exception ?? new Exception()); } } - - private static void UnhandledTaskException(object? sender, UnobservedTaskExceptionEventArgs e) - { - Log.FatalException("An error occurred that caused the task to terminate.", e.Exception); - e.SetObserved(); - } - - private static void UnhandledException(object? sender, UnhandledExceptionEventArgs e) - { - Log.FatalException("An error occurred that caused the application to close.", e.ExceptionObject as Exception ?? new Exception()); - } } \ No newline at end of file diff --git a/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkDarkModeProvider.cs b/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkDarkModeProvider.cs new file mode 100644 index 0000000000..1db3f50f66 --- /dev/null +++ b/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkDarkModeProvider.cs @@ -0,0 +1,29 @@ +using Gtk; + +namespace NAPS2.EtoForms.Gtk; + +public class GtkDarkModeProvider : IDarkModeProvider +{ + // We need a real style context attached to the window, so it needs to be injected externally + public StyleContext? StyleContext { get; set; } + + public bool IsDarkModeEnabled + { + get + { + var settings = Settings.GetForScreen(Gdk.Screen.Default); + bool isDarkByStyleContext = false; + if (StyleContext != null) + { + var color = StyleContext.GetColor(StateFlags.Normal); + isDarkByStyleContext = color is { Red: > 0.5, Green: > 0.5, Blue: > 0.5 }; + } + return settings.ApplicationPreferDarkTheme || isDarkByStyleContext; + } + } + + // Not sure if it's possible to detect live changes with Gtk +#pragma warning disable CS0067 + public event EventHandler? DarkModeChanged; +#pragma warning restore CS0067 +} \ No newline at end of file diff --git a/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs b/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs index 185bea5360..758b3a6340 100644 --- a/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs +++ b/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs @@ -4,8 +4,9 @@ using Eto.Forms; using Eto.GtkSharp; using Eto.GtkSharp.Drawing; +using NAPS2.EtoForms.Widgets; using NAPS2.Images.Gtk; -using gtk = Gtk; +using GTK = Gtk; namespace NAPS2.EtoForms.Gtk; @@ -14,10 +15,13 @@ public class GtkEtoPlatform : EtoPlatform // TODO: Can we determine this dynamically? Tried container.GetAllocatedSize.Left/Top which works on LxQT but not Gnome private const int X_OFF = 2; private const int Y_OFF = 2; - + public override bool IsGtk => true; - public override Application CreateApplication() + public override IIconProvider IconProvider { get; } = new DefaultIconProvider(); + public override IDarkModeProvider DarkModeProvider { get; } = new GtkDarkModeProvider(); + + public override Application CreateApplicationCore() { var application = new Application(Platforms.Gtk); application.Initialized += (_, _) => @@ -33,17 +37,31 @@ public override Application CreateApplication() public override IListView CreateListView(ListViewBehavior behavior) => new GtkListView(behavior); - public override void ConfigureImageButton(Button button, bool big) + public override void ConfigureImageButton(Button button, ButtonFlags flags) { + AttachDpiDependency(button, _ => button.ScaleImage()); } public override Bitmap ToBitmap(IMemoryImage image) { - var pixbuf = ((GtkImage) image).Pixbuf; + var pixbuf = ((GtkImage) image).Pixbuf.Copy(); return new Bitmap(new BitmapHandler(pixbuf)); } - public override IMemoryImage DrawHourglass(ImageContext imageContext, IMemoryImage image) + public override IMemoryImage FromBitmap(Bitmap bitmap) + { + return new GtkImage(bitmap.ToGdk()); + } + + public override void SetClipboardImage(Clipboard clipboard, ProcessedImage processedImage, IMemoryImage memoryImage) + { + // We deliberately don't dispose the image here as otherwise Gtk gives errors on paste. + // Presumably it assumes the application will keep the Pixbuf around + // (while on Windows/Mac we can just dispose right away). + clipboard.Image = memoryImage.ToEtoImage(); + } + + public override IMemoryImage DrawHourglass(IMemoryImage image) { // TODO return image; @@ -51,83 +69,208 @@ public override IMemoryImage DrawHourglass(ImageContext imageContext, IMemoryIma public override void SetFrame(Control container, Control control, Point location, Size size, bool inOverlay) { - var overlay = (gtk.Overlay) container.ToNative(); - var panel = (gtk.Fixed) overlay.Children[inOverlay ? 1 : 0]; - panel.Move(control.ToNative(), location.X - X_OFF, location.Y - Y_OFF); - control.ToNative().SetSizeRequest(size.Width, size.Height); + // TODO: Getting some errors when starting naps2 with the sidebar hidden then trying to show it + if (location.X < 0 || location.Y < 0) throw new InvalidOperationException(); + var overlay = container.ToNative() as GTK.Overlay; + var panel = container.ToNative() as GTK.Fixed; + var widget = control.ToNative(); + var parent = overlay?.Parent ?? panel?.Parent.Parent; + int xOff = 0; + int yOff = 0; + if (parent is GTK.Alignment) + { + // Top-level container, so we offset + xOff = X_OFF; + yOff = Y_OFF; + } + if (overlay != null) + { + // TODO: Ideally we would use GetChildPosition instead of margin but that signal is not firing, not sure why + widget.MarginTop = location.Y - yOff; + widget.MarginStart = location.X - xOff; + } + if (panel != null) + { + if (widget.Parent != panel) throw new InvalidOperationException("Invalid parent"); + panel.Move(widget, location.X - xOff, location.Y - yOff); + } + widget.SetSizeRequest(size.Width, size.Height); + if (widget is GTK.Overlay childOverlay) + { + var childPanel = (GTK.Fixed) childOverlay.Child; + childPanel.SetSizeRequest(size.Width, size.Height); + } } - public override Control CreateContainer() - { - var overlay = new gtk.Overlay(); - overlay.Add(new gtk.Fixed()); - var overlayPanel = new gtk.Fixed(); - overlay.AddOverlay(overlayPanel); - overlay.SetOverlayPassThrough(overlayPanel, true); - return overlay.ToEto(); - } + public override Control CreateContainer() => new GTK.Fixed().AsEto(); public override void AddToContainer(Control container, Control control, bool inOverlay) { - var overlay = (gtk.Overlay) container.ToNative(); - var panel = (gtk.Fixed) overlay.Children[inOverlay ? 1 : 0]; + var overlay = container.ToNative() as GTK.Overlay; + var panel = container.ToNative() as GTK.Fixed; var widget = control.ToNative(); - panel.Add(widget); + if (overlay != null) + { + overlay.AddOverlay(widget); + widget.Halign = GTK.Align.Start; + widget.Valign = GTK.Align.Start; + } + if (panel != null) + { + panel.Add(widget); + } widget.ShowAll(); } + public override Control? MaybeCreateOverlayContainer() + { + var overlay = new GTK.Overlay(); + var panel = new GTK.Fixed(); + overlay.Child = panel; + return overlay.AsEto(); + } + + public override Control? GetOverlayContainer(Control? container, bool inOverlay) + { + var overlay = (GTK.Overlay) container.ToNative(); + return inOverlay ? overlay.AsEto() : overlay.Child.AsEto(); + } + + public override void RemoveFromContainer(Control container, Control control) + { + var overlay = container.ToNative() as GTK.Overlay; + var panel = container.ToNative() as GTK.Fixed; + var widget = control.ToNative(); + overlay?.Remove(widget); + panel?.Remove(widget); + widget.Unrealize(); + } + public override void SetContainerSize(Window _window, Control container, Size size, int padding) { - var overlay = (gtk.Overlay) container.ToNative(); + var native = (GTK.Fixed) container.ToNative(); if (!_window.Resizable) { // This ensures the window has the appropriate margins, otherwise with resizable=false it changes to fit // the contents - overlay.MarginBottom = padding - Y_OFF; - overlay.MarginEnd = padding - X_OFF; + native.MarginBottom = padding - Y_OFF; + native.MarginEnd = padding - X_OFF; } } public override Size GetFormSize(Window window) { - var gtkWindow = (gtk.Window) window.ToNative(); + var gtkWindow = (GTK.Window) window.ToNative(); gtkWindow.GetSize(out int w, out int h); return new Size(w, h); } public override void SetFormSize(Window window, Size size) { - var gtkWindow = (gtk.Window) window.ToNative(); + var gtkWindow = (GTK.Window) window.ToNative(); gtkWindow.SetDefaultSize(size.Width, size.Height); } public override SizeF GetPreferredSize(Control control, SizeF availableSpace) { var widget = control.ToNative(); - if (widget.IsRealized) + // TODO: This is a hack to make overlays work. Gtk Overlay uses the margin to adjust the control position but + // then the margin messes with size calculations. If we set the margin to 0 here (which should be fine as we + // don't use margin otherwise) then the size calculations normalize, while the overlay has already determined + // the position so it doesn't affect that any more. However, there is a chance this will break in some edge + // cases. + widget.Margin = 0; + if (control is ImageView && control.Size.Width > 1) + { + return control.Size; + } + if (widget.IsRealized && widget is not GTK.DrawingArea) { - return base.GetPreferredSize(control, availableSpace); + widget.GetSizeRequest(out var oldWidth, out var oldHeight); + widget.SetSizeRequest(0, 0); + try + { + return base.GetPreferredSize(control, availableSpace); + } + finally + { + widget.SetSizeRequest(oldWidth, oldHeight); + } } widget.GetPreferredSize(out var minSize, out var naturalSize); return new SizeF(naturalSize.Width, naturalSize.Height); } - public override Size GetClientSize(Window window) + public override SizeF GetWrappedSize(Control control, int defaultWidth) + { + var widget = control.ToNative(); + if (widget is GTK.Bin { Child: GTK.Label label }) + { + label.MaxWidthChars = EstimateCharactersWide(defaultWidth, label.GetFont()); + label.GetPreferredSize(out var minSize, out var naturalSize); + label.GetPreferredHeightForWidth(defaultWidth, out var minHeight, out var naturalHeight); + return new SizeF(Math.Min(naturalSize.Width + 10, defaultWidth), naturalHeight); + } + return base.GetWrappedSize(control, defaultWidth); + } + + private static int EstimateCharactersWide(int pixelWidth, Pango.FontDescription font) { - var gtkWindow = (gtk.Window) window.ToNative(); + // TODO: This could vary based on font and text. Can we do better somehow? + // Ideally we'd be able to wrap based on a pixel width. Maybe if we put the label in a container? + var fontSize = font.Size / Pango.Scale.PangoScale; + var approxCharWidth = fontSize * 0.75; + return (int) Math.Floor(pixelWidth / approxCharWidth); + } + + public override void ConfigureEllipsis(Label label) + { + var eventBox = (GTK.EventBox) label.ToNative(); + var gtkLabel = (GTK.Label) eventBox.Child; + gtkLabel.Ellipsize = Pango.EllipsizeMode.End; + } + + public override void ConfigureDropDown(DropDown dropDown, bool scale) + { + if (scale) + { + var native = (GTK.ComboBox) ((GTK.EventBox) dropDown.ToNative()).Child; + var cell = native.Cells.OfType().FirstOrDefault()!; + // TODO: I should probably test this on more desktop environments + // Setting the renderer width to 1 allows the width to be set properly as part of layouting + cell.Width = 1; + cell.Height = 25; + } + } + + public override Size GetClientSize(Window window, bool excludeToolbars) + { + var gtkWindow = (GTK.Window) window.ToNative(); gtkWindow.GetSize(out var w, out var h); - return new Size(w, h); + var size = new Size(w, h); + if (excludeToolbars && window.ToolBar != null) + { + var toolbar = (GTK.Toolbar) window.ToolBar.ControlObject; + var vbox = (GTK.VBox) toolbar.Parent; + var heights = vbox.Children.OfType().Select(x => + { + x.GetPreferredHeight(out _, out int naturalHeight); + return naturalHeight; + }); + size -= new Size(0, heights.Sum()); + } + return size; } public override void SetClientSize(Window window, Size clientSize) { - var gtkWindow = (gtk.Window) window.ToNative(); + var gtkWindow = (GTK.Window) window.ToNative(); gtkWindow.Resize(clientSize.Width, clientSize.Height); } public override void SetMinimumClientSize(Window window, Size minSize) { - var gtkWindow = (gtk.Window) window.ToNative(); + var gtkWindow = (GTK.Window) window.ToNative(); gtkWindow.SetSizeRequest(minSize.Width, minSize.Height); } @@ -136,26 +279,87 @@ public override void SetFormLocation(Window window, Point location) // TODO: Gtk windows drift if we remember location. For now using the default location is fine. } - public override Control AccessibleImageButton(Image image, string text, Action onClick, - int xOffset = 0, int yOffset = 0) + public override float GetScaleFactor(Window window) + { + // GTK scale factors are integers. Any fractional scaling (e.g. 1.5x) works by rendering at 2x and then scaling + // down. + return window.ToNative().ScaleFactor; + } + + public override void AttachDpiDependency(Control control, Action callback) { - var button = new gtk.Button + if (control.Loaded) { - // Label = text, - Image = image.ToGtk(), - ImagePosition = gtk.PositionType.Left - }; - button.StyleContext.AddClass("accessible-image-button"); - button.Clicked += (_, _) => onClick(); - return button.ToEto(); + callback(GetScaleFactor(control.ParentWindow)); + } + else + { + control.Load += (_, _) => callback(GetScaleFactor(control.ParentWindow)); + } } - public override void ConfigureZoomButton(Button button) + public override void ConfigureDonateButton(Button button) { + var native = (GTK.Button) button.ToNative(); + native.StyleContext.AddClass("donate-button"); + } + + public override void ConfigureZoomButton(Button button, string icon) + { + var gtkButton = button.ToNative(); button.Text = ""; + button.Image = IconProvider.GetIcon(icon, gtkButton.ScaleFactor); + button.ScaleImage(); button.Size = Size.Empty; - var gtkButton = button.ToNative(); gtkButton.StyleContext.AddClass("zoom-button"); gtkButton.SetSizeRequest(0, 0); } + + public override void AttachMouseWheelEvent(Control control, EventHandler eventHandler) + { + var native = control.ToNative(); + // Attach to the child so that the scrollbars don't steal the event from us + if (native is GTK.EventBox eventBox) + { + native = eventBox.Child; + } + if (native is GTK.ScrolledWindow scrolledWindow) + { + native = scrolledWindow.Child; + } + native.ScrollEvent += (sender, args) => + { + var ev = args.Event; + var newArgs = new MouseEventArgs( + MouseButtons.None, + ev.State.ToEtoKey(), + new PointF((float) ev.X, (float) ev.Y), + // Negate deltaY to match WinForms + new SizeF((float) ev.DeltaX, (float) -ev.DeltaY)); + eventHandler.Invoke(sender, newArgs); + args.RetVal = newArgs.Handled; + }; + } + + public override void SetSplitterPosition(Splitter splitter, int pos) + { + if (splitter.Width == 1) + { + // TODO: Fix Eto so that SplitterHandler.EnsurePosition doesn't ignore the specified position + var paned = (GTK.Paned) splitter.ControlObject; + void OnDrawn(object o, EventArgs drawnArgs) + { + if (splitter.Width != 1) + { + paned.Drawn -= OnDrawn; + splitter.Position = pos; + } + } + paned.Drawn += OnDrawn; + } + else + { + splitter.Position = pos; + } + } } \ No newline at end of file diff --git a/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkListView.cs b/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkListView.cs index 9dd257def1..c8a3718e05 100644 --- a/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkListView.cs +++ b/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkListView.cs @@ -1,6 +1,9 @@ using Eto.Forms; using Eto.GtkSharp; +using Gdk; using Gtk; +using NAPS2.EtoForms.Widgets; +using Drag = Gtk.Drag; using Label = Gtk.Label; using Orientation = Gtk.Orientation; @@ -8,13 +11,20 @@ namespace NAPS2.EtoForms.Gtk; public class GtkListView : IListView where T : notnull { + private static readonly TargetEntry[] DragTargetEntries = + { + // TODO: Maybe use a different mime for different list types (profiles/images)? + // i.e. something similar to _behavior.CustomDragDataType but in mime format (maybe) + new("application/x-naps2-items", 0, 0) + }; + private readonly ListViewBehavior _behavior; private ListSelection _selection = ListSelection.Empty(); private bool _refreshing; private readonly ScrolledWindow _scrolledWindow; private readonly FlowBox _flowBox; - private List _entries = new(); + private List _entries = []; public GtkListView(ListViewBehavior behavior) { @@ -40,37 +50,51 @@ public GtkListView(ListViewBehavior behavior) { _flowBox.SelectedChildrenChanged += FlowBoxSelectionChanged; } - _scrolledWindow.Add(_flowBox); + _flowBox.ChildActivated += OnChildActivated; + _flowBox.ButtonPressEvent += OnButtonPress; + var eventBox = new EventBox(); + eventBox.Child = _flowBox; + if (_behavior.AllowDragDrop) + { + Drag.DestSet(eventBox, DestDefaults.All, GetDropTargetEntries(), DragAction.Copy | DragAction.Move); + eventBox.DragDataReceived += OnDragDataReceived; + eventBox.DragMotion += OnDragMotion; + eventBox.DragLeave += OnDragLeave; + } + _scrolledWindow.Add(eventBox); _scrolledWindow.StyleContext.AddClass("listview"); + Control = _scrolledWindow.AsEto(); } - public int ImageSize { get; set; } + private void OnButtonPress(object o, ButtonPressEventArgs args) + { + if (args.Event.Button == 3) + { + // Right click + ContextMenu?.Show(); + } + } - // TODO: Properties here vs on behavior? - public bool AllowDrag { get; set; } + private void OnChildActivated(object o, ChildActivatedArgs args) + { + ItemClicked?.Invoke(this, EventArgs.Empty); + } - public bool AllowDrop { get; set; } + public Eto.Drawing.Size ImageSize { get; set; } public ScrolledWindow NativeControl => _scrolledWindow; - public Control Control => _scrolledWindow.ToEto(); + public Control Control { get; } - // TODO: Make this work public ContextMenu? ContextMenu { get; set; } public event EventHandler? Updated; public event EventHandler? SelectionChanged; - // TODO: Implement item double-click -#pragma warning disable CS0067 public event EventHandler? ItemClicked; -#pragma warning restore CS0067 - // TODO: Implement drag/drop -#pragma warning disable CS0067 public event EventHandler? Drop; -#pragma warning restore CS0067 public void SetItems(IEnumerable items) { @@ -114,8 +138,8 @@ private Widget GetItemWidget(T item) } else { - using var image = _behavior.GetImage(item, ImageSize); - var imageWidget = image.ToGtk(); + using var image = _behavior.GetImage(this, item); + var imageWidget = image.ToGdk().ToScaledImage(_flowBox.ScaleFactor); // TODO: Is there a better way to prevent the image from expanding in both dimensions? var hframe = new Box(Orientation.Horizontal, 0); hframe.Halign = Align.Center; @@ -134,20 +158,31 @@ private Widget GetItemWidget(T item) }; vframe.Add(label); } - flowBoxChild.Add(vframe); + // TODO: Event box around the image instead of the frame? + var eventBox = new EventBox(); + eventBox.Child = vframe; + if (_behavior.AllowDragDrop || _behavior.AllowFileDrop) + { + eventBox.DragBegin += OnDragBegin; + eventBox.DragDataGet += OnDragDataGet; + Drag.SourceSet(eventBox, ModifierType.Button1Mask, DragTargetEntries, DragAction.Move); + } + flowBoxChild.Add(eventBox); } flowBoxChild.StyleContext.AddClass("listview-item"); return flowBoxChild; } - // TODO: Do we need this method? Clean up the name/doc at least - // TODO: Seems like we might not need it at all, the syncer is working? Or is the idea this is faster for WinForms? But in that case we should probably not update on sync. public void RegenerateImages() { if (_refreshing) { throw new InvalidOperationException(); } + if (_entries.Count == 0) + { + return; + } _refreshing = true; foreach (var entry in _entries) { @@ -289,6 +324,142 @@ private static CheckButton GetCheckButton(Widget widget) return (CheckButton) ((FlowBoxChild) widget).Child; } + private TargetEntry[] GetDropTargetEntries() + { + var list = new List(); + if (_behavior.AllowDragDrop) + { + list.Add(new("application/x-naps2-items", 0, 0)); + } + if (_behavior.AllowFileDrop) + { + list.Add(new("text/uri-list", 0, 0)); + } + return list.ToArray(); + } + + private void OnDragBegin(object sender, DragBeginArgs args) + { + // Select the item under the mouse cursor if not already. + var dragWidget = (FlowBoxChild) ((EventBox) sender).Parent; + var dragItem = ByWidget()[dragWidget].Item; + if (!Selection.Contains(dragItem)) + { + Selection = ListSelection.Of(dragItem); + } + } + + private void OnDragDataGet(object sender, DragDataGetArgs args) + { + if (Selection.Any()) + { + // TODO: Can we set a pixbuf for the drag? + args.SelectionData.Set( + Atom.Intern(_behavior.CustomDragDataType, false), + 8, + _behavior.SerializeCustomDragData(Selection.ToArray())); + } + } + + private void OnDragDataReceived(object sender, DragDataReceivedArgs args) + { + var index = GetDragIndex(); + if (args.SelectionData.DataType.Name == _behavior.CustomDragDataType && _behavior.AllowDragDrop) + { + Drop?.Invoke(this, new DropEventArgs(index, args.SelectionData.Data)); + } + else if (args.SelectionData.Uris.Any() && _behavior.AllowFileDrop) + { + Drop?.Invoke(this, new DropEventArgs(index, args.SelectionData.Uris.Select(x => new Uri(x).LocalPath))); + } + } + + private void OnDragMotion(object sender, DragMotionArgs args) + { + if (args.Context.SelectedAction != DragAction.Move) return; + // Show a visual indicator of the drop location + ClearDropIndicator(); + var index = GetDragIndex(); + if (index == -1) return; + var widgets = _flowBox.Children; + // Show on the left (of the image to the right) if we're moving to the left, or on the right (of the image to + // the left) if we're moving to the right. + // This gives an accurate indication of where the image will appear especially if we're dragging across rows. + // If the drop will have no effect (because we're dropping next to the selected image) this will show nothing. + // TODO: This doesn't show a drop indicator if we're dropping inside a disjointed selection + var selectedIndices = Selection.ToSelectedIndices(_entries.Select(x => x.Item).ToList()).ToList(); + var selectionMin = selectedIndices.Min(); + var selectionMax = selectedIndices.Max() + 1; + if (index < selectionMin) + { + widgets[index].StyleContext.AddClass("drop-before"); + } + if (index > selectionMax) + { + widgets[index - 1].StyleContext.AddClass("drop-after"); + } + } + + private void OnDragLeave(object sender, DragLeaveArgs args) + { + ClearDropIndicator(); + } + + private void ClearDropIndicator() + { + foreach (var widget in _flowBox.Children) + { + widget.StyleContext.RemoveClass("drop-before"); + widget.StyleContext.RemoveClass("drop-after"); + } + } + + private int GetDragIndex() + { + if (_entries.Count == 0) + { + return 0; + } + var cp = GetMousePosRelativeToFlowbox(); + var dragToItem = _flowBox.GetChildAtPos(cp.X, cp.Y); + if (dragToItem == null) + { + var items = _flowBox.Children.Cast().ToList(); + var minY = items.Select(x => x.Allocation.Top).Min(); + var maxY = items.Select(x => x.Allocation.Bottom).Max(); + if (cp.Y < minY) + { + cp.Y = minY; + } + if (cp.Y > maxY) + { + cp.Y = maxY; + } + var row = items.Where(x => x.Allocation.Top <= cp.Y && x.Allocation.Bottom >= cp.Y) + .OrderBy(x => x.Allocation.X) + .ToList(); + dragToItem = row.FirstOrDefault(x => x.Allocation.Right >= cp.X) ?? row.LastOrDefault(); + } + if (dragToItem == null) + { + return -1; + } + int dragToIndex = dragToItem.Index; + if (cp.X > (dragToItem.Allocation.X + dragToItem.Allocation.Width / 2)) + { + dragToIndex++; + } + return dragToIndex; + } + + private Point GetMousePosRelativeToFlowbox() + { + _flowBox.Window.GetOrigin(out var boxX, out var boxY); + var mousePos = Mouse.Position; + var cp = new Point((int) mousePos.X - boxX, (int) mousePos.Y - boxY); + return cp; + } + private class Entry { public required T Item { get; set; } diff --git a/NAPS2.Lib.Gtk/EtoForms/Ui/GtkDesktopForm.cs b/NAPS2.Lib.Gtk/EtoForms/Ui/GtkDesktopForm.cs index 4593b27dc2..b9ca358ff2 100644 --- a/NAPS2.Lib.Gtk/EtoForms/Ui/GtkDesktopForm.cs +++ b/NAPS2.Lib.Gtk/EtoForms/Ui/GtkDesktopForm.cs @@ -1,66 +1,82 @@ -using System.Threading; using Eto.GtkSharp; using Eto.GtkSharp.Forms.ToolBar; using Gdk; using Gtk; using NAPS2.EtoForms.Desktop; using NAPS2.EtoForms.Gtk; -using NAPS2.EtoForms.Layout; -using NAPS2.ImportExport.Images; +using NAPS2.EtoForms.Notifications; +using NAPS2.EtoForms.Widgets; +using NAPS2.Scan; using Command = Eto.Forms.Command; namespace NAPS2.EtoForms.Ui; public class GtkDesktopForm : DesktopForm { + private readonly Dictionary _menuButtons = new(); private Toolbar _toolbar = null!; private int _toolbarButtonCount; private int _toolbarMenuToggleCount; private int _toolbarPadding; private CssProvider? _toolbarPaddingCssProvider; + private Toolbar _profilesToolbar = null!; public GtkDesktopForm( Naps2Config config, DesktopKeyboardShortcuts keyboardShortcuts, - INotificationManager notify, + NotificationManager notificationManager, CultureHelper cultureHelper, + ColorScheme colorScheme, IProfileManager profileManager, UiImageList imageList, - ImageTransfer imageTransfer, ThumbnailController thumbnailController, UiThumbnailProvider thumbnailProvider, DesktopController desktopController, IDesktopScanController desktopScanController, ImageListActions imageListActions, + ImageListViewBehavior imageListViewBehavior, DesktopFormProvider desktopFormProvider, IDesktopSubFormController desktopSubFormController, - DesktopCommands commands) - : base(config, keyboardShortcuts, notify, cultureHelper, profileManager, - imageList, imageTransfer, thumbnailController, thumbnailProvider, desktopController, desktopScanController, - imageListActions, desktopFormProvider, desktopSubFormController, commands) + Lazy commands, + IDarkModeProvider darkModeProvider, + Sidebar sidebar, + IIconProvider iconProvider) + : base(config, keyboardShortcuts, notificationManager, cultureHelper, colorScheme, profileManager, imageList, + thumbnailController, thumbnailProvider, desktopController, desktopScanController, imageListActions, + imageListViewBehavior, desktopFormProvider, desktopSubFormController, commands, sidebar, iconProvider) { + ((GtkDarkModeProvider) darkModeProvider).StyleContext = + Eto.Forms.Gtk3Helpers.ToNative(this).StyleContext; var cssProvider = new CssProvider(); + var bgColor = colorScheme.BackgroundColor.ToHex(false); + var fgColor = colorScheme.ForegroundColor.ToHex(false); + var sepColor = colorScheme.SeparatorColor.ToHex(false); + var brdColor = colorScheme.BorderColor.ToHex(false); cssProvider.LoadFromData(@" .desktop-toolbar-button * { min-width: 0; padding-left: 0; padding-right: 0; } .desktop-toolbar .image-button { min-width: 50px; padding-left: 0; padding-right: 0; } .desktop-toolbar .toggle { min-width: 0; padding-left: 0; padding-right: 0; } - .desktop-toolbar { border-bottom: 1px solid #ddd; } - .listview .frame { background-color: #fff; } - .desktop-listview .listview-item image { border: 1px solid #000; } + .preview-toolbar-button * { min-width: 0; padding-left: 0; padding-right: 0; } + .preview-toolbar-button button { padding: 0 5px; } + toolbar { border-bottom: 1px solid " + sepColor + @"; } + .listview .frame { background-color: " + bgColor + @"; } + .listview .drop-before { border-radius: 0; border-left: 3px solid " + fgColor + @"; padding-left: 0; } + .listview .drop-after { border-radius: 0; border-right: 3px solid " + fgColor + @"; padding-right: 0; } + .desktop-listview .listview-item image { border: 1px solid " + brdColor + @"; } .link { padding: 0; } - .accessible-image-button { border: none; background: none; } - .zoom-button { background: white; border: 1px solid; border-radius: 0; } + .donate-button { border: 1px solid #fbad5f; background: #feda96; } + .donate-button:hover { background: #eeca86; } + .zoom-button { background: " + bgColor + @"; border: 1px solid " + brdColor + @"; border-radius: 0; } "); StyleContext.AddProviderForScreen(Gdk.Screen.Default, cssProvider, 800); } protected override void OnLoad(EventArgs e) { - // TODO: What's the best place to initialize this? It needs to happen from the UI event loop. - Invoker.Current = new SyncContextInvoker(SynchronizationContext.Current!); base.OnLoad(e); var listView = (GtkListView) _listView; listView.NativeControl.StyleContext.AddClass("desktop-listview"); + PlaceProfilesToolbar(); } protected override void OnSizeChanged(EventArgs e) @@ -91,32 +107,70 @@ private void UpdateToolbarPadding() "); } - protected override LayoutElement GetMainContent() + protected override void ConfigureToolbars() { - return L.Column( - Eto.Forms.Gtk3Helpers.ToEto(_toolbar), - _listView.Control.Scale() - ).Spacing(0); + _toolbar = ((ToolBarHandler) ToolBar.Handler).Control; + _toolbar.Style = ToolbarStyle.Both; + _toolbar.StyleContext.AddClass("desktop-toolbar"); + + _profilesToolbar = new Toolbar(); + _profilesToolbar.Style = ToolbarStyle.BothHoriz; } - protected override void ConfigureToolbar() + public override void PlaceProfilesToolbar() { - // TODO: Zoom is behaving weirdly - full zoomout and images become invisible - // TODO: Also getting "g_sequence_remove: assertion 'iter != NULL' failed" on zoom - // TODO: Also zoom buttons (and listview) are 2px above the bottom for no apparent reason + if (Config.Get(c => c.ShowProfilesToolbar) && _profilesToolbar.Parent == null) + { + ((VBox) _toolbar.Parent).Add(_profilesToolbar); + _profilesToolbar.ShowAll(); + LayoutController.Invalidate(); + } + if (!Config.Get(c => c.ShowProfilesToolbar) && _profilesToolbar.Parent != null) + { + ((VBox) _toolbar.Parent).Remove(_profilesToolbar); + LayoutController.Invalidate(); + } + } - _toolbar = ((ToolBarHandler) ToolBar.Handler).Control; - // Remove the toolbar from the container as we will manually layout it - // TODO: Clean this up - var parent = (Container) _toolbar.Parent; - parent.Remove(_toolbar); - _toolbar.Style = ToolbarStyle.Both; - _toolbar.StyleContext.AddClass("desktop-toolbar"); + protected override void UpdateProfilesToolbar() + { + var profiles = _profileManager.Profiles; + var extra = _profilesToolbar.NItems - profiles.Count; + var missing = profiles.Count - _profilesToolbar.NItems; + for (int i = 0; i < extra; i++) + { + _profilesToolbar.Remove(_profilesToolbar.Children.Last()); + } + for (int i = 0; i < missing; i++) + { + var item = new ToolButton(Icons.control_play_blue_small.ToEtoImage().ToGtk(), "test") + { + Homogeneous = false, + IsImportant = true + }; + item.Clicked += (_, _) => _desktopScanController.ScanWithProfile((ScanProfile) item.Data["naps2_profile"]!); + _profilesToolbar.Add(item); + } + for (int i = 0; i < profiles.Count; i++) + { + var profile = profiles[i]; + var item = (ToolButton) _profilesToolbar.GetNthItem(i); + item.Data["naps2_profile"] = profile; + if (item.Label != profile.DisplayName) + { + item.Label = profile.DisplayName; + } + } + } + + protected override void RecreateToolbarsAndMenus() + { + // Recreating toolbars doesn't work well on Gtk and isn't necessary anyway } protected override void CreateToolbarButton(Command command) { - var button = new ToolButton(command.Image.ToGtk(), command.ToolBarText) + var button = new ToolButton(GetCommandImage(command), command.ToolBarText) { Homogeneous = false, Sensitive = command.Enabled @@ -135,87 +189,71 @@ protected override void CreateToolbarSeparator() protected override void CreateToolbarStackedButtons(Command command1, Command command2) { + // Simply create a ToolItem with two buttons stacked on top of each other var button1 = CreateToolButton(command1, Orientation.Horizontal); var button2 = CreateToolButton(command2, Orientation.Horizontal); var vbox = new Box(Orientation.Vertical, 0); vbox.Add(button1); vbox.Add(button2); - AddCustomToolItem(vbox); + var toolItem = new ToolItem(); + toolItem.Add(vbox); + + // Handle the toolbar overflowing into a menu + toolItem.CreateMenuProxy += (o, args) => + { + var menuItem1 = (ImageMenuItem) new Eto.Forms.ButtonMenuItem(command1).ControlObject; + var menuItem2 = (ImageMenuItem) new Eto.Forms.ButtonMenuItem(command2).ControlObject; + // Hack to show two menu items instead of one. Seems to work for now. + menuItem1.ParentSet += (_, _) => ((Container) menuItem1.Parent)?.Add(menuItem2); + toolItem.SetProxyMenuItem("", menuItem1); + }; + // GTK ties the menu item's sensitivity to the ToolItem's sensitivity. We only need to check the first command + // since the second command's menu item is hacked in. + toolItem.Sensitive = command1.Enabled; + command1.EnabledChanged += (_, _) => toolItem.Sensitive = command1.Enabled; + + _toolbar.Add(toolItem); _toolbarButtonCount++; } - protected override void CreateToolbarButtonWithMenu(Command command, MenuProvider menu) + protected override void CreateToolbarButtonWithMenu(Command command, DesktopToolbarMenuType menuType, + MenuProvider menu) { - var button = new MenuToolButton(command.Image.ToGtk(), command.ToolBarText) + var button = new MenuToolButton(GetCommandImage(command), command.ToolBarText) { Homogeneous = false, Sensitive = command.Enabled }; button.Clicked += (_, _) => command.Execute(); command.EnabledChanged += (_, _) => button.Sensitive = command.Enabled; - button.Menu = CreateMenuWidget(menu); + button.Menu = CreateMenuWidget(command, menu); button.StyleContext.AddClass("desktop-toolbar-button"); _toolbar.Add(button); _toolbarButtonCount++; _toolbarMenuToggleCount++; + _menuButtons[menuType] = button; } protected override void CreateToolbarMenu(Command command, MenuProvider menu) { - var button = new ToolButton(command.Image.ToGtk(), command.ToolBarText) + var button = new ToolButton(GetCommandImage(command), command.ToolBarText) { Homogeneous = false, Sensitive = command.Enabled }; command.EnabledChanged += (_, _) => button.Sensitive = command.Enabled; - var menuWidget = CreateMenuWidget(menu); - var menuDelegate = GetMenuDelegate(menuWidget, button); + var menuDelegate = GetMenuDelegate(CreateMenuWidget(command, menu), button); button.Clicked += menuDelegate; button.StyleContext.AddClass("desktop-toolbar-button"); _toolbar.Add(button); _toolbarButtonCount++; } - private Menu CreateMenuWidget(MenuProvider menu) + private Menu CreateMenuWidget(Command command, MenuProvider menu) { - var menuWidget = new Menu(); - menu.Handle(items => - { - foreach (var child in menuWidget.Children) - { - menuWidget.Remove(child); - } - foreach (var item in items) - { - switch (item) - { - case MenuProvider.CommandItem commandItem: - var menuItem = new MenuItem - { - Label = commandItem.Command.MenuText - }; - menuItem.Sensitive = commandItem.Command.Enabled; - commandItem.Command.EnabledChanged += - (_, _) => menuItem.Sensitive = commandItem.Command.Enabled; - menuItem.Activated += (_, _) => commandItem.Command.Execute(); - menuWidget.Add(menuItem); - break; - case MenuProvider.SeparatorItem: - menuWidget.Add(new SeparatorMenuItem()); - break; - case MenuProvider.SubMenuItem subMenuItem: - var subMenu = new MenuItem - { - Label = subMenuItem.Command.MenuText - }; - subMenu.Submenu = CreateMenuWidget(subMenuItem.MenuProvider); - menuWidget.Add(subMenu); - break; - } - } - menuWidget.ShowAll(); - }); - return menuWidget; + var subMenu = CreateSubMenu(command, menu); + var menuItem = (ImageMenuItem) subMenu.ControlObject; + return (Menu) menuItem.Submenu; } private EventHandler GetMenuDelegate(Menu menuWidget, Widget button) @@ -223,18 +261,17 @@ private EventHandler GetMenuDelegate(Menu menuWidget, Widget button) return (_, _) => menuWidget.PopupAtWidget(button, Gravity.SouthWest, Gravity.NorthWest, null); } - private void AddCustomToolItem(Widget item) + public override void ShowToolbarMenu(DesktopToolbarMenuType menuType) { - var toolItem = new ToolItem(); - toolItem.Add(item); - _toolbar.Add(toolItem); + var button = _menuButtons.Get(menuType); + (button?.Menu as Menu)?.PopupAtWidget(button, Gravity.SouthWest, Gravity.NorthWest, null); } - private static Button CreateToolButton(Command command, Orientation orientation = Orientation.Vertical, + private Button CreateToolButton(Command command, Orientation orientation = Orientation.Vertical, int spacing = 4) { var box = new Box(orientation, spacing); - box.Add(command.Image.ToGtk()); + box.Add(GetCommandImage(command)); var label = new Label(command.ToolBarText); box.Add(label); var button = new Button(box) @@ -247,4 +284,11 @@ private static Button CreateToolButton(Command command, Orientation orientation (_, _) => button.Sensitive = command.Enabled; return button; } + + private Image GetCommandImage(Command command) + { + var scale = EtoPlatform.Current.GetScaleFactor(this); + var image = ((ActionCommand) command).GetIconImage(scale); + return image.ToGdk().ToScaledImage((int) scale); + } } \ No newline at end of file diff --git a/NAPS2.Lib.Gtk/EtoForms/Ui/GtkPreviewForm.cs b/NAPS2.Lib.Gtk/EtoForms/Ui/GtkPreviewForm.cs new file mode 100644 index 0000000000..96b3f5abcf --- /dev/null +++ b/NAPS2.Lib.Gtk/EtoForms/Ui/GtkPreviewForm.cs @@ -0,0 +1,29 @@ +using Gtk; +using NAPS2.EtoForms.Desktop; + +namespace NAPS2.EtoForms.Ui; + +public class GtkPreviewForm : PreviewForm +{ + public GtkPreviewForm(Naps2Config config, DesktopCommands desktopCommands, UiImageList imageList, + IIconProvider iconProvider, ColorScheme colorScheme) : base(config, + desktopCommands, imageList, iconProvider, colorScheme) + { + } + + protected override void CreateToolbar() + { + base.CreateToolbar(); + var toolBar = (Toolbar) ToolBar.ControlObject; + toolBar.IconSize = IconSize.SmallToolbar; + toolBar.Style = ToolbarStyle.Icons; + foreach (var item in toolBar.Children) + { + if (item is ToolItem toolItem) + { + toolItem.Homogeneous = false; + item.StyleContext.AddClass("preview-toolbar-button"); + } + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Gtk/ImportExport/GtkScannedImagePrinter.cs b/NAPS2.Lib.Gtk/ImportExport/GtkScannedImagePrinter.cs new file mode 100644 index 0000000000..c74cbae1ca --- /dev/null +++ b/NAPS2.Lib.Gtk/ImportExport/GtkScannedImagePrinter.cs @@ -0,0 +1,77 @@ +using Gtk; +using NAPS2.Images.Gtk; + +namespace NAPS2.ImportExport; + +public class GtkScannedImagePrinter : IScannedImagePrinter +{ + public Task PromptToPrint( + Eto.Forms.Window parentWindow, IList images, IList selectedImages) + { + if (!images.Any()) + { + return Task.FromResult(false); + } + var printOp = new PrintOperation + { + NPages = images.Count, + UseFullPage = true, + HasSelection = selectedImages.Count > 1, + SupportSelection = selectedImages.Count > 1 + }; + if (selectedImages.Count == 1) + { + printOp.CurrentPage = images.IndexOf(selectedImages[0]); + } + var printTarget = images; + printOp.BeginPrint += (_, args) => + { + if (printOp.PrintSettings.PrintPages == PrintPages.Selection) + { + printTarget = selectedImages; + printOp.NPages = printTarget.Count; + } + }; + printOp.DrawPage += (_, args) => + { + var image = printTarget[args.PageNr].Render(); + try + { + var ctx = args.Context; + var cairoCtx = ctx.CairoContext; + + if (Math.Sign(image.Width - image.Height) != Math.Sign(ctx.Width - ctx.Height)) + { + // Flip portrait/landscape to match output + image = image.PerformTransform(new RotationTransform(90)); + } + + // Fit the image into the output rect (centered) while maintaining its aspect ratio + var heightBound = image.Width / ctx.Width < image.Height / ctx.Height; + var targetWidth = heightBound ? image.Width * ctx.Height / image.Height : ctx.Width; + var targetHeight = heightBound ? ctx.Height : image.Height * ctx.Width / image.Width; + var targetX = (ctx.Width - targetWidth) / 2; + var targetY = (ctx.Height - targetHeight) / 2; + cairoCtx.Translate(targetX, targetY); + cairoCtx.Scale(targetWidth / image.Width, targetHeight / image.Height); + + Gdk.CairoHelper.SetSourcePixbuf(cairoCtx, image.AsPixbuf(), 0, 0); + cairoCtx.Paint(); + } + finally + { + image.Dispose(); + } + }; + printOp.EndPrint += (_, args) => + { + Log.Event(EventType.Print, new EventParams + { + Name = MiscResources.Print, + Pages = printOp.NPagesToPrint + }); + }; + var result = printOp.Run(PrintOperationAction.PrintDialog, (Window) parentWindow.ControlObject); + return Task.FromResult(result == PrintOperationResult.Apply); + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Gtk/Modules/GtkImagesModule.cs b/NAPS2.Lib.Gtk/Modules/GtkImagesModule.cs new file mode 100644 index 0000000000..2c95905464 --- /dev/null +++ b/NAPS2.Lib.Gtk/Modules/GtkImagesModule.cs @@ -0,0 +1,13 @@ +using Autofac; +using NAPS2.Images.Gtk; + +namespace NAPS2.Modules; + +public class GtkImagesModule : Module +{ + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().As(); + builder.RegisterType().AsSelf(); + } +} diff --git a/NAPS2.Lib.Gtk/Modules/GtkModule.cs b/NAPS2.Lib.Gtk/Modules/GtkModule.cs index 8d812d7382..33f4ead959 100644 --- a/NAPS2.Lib.Gtk/Modules/GtkModule.cs +++ b/NAPS2.Lib.Gtk/Modules/GtkModule.cs @@ -1,13 +1,7 @@ using Autofac; -using NAPS2.EtoForms; -using NAPS2.EtoForms.Desktop; -using NAPS2.EtoForms.Gtk; using NAPS2.EtoForms.Ui; -using NAPS2.Images.Gtk; using NAPS2.ImportExport; -using NAPS2.ImportExport.Pdf; -using NAPS2.Scan; -using NAPS2.Update; +using NAPS2.ImportExport.Email; namespace NAPS2.Modules; @@ -15,73 +9,14 @@ public class GtkModule : Module { protected override void Load(ContainerBuilder builder) { - // builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As().SingleInstance(); - builder.Register(ctx => ctx.Resolve()); - builder.RegisterType().As(); - builder.RegisterType().AsSelf().SingleInstance(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().AsSelf().SingleInstance(); - builder.RegisterType().As(); - builder.RegisterType().AsSelf(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As().WithParameter("systemDefault", true); + builder.RegisterType().As(); builder.RegisterType().As(); - - EtoPlatform.Current = new GtkEtoPlatform(); - // Log.EventLogger = new WindowsEventLogger(Kernel!.Get()); - } -} - -public class StubScannedImagePrinter : IScannedImagePrinter -{ - public Task PromptToPrint(IList images, IList selectedImages) - { - return Task.FromResult(false); - } -} - -public class StubNotificationManager : INotificationManager -{ - public void PdfSaved(string path) - { - } - - public void ImagesSaved(int imageCount, string path) - { - } - - public void DonatePrompt() - { - } - - public void OperationProgress(OperationProgress opModalProgress, IOperation op) - { - } - - public void UpdateAvailable(IUpdateChecker updateChecker, UpdateInfo update) - { - } - - public void Rebuild() - { + builder.RegisterType().As(); } } - -public class StubPdfPasswordProvider : IPdfPasswordProvider -{ - public bool ProvidePassword(string fileName, int attemptCount, out string password) - { - password = null!; - return false; - } -} \ No newline at end of file diff --git a/NAPS2.Lib.Gtk/NAPS2.Lib.Gtk.csproj b/NAPS2.Lib.Gtk/NAPS2.Lib.Gtk.csproj index e78176f147..a2435d42f4 100644 --- a/NAPS2.Lib.Gtk/NAPS2.Lib.Gtk.csproj +++ b/NAPS2.Lib.Gtk/NAPS2.Lib.Gtk.csproj @@ -1,14 +1,13 @@ - net6 + net9 enable true NAPS2 NAPS2 - Not Another PDF Scanner NAPS2 - Not Another PDF Scanner - Copyright 2009, 2012-2020 NAPS2 Contributors; Icons from http://www.fatcow.com/free-icons @@ -17,8 +16,8 @@ - - + + diff --git a/NAPS2.Lib.Gtk/Platform/LinuxApplicationLifecycle.cs b/NAPS2.Lib.Gtk/Platform/LinuxApplicationLifecycle.cs new file mode 100644 index 0000000000..d0012786be --- /dev/null +++ b/NAPS2.Lib.Gtk/Platform/LinuxApplicationLifecycle.cs @@ -0,0 +1,11 @@ +using NAPS2.Remoting; + +namespace NAPS2.Platform; + +public class LinuxApplicationLifecycle( + ProcessCoordinator processCoordinator, + IOsServiceManager serviceManager, + Naps2Config config) + : ApplicationLifecycle(processCoordinator, serviceManager, config) +{ +} \ No newline at end of file diff --git a/NAPS2.Lib.Gtk/Platform/LinuxOpenWith.cs b/NAPS2.Lib.Gtk/Platform/LinuxOpenWith.cs new file mode 100644 index 0000000000..7fd65b3f86 --- /dev/null +++ b/NAPS2.Lib.Gtk/Platform/LinuxOpenWith.cs @@ -0,0 +1,93 @@ +using System.Text.RegularExpressions; +using Gdk; +using Gtk; +using NAPS2.Images.Gtk; + +namespace NAPS2.Platform; + +public class LinuxOpenWith : IOpenWith +{ + private readonly ImageContext _imageContext; + + public LinuxOpenWith(ImageContext imageContext) + { + _imageContext = imageContext; + } + + public IEnumerable GetEntries(string fileExt) + { + string mimeType = fileExt switch + { + ".jpg" => "image/jpeg", + _ => throw new NotSupportedException("Unsupported mime type/extension") + }; + var desktopFiles = new List(); + var systemAppDir = new DirectoryInfo("/usr/share/applications"); + var userAppDir = new DirectoryInfo(Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".local/share/applications")); + if (systemAppDir.Exists) + { + desktopFiles.AddRange(systemAppDir.EnumerateFiles("*.desktop")); + } + if (userAppDir.Exists) + { + desktopFiles.AddRange(userAppDir.EnumerateFiles("*.desktop")); + } + foreach (var file in desktopFiles) + { + var data = ParseDesktopFile(file); + var mimeTypes = data.Get("MimeType", "").Split(";"); + if (!mimeTypes.Contains(mimeType)) continue; + yield return new OpenWithEntry( + file.FullName, + data.Get("Name") ?? Path.GetFileNameWithoutExtension(file.Name), + data.Get("Icon") ?? "", + 0); + } + } + + private Dictionary ParseDesktopFile(FileInfo file) + { + var data = new Dictionary(); + using var stream = file.OpenText(); + while (stream.ReadLine() is { } line) + { + var parts = line.Split("="); + if (parts.Length == 2) + { + data[parts[0]] = parts[1].TrimEnd(); + } + } + return data; + } + + public void OpenWith(string entryId, IEnumerable filePaths) + { + var data = ParseDesktopFile(new FileInfo(entryId)); + var exec = data.Get("Exec"); + if (exec == null) throw new InvalidOperationException($"Could not find Exec for {entryId}"); + var parts = exec.Split(" ", 2); + string exe = parts[0]; + string argsSpec = parts.Length > 1 ? parts[1]: ""; + // https://specifications.freedesktop.org/desktop-entry-spec/latest/exec-variables.html + var match = Regex.Match(argsSpec, "%[ufUF]"); + string expandedFilePaths = string.Join(" ", filePaths.Select(path => $"\"{path}\"")); + string args = match.Success ? match.Result(expandedFilePaths) : $"{argsSpec} {expandedFilePaths}"; + Process.Start(exe, args); + } + + public IMemoryImage? LoadIcon(OpenWithEntry entry) + { + if (entry.IconPath.StartsWith("/")) + { + return _imageContext.Load(entry.IconPath); + } + Pixbuf? resolvedIcon = IconTheme.Default.LookupIcon(entry.IconPath, 64, IconLookupFlags.UseBuiltin)?.LoadIcon(); + if (resolvedIcon != null) + { + return new GtkImage(resolvedIcon); + } + return null; + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Gtk/Platform/LinuxServiceManager.cs b/NAPS2.Lib.Gtk/Platform/LinuxServiceManager.cs new file mode 100644 index 0000000000..de95a0d44d --- /dev/null +++ b/NAPS2.Lib.Gtk/Platform/LinuxServiceManager.cs @@ -0,0 +1,82 @@ +namespace NAPS2.Platform; + +/// +/// Manages a user-level systemd service on Linux. +/// +public class LinuxServiceManager : IOsServiceManager +{ + // At the moment we only support systemd on Linux and without Flatpak + public bool CanRegister => Directory.Exists("/run/systemd/system/") && !File.Exists("/.flatpak-info"); + + private static string UnitPath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config/systemd/user/naps2-sharing-server.service"); + + public bool IsRegistered => File.Exists(UnitPath); + + public bool Register() + { + var unitDef = $""" + [Unit] + Description=NAPS2 Scanner Sharing Server + + [Service] + Type=simple + Restart=always + RestartSec=1 + ExecStart={Environment.ProcessPath} server + KillMode=process + + [Install] + WantedBy=default.target + """; + try + { + Directory.CreateDirectory(Path.GetDirectoryName(UnitPath)!); + File.WriteAllText(UnitPath, unitDef); + } + catch (Exception ex) + { + Log.ErrorException($"Error creating systemd unit: {UnitPath}", ex); + } + if (!ProcessHelper.TryRun("systemctl", "--user daemon-reload", 1000)) + { + Log.Error("Could not run systemctl daemon-reload"); + } + if (!ProcessHelper.TryRun("systemctl", "--user enable naps2-sharing-server", 1000)) + { + Log.Error("Could not enable service naps2-sharing-server"); + } + if (!ProcessHelper.TryRun("systemctl", "--user start naps2-sharing-server", 1000)) + { + Log.Error("Could not start service naps2-sharing-server"); + return false; + } + return true; + } + + public void Unregister() + { + if (!ProcessHelper.TryRun("systemctl", "--user stop naps2-sharing-server", 1000)) + { + Log.Error("Could not stop service naps2-sharing-server"); + } + if (!ProcessHelper.TryRun("systemctl", "--user disable naps2-sharing-server", 1000)) + { + Log.Error("Could not disable service naps2-sharing-server"); + } + try + { + File.Delete(UnitPath); + } + catch (Exception ex) + { + Log.ErrorException($"Error deleting systemd unit: {UnitPath}", ex); + } + if (!ProcessHelper.TryRun("systemctl", "--user daemon-reload", 1000)) + { + Log.Error("Could not run systemctl daemon-reload"); + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Gtk/Util/GLibLogInterceptor.cs b/NAPS2.Lib.Gtk/Util/GLibLogInterceptor.cs new file mode 100644 index 0000000000..1fffeca5ba --- /dev/null +++ b/NAPS2.Lib.Gtk/Util/GLibLogInterceptor.cs @@ -0,0 +1,85 @@ +using System.Globalization; +using System.Runtime.InteropServices; +using GLib; +using Log = NAPS2.Logging.Log; + +namespace NAPS2.Util; + +public static class GLibLogInterceptor +{ + /// + /// Intercepts GLib/Gtk logging and writes it to the NAPS2 debuglog instead of stdout. + /// + public static void WriteToDebugLog() + { + g_log_set_writer_func(Write, IntPtr.Zero, IntPtr.Zero); + } + + private static void Write(LogLevelFlags flags, IntPtr fields, nint nFields, IntPtr userData) + { + string glibDomain = "?"; + string message = "?"; + string priority = "?"; + + var size = Marshal.SizeOf(); + for (int i = 0; i < nFields; i++) + { + var field = Marshal.PtrToStructure(fields + i * size); + if (field.key == "PRIORITY") + { + var valueStr = Marshal.PtrToStringUTF8(field.value)!; + if (int.TryParse(valueStr, CultureInfo.InvariantCulture, out var parsedPriority)) + { + if (parsedPriority is < 0 or > 4) + { + // Only log warning or worse + return; + } + priority = parsedPriority switch + { + 0 => "EMERG", + 1 => "ALERT", + 2 => "CRITICAL", + 3 => "ERROR", + 4 => "WARNING", + _ => throw new InvalidOperationException() + }; + } + } + if (field.key == "GLIB_DOMAIN") + { + var valueStr = Marshal.PtrToStringUTF8(field.value)!; + glibDomain = valueStr; + } + if (field.key == "MESSAGE") + { + var valueStr = Marshal.PtrToStringUTF8(field.value)!; + message = valueStr; + } + } + + Log.Debug($"{glibDomain}-{priority}: {message}"); + } + + [DllImport("libglib-2.0.so.0")] + private static extern uint g_log_set_writer_func( + GLogWriterFunc log_func, + IntPtr user_data, + IntPtr user_data_free); + + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void GLogWriterFunc( + LogLevelFlags flags, + IntPtr fields, + nint nFields, + IntPtr user_data); + + [StructLayout(LayoutKind.Sequential)] + struct GLogField + { + public string key; + public IntPtr value; + public nint length; + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Gtk/Util/GtkEtoExtensions.cs b/NAPS2.Lib.Gtk/Util/GtkEtoExtensions.cs new file mode 100644 index 0000000000..bed8f0004e --- /dev/null +++ b/NAPS2.Lib.Gtk/Util/GtkEtoExtensions.cs @@ -0,0 +1,61 @@ +using System.Reflection; +using Eto.Forms; +using Eto.GtkSharp; +using Eto.GtkSharp.Forms.Controls; +using Gdk; +using Gtk; + +namespace NAPS2.Util; + +public static class GtkEtoExtensions +{ + private static readonly FieldInfo GtkImageField = + typeof(ButtonHandler).GetField("gtkimage", + BindingFlags.Instance | BindingFlags.NonPublic)!; + + private static readonly MethodInfo SetImagePositionMethod = + typeof(ButtonHandler).GetMethod( + "SetImagePosition", BindingFlags.Instance | BindingFlags.NonPublic)!; + + public static Image ToScaledImage(this Pixbuf pixbuf, int scaleFactor) + { + // Creating a Gtk.Image directly from a Pixbuf doesn't work if we want to render at high dpi. + // For example, if we have a 32x32 logical image size that actually gets rendered at 64x64 pixels due to a 2x + // display scaling, naively using the 64x64 image will get displayed with 64x64 logical size and 128x128 + // physical size, which will look too big and blurry. To get an actual crisp image rendered at 32x32 logical + // pixels and 64x64 physical pixels, we first create a surface with a 2x scale factor and then create the + // Gtk.Image from that. + using var surface = Gdk.CairoHelper.SurfaceCreateFromPixbuf(pixbuf, scaleFactor, null); + return new Image(surface); + } + + public static void SetImage(this Eto.Forms.Button button, Image image) + { + // Hack to inject a Gtk.Image directly into an Eto.Forms.Button. Normally Eto only takes an Eto.Drawing.Image + // (which wraps a Pixbuf) but to correctly scale images at high dpi we need a Gtk.Image constructed from a + // surface. + image.Show(); + GtkImageField.SetValue(button.Handler, image); + SetImagePositionMethod.Invoke(button.Handler, []); + } + + public static void ScaleImage(this Eto.Forms.Button button) + { + // Helper to read the Eto.Drawing.Image from the Eto.Forms.Button, scale it, then inject it back into the + // button. + int scaleFactor = button.ToNative().ScaleFactor; + var image = button.Image.ToGdk().ToScaledImage(scaleFactor); + button.SetImage(image); + } + + // Workaround for https://github.com/picoe/Eto/issues/2601 with ToEto() + public static Control AsEto(this Widget widget) => new(new NativeHandler(widget)); + + private class NativeHandler : Eto.GtkSharp.Forms.GtkControl + { + public NativeHandler(Widget widget) + { + Control = widget; + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Mac/EntryPoints/MacEntryPoint.cs b/NAPS2.Lib.Mac/EntryPoints/MacEntryPoint.cs index 56e44dff87..b643981998 100644 --- a/NAPS2.Lib.Mac/EntryPoints/MacEntryPoint.cs +++ b/NAPS2.Lib.Mac/EntryPoints/MacEntryPoint.cs @@ -1,39 +1,16 @@ -using Autofac; -using NAPS2.EtoForms; -using NAPS2.EtoForms.Ui; +using NAPS2.EtoForms; +using NAPS2.EtoForms.Mac; using NAPS2.Modules; -using NAPS2.Remoting.Worker; -using UnhandledExceptionEventArgs = Eto.UnhandledExceptionEventArgs; namespace NAPS2.EntryPoints; /// -/// The entry point logic for NAPS2.exe, the NAPS2 GUI. +/// The entry point logic for the Mac NAPS2 executable. /// public static class MacEntryPoint { public static int Run(string[] args) { - if (args.Length > 0 && args[0] is "cli" or "console") - { - return ConsoleEntryPoint.Run(args.Skip(1).ToArray(), new MacModule()); - } - if (args.Length > 0 && args[0] == "worker") - { - return WorkerEntryPoint.Run(args.Skip(1).ToArray(), new MacModule()); - } - - // Initialize Autofac (the DI framework) - var container = AutoFacHelper.FromModules( - new CommonModule(), new MacModule(), new RecoveryModule(), new ContextModule()); - - Paths.ClearTemp(); - - // Set up basic application configuration - container.Resolve().SetCulturesFromConfig(); - TaskScheduler.UnobservedTaskException += UnhandledTaskException; - Trace.Listeners.Add(new ConsoleTraceListener()); - Runtime.MarshalManagedException += (_, eventArgs) => { Log.ErrorException("Marshalling managed exception", eventArgs.Exception); @@ -44,29 +21,15 @@ public static int Run(string[] args) Log.Error($"Marshalling ObjC exception: {eventArgs.Exception.Description}"); }; - // Start a pending worker process - container.Resolve().Init(); + EtoPlatform.Current = new MacEtoPlatform(); - // Show the main form - var application = EtoPlatform.Current.CreateApplication(); - application.UnhandledException += UnhandledException; - var formFactory = container.Resolve(); - var desktop = formFactory.Create(); - // Invoker.Current = new WinFormsInvoker(desktop.ToNative()); - - application.Run(desktop); - return 0; - } - - private static void UnhandledTaskException(object? sender, UnobservedTaskExceptionEventArgs e) - { - Log.FatalException("An error occurred that caused the task to terminate.", e.Exception); - e.SetObserved(); - } - - private static void UnhandledException(object? sender, UnhandledExceptionEventArgs e) - { - Log.FatalException("An error occurred that caused the application to close.", - e.ExceptionObject as Exception ?? new Exception()); + var subArgs = args.Skip(1).ToArray(); + return args switch + { + ["cli" or "console", ..] => ConsoleEntryPoint.Run(subArgs, new MacImagesModule(), new MacModule()), + ["worker", ..] => MacWorkerEntryPoint.Run(subArgs), + ["server", ..] => ServerEntryPoint.Run(subArgs, new MacImagesModule(), new MacModule()), + _ => GuiEntryPoint.Run(args, new MacImagesModule(), new MacModule()) + }; } } \ No newline at end of file diff --git a/NAPS2.Lib.Mac/EntryPoints/MacWorkerEntryPoint.cs b/NAPS2.Lib.Mac/EntryPoints/MacWorkerEntryPoint.cs new file mode 100644 index 0000000000..9f7d0387e0 --- /dev/null +++ b/NAPS2.Lib.Mac/EntryPoints/MacWorkerEntryPoint.cs @@ -0,0 +1,12 @@ +using NAPS2.Modules; + +namespace NAPS2.EntryPoints; + +public static class MacWorkerEntryPoint +{ + public static int Run(string[] args) + { + var app = NSApplication.SharedApplication; + return WorkerEntryPoint.Run(args, new MacImagesModule(), () => app.Run(), () => app.Terminate(null)); + } +} diff --git a/NAPS2.Lib.Mac/EtoForms/Mac/ListViewDataSource.cs b/NAPS2.Lib.Mac/EtoForms/Mac/ListViewDataSource.cs index 34ee3b77d8..ea0b47e6a5 100644 --- a/NAPS2.Lib.Mac/EtoForms/Mac/ListViewDataSource.cs +++ b/NAPS2.Lib.Mac/EtoForms/Mac/ListViewDataSource.cs @@ -1,17 +1,24 @@ +using NAPS2.EtoForms.Widgets; + namespace NAPS2.EtoForms.Mac; public class ListViewDataSource : NSCollectionViewDataSource where T : notnull { private readonly IListView _listView; private readonly ListViewBehavior _behavior; + private readonly Action _itemChecked; + private readonly Action _itemActivated; - public ListViewDataSource(IListView listView, ListViewBehavior behavior) + public ListViewDataSource(IListView listView, ListViewBehavior behavior, Action itemChecked, + Action itemActivated) { _listView = listView; _behavior = behavior; + _itemChecked = itemChecked; + _itemActivated = itemActivated; } - public List Items { get; } = new(); + public List Items { get; } = []; public override nint GetNumberofItems(NSCollectionView collectionView, nint section) { @@ -21,8 +28,13 @@ public override nint GetNumberofItems(NSCollectionView collectionView, nint sect public override NSCollectionViewItem GetItem(NSCollectionView collectionView, NSIndexPath indexPath) { var i = (int) indexPath.Item; - var image = _behavior.GetImage(Items[i], _listView.ImageSize); - var label = _behavior.ShowLabels ? _behavior.GetLabel(Items[i]) : null; - return new ListViewItem(image, label); + var item = Items[i]; + var image = _behavior.Checkboxes ? null : _behavior.GetImage(_listView, item); + var label = _behavior.ShowLabels ? _behavior.GetLabel(item) : null; + return new ListViewItem( + image, label, _behavior.Checkboxes, _behavior.ColorScheme, + isChecked => _itemChecked(item, isChecked), + _listView.Selection.Contains(item), + () => _itemActivated(item)); } } \ No newline at end of file diff --git a/NAPS2.Lib.Mac/EtoForms/Mac/ListViewItem.cs b/NAPS2.Lib.Mac/EtoForms/Mac/ListViewItem.cs index 86a728cc36..c1443f4b3e 100644 --- a/NAPS2.Lib.Mac/EtoForms/Mac/ListViewItem.cs +++ b/NAPS2.Lib.Mac/EtoForms/Mac/ListViewItem.cs @@ -7,15 +7,25 @@ namespace NAPS2.EtoForms.Mac; public class ListViewItem : NSCollectionViewItem { - private readonly Image _itemImage; + private readonly Image? _itemImage; private readonly string? _label; + private readonly bool _checkbox; + private readonly Action? _checkedChanged; + private readonly Action _onActivate; private bool _selected; + private readonly ColorScheme _colorScheme; private NSImageView? _imageView; - public ListViewItem(Image itemImage, string? label) + public ListViewItem(Image? itemImage, string? label, bool checkbox, ColorScheme colorScheme, + Action? checkedChanged, bool selected, Action onActivate) { _itemImage = itemImage; _label = label; + _checkbox = checkbox; + _checkedChanged = checkedChanged; + _selected = selected; + _colorScheme = colorScheme; + _onActivate = onActivate; } public override void LoadView() @@ -23,11 +33,26 @@ public override void LoadView() // TODO: Add padding/insets for the image from its border? Ideally the border shouldn't cover the actual image // The Photos app also interestingly has a 1px white between the image and the blue selection border // Though we're doing it differently as we have the black border always - if (_label != null) + if (_checkbox) { + // Note we can't use NSButton.CreateCheckbox due to https://github.com/xamarin/xamarin-macios/issues/17635 + var button = new NSButton + { + Title = _label!, + State = _selected ? NSCellStateValue.On : NSCellStateValue.Off + }; + button.SetButtonType(NSButtonType.Switch); + button.WithAction(() => _checkedChanged!(button.State == NSCellStateValue.On)); + View = button; + } + else if (_label != null) + { + var image = _itemImage.ToNS(); + // Resize high-dpi images so they render correctly on retina displays + image.Size = new CGSize(image.Size.Width / 2, image.Size.Height / 2); _imageView = new NSImageView { - Image = _itemImage.ToNS() + Image = image }; var stack = NSStackView.FromViews(new NSView[] { @@ -60,6 +85,15 @@ public override void LoadView() } View.WantsLayer = true; UpdateViewForSelectedState(); + + if (!_checkbox) + { + View.AddGestureRecognizer(new NSClickGestureRecognizer(_onActivate) + { + NumberOfClicksRequired = 2, + DelaysPrimaryMouseButtonEvents = false + }); + } } public override bool Selected @@ -74,9 +108,10 @@ public override bool Selected private void UpdateViewForSelectedState() { + if (_checkbox) return; var layer = View.Layer!; layer.BorderWidth = Selected ? 3 : _label == null ? 1 : 0; layer.CornerRadius = _label == null ? 0 : 4; - layer.BorderColor = Selected ? NSColor.SelectedContentBackground.ToCG() : NSColor.Black.ToCG(); + layer.BorderColor = Selected ? NSColor.SelectedContentBackground.ToCG() : _colorScheme.BorderColor.ToCG(); } } \ No newline at end of file diff --git a/NAPS2.Lib.Mac/EtoForms/Mac/MacDarkModeProvider.cs b/NAPS2.Lib.Mac/EtoForms/Mac/MacDarkModeProvider.cs new file mode 100644 index 0000000000..fb55a7f63f --- /dev/null +++ b/NAPS2.Lib.Mac/EtoForms/Mac/MacDarkModeProvider.cs @@ -0,0 +1,12 @@ +namespace NAPS2.EtoForms.Mac; + +public class MacDarkModeProvider : IDarkModeProvider +{ + public bool IsDarkModeEnabled => + NSUserDefaults.StandardUserDefaults.StringForKey("AppleInterfaceStyle") == "Dark"; + + // TODO: Is it possible to detect the change? +#pragma warning disable CS0067 + public event EventHandler? DarkModeChanged; +#pragma warning restore CS0067 +} \ No newline at end of file diff --git a/NAPS2.Lib.Mac/EtoForms/Mac/MacEtoPlatform.cs b/NAPS2.Lib.Mac/EtoForms/Mac/MacEtoPlatform.cs index a3ae40ccef..9fc9c5c81a 100644 --- a/NAPS2.Lib.Mac/EtoForms/Mac/MacEtoPlatform.cs +++ b/NAPS2.Lib.Mac/EtoForms/Mac/MacEtoPlatform.cs @@ -1,10 +1,13 @@ +using System.Reflection; using Eto; using Eto.Drawing; using Eto.Forms; using Eto.Mac; using Eto.Mac.Drawing; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; using NAPS2.Images.Mac; +using NAPS2.Remoting; namespace NAPS2.EtoForms.Mac; @@ -12,33 +15,68 @@ public class MacEtoPlatform : EtoPlatform { public override bool IsMac => true; - public override Application CreateApplication() + public override IIconProvider IconProvider { get; } = new MacIconProvider(new DefaultIconProvider()); + public override IDarkModeProvider DarkModeProvider { get; } = new MacDarkModeProvider(); + + public override void InitializeForegroundApp() + { + // We start the process as a background process (by setting LSBackgroundOnly in Info.plist) and only turn it + // into a foreground process once we know we're not in worker or console mode. This ensures workers don't have + // a chance to show in the dock. + MacProcessHelper.TransformThisProcessToForeground(); + } + + public override Application CreateApplicationCore() { - return new Application(Platforms.macOS); + var application = new Application(Platforms.macOS); + ((NSApplication) application.ControlObject).Delegate = new MacAppDelegate(); + return application; + } + + public override void Invoke(Application application, Action action) + { + // TODO: Eto PR to always use InvokeOnMainThread, don't execute the action directly + // even if we're already on the main thread. + NSApplication.SharedApplication.InvokeOnMainThread(action); + } + + public override void AsyncInvoke(Application application, Action action) + { + NSApplication.SharedApplication.BeginInvokeOnMainThread(action); } public override IListView CreateListView(ListViewBehavior behavior) => new MacListView(behavior); - public override void ConfigureImageButton(Button button, bool big) + public override void ConfigureImageButton(Button button, ButtonFlags flags) { + var nsButton = (NSButton) button.ToNative(); if (button.ImagePosition == ButtonImagePosition.Above) { - var nsButton = (NSButton) button.ToNative(); nsButton.ImageHugsTitle = true; + nsButton.Title = Environment.NewLine + nsButton.Title; + } + var image = nsButton.Image!; + if (image.Representations() is [NSBitmapImageRep rep, ..]) + { + image.Size = new CGSize(rep.PixelsWide / 2f, rep.PixelsHigh / 2f); + nsButton.Image = image; } } public override Bitmap ToBitmap(IMemoryImage image) { - // TODO: This is kind of busted in terms of image size. - // Eto seems to use NsImage.Size instead of Rep.PixelsWide/High. - // That can be incorrect (see MacImageTransformer.DoScale). - var nsImage = ((MacImage) image).NsImage; - return new Bitmap(new BitmapHandler(nsImage)); + var copy = (NSImage) image.AsNsImage().Copy(); + copy.Size = new CGSize(image.Width, image.Height); + return new Bitmap(new BitmapHandler(copy)); + } + + public override IMemoryImage FromBitmap(Bitmap bitmap) + { + return new MacImage(bitmap.ToNS()); } - public override IMemoryImage DrawHourglass(ImageContext imageContext, IMemoryImage image) + public override IMemoryImage DrawHourglass(IMemoryImage image) { // TODO return image; @@ -46,6 +84,12 @@ public override IMemoryImage DrawHourglass(ImageContext imageContext, IMemoryIma public override void SetFrame(Control container, Control control, Point location, Size size, bool inOverlay) { + if (control is Button) + { + // EtoButton has some weird IsAutoSize logic that conflicts with frame setting unless w/h are defined + control.Width = size.Width; + control.Height = size.Height; + } var rect = new CGRect(location.X, container.Height - location.Y - size.Height, size.Width, size.Height); var view = control.ToNative(); view.Frame = view.GetFrameForAlignmentRect(rect); @@ -61,11 +105,17 @@ public override void AddToContainer(Control container, Control control, bool inO container.ToNative().AddSubview(control.ToNative()); } - public override Control AccessibleImageButton(Image image, String text, Action onClick, - int xOffset = 0, int yOffset = 0) + public override void RemoveFromContainer(Control container, Control control) { - // TODO - return new NSView().ToEto(); + control.ToNative().RemoveFromSuperview(); + } + + public override float GetScaleFactor(Window window) => 2f; + + public override void ConfigureDonateButton(Button button) + { + var native = (NSButton) button.ToNative(); + native.Bordered = false; } public override LayoutElement CreateGroupBox(string title, LayoutElement content) @@ -85,4 +135,100 @@ public override LayoutElement CreateGroupBox(string title, LayoutElement content L.Buffer(content, 6, 32, 6, 6) ); } + + public override void SetClientSize(Window window, Size clientSize) + { + if (window.Loaded) + { + // The Eto ClientSize setter also changes the y-position and causes jumps + var nsWindow = window.ToNative(); + nsWindow.SetContentSize(clientSize.ToNS()); + } + else + { + base.SetClientSize(window, clientSize); + } + } + + public override void HandleKeyDown(Control control, Func handle) + { + var view = control.ToNative(); + var monitor = NSEvent.AddLocalMonitorForEventsMatchingMask(NSEventMask.KeyDown, evt => + { + if (ReferenceEquals(evt.Window, view.Window)) + { + var args = evt.ToEtoKeyEventArgs(); + return handle(args.KeyData) ? null! : evt; + } + return evt; + }); + control.UnLoad += (_, _) => NSEvent.RemoveMonitor(monitor); + } + + public override void AttachMouseWheelEvent(Control control, EventHandler eventHandler) + { + var view = control.ToNative(); + var monitor = NSEvent.AddLocalMonitorForEventsMatchingMask(NSEventMask.ScrollWheel, evt => + { + if (ReferenceEquals(evt.Window, view.Window) && + view.HitTest(evt.LocationInWindow) != null!) + { + var newArgs = new MouseEventArgs( + MouseButtons.None, + evt.ModifierFlags.ToEto(), + evt.LocationInWindow.ToEto(), + new SizeF((float) evt.DeltaX, (float) evt.DeltaY)); + eventHandler(control, newArgs); + return newArgs.Handled ? null! : evt; + } + return evt; + }); + control.UnLoad += (_, _) => NSEvent.RemoveMonitor(monitor); + } + + private class MacAppDelegate : AppDelegate + { + public override bool OpenFile(NSApplication sender, string filename) + { + Task.Run(() => + ProcessCoordinator.CreateDefault().OpenFile(Process.GetCurrentProcess(), 100, filename)); + return true; + } + + public override void OpenFiles(NSApplication sender, string[] filenames) + { + Task.Run(() => + ProcessCoordinator.CreateDefault().OpenFile(Process.GetCurrentProcess(), 100, filenames)); + } + } + + public override void ConfigureFileDialog(FileDialog fileDialog) + { + if (fileDialog is SaveFileDialog sd) + { + var savePanel = (NSSavePanel) sd.ControlObject; + // Ensure the correct extension is added on save + savePanel.AllowsOtherFileTypes = false; + } + + // Add some padding to the left of the file type selector + var createMethod = fileDialog.Handler.GetType().BaseType? + .GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); + // Ensure the accessory view is created first + createMethod?.Invoke(fileDialog.Handler, []); + var view = fileDialog switch + { + SaveFileDialog { ControlObject: NSSavePanel panel } => panel.AccessoryView, + OpenFileDialog { ControlObject: NSOpenPanel panel } => panel.AccessoryView, + _ => null + }; + if (view != null) + { + var label = view.Subviews[0]; + label.Frame = new CGRect(label.Frame.X + 20, label.Frame.Y, label.Frame.Width, label.Frame.Height); + var dropdown = view.Subviews[1]; + dropdown.Frame = new CGRect(dropdown.Frame.X + 20, dropdown.Frame.Y, dropdown.Frame.Width, + dropdown.Frame.Height); + } + } } \ No newline at end of file diff --git a/NAPS2.Lib.Mac/EtoForms/Mac/MacIconProvider.cs b/NAPS2.Lib.Mac/EtoForms/Mac/MacIconProvider.cs index 73c67317ab..ce0c0ba708 100644 --- a/NAPS2.Lib.Mac/EtoForms/Mac/MacIconProvider.cs +++ b/NAPS2.Lib.Mac/EtoForms/Mac/MacIconProvider.cs @@ -8,17 +8,21 @@ public class MacIconProvider : IIconProvider private static readonly Dictionary IconMap = new() { { "control_play_blue", "play" }, + { "add", "plus.circle" }, + { "pencil", "pencil" }, + { "cross", "trash" }, + { "cross_small", "xmark" }, + { "accept", "checkmark.circle" }, + { "wireless", "wifi" }, { "blueprints", "list.bullet" }, { "folder_picture", "folder" }, { "diskette", "square.and.arrow.down" }, { "zoom", "viewfinder" }, - { "zoom_small", "viewfinder" }, { "arrow_rotate_anticlockwise", "arrow.counterclockwise" }, - { "arrow_rotate_anticlockwise_small", "arrow.counterclockwise" }, - { "arrow_rotate_clockwise_small", "arrow.clockwise" }, - { "arrow_switch_small", "arrow.2.squarepath" }, - { "arrow_up_small", "arrow.up" }, - { "arrow_down_small", "arrow.down" }, + { "arrow_rotate_clockwise", "arrow.clockwise" }, + { "arrow_switch", "arrow.2.squarepath" }, + { "arrow_up", "arrow.up" }, + { "arrow_down", "arrow.down" }, { "arrow_left", "arrow.left" }, { "arrow_right", "arrow.right" }, { "transform_crop", "crop" }, @@ -30,10 +34,33 @@ public class MacIconProvider : IIconProvider { "contrast", "circle.righthalf.filled" }, { "contrast_high", "circle.righthalf.filled" }, { "sharpen", "rhombus" }, - { "cross", "trash" }, - // TODO: Probably just use the save icon for these { "file_extension_pdf", "doc.richtext" }, - { "pictures", "photo" } + { "pictures", "photo" }, + { "document", "doc.text" }, + { "split", "squareshape.split.2x2.dotted" }, + { "text_align_justify", "text.justify" }, + { "large_tiles", "square.grid.2x2" }, + { "exclamation", "exclamationmark.triangle" }, + { "application_side_list", "sidebar.left" }, + { "ask", "questionmark" }, + { "split_ver", "square.split.2x1" }, + { "split_hor", "square.split.1x2" }, + { "shape_align_left", "align.horizontal.left" }, + { "shape_align_center", "align.horizontal.center" }, + { "shape_align_right", "align.horizontal.right" }, + { "shape_align_top", "align.vertical.top" }, + { "shape_align_middle", "align.vertical.center" }, + { "shape_align_bottom", "align.vertical.bottom" }, + { "combine", "square.split.1x2" }, + { "combine_hor", "square.split.2x1" }, + { "combine_ver", "square.split.1x2" }, + { "switch_hor", "arrow.left.and.right" }, + { "switch_ver", "arrow.up.and.down" } + // TODO: This doesn't render properly as it's very wide and gets squished + // { "keyboard", "keyboard" }, + // TODO: Consider these + // { "network_ip", "wifi.router" }, + // { "device", "scanner" }, }; private readonly DefaultIconProvider _defaultIconProvider; @@ -43,7 +70,7 @@ public MacIconProvider(DefaultIconProvider defaultIconProvider) _defaultIconProvider = defaultIconProvider; } - public Image? GetIcon(string name) + public Bitmap? GetIcon(string name, float scale = 1f, bool oversized = false) { if (!OperatingSystem.IsMacOSVersionAtLeast(11) && name == "arrow_rotate_anticlockwise") { @@ -51,14 +78,33 @@ public MacIconProvider(DefaultIconProvider defaultIconProvider) // TODO: Also maybe map other icons to 16x16 versions (e.g. control_play_blue) for better rendering return _defaultIconProvider.GetIcon("arrow_rotate_anticlockwise_small"); } - if (OperatingSystem.IsMacOSVersionAtLeast(11) && IconMap.ContainsKey(name)) + if (OperatingSystem.IsMacOSVersionAtLeast(11)) { - var symbol = NSImage.GetSystemSymbol(IconMap[name], null); - if (symbol != null) + if (!IconMap.ContainsKey(name) && name.EndsWith("_small")) { - return new Bitmap(new BitmapHandler(symbol)); + name = name.Substring(0, name.Length - 6); + } + if (IconMap.ContainsKey(name)) + { + var symbol = NSImage.GetSystemSymbol(IconMap[name], null); + if (symbol != null) + { + if (oversized) + { + symbol = symbol.GetImage(NSImageSymbolConfiguration.Create(32, 0.1))!; + } + if (name == "ask") + { + // Needs to be rendered at fixed dimensions to display properly in the Choose Device listview + symbol = symbol.GetImage(NSImageSymbolConfiguration.Create(60, 0.1)); + return (Bitmap) new Bitmap(new BitmapHandler(symbol)).PadTo(new Size(64, 64)); + } + return new Bitmap(new BitmapHandler(symbol)); + } } } - return _defaultIconProvider.GetIcon(name); + return _defaultIconProvider.GetIcon(name, scale); } + + public Icon? GetFormIcon(string name, float scale = 1f) => null; } \ No newline at end of file diff --git a/NAPS2.Lib.Mac/EtoForms/Mac/MacListView.cs b/NAPS2.Lib.Mac/EtoForms/Mac/MacListView.cs index 24ebc48238..df44fe05c3 100644 --- a/NAPS2.Lib.Mac/EtoForms/Mac/MacListView.cs +++ b/NAPS2.Lib.Mac/EtoForms/Mac/MacListView.cs @@ -1,57 +1,84 @@ +using Eto.Drawing; using Eto.Forms; using Eto.Mac; using Eto.Mac.Forms.Menu; +using NAPS2.EtoForms.Widgets; namespace NAPS2.EtoForms.Mac; +// NSPasteboardTypeFileUrl is deprecated but still needed +#pragma warning disable CS0618 // Type or member is obsolete + public class MacListView : NSCollectionViewDelegateFlowLayout, IListView where T : notnull { private readonly ListViewBehavior _behavior; private readonly NSCollectionView _view = new(); private readonly NSScrollView _scrollView = new(); private readonly Panel _panel = new(); - private readonly NSCollectionViewFlowLayout _layout; + private readonly NSCollectionViewLayout _layout; private readonly ListViewDataSource _dataSource; private ListSelection _selection = ListSelection.Empty(); private bool _refreshing; private ContextMenu? _contextMenu; + private NSIndexPath? _lastClickedIndexPath; public MacListView(ListViewBehavior behavior) { _behavior = behavior; - _layout = new CustomFlowLayout - { - SectionInset = behavior.ShowLabels - ? new NSEdgeInsets(5, 5, 5, 5) - : new NSEdgeInsets(20, 20, 20, 20), - MinimumInteritemSpacing = behavior.ShowLabels ? 5 : 15, - MinimumLineSpacing = behavior.ShowLabels ? 5 : 15, - TopAlign = behavior.ShowLabels, - LeftGravity = false // TODO: I prefer this true, but it glitches out selection - }; - _dataSource = new ListViewDataSource(this, _behavior); + _layout = _behavior.Checkboxes + ? new NSCollectionViewGridLayout + { + MinimumInteritemSpacing = 0, + MinimumLineSpacing = 0, + MinimumItemSize = new CGSize(200, 17) + } + : new CustomFlowLayout + { + SectionInset = behavior.ShowLabels + ? new NSEdgeInsets(5, 5, 5, 5) + : new NSEdgeInsets(20, 20, 20, 20), + MinimumInteritemSpacing = behavior.ShowLabels ? 5 : 15, + MinimumLineSpacing = behavior.ShowLabels ? 5 : 15, + TopAlign = behavior.ShowLabels, + LeftGravity = false // TODO: I prefer this true, but it glitches out selection + }; + _dataSource = new ListViewDataSource(this, _behavior, ItemChecked, ItemActivated); _view.DataSource = _dataSource; _view.Delegate = this; _view.CollectionViewLayout = _layout; - _view.Selectable = true; - _view.AllowsMultipleSelection = behavior.MultiSelect; + _view.Selectable = !behavior.Checkboxes; + _view.AllowsMultipleSelection = !behavior.Checkboxes && behavior.MultiSelect; if (_behavior.AllowDragDrop) { - _view.RegisterForDraggedTypes(new[] { _behavior.CustomDragDataType }); + _view.RegisterForDraggedTypes([_behavior.CustomDragDataType]); } if (_behavior.AllowFileDrop) { - _view.RegisterForDraggedTypes(new string[] { NSPasteboard.NSPasteboardTypeFileUrl }); + _view.RegisterForDraggedTypes([NSPasteboard.NSPasteboardTypeFileUrl]); } _scrollView.DocumentView = _view; _panel.Content = _scrollView.ToEto(); } - public int ImageSize + private void ItemChecked(T item, bool isChecked) + { + _selection = ListSelection.From(isChecked ? _selection.Append(item) : _selection.Except([item])); + SelectionChanged?.Invoke(this, EventArgs.Empty); + } + + private void ItemActivated(T item) { - get => (int) _layout.ItemSize.Width; - set => _layout.ItemSize = new CGSize(value, value); + // TODO: Propagate the item back to the click handler + // That doesn't matter on e.g. Windows where double clicking sets the item as the sole selection. But on Mac + // it's possible to select multiple items and double-click on one without losing your selection. + ItemClicked?.Invoke(this, EventArgs.Empty); + } + + public Size ImageSize + { + get => Size.Truncate(((NSCollectionViewFlowLayout) _layout).ItemSize.ToEto()); + set => ((NSCollectionViewFlowLayout) _layout).ItemSize = new CGSize(value.Width, value.Height); } public Control Control => _panel; @@ -68,10 +95,7 @@ public ContextMenu? ContextMenu public event EventHandler? SelectionChanged; - // TODO: Implement item double-click -#pragma warning disable CS0067 public event EventHandler? ItemClicked; -#pragma warning restore CS0067 public event EventHandler? Drop; @@ -82,9 +106,12 @@ public void SetItems(IEnumerable items) _view.ReloadData(); } - // TODO: Do we need this method? Clean up the name/doc at least public void RegenerateImages() { + if (_dataSource.Items.Count == 0) + { + return; + } _view.ReloadData(); } @@ -112,6 +139,12 @@ public void ApplyDiffs(ListViewDiffs diffs) var animator = (NSCollectionView) _view.Animator; animator.MoveItem(NSIndexPath.Create(0, fromIndex), NSIndexPath.Create(0, toIndex)); } + else if (!diffs.AppendOperations.Any() && !diffs.TrimOperations.Any()) + { + // Only updating items + var indexPaths = diffs.ReplaceOperations.Select(op => NSIndexPath.Create(0, op.Index)).ToArray(); + _view.ReloadItems(new NSSet(indexPaths)); + } else { _view.ReloadData(); @@ -180,14 +213,22 @@ private void UpdateViewSelection() if (!_refreshing) { _refreshing = true; - _view.SelectionIndexes = - NSIndexSet.FromArray(_selection.ToSelectedIndices(_dataSource.Items).Where(x => x != -1).ToArray()); + if (_behavior.Checkboxes) + { + // TODO: Support this? + } + else + { + _view.SelectionIndexes = + NSIndexSet.FromArray(_selection.ToSelectedIndices(_dataSource.Items).Where(x => x != -1).ToArray()); + } _refreshing = false; } } public override void ItemsSelected(NSCollectionView collectionView, NSSet indexPaths) { + if (_behavior.Checkboxes) return; _selection = ListSelection.FromSelectedIndices(_dataSource.Items, _view.SelectionIndexes.Select(x => (int) x)); SelectionChanged?.Invoke(this, EventArgs.Empty); @@ -195,11 +236,45 @@ public override void ItemsSelected(NSCollectionView collectionView, NSSet indexP public override void ItemsDeselected(NSCollectionView collectionView, NSSet indexPaths) { + if (_behavior.Checkboxes) return; _selection = ListSelection.FromSelectedIndices(_dataSource.Items, _view.SelectionIndexes.Select(x => (int) x)); SelectionChanged?.Invoke(this, EventArgs.Empty); } + public override NSSet ShouldDeselectItems(NSCollectionView collectionView, NSSet indexPaths) + { + _lastClickedIndexPath = null; + return indexPaths; + } + + public override NSSet ShouldSelectItems(NSCollectionView collectionView, NSSet indexPaths) + { + // We want shift+click to select a range, instead of behaving the same as command+click. + if (indexPaths.Count != 1) + { + _lastClickedIndexPath = null; + return indexPaths; + } + var indexPath = (NSIndexPath) indexPaths.AnyObject; + var prevIndexPath = _lastClickedIndexPath; + _lastClickedIndexPath = indexPath; + + var isShiftHeld = (NSEvent.CurrentModifierFlags & NSEventModifierMask.ShiftKeyMask) != 0; + if (!isShiftHeld || prevIndexPath == null) + { + return indexPaths; + } + var newIndexPaths = new NSMutableSet(); + var min = Math.Min((int) indexPath.Item, (int) prevIndexPath.Item); + var max = Math.Max((int) indexPath.Item, (int) prevIndexPath.Item); + for (var i = min; i <= max; i++) + { + newIndexPaths.Add(NSIndexPath.Create(0, i)); + } + return newIndexPaths; + } + public override void ItemsChanged(NSCollectionView collectionView, NSSet indexPaths, NSCollectionViewItemHighlightState highlightState) { @@ -212,16 +287,17 @@ public override CGSize SizeForItem(NSCollectionView collectionView, NSCollection var item = _dataSource.Items[(int) indexPath.Item]; if (_behavior.ShowLabels) { - using var image = _behavior.GetImage(item, ImageSize); - using var listItem = new ListViewItem(image, _behavior.GetLabel(item)); + using var image = _behavior.Checkboxes ? null : _behavior.GetImage(this, item); + using var listItem = new ListViewItem( + image, _behavior.GetLabel(item), _behavior.Checkboxes, _behavior.ColorScheme, null, false, () => { }); listItem.LoadView(); return listItem.View.FittingSize; } else { - var size = _behavior.GetImage(item, ImageSize).Size; + var size = _behavior.GetImage(this, item).Size; var max = (double) Math.Max(size.Width, size.Height); - return new CGSize(size.Width * ImageSize / max, size.Height * ImageSize / max); + return new CGSize(size.Width * ImageSize.Width / max, size.Height * ImageSize.Width / max); } } @@ -254,8 +330,7 @@ public override bool AcceptDrop(NSCollectionView collectionView, INSDraggingInfo private bool GetCustomData(INSDraggingInfo draggingInfo, out byte[] data) { - if (draggingInfo.DraggingPasteboard.CanReadItemWithDataConformingToTypes(new[] - { _behavior.CustomDragDataType })) + if (draggingInfo.DraggingPasteboard.CanReadItemWithDataConformingToTypes([_behavior.CustomDragDataType])) { var items = draggingInfo.DraggingPasteboard.PasteboardItems; data = items.Length > 1 @@ -270,8 +345,8 @@ private bool GetCustomData(INSDraggingInfo draggingInfo, out byte[] data) private bool GetFilePaths(INSDraggingInfo draggingInfo, out IEnumerable filePaths) { - if (draggingInfo.DraggingPasteboard.CanReadItemWithDataConformingToTypes(new string[] - { NSPasteboard.NSPasteboardTypeFileUrl })) + if (draggingInfo.DraggingPasteboard.CanReadItemWithDataConformingToTypes( + [NSPasteboard.NSPasteboardTypeFileUrl])) { var items = draggingInfo.DraggingPasteboard.PasteboardItems; filePaths = @@ -309,8 +384,7 @@ public override NSDragOperation ValidateDrop(NSCollectionView collectionView, IN return _behavior.GetCustomDragEffect(data).ToNS(); } if (_behavior.AllowFileDrop && draggingInfo.DraggingPasteboard.CanReadItemWithDataConformingToTypes( - new string[] - { NSPasteboard.NSPasteboardTypeFileUrl })) + [NSPasteboard.NSPasteboardTypeFileUrl])) { return NSDragOperation.Copy; } @@ -332,7 +406,7 @@ public override bool CanDragItems(NSCollectionView collectionView, NSIndexSet in try { var item = new NSPasteboardItem(); - var binaryData = _behavior.SerializeCustomDragData(new[] { _dataSource.Items[(int) indexPath.Item] }); + var binaryData = _behavior.SerializeCustomDragData([_dataSource.Items[(int) indexPath.Item]]); item.SetDataForType(NSData.FromArray(binaryData), _behavior.CustomDragDataType); return item; } diff --git a/NAPS2.Lib.Mac/EtoForms/Mac/MacToolbarEntry.cs b/NAPS2.Lib.Mac/EtoForms/Mac/MacToolbarEntry.cs index abd8ab6a07..f5375fc01b 100644 --- a/NAPS2.Lib.Mac/EtoForms/Mac/MacToolbarEntry.cs +++ b/NAPS2.Lib.Mac/EtoForms/Mac/MacToolbarEntry.cs @@ -11,12 +11,12 @@ public static NSToolbarItem CreateSeparator(string id) return new ToolBarHandler.DividerToolbarItem(true); } - public static NSToolbarItem Create(string id, Command command, string? title = null, string? tooltip = null, + public static NSToolbarItem Create(string id, ActionCommand command, string? title = null, string? tooltip = null, bool nav = false) { var item = new NSToolbarItem(id) { - Image = command.Image?.ToNS(), + Image = command.GetIconImage(1)?.ToNS(), Title = title ?? "", Label = command.ToolBarText ?? "", // TODO: Verify this fixes label display on macOS 10.15 @@ -31,12 +31,12 @@ public static NSToolbarItem Create(string id, Command command, string? title = n return item; } - public static NSToolbarItem CreateMenu(string id, Command menuCommand, MenuProvider menuProvider, + public static NSToolbarItem CreateMenu(string id, ActionCommand menuCommand, MenuProvider menuProvider, string? title = null, string? tooltip = null) { return new NSMenuToolbarItem(id) { - Image = menuCommand.Image?.ToNS(), + Image = menuCommand.GetIconImage(1)?.ToNS(), Label = menuCommand.ToolBarText ?? "", Title = title ?? "", ToolTip = tooltip ?? menuCommand.ToolBarText ?? "", diff --git a/NAPS2.Lib.Mac/EtoForms/Ui/MacDesktopForm.cs b/NAPS2.Lib.Mac/EtoForms/Ui/MacDesktopForm.cs index 7a7b7894f2..1357d5d71c 100644 --- a/NAPS2.Lib.Mac/EtoForms/Ui/MacDesktopForm.cs +++ b/NAPS2.Lib.Mac/EtoForms/Ui/MacDesktopForm.cs @@ -1,44 +1,40 @@ -using System.Threading; using Eto.Forms; using NAPS2.EtoForms.Desktop; using NAPS2.EtoForms.Layout; using NAPS2.EtoForms.Mac; -using NAPS2.ImportExport.Images; +using NAPS2.EtoForms.Notifications; +using NAPS2.EtoForms.Widgets; using NAPS2.Scan; namespace NAPS2.EtoForms.Ui; public class MacDesktopForm : DesktopForm { + private NSSlider? _zoomSlider; + public MacDesktopForm( Naps2Config config, DesktopKeyboardShortcuts keyboardShortcuts, - INotificationManager notify, + NotificationManager notificationManager, CultureHelper cultureHelper, + ColorScheme colorScheme, IProfileManager profileManager, UiImageList imageList, - ImageTransfer imageTransfer, ThumbnailController thumbnailController, UiThumbnailProvider thumbnailProvider, DesktopController desktopController, IDesktopScanController desktopScanController, ImageListActions imageListActions, + ImageListViewBehavior imageListViewBehavior, DesktopFormProvider desktopFormProvider, IDesktopSubFormController desktopSubFormController, - DesktopCommands commands) - : base(config, keyboardShortcuts, notify, cultureHelper, profileManager, - imageList, imageTransfer, thumbnailController, thumbnailProvider, desktopController, desktopScanController, - imageListActions, desktopFormProvider, desktopSubFormController, commands) + Lazy commands, + Sidebar sidebar, + IIconProvider iconProvider) + : base(config, keyboardShortcuts, notificationManager, cultureHelper, colorScheme, profileManager, imageList, + thumbnailController, thumbnailProvider, desktopController, desktopScanController, imageListActions, + imageListViewBehavior, desktopFormProvider, desktopSubFormController, commands, sidebar, iconProvider) { - // For retina screens - _thumbnailController.Oversample = 2.0; - } - - protected override void OnLoad(EventArgs e) - { - // TODO: What's the best place to initialize this? It needs to happen from the UI event loop. - Invoker.Current = new SyncContextInvoker(SynchronizationContext.Current!); - base.OnLoad(e); } protected override void UpdateTitle(ScanProfile? defaultProfile) @@ -54,22 +50,20 @@ protected override void CreateToolbarsAndMenus() { Commands.MoveDown.ToolBarText = ""; Commands.MoveUp.ToolBarText = ""; - Commands.SaveAllPdf.Shortcut = Application.Instance.CommonModifier | Keys.S; - Commands.SaveSelectedPdf.Shortcut = Application.Instance.CommonModifier | Keys.Shift | Keys.S; - Commands.SaveAllImages.Shortcut = Application.Instance.CommonModifier | Keys.M; - Commands.SaveSelectedImages.Shortcut = Application.Instance.CommonModifier | Keys.Shift | Keys.M; Menu = new MenuBar { AboutItem = Commands.About, ApplicationItems = { + Commands.Settings, CreateSubMenu(Commands.LanguageMenu, GetLanguageMenuProvider()) }, Items = { new SubMenuItem { + // TODO: Localize this? Text = "File", Items = { @@ -77,65 +71,101 @@ protected override void CreateToolbarsAndMenus() new SeparatorMenuItem(), Commands.SaveAll, Commands.SaveSelected, - // TODO: Implement print/email on Mac/Linux - // new SeparatorMenuItem(), - // Commands.EmailAll, - // Commands.EmailSelected, - // Commands.Print, + new SeparatorMenuItem(), + Commands.EmailAll, + Commands.EmailSelected, + Commands.Print, new SeparatorMenuItem(), Commands.PdfSettings, Commands.ImageSettings, - // Commands.EmailSettings, + Commands.EmailSettings, new SeparatorMenuItem(), Commands.ClearAll } }, new SubMenuItem { - Text = "Edit" - }, - new SubMenuItem - { - Text = "Scan", + // The space makes sure this doesn't match the default "Edit" menu + Text = UiStrings.Edit + " ", Items = { - Commands.Scan, - Commands.NewProfile + Commands.SelectAll, + Commands.Copy, + Commands.Paste, + new SeparatorMenuItem(), + Commands.Undo, + Commands.Redo, + new SeparatorMenuItem(), + Commands.Delete } }, + CreateSubMenu(Commands.Scan, new MenuProvider() + .Dynamic(_scanMenuCommands) + .Separator() + .Append(Commands.NewProfile) + .Append(Commands.BatchScan)), + CreateSubMenu(Commands.ImageMenu, new MenuProvider() + .Append(Commands.ViewImage) + .Append(Commands.ZoomIn) + .Append(Commands.ZoomOut) + .Separator() + .Append(Commands.Crop) + .Append(Commands.BrightCont) + .Append(Commands.HueSat) + .Append(Commands.BlackWhite) + .Append(Commands.Sharpen) + .Append(Commands.DocumentCorrection) + .Separator() + .Append(Commands.Split) + .Append(Commands.Combine) + .Separator() + .Append(Commands.RotateLeft) + .Append(Commands.RotateRight) + .Append(Commands.Flip) + .Append(Commands.Deskew) + .Append(Commands.CustomRotate) + .Separator() + .Dynamic(_editWithCommands) + .Append(Commands.EditWithPick) + .Separator() + .Append(Commands.ResetImage)), new SubMenuItem { - Text = "Image", + Text = UiStrings.Reorder, Items = { - Commands.ViewImage, + Commands.MoveUp, + Commands.MoveDown, + new SeparatorMenuItem(), + Commands.Interleave, + Commands.Deinterleave, new SeparatorMenuItem(), - Commands.Crop, - Commands.BrightCont, - Commands.HueSat, - Commands.BlackWhite, - Commands.Sharpen, - Commands.DocumentCorrection, + Commands.AltInterleave, + Commands.AltDeinterleave, new SeparatorMenuItem(), - Commands.ResetImage + Commands.ReverseAll, + Commands.ReverseSelected } }, new SubMenuItem { - Text = "Tools", + Text = UiStrings.Tools, Items = { + Commands.Profiles, Commands.BatchScan, + Commands.ScannerSharing, Commands.Ocr } } } }; + // TODO: If we ever make significant changes to the toolbar layout, maybe add ".v2" so it resets saved config var toolbar = new NSToolbar("naps2.desktop.toolbar"); toolbar.Delegate = new MacToolbarDelegate(CreateMacToolbarItems()); toolbar.AllowsUserCustomization = true; - // toolbar.AutosavesConfiguration = true; + toolbar.AutosavesConfiguration = true; var window = this.ToNative(); if (OperatingSystem.IsMacOSVersionAtLeast(11)) @@ -155,8 +185,19 @@ protected override void CreateToolbarsAndMenus() private List CreateMacToolbarItems() { - return new List + _zoomSlider = new NSSlider { + MinValue = 0, + MaxValue = 1, + DoubleValue = ThumbnailSizes.SizeToCurve(_thumbnailController.VisibleSize), + ToolTip = UiStrings.Zoom + }; + _zoomSlider.WithAction(() => + _thumbnailController.VisibleSize = ThumbnailSizes.CurveToSize(_zoomSlider.DoubleValue)); + _thumbnailController.ThumbnailSizeChanged += ThumbnailController_ThumbnailSizeChanged; + + return + [ MacToolbarItems.Create("scan", Commands.Scan), MacToolbarItems.Create("profiles", Commands.Profiles), MacToolbarItems.CreateSpace(), @@ -167,29 +208,30 @@ protected override void CreateToolbarsAndMenus() MacToolbarItems.CreateMenu("rotate", Commands.RotateMenu, GetRotateMenuProvider()), MacToolbarItems.Create("moveUp", Commands.MoveUp, tooltip: UiStrings.MoveUp), MacToolbarItems.Create("moveDown", Commands.MoveDown, tooltip: UiStrings.MoveDown), + MacToolbarItems.Create("sidebar", Commands.ToggleSidebar, tooltip: UiStrings.ToggleSidebar, nav: true), new NSToolbarItem("zoom") { - View = new NSSlider - { - MinValue = 0, - MaxValue = 1, - DoubleValue = ThumbnailSizes.SizeToCurve(_thumbnailController.VisibleSize), - ToolTip = UiStrings.Zoom - }.WithAction(ZoomUpdated), + View = _zoomSlider, // MaxSize still works even though it's deprecated #pragma warning disable CA1416 #pragma warning disable CA1422 - MaxSize = new CGSize(64, 999) + MaxSize = new CGSize(64, 24) #pragma warning restore CA1422 #pragma warning restore CA1416 } - }; + ]; } - protected override LayoutElement GetZoomButtons() => C.Spacer(); + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + _thumbnailController.ThumbnailSizeChanged -= ThumbnailController_ThumbnailSizeChanged; + } - private void ZoomUpdated(NSSlider sender) + private void ThumbnailController_ThumbnailSizeChanged(object? o, EventArgs eventArgs) { - _thumbnailController.VisibleSize = ThumbnailSizes.CurveToSize(sender.DoubleValue); + _zoomSlider!.DoubleValue = ThumbnailSizes.SizeToCurve(_thumbnailController.VisibleSize); } + + protected override LayoutElement GetControlButtons() => C.Spacer(); } \ No newline at end of file diff --git a/NAPS2.Lib.Mac/EtoForms/Ui/MacPreviewForm.cs b/NAPS2.Lib.Mac/EtoForms/Ui/MacPreviewForm.cs index 9e6b7424ec..33668f77fa 100644 --- a/NAPS2.Lib.Mac/EtoForms/Ui/MacPreviewForm.cs +++ b/NAPS2.Lib.Mac/EtoForms/Ui/MacPreviewForm.cs @@ -1,13 +1,29 @@ using Eto.Forms; +using NAPS2.EtoForms.Desktop; using NAPS2.EtoForms.Mac; namespace NAPS2.EtoForms.Ui; public class MacPreviewForm : PreviewForm { + private readonly NSSlider _zoomSlider; + public MacPreviewForm(Naps2Config config, DesktopCommands desktopCommands, UiImageList imageList, - IIconProvider iconProvider) : base(config, desktopCommands, imageList, iconProvider) + IIconProvider iconProvider, ColorScheme colorScheme) : base(config, + desktopCommands, imageList, iconProvider, colorScheme) { + _zoomSlider = new NSSlider + { + MinValue = -2, + MaxValue = 1, + DoubleValue = 0, + ToolTip = UiStrings.Zoom + }.WithAction(ZoomUpdated); + ImageViewer.ZoomChanged += (_, _) => + { + _zoomSlider.DoubleValue = Math.Log10(ImageViewer.ZoomFactor); + _zoomSlider.MaxValue = Math.Log10(ImageViewer.MaxZoom); + }; } protected override void CreateToolbar() @@ -38,8 +54,8 @@ protected override void UpdatePage() private List CreateMacToolbarItems() { - return new List - { + return + [ MacToolbarItems.Create("prev", GoToPrevCommand, nav: true), MacToolbarItems.Create("next", GoToNextCommand, nav: true), MacToolbarItems.CreateMenu("rotate", Commands.RotateMenu, new MenuProvider() @@ -53,11 +69,31 @@ protected override void UpdatePage() MacToolbarItems.Create("huesat", Commands.HueSat), MacToolbarItems.Create("blackwhite", Commands.BlackWhite), MacToolbarItems.Create("sharpen", Commands.Sharpen), + MacToolbarItems.Create("documentcorrection", Commands.DocumentCorrection), + MacToolbarItems.Create("split", Commands.Split), + MacToolbarItems.Create("combine", Commands.Combine), + MacToolbarItems.Create("editwith", Commands.EditWithApp), MacToolbarItems.CreateSeparator("sep0"), MacToolbarItems.Create("save", Commands.SaveSelected), // TODO: Fix this // MacToolbarItems.CreateSeparator("sep1"), - MacToolbarItems.Create("delete", Commands.Delete) - }; + MacToolbarItems.Create("delete", DeleteCurrentImageCommand), + // TODO: Using the slider is a bit janky + new NSToolbarItem("zoom") + { + View = _zoomSlider, + // MaxSize still works even though it's deprecated +#pragma warning disable CA1416 +#pragma warning disable CA1422 + MaxSize = new CGSize(64, 24) +#pragma warning restore CA1422 +#pragma warning restore CA1416 + } + ]; + } + + private void ZoomUpdated(NSSlider sender) + { + ImageViewer.SetZoom((float) Math.Pow(10, sender.DoubleValue)); } } \ No newline at end of file diff --git a/NAPS2.Lib.Mac/ImportExport/AppleMailEmailProvider.cs b/NAPS2.Lib.Mac/ImportExport/AppleMailEmailProvider.cs new file mode 100644 index 0000000000..eac86f9f84 --- /dev/null +++ b/NAPS2.Lib.Mac/ImportExport/AppleMailEmailProvider.cs @@ -0,0 +1,84 @@ +using NAPS2.ImportExport.Email; + +namespace NAPS2.ImportExport; + +internal class AppleMailEmailProvider : IAppleMailEmailProvider +{ + [DllImport("/System/Library/Frameworks/CoreServices.framework/CoreServices")] + static extern IntPtr LSCopyDefaultHandlerForURLScheme(IntPtr urlScheme); + + public bool ShowInList => true; + + // Apple Mail only works if it's set as the default email reader + public bool CanSelectInList + { + get + { + using (var scheme = new NSString("mailto")) + { + IntPtr resultPtr = LSCopyDefaultHandlerForURLScheme(scheme.Handle); + if (resultPtr != IntPtr.Zero) + { + var bundleId = Runtime.GetNSObject(resultPtr); + return bundleId == "com.apple.mail"; + } + } + return false; + } + } + + public Task SendEmail(EmailMessage emailMessage, ProgressHandler progress = default) + { + return Task.Run(async () => + { + EmailServiceDelegate d = null!; + Invoker.Current.Invoke(() => + { + var service = NSSharingService.GetSharingService(NSSharingServiceName.ComposeEmail); + if (service == null) + { + throw new InvalidOperationException("Could not get ComposeEmail sharing service"); + } + if (emailMessage.Subject != null) + { + service.Subject = emailMessage.Subject; + } + if (emailMessage.Recipients.Any()) + { + service.Recipients = emailMessage.Recipients.Select(x => (NSObject) new NSString(x.Address)) + .ToArray(); + } + var items = new List(); + if (emailMessage.BodyText != null) + { + items.Add(new NSString(emailMessage.BodyText)); + } + foreach (var attachment in emailMessage.Attachments) + { + items.Add(NSUrl.FromFilename(attachment.FilePath)); + } + d = new EmailServiceDelegate(); + service.Delegate = d; + service.PerformWithItems(items.ToArray()); + }); + return await d.Task; + }); + } + + private class EmailServiceDelegate : NSSharingServiceDelegate + { + private readonly TaskCompletionSource _tcs = new(); + + public override void DidShareItems(NSSharingService sharingService, NSObject[] items) + { + _tcs.SetResult(true); + } + + public override void DidFailToShareItems(NSSharingService sharingService, NSObject[] items, NSError error) + { + _tcs.SetResult(false); + } + + public Task Task => _tcs.Task; + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Mac/ImportExport/MacScannedImagePrinter.cs b/NAPS2.Lib.Mac/ImportExport/MacScannedImagePrinter.cs new file mode 100644 index 0000000000..9a5e61caea --- /dev/null +++ b/NAPS2.Lib.Mac/ImportExport/MacScannedImagePrinter.cs @@ -0,0 +1,115 @@ +using NAPS2.Images.Mac; + +namespace NAPS2.ImportExport; + +public class MacScannedImagePrinter : IScannedImagePrinter +{ + public Task PromptToPrint( + Eto.Forms.Window parentWindow, IList images, IList selectedImages) + { + if (!images.Any()) + { + return Task.FromResult(false); + } + using var view = new PaginatedImageView(images); + var printOp = NSPrintOperation.FromView(view, new NSPrintInfo + { + LeftMargin = 0, + BottomMargin = 0, + RightMargin = 0, + TopMargin = 0, + HorizontalPagination = NSPrintingPaginationMode.Fit, + VerticalPagination = NSPrintingPaginationMode.Fit, + HorizontallyCentered = true, + VerticallyCentered = true, + Orientation = view.CurrentImage!.Width > view.CurrentImage.Height + ? NSPrintingOrientation.Landscape + : NSPrintingOrientation.Portrait + }); + if (printOp.RunOperation()) + { + Log.Event(EventType.Print, new EventParams + { + Name = MiscResources.Print, + Pages = (int) printOp.PageRange.Length + }); + return Task.FromResult(true); + } + return Task.FromResult(false); + } + + private class PaginatedImageView : NSBox + { + private readonly IList _images; + + private int _pageToRender; + private int _lastRenderedPage = -1; + + public PaginatedImageView(IList images) + { + _images = images; + // TODO: Fix deprecation issue +#pragma warning disable CA1422 + BorderType = NSBorderType.NoBorder; +#pragma warning restore CA1422 + TitlePosition = NSTitlePosition.NoTitle; + LoadImage(); + } + + public IMemoryImage? CurrentImage { get; private set; } + + public override bool KnowsPageRange(ref NSRange range) + { + range = new NSRange(1, _images.Count); + return true; + } + + public override void BeginPage(CGRect rect, CGPoint placement) + { + bool loaded = LoadImage(); + if (loaded && Math.Sign(CurrentImage!.Width - CurrentImage.Height) != Math.Sign(Frame.Width - Frame.Height)) + { + // Flip portrait/landscape to match output + var isOriginalPortrait = CurrentImage!.Width < CurrentImage.Height; + var angle = isOriginalPortrait ? -90 : 90; + CurrentImage = CurrentImage.PerformTransform(new RotationTransform(angle)); + } + ContentView = new NSImageView + { + Image = CurrentImage!.AsNsImage(), + ImageAlignment = NSImageAlignment.Center, + ImageScaling = NSImageScale.ProportionallyUpOrDown + }; + base.BeginPage(rect, placement); + } + + public override CGRect RectForPage(nint pageNumber) + { + _pageToRender = (int) pageNumber - 1; + var operation = NSPrintOperation.CurrentOperation; + if (Frame.Size != operation.PrintInfo.PaperSize) + { + SetFrameSize(operation.PrintInfo.PaperSize); + } + return new CGRect(new CGPoint(0, 0), operation.PrintInfo.PaperSize); + } + + private bool LoadImage() + { + if (_lastRenderedPage == _pageToRender) return false; + _lastRenderedPage = _pageToRender; + CurrentImage?.Dispose(); + CurrentImage = _images[_pageToRender].Render(); + return true; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + CurrentImage?.Dispose(); + } + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Mac/Modules/MacImagesModule.cs b/NAPS2.Lib.Mac/Modules/MacImagesModule.cs new file mode 100644 index 0000000000..d775091e25 --- /dev/null +++ b/NAPS2.Lib.Mac/Modules/MacImagesModule.cs @@ -0,0 +1,13 @@ +using Autofac; +using NAPS2.Images.Mac; + +namespace NAPS2.Modules; + +public class MacImagesModule : Module +{ + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().As(); + builder.RegisterType().AsSelf(); + } +} diff --git a/NAPS2.Lib.Mac/Modules/MacModule.cs b/NAPS2.Lib.Mac/Modules/MacModule.cs index a1a7140c1b..8a134c7c90 100644 --- a/NAPS2.Lib.Mac/Modules/MacModule.cs +++ b/NAPS2.Lib.Mac/Modules/MacModule.cs @@ -1,13 +1,9 @@ using Autofac; using NAPS2.EtoForms; -using NAPS2.EtoForms.Desktop; using NAPS2.EtoForms.Mac; using NAPS2.EtoForms.Ui; -using NAPS2.Images.Mac; using NAPS2.ImportExport; -using NAPS2.ImportExport.Pdf; -using NAPS2.Scan; -using NAPS2.Update; +using NAPS2.ImportExport.Email; namespace NAPS2.Modules; @@ -15,75 +11,16 @@ public class MacModule : Module { protected override void Load(ContainerBuilder builder) { - // builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As().SingleInstance(); - builder.Register(ctx => ctx.Resolve()); - builder.RegisterType().As(); - builder.RegisterType().AsSelf().SingleInstance(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().AsSelf().SingleInstance(); - builder.RegisterType().As(); - builder.RegisterType().AsSelf(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As().WithParameter("systemDefault", true); + builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); - - EtoPlatform.Current = new MacEtoPlatform(); - // Log.EventLogger = new WindowsEventLogger(Kernel!.Get()); - } -} - -public class StubScannedImagePrinter : IScannedImagePrinter -{ - public Task PromptToPrint(IList images, IList selectedImages) - { - return Task.FromResult(false); - } -} - -public class StubNotificationManager : INotificationManager -{ - public void PdfSaved(string path) - { - } - - public void ImagesSaved(int imageCount, string path) - { - } - - public void DonatePrompt() - { - } - - public void OperationProgress(OperationProgress opModalProgress, IOperation op) - { - } - - public void UpdateAvailable(IUpdateChecker updateChecker, UpdateInfo update) - { - } - - public void Rebuild() - { } } - -public class StubPdfPasswordProvider : IPdfPasswordProvider -{ - public bool ProvidePassword(string fileName, int attemptCount, out string password) - { - password = null!; - return false; - } -} \ No newline at end of file diff --git a/NAPS2.Lib.Mac/NAPS2.Lib.Mac.csproj b/NAPS2.Lib.Mac/NAPS2.Lib.Mac.csproj index 0c5b0d44e7..cc77b58c67 100644 --- a/NAPS2.Lib.Mac/NAPS2.Lib.Mac.csproj +++ b/NAPS2.Lib.Mac/NAPS2.Lib.Mac.csproj @@ -1,14 +1,13 @@ - net7-macos10.15 + net9-macos enable true NAPS2 NAPS2 - Not Another PDF Scanner NAPS2 - Not Another PDF Scanner - Copyright 2009, 2012-2020 NAPS2 Contributors; Icons from http://www.fatcow.com/free-icons @@ -17,7 +16,7 @@ - + diff --git a/NAPS2.Lib.Mac/Platform/MacApplicationLifecycle.cs b/NAPS2.Lib.Mac/Platform/MacApplicationLifecycle.cs new file mode 100644 index 0000000000..1a6c3066a2 --- /dev/null +++ b/NAPS2.Lib.Mac/Platform/MacApplicationLifecycle.cs @@ -0,0 +1,16 @@ +using NAPS2.Remoting; + +namespace NAPS2.Platform; + +public class MacApplicationLifecycle( + ProcessCoordinator processCoordinator, + IOsServiceManager serviceManager, + Naps2Config config) + : ApplicationLifecycle(processCoordinator, serviceManager, config) +{ + protected override void HandleSingleInstance() + { + // Mac is single-instance by default and doesn't need any special handling. + // "Open With" also uses NSApplicationDelegate so we don't need to communicate that cross-process. + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Mac/Platform/MacOpenWith.cs b/NAPS2.Lib.Mac/Platform/MacOpenWith.cs new file mode 100644 index 0000000000..a4cad8cefd --- /dev/null +++ b/NAPS2.Lib.Mac/Platform/MacOpenWith.cs @@ -0,0 +1,42 @@ +using NAPS2.Images.Mac; +using UniformTypeIdentifiers; + +namespace NAPS2.Platform; + +public class MacOpenWith : IOpenWith +{ + public IEnumerable GetEntries(string fileExt) + { + UTType contentType = fileExt switch + { + ".jpg" => UTTypes.Jpeg, + _ => throw new NotSupportedException("Unsupported mime type/extension") + }; + var appUrls = NSWorkspace.SharedWorkspace.GetUrlsForApplicationsToOpenContentType(contentType); + foreach (var appUrl in appUrls) + { + yield return new OpenWithEntry( + appUrl.Path!, + Path.GetFileNameWithoutExtension(appUrl.Path!), + "", + 0 + ); + } + } + + public void OpenWith(string entryId, IEnumerable filePaths) + { + string expandedFilePaths = string.Join(" ", filePaths.Select(path => $"\"{path}\"")); + Process.Start("open", $"-a \"{entryId}\" {expandedFilePaths}"); + } + + public IMemoryImage LoadIcon(OpenWithEntry entry) + { + NSImage allReps = NSWorkspace.SharedWorkspace.IconForFile(entry.Id); + // TODO: Any cleaner way to do this conversion? + NSImageRep rep = allReps.BestRepresentation(new CGRect(0, 0, 64, 64), null, null); + NSImage image = new NSImage(); + image.AddRepresentation(new NSBitmapImageRep(rep.CGImage)); + return new MacImage(image); + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Mac/Platform/MacServiceManager.cs b/NAPS2.Lib.Mac/Platform/MacServiceManager.cs new file mode 100644 index 0000000000..2e54305f8e --- /dev/null +++ b/NAPS2.Lib.Mac/Platform/MacServiceManager.cs @@ -0,0 +1,72 @@ +namespace NAPS2.Platform; + +/// +/// Manages a user-level launchd (https://www.launchd.info/) service on macOS. +/// +public class MacServiceManager : IOsServiceManager +{ + private const string SERVICE_NAME = "com.naps2.ScannerSharing"; + + private static string PlistPath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + $"Library/LaunchAgents/{SERVICE_NAME}.plist"); + + public bool CanRegister => true; + + public bool IsRegistered => File.Exists(PlistPath); + + public bool Register() + { + var serviceDef = $""" + + + + + Label + {SERVICE_NAME} + Program + {Environment.ProcessPath} + ProgramArguments + + {Environment.ProcessPath} + server + + RunAtLoad + + + + """; + try + { + File.WriteAllText(PlistPath, serviceDef); + } + catch (Exception ex) + { + Log.ErrorException("Error creating sharing service PLIST", ex); + } + if (!ProcessHelper.TryRun("launchctl", $"load \"{PlistPath}\"", 1000)) + { + Log.Error($"Could not load service {SERVICE_NAME}"); + return false; + } + return true; + } + + public void Unregister() + { + // TODO: Longer timeout / run async? + if (!ProcessHelper.TryRun("launchctl", $"unload \"{PlistPath}\"", 1000)) + { + Log.Error($"Could not unload service {SERVICE_NAME}"); + } + try + { + File.Delete(PlistPath); + } + catch (Exception ex) + { + Log.ErrorException("Error deleting sharing service PLIST", ex); + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Mac/Util/MacProcessHelper.cs b/NAPS2.Lib.Mac/Util/MacProcessHelper.cs new file mode 100644 index 0000000000..0aa4a48aa2 --- /dev/null +++ b/NAPS2.Lib.Mac/Util/MacProcessHelper.cs @@ -0,0 +1,26 @@ +namespace NAPS2.Util; + +public class MacProcessHelper +{ + [DllImport("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices")] + private static extern int TransformProcessType(ref ProcessSerialNumber psn, int type); + + private static ProcessSerialNumber _currentProcess = new() { hi = 0, lo = 2 }; + + public static void TransformThisProcessToBackground() + { + TransformProcessType(ref _currentProcess, 2); + } + + public static void TransformThisProcessToForeground() + { + TransformProcessType(ref _currentProcess, 1); + } + + [StructLayout(LayoutKind.Sequential)] + private struct ProcessSerialNumber + { + public int hi; + public int lo; + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Tests/Automation/AutomationHelper.cs b/NAPS2.Lib.Tests/Automation/AutomationHelper.cs index b1e8138a81..1d02c0b96f 100644 --- a/NAPS2.Lib.Tests/Automation/AutomationHelper.cs +++ b/NAPS2.Lib.Tests/Automation/AutomationHelper.cs @@ -4,42 +4,51 @@ using NAPS2.Scan.Internal; using NAPS2.Sdk.Tests; using NAPS2.Sdk.Tests.Mocks; -using Xunit.Abstractions; namespace NAPS2.Lib.Tests.Automation; internal class AutomationHelper { private readonly ContextualTests _testClass; - private readonly ITestOutputHelper _testOutputHelper; + private readonly TestOutputTextWriter _outputWriter; + private Action _containerBuilderSetup; + private Action _containerSetup; - public AutomationHelper(ContextualTests testClass, ITestOutputHelper testOutputHelper) + public AutomationHelper(ContextualTests testClass, TestOutputTextWriter outputWriter) { _testClass = testClass; - _testOutputHelper = testOutputHelper; + _outputWriter = outputWriter; } - public Task RunCommand(AutomatedScanningOptions options, params byte[][] imagesToScan) + public string Output => _outputWriter.Output; + + public AutomationHelper WithContainer(Action setup) { - return RunCommand(options, null, imagesToScan); + return new AutomationHelper(_testClass, _outputWriter) + { + _containerSetup = setup + }; } - public Task RunCommand(AutomatedScanningOptions options, Action setup, params byte[][] imagesToScan) + public AutomationHelper WithContainerBuilder(Action setup) { - return RunCommand(options, setup, new ScanDriverFactoryBuilder().WithScannedImages(imagesToScan).Build()); + return new AutomationHelper(_testClass, _outputWriter) + { + _containerBuilderSetup = setup + }; } - public Task RunCommand(AutomatedScanningOptions options, IScanDriverFactory scanDriverFactory) + public Task RunCommand(AutomatedScanningOptions options, params byte[][] imagesToScan) { - return RunCommand(options, null, scanDriverFactory); + return RunCommand(options, new ScanDriverFactoryBuilder().WithScannedImages(imagesToScan).Build()); } - public async Task RunCommand(AutomatedScanningOptions options, Action setup, IScanDriverFactory scanDriverFactory) + public async Task RunCommand(AutomatedScanningOptions options, IScanDriverFactory scanDriverFactory) { - var container = AutoFacHelper.FromModules(new CommonModule(), new ConsoleModule(options), - new TestModule(_testClass.ScanningContext, _testClass.ImageContext, scanDriverFactory, _testOutputHelper, - _testClass.FolderPath)); - setup?.Invoke(container); + var testModule = new TestModule(_testClass.ScanningContext, _testClass.ImageContext, scanDriverFactory, + _outputWriter, _testClass.FolderPath, _containerBuilderSetup); + var container = AutoFacHelper.FromModules(new CommonModule(), new ConsoleModule(options), testModule); + _containerSetup?.Invoke(container); var automatedScanning = container.Resolve(); await automatedScanning.Execute(); } diff --git a/NAPS2.Lib.Tests/Automation/CommandLineIntegrationTests.cs b/NAPS2.Lib.Tests/Automation/CommandLineIntegrationTests.cs index d7463acaf3..d8d6bfe0d0 100644 --- a/NAPS2.Lib.Tests/Automation/CommandLineIntegrationTests.cs +++ b/NAPS2.Lib.Tests/Automation/CommandLineIntegrationTests.cs @@ -1,9 +1,14 @@ +using System.Threading; using Autofac; using NAPS2.Automation; -using NAPS2.ImportExport.Pdf; +using NAPS2.ImportExport.Email; +using NAPS2.Pdf; +using NAPS2.Scan; +using NAPS2.Scan.Internal; using NAPS2.Sdk.Tests; using NAPS2.Sdk.Tests.Asserts; using NAPS2.Sdk.Tests.Mocks; +using NSubstitute; using Xunit; using Xunit.Abstractions; @@ -24,8 +29,9 @@ public class CommandLineIntegrationTests : ContextualTests private readonly AutomationHelper _automationHelper; public CommandLineIntegrationTests(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) { - _automationHelper = new AutomationHelper(this, testOutputHelper); + _automationHelper = new AutomationHelper(this, new TestOutputTextWriter(testOutputHelper)); } [Fact] @@ -58,9 +64,31 @@ await _automationHelper.RunCommand( } [Fact] - public async Task ScanWithOcr() + public async Task ScanWithNoOcr() { - SetUpOcr(); + SetUpFakeOcr(new() + { + { LoadImage(ImageResources.ocr_test), "ADVERTISEMENT." } + }); + var path = $"{FolderPath}/test.pdf"; + await _automationHelper.RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + Verbose = true + }, + ImageResources.ocr_test); + PdfAsserts.AssertDoesNotContainText("ADVERTISEMENT.", path); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ScanWithOcrLang() + { + SetUpFakeOcr(new() + { + { LoadImage(ImageResources.ocr_test), "ADVERTISEMENT." } + }); var path = $"{FolderPath}/test.pdf"; await _automationHelper.RunCommand( new AutomatedScanningOptions @@ -75,26 +103,122 @@ await _automationHelper.RunCommand( } [Fact] - public async Task ScanPdfSettings_DefaultMetadata() + public async Task ScanWithEnableOcr() { + SetUpFakeOcr(new() + { + { LoadImage(ImageResources.ocr_test), "ADVERTISEMENT." } + }); var path = $"{FolderPath}/test.pdf"; - await _automationHelper.RunCommand( + await _automationHelper.WithContainer(container => + { + var config = container.Resolve(); + config.User.Set(c => c.OcrLanguageCode, "eng"); + }).RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + Verbose = true, + EnableOcr = true + }, + ImageResources.ocr_test); + PdfAsserts.AssertContainsTextOnce("ADVERTISEMENT.", path); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ScanWithDisableOcr() + { + SetUpFakeOcr(new() + { + { LoadImage(ImageResources.ocr_test), "ADVERTISEMENT." } + }); + var path = $"{FolderPath}/test.pdf"; + await _automationHelper.WithContainer(container => + { + var config = container.Resolve(); + config.User.Set(c => c.OcrLanguageCode, "eng"); + config.User.Set(c => c.EnableOcr, true); + }).RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + Verbose = true, + DisableOcr = true + }, + ImageResources.ocr_test); + PdfAsserts.AssertDoesNotContainText("ADVERTISEMENT.", path); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ScanWithOcrSettingsFromGui() + { + SetUpFakeOcr(new() + { + { LoadImage(ImageResources.ocr_test), "ADVERTISEMENT." } + }); + var path = $"{FolderPath}/test.pdf"; + await _automationHelper.WithContainer(container => + { + var config = container.Resolve(); + config.User.Set(c => c.OcrLanguageCode, "eng"); + config.User.Set(c => c.EnableOcr, true); + }).RunCommand( new AutomatedScanningOptions { OutputPath = path, Verbose = true }, - container => + ImageResources.ocr_test); + PdfAsserts.AssertContainsTextOnce("ADVERTISEMENT.", path); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ImportAndExportWithOcr() + { + SetUpFakeOcr(new() + { + { LoadImage(PdfResources.word_p1), "Page one." }, + { LoadImage(PdfResources.word_p2), "Page two." }, + { LoadImage(PdfResources.word_patcht_p1), "Sized for printing unscaled" } + }); + var importPath = $"{FolderPath}/import.pdf"; + File.WriteAllBytes(importPath, PdfResources.word_patcht_pdf); + var path = $"{FolderPath}/test.pdf"; + await _automationHelper.RunCommand( + new AutomatedScanningOptions + { + ImportPath = importPath, + OutputPath = path, + Verbose = true, + OcrLang = "eng" + }); + PdfAsserts.AssertContainsTextOnce("Sized for printing unscaled", path); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ScanPdfSettings_DefaultMetadata() + { + var path = $"{FolderPath}/test.pdf"; + await _automationHelper.WithContainer(container => + { + var config = container.Resolve(); + config.User.Set(c => c.PdfSettings.Metadata, new PdfMetadata + { + Author = "author1", + Creator = "creator1", + Keywords = "keywords1", + Subject = "subject1", + Title = "title1" + }); + }).RunCommand( + new AutomatedScanningOptions { - var config = container.Resolve(); - config.User.Set(c => c.PdfSettings.Metadata, new PdfMetadata - { - Author = "author1", - Creator = "creator1", - Keywords = "keywords1", - Subject = "subject1", - Title = "title1" - }); + OutputPath = path, + Verbose = true }, Image1); PdfAsserts.AssertMetadata(new PdfMetadata @@ -112,25 +236,24 @@ await _automationHelper.RunCommand( public async Task ScanPdfSettings_SavedMetadata() { var path = $"{FolderPath}/test.pdf"; - await _automationHelper.RunCommand( + await _automationHelper.WithContainer(container => + { + var config = container.Resolve(); + config.User.Set(c => c.PdfSettings.Metadata, new PdfMetadata + { + Author = "author1", + Creator = "creator1", + Keywords = "keywords1", + Subject = "subject1", + Title = "title1" + }); + }).RunCommand( new AutomatedScanningOptions { OutputPath = path, UseSavedMetadata = true, Verbose = true }, - container => - { - var config = container.Resolve(); - config.User.Set(c => c.PdfSettings.Metadata, new PdfMetadata - { - Author = "author1", - Creator = "creator1", - Keywords = "keywords1", - Subject = "subject1", - Title = "title1" - }); - }, Image1); PdfAsserts.AssertMetadata(new PdfMetadata { @@ -173,25 +296,24 @@ await _automationHelper.RunCommand( public async Task ScanPdfSettings_SavedEncryptConfig() { var path = $"{FolderPath}/test.pdf"; - await _automationHelper.RunCommand( + await _automationHelper.WithContainer(container => + { + var config = container.Resolve(); + config.User.Set(c => c.PdfSettings.Encryption, new PdfEncryption + { + EncryptPdf = true, + OwnerPassword = "hello", + UserPassword = "world", + AllowAnnotations = true, + AllowContentCopying = false + }); + }).RunCommand( new AutomatedScanningOptions { OutputPath = path, UseSavedEncryptConfig = true, Verbose = true }, - container => - { - var config = container.Resolve(); - config.User.Set(c => c.PdfSettings.Encryption, new PdfEncryption - { - EncryptPdf = true, - OwnerPassword = "hello", - UserPassword = "world", - AllowAnnotations = true, - AllowContentCopying = false - }); - }, Image1); PdfAsserts.AssertEncrypted(path, "hello", "world", x => { @@ -251,7 +373,7 @@ await _automationHelper.RunCommand( } [Fact] - public async Task ExistingFile_NoOverwrite() + public async Task ExistingPdf_NoOverwrite() { var path = $"{FolderPath}/test.pdf"; File.WriteAllText(path, "blah"); @@ -267,7 +389,7 @@ await _automationHelper.RunCommand( } [Fact] - public async Task ExistingFile_ForceOverwrite() + public async Task ExistingPdf_ForceOverwrite() { var path = $"{FolderPath}/test.pdf"; File.WriteAllText(path, "blah"); @@ -283,6 +405,75 @@ await _automationHelper.RunCommand( AssertRecoveryCleanedUp(); } + [Fact] + public async Task ExistingPdf_ForceOverwrite_InUse() + { + var path = $"{FolderPath}/test.pdf"; + File.WriteAllText(path, "blah"); + using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None); + await _automationHelper.RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + ForceOverwrite = true, + Verbose = true + }, + Image1); + Assert.Contains("The file could not be overwritten because it is currently in use.", _automationHelper.Output); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ExistingImage_NoOverwrite() + { + var path = $"{FolderPath}/test.jpg"; + File.WriteAllText(path, "blah"); + await _automationHelper.RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + Verbose = true + }, + Image1); + Assert.Equal("blah", File.ReadAllText(path)); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ExistingImage_ForceOverwrite() + { + var path = $"{FolderPath}/test.jpg"; + File.WriteAllText(path, "blah"); + await _automationHelper.RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + ForceOverwrite = true, + Verbose = true + }, + Image1); + ImageAsserts.Similar(Image1, ImageContext.Load(path)); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ExistingImage_ForceOverwrite_InUse() + { + var path = $"{FolderPath}/test.jpg"; + File.WriteAllText(path, "blah"); + using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None); + await _automationHelper.RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + ForceOverwrite = true, + Verbose = true + }, + Image1); + Assert.Contains("The file could not be overwritten because it is currently in use.", _automationHelper.Output); + AssertRecoveryCleanedUp(); + } + [Fact] public async Task MultipleImages() { @@ -412,6 +603,26 @@ await _automationHelper.RunCommand( AssertRecoveryCleanedUp(); } + [Fact] + public async Task NoSplitWithMultipleScans() + { + var path = $"{FolderPath}/test$(n).pdf"; + await _automationHelper.RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + Number = 3, + Verbose = true + }, + new ScanDriverFactoryBuilder() + .WithScannedImages(Image1, Image2, Image3) + .WithScannedImages(Image4, Image5) + .WithScannedImages() + .Build()); + PdfAsserts.AssertImages($"{FolderPath}/test1.pdf", Image1, Image2, Image3, Image4, Image5); + AssertRecoveryCleanedUp(); + } + [Fact] public async Task SplitWithNoPlaceholder() { @@ -536,6 +747,482 @@ await _automationHelper.RunCommand( AssertRecoveryCleanedUp(); } + [Fact] + public async Task Rotate90() + { + // Test both import and scanning paths + var importPath = $"{FolderPath}/import.png"; + File.WriteAllBytes(importPath, ImageResources.dog_png); + var outputPath = $"{FolderPath}/test.pdf"; + await _automationHelper.RunCommand( + new AutomatedScanningOptions + { + ImportPath = importPath, + RotateDegrees = 90, + OutputPath = outputPath, + Verbose = true + }, + Image1); + PdfAsserts.AssertImages(outputPath, ImageResources.dog_r_p90, ImageResources.dog_r_p90); + AssertRecoveryCleanedUp(); + } + + [Fact(Skip = "needs fix for delta slightly above threshold")] + public async Task Deskew() + { + // Test both import and scanning paths + var importPath = $"{FolderPath}/import.jpg"; + File.WriteAllBytes(importPath, ImageResources.skewed); + var outputPath = $"{FolderPath}/test.pdf"; + await _automationHelper.RunCommand( + new AutomatedScanningOptions + { + ImportPath = importPath, + Deskew = true, + OutputPath = outputPath, + Verbose = true + }, + ImageResources.skewed); + PdfAsserts.AssertImages(outputPath, ImageResources.deskewed, ImageResources.deskewed); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task IgnoreSinglePagePdfSetting() + { + var path = $"{FolderPath}/test.pdf"; + await _automationHelper.WithContainer(container => + { + var config = container.Resolve(); + config.User.Set(c => c.PdfSettings.SinglePagePdfs, true); + }).RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + Verbose = true + }, + new[] { Image1, Image2, Image3 }); + PdfAsserts.AssertImages(path, Image1, Image2, Image3); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ScanToImageFiles() + { + var path = $"{FolderPath}/test$(n).jpg"; + await _automationHelper.RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + Split = true, + Verbose = true + }, + new[] { Image1, Image2, Image3 }); + ImageAsserts.Similar(Image1, ImageContext.Load($"{FolderPath}/test1.jpg")); + ImageAsserts.Similar(Image2, ImageContext.Load($"{FolderPath}/test2.jpg")); + ImageAsserts.Similar(Image3, ImageContext.Load($"{FolderPath}/test3.jpg")); + Assert.False(File.Exists($"{FolderPath}/test4.jpg")); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task EmailPdf() + { + var emailProviderFactory = new MockEmailProviderFactory(message => + { + Assert.Equal("Hello", message.Subject); + Assert.Equal(4, message.Recipients.Count); + Assert.Contains(message.Recipients, x => x.Address == "a@example.com" && x.Type == EmailRecipientType.To); + Assert.Contains(message.Recipients, x => x.Address == "b@example.com" && x.Type == EmailRecipientType.Cc); + Assert.Contains(message.Recipients, x => x.Address == "c@example.com" && x.Type == EmailRecipientType.Bcc); + Assert.Contains(message.Recipients, x => x.Address == "d@example.com" && x.Type == EmailRecipientType.To); + Assert.Equal("Hello world", message.BodyText); + Assert.True(message.AutoSend); + Assert.True(message.SilentSend); + Assert.Single(message.Attachments); + Assert.Equal("attachment.pdf", message.Attachments[0].AttachmentName); + PdfAsserts.AssertImages(message.Attachments[0].FilePath, Image1, Image2); + }); + + await _automationHelper.WithContainerBuilder(builder => + { + builder.RegisterInstance(emailProviderFactory); + }).RunCommand( + new AutomatedScanningOptions + { + EmailFileName = "attachment.pdf", + EmailSubject = "Hello", + EmailTo = "a@example.com,d@example.com", + EmailCc = "b@example.com", + EmailBcc = "c@example.com", + EmailBody = "Hello world", + EmailAutoSend = true, + EmailSilentSend = true, + Verbose = true + }, + Image1, Image2); + + emailProviderFactory.CheckAsserts(); + emailProviderFactory.VerifyExactlyOneMessageSent(); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task EmailSplitPdfs() + { + var emailProviderFactory = new MockEmailProviderFactory(message => + { + Assert.Equal(2, message.Attachments.Count); + Assert.Equal("attachment1.pdf", message.Attachments[0].AttachmentName); + PdfAsserts.AssertImages(message.Attachments[0].FilePath, Image1); + Assert.Equal("attachment2.pdf", message.Attachments[1].AttachmentName); + PdfAsserts.AssertImages(message.Attachments[1].FilePath, Image2); + }); + + await _automationHelper.WithContainerBuilder(builder => + { + builder.RegisterInstance(emailProviderFactory); + }).RunCommand( + new AutomatedScanningOptions + { + EmailFileName = "attachment$(n).pdf", + Split = true, + Verbose = true + }, + Image1, Image2); + + emailProviderFactory.CheckAsserts(); + emailProviderFactory.VerifyExactlyOneMessageSent(); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task EmailImageFiles() + { + var emailProviderFactory = new MockEmailProviderFactory(message => + { + Assert.Equal(2, message.Attachments.Count); + Assert.Equal("attachment1.jpg", message.Attachments[0].AttachmentName); + ImageAsserts.Similar(Image1, ImageContext.Load(message.Attachments[0].FilePath)); + Assert.Equal("attachment2.jpg", message.Attachments[1].AttachmentName); + ImageAsserts.Similar(Image2, ImageContext.Load(message.Attachments[1].FilePath)); + }); + + await _automationHelper.WithContainerBuilder(builder => + { + builder.RegisterInstance(emailProviderFactory); + }).RunCommand( + new AutomatedScanningOptions + { + EmailFileName = "attachment$(n).jpg", + Split = true, + Verbose = true + }, + Image1, Image2); + + emailProviderFactory.CheckAsserts(); + emailProviderFactory.VerifyExactlyOneMessageSent(); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task EmailAndSavePdf() + { + var path = $"{FolderPath}/test.pdf"; + var emailProviderFactory = new MockEmailProviderFactory(message => + { + Assert.Single(message.Attachments); + Assert.Equal("attachment.pdf", message.Attachments[0].AttachmentName); + PdfAsserts.AssertImages(message.Attachments[0].FilePath, Image1, Image2); + }); + + await _automationHelper.WithContainerBuilder(builder => + { + builder.RegisterInstance(emailProviderFactory); + }).RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + EmailFileName = "attachment.pdf", + Verbose = true + }, + Image1, Image2); + + emailProviderFactory.CheckAsserts(); + emailProviderFactory.VerifyExactlyOneMessageSent(); + PdfAsserts.AssertImages(path, Image1, Image2); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task EmailAndSaveImageFiles() + { + var path = $"{FolderPath}/test$(n).jpg"; + var emailProviderFactory = new MockEmailProviderFactory(message => + { + Assert.Equal(2, message.Attachments.Count); + Assert.Equal("attachment1.jpg", message.Attachments[0].AttachmentName); + ImageAsserts.Similar(Image1, ImageContext.Load(message.Attachments[0].FilePath)); + Assert.Equal("attachment2.jpg", message.Attachments[1].AttachmentName); + ImageAsserts.Similar(Image2, ImageContext.Load(message.Attachments[1].FilePath)); + }); + + await _automationHelper.WithContainerBuilder(builder => + { + builder.RegisterInstance(emailProviderFactory); + }).RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + EmailFileName = "attachment$(n).jpg", + Verbose = true + }, + Image1, Image2); + + emailProviderFactory.CheckAsserts(); + emailProviderFactory.VerifyExactlyOneMessageSent(); + ImageAsserts.Similar(Image1, ImageContext.Load($"{FolderPath}/test1.jpg")); + ImageAsserts.Similar(Image2, ImageContext.Load($"{FolderPath}/test2.jpg")); + Assert.False(File.Exists($"{FolderPath}/test3.jpg")); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task EmailPdfAndSaveImageFiles() + { + var path = $"{FolderPath}/test$(n).jpg"; + var emailProviderFactory = new MockEmailProviderFactory(message => + { + Assert.Single(message.Attachments); + Assert.Equal("attachment.pdf", message.Attachments[0].AttachmentName); + PdfAsserts.AssertImages(message.Attachments[0].FilePath, Image1, Image2); + }); + + await _automationHelper.WithContainerBuilder(builder => + { + builder.RegisterInstance(emailProviderFactory); + }).RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + EmailFileName = "attachment.pdf", + Verbose = true + }, + Image1, Image2); + + emailProviderFactory.CheckAsserts(); + emailProviderFactory.VerifyExactlyOneMessageSent(); + ImageAsserts.Similar(Image1, ImageContext.Load($"{FolderPath}/test1.jpg")); + ImageAsserts.Similar(Image2, ImageContext.Load($"{FolderPath}/test2.jpg")); + Assert.False(File.Exists($"{FolderPath}/test3.jpg")); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task EmailImageFilesAndSavePdf() + { + var path = $"{FolderPath}/test.pdf"; + var emailProviderFactory = new MockEmailProviderFactory(message => + { + Assert.Equal(2, message.Attachments.Count); + Assert.Equal("attachment1.jpg", message.Attachments[0].AttachmentName); + ImageAsserts.Similar(Image1, ImageContext.Load(message.Attachments[0].FilePath)); + Assert.Equal("attachment2.jpg", message.Attachments[1].AttachmentName); + ImageAsserts.Similar(Image2, ImageContext.Load(message.Attachments[1].FilePath)); + }); + + await _automationHelper.WithContainerBuilder(builder => + { + builder.RegisterInstance(emailProviderFactory); + }).RunCommand( + new AutomatedScanningOptions + { + OutputPath = path, + EmailFileName = "attachment$(n).jpg", + Verbose = true + }, + Image1, Image2); + + emailProviderFactory.CheckAsserts(); + emailProviderFactory.VerifyExactlyOneMessageSent(); + PdfAsserts.AssertImages(path, Image1, Image2); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ScanWithDefaultProfile() + { + var (scanDriverMock, scanDriverFactoryMock) = CreateDriverMocks(); + + await _automationHelper.RunCommand( + new AutomatedScanningOptions + { + OutputPath = $"{FolderPath}/test.jpg", + Verbose = true + }, + scanDriverFactoryMock); + + _ = scanDriverMock.Received().Scan( + Arg.Is(options => + options.PaperSource == PaperSource.Flatbed && + options.BitDepth == BitDepth.Color && + options.Dpi == 200 && + options.PageSize == PageSize.Letter), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ScanWithNonDefaultProfile() + { + var (scanDriverMock, scanDriverFactoryMock) = CreateDriverMocks(); + + await _automationHelper.WithContainer(container => + { + var profileManager = container.Resolve(); + profileManager.Mutate(new ListMutation.Append(new ScanProfile + { + DisplayName = "second_profile", + Device = new ScanProfileDevice("test_id", "test_name"), + PaperSource = ScanSource.Feeder, + BitDepth = ScanBitDepth.Grayscale, + Resolution = new ScanResolution { Dpi = 300 }, + PageSize = ScanPageSize.A4 + }), ListSelection.Empty()); + }).RunCommand( + new AutomatedScanningOptions + { + ProfileName = "second_profile", + OutputPath = $"{FolderPath}/test.jpg", + Verbose = true + }, + scanDriverFactoryMock); + + _ = scanDriverMock.Received().Scan( + Arg.Is(options => + options.PaperSource == PaperSource.Feeder && + options.BitDepth == BitDepth.Grayscale && + options.Dpi == 300 && + options.PageSize == PageSize.A4), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ScanWithNoProfile() + { + var (scanDriverMock, scanDriverFactoryMock) = CreateDriverMocks(); + + await _automationHelper.WithContainer(container => + { + var profileManager = container.Resolve(); + var profile = profileManager.Profiles[0]; + profile.PaperSource = ScanSource.Feeder; + profile.BitDepth = ScanBitDepth.Grayscale; + profile.Resolution = new ScanResolution { Dpi = 300 }; + profile.PageSize = ScanPageSize.A4; + var config = container.Resolve(); + config.User.Set(c => c.EnableOcr, true); + config.User.Set(c => c.OcrLanguageCode, "eng"); + }).RunCommand( + new AutomatedScanningOptions + { + NoProfile = true, + Driver = ScanPerformer.SystemDefaultDriverName, + Device = "name1", + OutputPath = $"{FolderPath}/test.pdf", + Verbose = true + }, + scanDriverFactoryMock); + + _ = scanDriverMock.Received().Scan( + Arg.Is(options => + options.PaperSource == PaperSource.Flatbed && + options.BitDepth == BitDepth.Color && + options.Dpi == 200 && + options.PageSize == PageSize.Letter && + options.OcrParams.LanguageCode == null), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ScanWithProfileOverrides() + { + var (scanDriverMock, scanDriverFactoryMock) = CreateDriverMocks(); + + await _automationHelper.RunCommand( + new AutomatedScanningOptions + { + Source = ScanSource.Feeder, + BitDepth = ConsoleBitDepth.Gray, + Dpi = 300, + PageSize = "a4", + Driver = "escl", + Device = "name1", + OutputPath = $"{FolderPath}/test.jpg", + Verbose = true + }, + scanDriverFactoryMock); + + _ = scanDriverMock.Received().Scan( + Arg.Is(options => + options.PaperSource == PaperSource.Feeder && + options.BitDepth == BitDepth.Grayscale && + options.Dpi == 300 && + options.PageSize == PageSize.A4 && + options.Driver == Driver.Escl && + options.Device.Name == "test_name1"), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + AssertRecoveryCleanedUp(); + } + + [Fact] + public async Task ListDevices() + { + var (_, scanDriverFactoryMock) = CreateDriverMocks(); + + var outputWriter = new StringWriter { NewLine = "\n" }; + await _automationHelper.WithContainerBuilder(container => + { + container.RegisterInstance(new ConsoleOutput(outputWriter)); + }).RunCommand( + new AutomatedScanningOptions + { + Driver = ScanPerformer.SystemDefaultDriverName, + ListDevices = true + }, scanDriverFactoryMock); + + Assert.Equal("test_name1\ntest_name2\n", outputWriter.ToString()); + AssertRecoveryCleanedUp(); + } + + private static (IScanDriver, IScanDriverFactory) CreateDriverMocks() + { + var scanDriverMock = Substitute.For(); + var scanDriverFactoryMock = Substitute.For(); + scanDriverFactoryMock.Create(Arg.Any()).Returns(scanDriverMock); + scanDriverMock.GetDevices(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(x => + { + var callback = (Action) x[2]; + callback(new ScanDevice(Driver.Wia, "test_id1", "test_name1")); + callback(new ScanDevice(Driver.Wia, "test_id2", "test_name2")); + return Task.CompletedTask; + }); + scanDriverMock.Scan(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any>()).Returns(Task.CompletedTask); + return (scanDriverMock, scanDriverFactoryMock); + } + private void AssertRecoveryCleanedUp() { Assert.False(Directory.Exists(Path.Combine(FolderPath, "recovery"))); diff --git a/NAPS2.Lib.Tests/Automation/MockEmailProviderFactory.cs b/NAPS2.Lib.Tests/Automation/MockEmailProviderFactory.cs new file mode 100644 index 0000000000..53df8b52fa --- /dev/null +++ b/NAPS2.Lib.Tests/Automation/MockEmailProviderFactory.cs @@ -0,0 +1,49 @@ +using NAPS2.ImportExport.Email; +using NAPS2.Sdk.Tests; +using NSubstitute; + +namespace NAPS2.Lib.Tests.Automation; + +internal class MockEmailProviderFactory : IEmailProviderFactory +{ + private Exception _assertException; + + public MockEmailProviderFactory(Action messageAsserts) + { + EmailProviderMock.SendEmail(Arg.Any(), Arg.Any()) + .Returns(x => + { + var message = (EmailMessage) x[0]; + try + { + messageAsserts.Invoke(message); + } + catch (Exception ex) + { + ex.PreserveStackTrace(); + _assertException = ex; + } + return Task.FromResult(true); + }); + } + + public IEmailProvider Create(EmailProviderType type) => EmailProviderMock; + + public IEmailProvider Default => EmailProviderMock; + + public IEmailProvider EmailProviderMock { get; } = Substitute.For(); + + public void CheckAsserts() + { + if (_assertException != null) + { + throw _assertException; + } + } + + public void VerifyExactlyOneMessageSent() + { + EmailProviderMock.Received().SendEmail(Arg.Any(), Arg.Any()); + EmailProviderMock.ReceivedCallsCount(1); + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Tests/Automation/TestModule.cs b/NAPS2.Lib.Tests/Automation/TestModule.cs index 0f469ede21..6a49b0d4bf 100644 --- a/NAPS2.Lib.Tests/Automation/TestModule.cs +++ b/NAPS2.Lib.Tests/Automation/TestModule.cs @@ -5,7 +5,6 @@ using NAPS2.Scan; using NAPS2.Scan.Internal; using NAPS2.Sdk.Tests.Mocks; -using Xunit.Abstractions; namespace NAPS2.Lib.Tests.Automation; @@ -14,27 +13,29 @@ internal class TestModule : Module private readonly ScanningContext _scanningContext; private readonly ImageContext _imageContext; private readonly IScanDriverFactory _scanDriverFactory; - private readonly ITestOutputHelper _testOutputHelper; + private readonly TextWriter _outputWriter; private readonly string _folderPath; + private readonly Action _containerBuilderSetup; - public TestModule(ScanningContext scanningContext, ImageContext imageContext, - IScanDriverFactory scanDriverFactory, - ITestOutputHelper testOutputHelper, string folderPath) + public TestModule(ScanningContext scanningContext, ImageContext imageContext, IScanDriverFactory scanDriverFactory, + TextWriter outputWriter, string folderPath, Action containerBuilderSetup) { _scanningContext = scanningContext; _imageContext = imageContext; _scanDriverFactory = scanDriverFactory; - _testOutputHelper = testOutputHelper; + _outputWriter = outputWriter; _folderPath = folderPath; + _containerBuilderSetup = containerBuilderSetup; } protected override void Load(ContainerBuilder builder) { + builder.RegisterInstance(_scanningContext); builder.RegisterInstance(_imageContext); builder.RegisterInstance(_scanDriverFactory); builder.RegisterType().As(); builder.RegisterType().AsSelf() - .WithParameter("writer", new TestOutputTextWriter(_testOutputHelper)); + .WithParameter("writer", _outputWriter); builder.RegisterInstance(Naps2Config.Stub()); builder.Register(_ => { @@ -44,7 +45,7 @@ protected override void Load(ContainerBuilder builder) var defaultProfile = new ScanProfile { IsDefault = true, - Device = new ScanDevice("001", "Some Scanner") + Device = new ScanProfileDevice("001", "Some Scanner") }; profileManager.Mutate( new ListMutation.Append(defaultProfile), @@ -65,6 +66,12 @@ protected override void Load(ContainerBuilder builder) builder.RegisterInstance(new FileStorageManager(recoveryFolderPath)); builder.RegisterBuildCallback(ctx => - ctx.Resolve().TempFolderPath = _scanningContext.TempFolderPath); + { + var scanningContext = ctx.Resolve(); + scanningContext.OcrEngine = ctx.Resolve(); + scanningContext.TempFolderPath = _scanningContext.TempFolderPath; + }); + + _containerBuilderSetup?.Invoke(builder); } } \ No newline at end of file diff --git a/NAPS2.Lib.Tests/Automation/TestOutputTextWriter.cs b/NAPS2.Lib.Tests/Automation/TestOutputTextWriter.cs index d3b31fba23..35411c84ab 100644 --- a/NAPS2.Lib.Tests/Automation/TestOutputTextWriter.cs +++ b/NAPS2.Lib.Tests/Automation/TestOutputTextWriter.cs @@ -5,7 +5,8 @@ namespace NAPS2.Lib.Tests.Automation; internal class TestOutputTextWriter : TextWriter { - readonly ITestOutputHelper _output; + private readonly StringBuilder _stringBuilder = new(); + private readonly ITestOutputHelper _output; public TestOutputTextWriter(ITestOutputHelper output) { @@ -14,7 +15,14 @@ public TestOutputTextWriter(ITestOutputHelper output) public override Encoding Encoding => Encoding.UTF8; - public override void WriteLine(string message) => _output.WriteLine(message); + public string Output => _stringBuilder.ToString(); - public override void WriteLine(string format, params object[] args) => _output.WriteLine(format, args); + public override void WriteLine(string message) + { + _stringBuilder.AppendLine(message); + _output.WriteLine(message); + } + + public override void WriteLine(string format, params object[] args) => + WriteLine(string.Format(format, args)); } \ No newline at end of file diff --git a/NAPS2.Lib.Tests/Config/ProfileSerializerTestsData.Designer.cs b/NAPS2.Lib.Tests/Config/ConfigData.Designer.cs similarity index 69% rename from NAPS2.Lib.Tests/Config/ProfileSerializerTestsData.Designer.cs rename to NAPS2.Lib.Tests/Config/ConfigData.Designer.cs index 77a9ca369a..2cb2081a46 100644 --- a/NAPS2.Lib.Tests/Config/ProfileSerializerTestsData.Designer.cs +++ b/NAPS2.Lib.Tests/Config/ConfigData.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -22,14 +21,14 @@ namespace NAPS2.Lib.Tests.Config { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class ProfileSerializerTestsData { + internal class ConfigData { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal ProfileSerializerTestsData() { + internal ConfigData() { } /// @@ -39,7 +38,7 @@ internal ProfileSerializerTestsData() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NAPS2.Lib.Tests.Config.ProfileSerializerTestsData", typeof(ProfileSerializerTestsData).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NAPS2.Lib.Tests.Config.ConfigData", typeof(ConfigData).Assembly); resourceMan = temp; } return resourceMan; @@ -60,6 +59,40 @@ internal ProfileSerializerTestsData() { } } + /// + /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8" ?> + ///<AppConfig> + /// <DefaultCulture></DefaultCulture> + /// <StartupMessageTitle></StartupMessageTitle> + /// <StartupMessageText></StartupMessageText> + /// <StartupMessageIcon>Information</StartupMessageIcon> + /// <SaveButtonDefaultAction>SaveAll</SaveButtonDefaultAction> + /// <HideOcrButton>false</HideOcrButton> + /// <HideImportButton>false</HideImportButton> + /// <HideSavePdfButton>false</HideSavePdfButton> + /// <HideSaveImagesButton>false</HideSaveImagesButton> + /// <HideEmailButton>fals [rest of string was truncated]";. + /// + internal static string AppSettings { + get { + return ResourceManager.GetString("AppSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8" ?> + ///<AppConfig> + /// <Version>3</Version> + /// <DeleteAfterSaving mode="override">true</DeleteAfterSaving> + /// <SingleInstance mode="default">true</SingleInstance> + ///</AppConfig>. + /// + internal static string NewAppSettings { + get { + return ResourceManager.GetString("NewAppSettings", resourceCulture); + } + } + /// /// Looks up a localized string similar to <?xml version="1.0"?> ///<ArrayOfExtendedScanSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> @@ -105,6 +138,34 @@ internal static string OldTwainProfile { } } + /// + /// Looks up a localized string similar to <?xml version="1.0"?> + ///<UserConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + /// <Version>2</Version> + /// <Culture>en</Culture> + /// <FormStates> + /// <FormState> + /// <Name>FDesktop</Name> + /// <Location> + /// <X>300</X> + /// <Y>300</Y> + /// </Location> + /// <Size> + /// <Width>1200</Width> + /// <Height>450</Height> + /// </Size> + /// <Maximized>false</Maximized> + /// </FormState> + /// </FormStates> + /// <BackgroundOperations /> + /// [rest of string was truncated]";. + /// + internal static string OldUserConfig { + get { + return ResourceManager.GetString("OldUserConfig", resourceCulture); + } + } + /// /// Looks up a localized string similar to <?xml version="1.0"?> ///<ArrayOfScanProfile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> diff --git a/NAPS2.Lib.Tests/Config/ProfileSerializerTestsData.resx b/NAPS2.Lib.Tests/Config/ConfigData.resx similarity index 67% rename from NAPS2.Lib.Tests/Config/ProfileSerializerTestsData.resx rename to NAPS2.Lib.Tests/Config/ConfigData.resx index a6171128a9..c4389201aa 100644 --- a/NAPS2.Lib.Tests/Config/ProfileSerializerTestsData.resx +++ b/NAPS2.Lib.Tests/Config/ConfigData.resx @@ -196,4 +196,98 @@ </ScanSettings> </ArrayOfScanSettings> + + <?xml version="1.0"?> +<UserConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <Version>2</Version> + <Culture>en</Culture> + <FormStates> + <FormState> + <Name>FDesktop</Name> + <Location> + <X>300</X> + <Y>300</Y> + </Location> + <Size> + <Width>1200</Width> + <Height>450</Height> + </Size> + <Maximized>false</Maximized> + </FormState> + </FormStates> + <BackgroundOperations /> + <CheckForUpdates>false</CheckForUpdates> + <LastUpdateCheckDate xsi:nil="true" /> + <FirstRunDate>2022-01-01T12:00:00.0000000-07:00</FirstRunDate> + <LastDonatePromptDate>2022-01-02T12:00:00.00000-07:00</LastDonatePromptDate> + <EnableOcr>true</EnableOcr> + <OcrMode>Best</OcrMode> + <OcrAfterScanning xsi:nil="true" /> + <LastImageExt>jpg</LastImageExt> + <EmailSetup> + <ProviderType>Gmail</ProviderType> + <SystemProviderName>Hotmail</SystemProviderName> + <SmtpPort xsi:nil="true" /> + <SmtpTls>false</SmtpTls> + </EmailSetup> + <ThumbnailSize>224</ThumbnailSize> + <LastBatchSettings> + <ProfileDisplayName>test_name</ProfileDisplayName> + <ScanType>MultipleWithPrompt</ScanType> + <ScanCount>0</ScanCount> + <ScanIntervalSeconds>0</ScanIntervalSeconds> + <OutputType>Load</OutputType> + <SaveSeparator>FilePerPage</SaveSeparator> + <SavePath /> + </LastBatchSettings> + <DesktopToolStripDock>Top</DesktopToolStripDock> + <CustomPageSizePresets /> + <SavedProxies /> +</UserConfig> + + + <?xml version="1.0" encoding="utf-8" ?> +<AppConfig> + <DefaultCulture></DefaultCulture> + <StartupMessageTitle></StartupMessageTitle> + <StartupMessageText></StartupMessageText> + <StartupMessageIcon>Information</StartupMessageIcon> + <SaveButtonDefaultAction>SaveAll</SaveButtonDefaultAction> + <HideOcrButton>false</HideOcrButton> + <HideImportButton>false</HideImportButton> + <HideSavePdfButton>false</HideSavePdfButton> + <HideSaveImagesButton>false</HideSaveImagesButton> + <HideEmailButton>false</HideEmailButton> + <HidePrintButton>false</HidePrintButton> + <HideDonateButton>false</HideDonateButton> + <DisableAutoSave>false</DisableAutoSave> + <LockSystemProfiles>false</LockSystemProfiles> + <LockUnspecifiedDevices>false</LockUnspecifiedDevices> + <NoUserProfiles>false</NoUserProfiles> + <AlwaysRememberDevice>true</AlwaysRememberDevice> + <NoUpdatePrompt>false</NoUpdatePrompt> + <DeleteAfterSaving>false</DeleteAfterSaving> + <DisableSaveNotifications>false</DisableSaveNotifications> + <SingleInstance>false</SingleInstance> + <ComponentsPath></ComponentsPath> + <OcrTimeoutInSeconds>600</OcrTimeoutInSeconds> + <OcrState>UserConfig</OcrState> + <OcrDefaultLanguage></OcrDefaultLanguage> + <OcrDefaultMode>Fast</OcrDefaultMode> + <OcrDefaultAfterScanning>true</OcrDefaultAfterScanning> + <ForcePdfCompat>PdfA1B</ForcePdfCompat> + <EventLogging>None</EventLogging> + <KeyboardShortcuts> + <ScanDefault>Ctrl+Enter</ScanDefault> + </KeyboardShortcuts> +</AppConfig> + + + <?xml version="1.0" encoding="utf-8" ?> +<AppConfig> + <Version>3</Version> + <DeleteAfterSaving mode="override">true</DeleteAfterSaving> + <SingleInstance mode="default">true</SingleInstance> +</AppConfig> + \ No newline at end of file diff --git a/NAPS2.Lib.Tests/Config/FileConfigScopeTests.cs b/NAPS2.Lib.Tests/Config/FileConfigScopeTests.cs index c742ad3bb7..b6e3f8842d 100644 --- a/NAPS2.Lib.Tests/Config/FileConfigScopeTests.cs +++ b/NAPS2.Lib.Tests/Config/FileConfigScopeTests.cs @@ -1,5 +1,7 @@ using System.Threading; using NAPS2.Config.Model; +using NAPS2.Ocr; +using NAPS2.Pdf; using NAPS2.Sdk.Tests; using Xunit; @@ -12,13 +14,15 @@ public class FileConfigScopeTests : ContextualTests public void FileScope() { var configPath = Path.Combine(FolderPath, "config.xml"); - var scope = new FileConfigScope(configPath, new ConfigSerializer(ConfigReadMode.All, ConfigRootName.UserConfig), ConfigScopeMode.ReadWrite); - + var scope = new FileConfigScope(configPath, + new ConfigSerializer(ConfigReadMode.All, ConfigRootName.UserConfig), ConfigScopeMode.ReadWrite, + TimeSpan.FromMilliseconds(100)); + // Nothing should be created yet Assert.False(File.Exists(configPath)); // Reading should get the default value - Assert.False(scope.TryGet(c => c.Culture, out _)); + Assert.False(scope.Has(c => c.Culture)); // Writing should save to the file scope.Set(c => c.Culture, "fr"); @@ -26,7 +30,7 @@ public void FileScope() var doc = XDocument.Load(configPath); Assert.Equal("UserConfig", doc.Root?.Name); - + var docValue = doc.Descendants("Culture").Single().Value; Assert.Equal("fr", docValue); @@ -49,9 +53,9 @@ public void FileScope() Assert.True(scope.GetOrDefault(c => c.CheckForUpdates)); // Now directly modify the file - Assert.False(scope.TryGet(c => c.DisableAutoSave, out _)); + Assert.False(scope.Has(c => c.DisableAutoSave)); DirectSetValue(stream, "DisableAutoSave", "true"); - Assert.False(scope.TryGet(c => c.DisableAutoSave, out _)); + Assert.False(scope.Has(c => c.DisableAutoSave)); } Thread.Sleep(500); @@ -86,9 +90,10 @@ public void ReadWithBadXml() { var configPath = Path.Combine(FolderPath, "config.xml"); File.WriteAllText(configPath, @"blah"); - var scope = new FileConfigScope(configPath, new ConfigSerializer(ConfigReadMode.All, ConfigRootName.UserConfig), ConfigScopeMode.ReadWrite); + var scope = new FileConfigScope(configPath, + new ConfigSerializer(ConfigReadMode.All, ConfigRootName.UserConfig), ConfigScopeMode.ReadWrite); - Assert.False(scope.TryGet(c => c.Culture, out _)); + Assert.False(scope.Has(c => c.Culture)); } [Fact] @@ -96,18 +101,75 @@ public void ReadWithBadConfig() { var configPath = Path.Combine(FolderPath, "config.xml"); File.WriteAllText(configPath, @"fr"); - var scope = new FileConfigScope(configPath, new ConfigSerializer(ConfigReadMode.DefaultOnly, ConfigRootName.UserConfig), ConfigScopeMode.ReadWrite); + var scope = new FileConfigScope(configPath, + new ConfigSerializer(ConfigReadMode.DefaultOnly, ConfigRootName.AppConfig), ConfigScopeMode.ReadWrite); - Assert.False(scope.TryGet(c => c.Culture, out _)); + Assert.False(scope.Has(c => c.Culture)); } [Fact] public void ReadWithMissingFile() { var configPath = Path.Combine(FolderPath, "config.xml"); - var scope = new FileConfigScope(configPath, new ConfigSerializer(ConfigReadMode.DefaultOnly, ConfigRootName.UserConfig), ConfigScopeMode.ReadWrite); + var scope = new FileConfigScope(configPath, + new ConfigSerializer(ConfigReadMode.DefaultOnly, ConfigRootName.AppConfig), ConfigScopeMode.ReadWrite); + + Assert.False(scope.Has(c => c.Culture)); + } + + [Fact] + public void ReadAppSettings() + { + var configPath = Path.Combine(FolderPath, "appsettings.xml"); + File.WriteAllText(configPath, ConfigData.AppSettings); + var defaultsScope = new FileConfigScope(configPath, + new ConfigSerializer(ConfigReadMode.DefaultOnly, ConfigRootName.AppConfig), ConfigScopeMode.ReadOnly); + var lockedScope = new FileConfigScope(configPath, + new ConfigSerializer(ConfigReadMode.LockedOnly, ConfigRootName.AppConfig), ConfigScopeMode.ReadOnly); + + Assert.False(lockedScope.Has(c => c.AlwaysRememberDevice)); + Assert.True(lockedScope.TryGet(c => c.PdfSettings.Compat, out var pdfCompat)); + Assert.Equal(PdfCompat.PdfA1B, pdfCompat); + + Assert.False(defaultsScope.Has(c => c.PdfSettings.Compat)); + Assert.True(defaultsScope.TryGet(c => c.AlwaysRememberDevice, out var alwaysRememberDevice)); + Assert.True(alwaysRememberDevice); + + Assert.False(lockedScope.Has(c => c.KeyboardShortcuts.ScanDefault)); + Assert.True(defaultsScope.TryGet(c => c.KeyboardShortcuts.ScanDefault, out var scanDefault)); + Assert.Equal("Mod+Enter", scanDefault); + } + + [Fact] + public void ReadNewAppSettings() + { + var configPath = Path.Combine(FolderPath, "appsettings.xml"); + File.WriteAllText(configPath, ConfigData.NewAppSettings); + var defaultsScope = new FileConfigScope(configPath, + new ConfigSerializer(ConfigReadMode.DefaultOnly, ConfigRootName.AppConfig), ConfigScopeMode.ReadOnly); + var lockedScope = new FileConfigScope(configPath, + new ConfigSerializer(ConfigReadMode.LockedOnly, ConfigRootName.AppConfig), ConfigScopeMode.ReadOnly); + + Assert.False(lockedScope.Has(c => c.SingleInstance)); + Assert.True(lockedScope.TryGet(c => c.DeleteAfterSaving, out var deleteAfterSaving)); + Assert.True(deleteAfterSaving); + + Assert.False(defaultsScope.Has(c => c.DeleteAfterSaving)); + Assert.True(defaultsScope.TryGet(c => c.SingleInstance, out var singleInstance)); + Assert.True(singleInstance); + } + + [Fact] + public void ReadWithOldConfig() + { + var configPath = Path.Combine(FolderPath, "config.xml"); + File.WriteAllText(configPath, ConfigData.OldUserConfig); + var scope = new FileConfigScope(configPath, + new ConfigSerializer(ConfigReadMode.All, ConfigRootName.UserConfig), ConfigScopeMode.ReadWrite); - Assert.False(scope.TryGet(c => c.Culture, out _)); + Assert.False(scope.Has(c => c.LockSystemProfiles)); + Assert.True(scope.TryGet(c => c.OcrMode, out var ocrMode)); + Assert.Equal(LocalizedOcrMode.Best, ocrMode); } private static void DirectSetValue(FileStream stream, string tagName, string value) diff --git a/NAPS2.Lib.Tests/Config/MemoryConfigScopeTests.cs b/NAPS2.Lib.Tests/Config/MemoryConfigScopeTests.cs index d98b8c058b..39fba2d7c3 100644 --- a/NAPS2.Lib.Tests/Config/MemoryConfigScopeTests.cs +++ b/NAPS2.Lib.Tests/Config/MemoryConfigScopeTests.cs @@ -9,10 +9,10 @@ public class MemoryConfigScopeTests public void GetReturnsFalseForUnset() { var scope = new MemoryConfigScope(); - Assert.False(scope.TryGet(c => c.UserName, out _)); - Assert.False(scope.TryGet(c => c.Sub.X, out _)); - Assert.False(scope.TryGet(c => c.Sub.Y, out _)); - Assert.False(scope.TryGet(c => c.Sub.SubSub.Val, out _)); + Assert.False(scope.Has(c => c.UserName)); + Assert.False(scope.Has(c => c.Sub.X)); + Assert.False(scope.Has(c => c.Sub.Y)); + Assert.False(scope.Has(c => c.Sub.SubSub.Val)); } [Fact] @@ -20,7 +20,7 @@ public void GetChildConfigThrows() { // Accessing a child config directly on the scope should fail var scope = new MemoryConfigScope(); - Assert.Throws(() => scope.TryGet(c => c.Sub, out _)); + Assert.Throws(() => scope.Has(c => c.Sub)); } [Fact] @@ -99,17 +99,17 @@ public void GetReturnsFalseAfterRemove() scope.Set(c => c.Sub.Y, null); scope.Set(c => c.Sub.SubSub.Val, "something"); - Assert.True(scope.TryGet(c => c.UserName, out _)); + Assert.True(scope.Has(c => c.UserName)); scope.Remove(c => c.UserName); - Assert.False(scope.TryGet(c => c.UserName, out _)); + Assert.False(scope.Has(c => c.UserName)); - Assert.True(scope.TryGet(c => c.Sub.X, out _)); - Assert.True(scope.TryGet(c => c.Sub.Y, out _)); - Assert.True(scope.TryGet(c => c.Sub.SubSub.Val, out _)); + Assert.True(scope.Has(c => c.Sub.X)); + Assert.True(scope.Has(c => c.Sub.Y)); + Assert.True(scope.Has(c => c.Sub.SubSub.Val)); scope.Remove(c => c.Sub); - Assert.False(scope.TryGet(c => c.Sub.X, out _)); - Assert.False(scope.TryGet(c => c.Sub.Y, out _)); - Assert.False(scope.TryGet(c => c.Sub.SubSub.Val, out _)); + Assert.False(scope.Has(c => c.Sub.X)); + Assert.False(scope.Has(c => c.Sub.Y)); + Assert.False(scope.Has(c => c.Sub.SubSub.Val)); } [Fact] @@ -124,10 +124,10 @@ public void RemoveWithIdentityRemovesAll() scope.Remove(c => c); - Assert.False(scope.TryGet(c => c.UserName, out _)); - Assert.False(scope.TryGet(c => c.Sub.X, out _)); - Assert.False(scope.TryGet(c => c.Sub.Y, out _)); - Assert.False(scope.TryGet(c => c.Sub.SubSub.Val, out _)); + Assert.False(scope.Has(c => c.UserName)); + Assert.False(scope.Has(c => c.Sub.X)); + Assert.False(scope.Has(c => c.Sub.Y)); + Assert.False(scope.Has(c => c.Sub.SubSub.Val)); } [Fact] @@ -138,10 +138,10 @@ public void CopyFromCopiesSetProperties() scope.CopyFrom(source); - Assert.False(scope.TryGet(c => c.UserName, out _)); - Assert.False(scope.TryGet(c => c.Sub.X, out _)); - Assert.False(scope.TryGet(c => c.Sub.Y, out _)); - Assert.False(scope.TryGet(c => c.Sub.SubSub.Val, out _)); + Assert.False(scope.Has(c => c.UserName)); + Assert.False(scope.Has(c => c.Sub.X)); + Assert.False(scope.Has(c => c.Sub.Y)); + Assert.False(scope.Has(c => c.Sub.SubSub.Val)); scope.Set(c => c.UserName, "blah"); scope.Set(c => c.Sub.SubSub.Val, "something"); @@ -151,13 +151,13 @@ public void CopyFromCopiesSetProperties() Assert.Equal("blah", userName); Assert.True(scope.TryGet(c => c.Sub.SubSub.Val, out var val)); Assert.Equal("something", val); - Assert.False(scope.TryGet(c => c.Sub.X, out _)); - Assert.False(scope.TryGet(c => c.Sub.Y, out _)); + Assert.False(scope.Has(c => c.Sub.X)); + Assert.False(scope.Has(c => c.Sub.Y)); source = new ConfigStorage(); scope.CopyFrom(source); - Assert.True(scope.TryGet(c => c.UserName, out _)); - Assert.True(scope.TryGet(c => c.Sub.SubSub.Val, out _)); + Assert.True(scope.Has(c => c.UserName)); + Assert.True(scope.Has(c => c.Sub.SubSub.Val)); } [Config] diff --git a/NAPS2.Lib.Tests/Config/ProfileManagerTests.cs b/NAPS2.Lib.Tests/Config/ProfileManagerTests.cs index 468ef66220..345cca954a 100644 --- a/NAPS2.Lib.Tests/Config/ProfileManagerTests.cs +++ b/NAPS2.Lib.Tests/Config/ProfileManagerTests.cs @@ -22,7 +22,7 @@ public void SaveOneProfile() { var profile = new ScanProfile { - Device = new ScanDevice("test_id", "test_name"), + Device = new ScanProfileDevice("test_id", "test_name"), DisplayName = "A Profile" }; _profileManager.Mutate(new ListMutation.Append(profile), new Selectable()); @@ -45,7 +45,7 @@ public void SaveOneProfile() [Fact] public void LoadOneProfile() { - var xml = ProfileSerializerTestsData.SingleProfile; + var xml = ConfigData.SingleProfile; File.WriteAllText(_userPath, xml); var profiles = _profileManager.Profiles; @@ -60,7 +60,7 @@ public void LoadOneProfile() [Fact] public void LoadVeryOldProfile() { - var xml = ProfileSerializerTestsData.VeryOldProfile; + var xml = ConfigData.VeryOldProfile; File.WriteAllText(_userPath, xml); var profiles = _profileManager.Profiles; @@ -68,7 +68,7 @@ public void LoadVeryOldProfile() Assert.Equal("wia", profiles[0].DriverName); Assert.Equal("test_id", profiles[0].Device?.ID); - Assert.Equal(ScanDpi.Dpi200, profiles[0].Resolution); + Assert.Equal(200, profiles[0].Resolution.Dpi); Assert.Equal(2, profiles[0].Version); Assert.Equal(0, profiles[0].UpgradedFrom); } @@ -76,7 +76,7 @@ public void LoadVeryOldProfile() [Fact] public void LoadOldProfile() { - var xml = ProfileSerializerTestsData.OldProfile; + var xml = ConfigData.OldProfile; File.WriteAllText(_userPath, xml); var profiles = _profileManager.Profiles; @@ -84,7 +84,7 @@ public void LoadOldProfile() Assert.Equal("wia", profiles[0].DriverName); Assert.Equal("test_id", profiles[0].Device?.ID); - Assert.Equal(ScanDpi.Dpi200, profiles[0].Resolution); + Assert.Equal(200, profiles[0].Resolution.Dpi); Assert.Equal(2, profiles[0].Version); Assert.Equal(1, profiles[0].UpgradedFrom); } @@ -92,7 +92,7 @@ public void LoadOldProfile() [Fact] public void LoadOldTwainProfile() { - var xml = ProfileSerializerTestsData.OldTwainProfile; + var xml = ConfigData.OldTwainProfile; File.WriteAllText(_userPath, xml); var profiles = _profileManager.Profiles; @@ -104,4 +104,6 @@ public void LoadOldTwainProfile() Assert.Equal(2, profiles[0].Version); Assert.Equal(1, profiles[0].UpgradedFrom); } + + // TODO: Add tests for LockSystemProfiles etc } \ No newline at end of file diff --git a/NAPS2.Lib.Tests/Config/TransactionConfigScopeTests.cs b/NAPS2.Lib.Tests/Config/TransactionConfigScopeTests.cs index 1c1f8c507f..658cf22629 100644 --- a/NAPS2.Lib.Tests/Config/TransactionConfigScopeTests.cs +++ b/NAPS2.Lib.Tests/Config/TransactionConfigScopeTests.cs @@ -1,5 +1,6 @@ -using Moq; using NAPS2.Config.Model; +using NAPS2.Sdk.Tests; +using NSubstitute; using Xunit; namespace NAPS2.Lib.Tests.Config; @@ -55,49 +56,49 @@ public void Rollback() [Fact] public void ChangeEvent() { - var mockHandler = new Mock(); - _transact.HasChangesChanged += mockHandler.Object; - mockHandler.VerifyNoOtherCalls(); + var mockHandler = Substitute.For(); + _transact.HasChangesChanged += mockHandler; + mockHandler.ReceivedCallsCount(0); _transact.Set(c => c.Culture, "de"); - mockHandler.Verify(x => x(_transact, EventArgs.Empty)); + mockHandler.Received()(_transact, EventArgs.Empty); _transact.Set(c => c.LastImageExt, ".png"); - mockHandler.VerifyNoOtherCalls(); + mockHandler.ReceivedCallsCount(1); _transact.Rollback(); - mockHandler.Verify(x => x(_transact, EventArgs.Empty)); - mockHandler.VerifyNoOtherCalls(); + mockHandler.Received()(_transact, EventArgs.Empty); + mockHandler.ReceivedCallsCount(2); _transact.Set(c => c.Culture, "de"); - mockHandler.Verify(x => x(_transact, EventArgs.Empty)); - mockHandler.VerifyNoOtherCalls(); + mockHandler.Received()(_transact, EventArgs.Empty); + mockHandler.ReceivedCallsCount(3); _transact.Commit(); - mockHandler.Verify(x => x(_transact, EventArgs.Empty)); - mockHandler.VerifyNoOtherCalls(); + mockHandler.Received()(_transact, EventArgs.Empty); + mockHandler.ReceivedCallsCount(4); } [Fact] public void SetThenRemove() { _transact.Set(c => c.Culture, "de"); - Assert.True(_transact.TryGet(c => c.Culture, out _)); + Assert.True(_transact.Has(c => c.Culture)); _transact.Remove(c => c.Culture); - Assert.False(_transact.TryGet(c => c.Culture, out _)); + Assert.False(_transact.Has(c => c.Culture)); Assert.True(_transact.HasChanges); _transact.Commit(); Assert.False(_transact.HasChanges); - Assert.False(_baseScope.TryGet(c => c.Culture, out _)); + Assert.False(_baseScope.Has(c => c.Culture)); } [Fact] public void RemoveThenSet() { _transact.Remove(c => c.Culture); - Assert.False(_transact.TryGet(c => c.Culture, out _)); + Assert.False(_transact.Has(c => c.Culture)); _transact.Set(c => c.Culture, "de"); - Assert.True(_transact.TryGet(c => c.Culture, out _)); + Assert.True(_transact.Has(c => c.Culture)); Assert.True(_transact.HasChanges); _transact.Commit(); @@ -109,9 +110,9 @@ public void RemoveThenSet() public void RemoveParentThenSet() { _transact.Remove(c => c.PdfSettings); - Assert.False(_transact.TryGet(c => c.PdfSettings.DefaultFileName, out _)); + Assert.False(_transact.Has(c => c.PdfSettings.DefaultFileName)); _transact.Set(c => c.PdfSettings.DefaultFileName, "test"); - Assert.True(_transact.TryGet(c => c.PdfSettings.DefaultFileName, out _)); + Assert.True(_transact.Has(c => c.PdfSettings.DefaultFileName)); Assert.True(_transact.HasChanges); _transact.Commit(); @@ -123,13 +124,13 @@ public void RemoveParentThenSet() public void SetThenRemoveParent() { _transact.Set(c => c.PdfSettings.DefaultFileName, "test"); - Assert.True(_transact.TryGet(c => c.PdfSettings.DefaultFileName, out _)); + Assert.True(_transact.Has(c => c.PdfSettings.DefaultFileName)); _transact.Remove(c => c.PdfSettings); - Assert.False(_transact.TryGet(c => c.PdfSettings.DefaultFileName, out _)); + Assert.False(_transact.Has(c => c.PdfSettings.DefaultFileName)); Assert.True(_transact.HasChanges); _transact.Commit(); Assert.False(_transact.HasChanges); - Assert.False(_transact.TryGet(c => c.PdfSettings.DefaultFileName, out _)); + Assert.False(_transact.Has(c => c.PdfSettings.DefaultFileName)); } } \ No newline at end of file diff --git a/NAPS2.Lib.Tests/Dependencies/DownloadControllerTests.cs b/NAPS2.Lib.Tests/Dependencies/DownloadControllerTests.cs new file mode 100644 index 0000000000..37a8d9dd9e --- /dev/null +++ b/NAPS2.Lib.Tests/Dependencies/DownloadControllerTests.cs @@ -0,0 +1,149 @@ +using NAPS2.Dependencies; +using NAPS2.Sdk.Tests; +using NSubstitute; +using RichardSzalay.MockHttp; +using Xunit; + +namespace NAPS2.Lib.Tests.Dependencies; + +public class DownloadControllerTests : ContextualTests +{ + private const string StockDogJpegSHA256 = "c49f763f54e5381e5d2fb27325f7937b7b68ee277bbbe5e2dcfa41729363d87d"; + + private const string DummyValidUrl = "http://localhost/f.zip"; + private const string DummyInvalidUrl = "http://localhost/g.zip"; + + + private readonly MemoryStream _animalsZipStream = new(BinaryResources.animals); + private readonly MemoryStream _dogsGzipStream = new(BinaryResources.stock_dog_jpeg); + private readonly MockHttpMessageHandler _httpHandler = new(); + private readonly IExternalComponent _mockComponent = Substitute.For(); + private readonly DownloadController _controller; + private byte[] _downloadData; + + public DownloadControllerTests() + { + _mockComponent.When(x => x.Install(Arg.Any())).Do(x => _downloadData = File.ReadAllBytes((string) x[0])); + _controller = new(ScanningContext, _httpHandler.ToHttpClient()); + } + + public override void Dispose() + { + _animalsZipStream.Dispose(); + _dogsGzipStream.Dispose(); + base.Dispose(); + } + + [Fact] + public async Task NoQueue() + { + MockHttpMessageHandler handler = new(); + DownloadController controller = new(ScanningContext, handler.ToHttpClient()); + + var mockHandler = Substitute.For(); + + controller.DownloadComplete += mockHandler; + Assert.True(await controller.StartDownloadsAsync()); + + Assert.Equal(0, _httpHandler.GetMatchCount(_httpHandler.Fallback)); + mockHandler.Received()(controller, EventArgs.Empty); + mockHandler.ReceivedCallsCount(1); + } + + [Fact] + public async Task NoUrl() + { + DownloadInfo info = new("", [], 0, "0000000000000000000000000000000000000000", DownloadFormat.Gzip); + + var mockHandler = Substitute.For>(); + + _controller.QueueFile(info, mockHandler); + + Assert.False(await _controller.StartDownloadsAsync()); + + Assert.Equal(0, _httpHandler.GetMatchCount(_httpHandler.Fallback)); + mockHandler.ReceivedCallsCount(0); + } + + [Fact] + public async Task InvalidChecksum() + { + _httpHandler.Expect(DummyValidUrl).Respond("application/gzip", _dogsGzipStream); + + var mockHandler = Substitute.For(); + + _controller.DownloadError += mockHandler; + + _mockComponent.DownloadInfo.Returns(new DownloadInfo("temp.gz", [new DownloadMirror(DummyValidUrl)], 0, "THIS IS NOT AN SHA256 AND WILL FAIL", DownloadFormat.Gzip)); + + _controller.QueueFile(_mockComponent); + Assert.False(await _controller.StartDownloadsAsync()); + + Assert.Equal(0, _httpHandler.GetMatchCount(_httpHandler.Fallback)); + mockHandler.Received()(_controller, EventArgs.Empty); + mockHandler.ReceivedCallsCount(1); + _ = _mockComponent.Received().DownloadInfo; + _mockComponent.ReceivedCallsCount(1); + } + + [Fact] + public async Task InvalidMirrorsChecksum() + { + _mockComponent.DownloadInfo.Returns(new DownloadInfo("temp.gz", + [new DownloadMirror(DummyInvalidUrl), new DownloadMirror(DummyValidUrl)], 0, StockDogJpegSHA256, DownloadFormat.Gzip)); + + _httpHandler.Expect(DummyInvalidUrl).Respond("application/zip", _animalsZipStream); + _httpHandler.Expect(DummyValidUrl).Respond("application/gzip", _dogsGzipStream); + + _controller.QueueFile(_mockComponent); + Assert.True(await _controller.StartDownloadsAsync()); + + _ = _mockComponent.Received().DownloadInfo; + _mockComponent.Received().Install(Arg.Is((string p) => !string.IsNullOrWhiteSpace(p))); + _mockComponent.ReceivedCallsCount(2); + Assert.Equal(BinaryResources.stock_dog, _downloadData); + + _httpHandler.VerifyNoOutstandingExpectation(); + Assert.Equal(0, _httpHandler.GetMatchCount(_httpHandler.Fallback)); + } + + [Fact] + public async Task Valid() + { + _mockComponent.DownloadInfo.Returns(new DownloadInfo("temp.gz", [new DownloadMirror(DummyValidUrl)], 0, StockDogJpegSHA256, DownloadFormat.Gzip)); + + _httpHandler.Expect(DummyValidUrl).Respond("application/gzip", _dogsGzipStream); + + _controller.QueueFile(_mockComponent); + Assert.True(await _controller.StartDownloadsAsync()); + + _ = _mockComponent.Received().DownloadInfo; + _mockComponent.Received().Install(Arg.Is((string p) => !string.IsNullOrWhiteSpace(p))); + _mockComponent.ReceivedCallsCount(2); + Assert.Equal(BinaryResources.stock_dog, _downloadData); + + _httpHandler.VerifyNoOutstandingExpectation(); + Assert.Equal(0, _httpHandler.GetMatchCount(_httpHandler.Fallback)); + } + + [Fact] + public async Task ValidUsingMirrorUrl() + { + _mockComponent.DownloadInfo.Returns(new DownloadInfo("temp.gz", + [new DownloadMirror(DummyInvalidUrl), new DownloadMirror(DummyValidUrl)], 0, StockDogJpegSHA256, DownloadFormat.Gzip)); + + _httpHandler.Expect(DummyInvalidUrl); + _httpHandler.Expect(DummyValidUrl).Respond("application/gzip", _dogsGzipStream); + + _controller.QueueFile(_mockComponent); + Assert.True(await _controller.StartDownloadsAsync()); + + _ = _mockComponent.Received().DownloadInfo; + _mockComponent.Received().Install(Arg.Is((string p) => !string.IsNullOrWhiteSpace(p))); + _mockComponent.ReceivedCallsCount(2); + Assert.Equal(BinaryResources.stock_dog, _downloadData); + + _httpHandler.VerifyNoOutstandingExpectation(); + Assert.Equal(0, _httpHandler.GetMatchCount(_httpHandler.Fallback)); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Dependencies/DownloadFormatTests.cs b/NAPS2.Lib.Tests/Dependencies/DownloadFormatTests.cs similarity index 67% rename from NAPS2.Sdk.Tests/Dependencies/DownloadFormatTests.cs rename to NAPS2.Lib.Tests/Dependencies/DownloadFormatTests.cs index 4316a92801..279c7004ea 100644 --- a/NAPS2.Sdk.Tests/Dependencies/DownloadFormatTests.cs +++ b/NAPS2.Lib.Tests/Dependencies/DownloadFormatTests.cs @@ -1,7 +1,8 @@ using NAPS2.Dependencies; +using NAPS2.Sdk.Tests; using Xunit; -namespace NAPS2.Sdk.Tests.Dependencies; +namespace NAPS2.Lib.Tests.Dependencies; public class DownloadFormatTests : ContextualTests { @@ -9,10 +10,12 @@ public class DownloadFormatTests : ContextualTests public void Gzip() { var path = Path.Combine(FolderPath, "f.gz"); - var gzData = BinaryResources.stock_dog_jpeg; - File.WriteAllBytes(path, gzData); - var extractedPath = DownloadFormat.Gzip.Prepare(path); + string extractedPath; + using (MemoryStream stream = new(BinaryResources.stock_dog_jpeg)) + { + extractedPath = DownloadFormat.Gzip.Prepare(stream, path); + } var expectedDog = BinaryResources.stock_dog; Assert.Equal(expectedDog, File.ReadAllBytes(extractedPath)); @@ -22,10 +25,12 @@ public void Gzip() public void Zip() { var path = Path.Combine(FolderPath, "f.zip"); - var zipData = BinaryResources.animals; - File.WriteAllBytes(path, zipData); - var extractedPath = DownloadFormat.Zip.Prepare(path); + string extractedPath; + using (MemoryStream stream = new(BinaryResources.animals)) + { + extractedPath = DownloadFormat.Zip.Prepare(stream, path); + } var dogPath = Path.Combine(extractedPath, "animals/dogs/stock-dog.jpeg"); var catPath = Path.Combine(extractedPath, "animals/cats/stock-cat.jpeg"); diff --git a/NAPS2.Lib.Tests/Images/UiImageListTests.cs b/NAPS2.Lib.Tests/Images/UiImageListTests.cs new file mode 100644 index 0000000000..21b095453a --- /dev/null +++ b/NAPS2.Lib.Tests/Images/UiImageListTests.cs @@ -0,0 +1,109 @@ +using NAPS2.Sdk.Tests; +using Xunit; + +namespace NAPS2.Lib.Tests.Images; + +// TODO: Add more tests +public class UiImageListTests : ContextualTests +{ + [Fact] + public void HasUnsavedChanges_EmptyList() + { + var list = new UiImageList(); + Assert.False(list.HasUnsavedChanges); + } + + [Fact] + public void HasUnsavedChanges_SingleImage() + { + var list = new UiImageList(); + var item = new UiImage(CreateScannedImage()); + + // Add an image + list.Mutate(new ListMutation.Append(item)); + Assert.True(list.HasUnsavedChanges); + + // Mark the image as saved + var listState1 = list.CurrentState; + var itemState1 = item.GetClonedImage(); + list.MarkSaved(listState1, new[] { itemState1 }); + Assert.False(list.HasUnsavedChanges); + + // Modify the image + item.AddTransform(new RotationTransform(90)); + Assert.True(list.HasUnsavedChanges); + + // Mark the image as saved but with a stale image state + var listState2 = list.CurrentState; + list.MarkSaved(listState2, new[] { itemState1 }); + Assert.True(list.HasUnsavedChanges); + + // Mark the image as saved but with a stale list state + var itemState2 = item.GetClonedImage(); + list.MarkSaved(listState1, new[] { itemState2 }); + Assert.True(list.HasUnsavedChanges); + + // Mark the image as saved with fresh states + list.MarkSaved(listState2, new[] { itemState2 }); + Assert.False(list.HasUnsavedChanges); + } + + [Fact] + public void HasUnsavedChanges_MultipleImages() + { + var list = new UiImageList(); + var item1 = new UiImage(CreateScannedImage()); + var item2 = new UiImage(CreateScannedImage()); + + // Add images + list.Mutate(new ListMutation.Append(item1)); + list.Mutate(new ListMutation.Append(item2)); + Assert.True(list.HasUnsavedChanges); + + // Mark the first image as saved + var listState1 = list.CurrentState; + list.MarkSaved(listState1, new[] { item1.GetClonedImage() }); + Assert.True(list.HasUnsavedChanges); + + // Mark the second image as saved + list.MarkSaved(listState1, new[] { item2.GetClonedImage() }); + Assert.False(list.HasUnsavedChanges); + + // Swap the images + list.Mutate(new ListMutation.MoveUp(), ListSelection.Of(item2)); + Assert.True(list.HasUnsavedChanges); + + // Mark something as saved again - we can't really differentiate better than that + var listState2 = list.CurrentState; + list.MarkSaved(listState2, new[] { item1.GetClonedImage() }); + Assert.False(list.HasUnsavedChanges); + + // Modify one image + item1.AddTransform(new RotationTransform(90)); + Assert.True(list.HasUnsavedChanges); + + // Mark the other image as saved + var listState3 = list.CurrentState; + list.MarkSaved(listState3, new[] { item2.GetClonedImage() }); + Assert.True(list.HasUnsavedChanges); + + // Mark the modified image as saved + list.MarkSaved(listState3, new[] { item1.GetClonedImage() }); + Assert.False(list.HasUnsavedChanges); + } + + [Fact] + public void HasUnsavedChanges_MarkAllSaved() + { + var list = new UiImageList(); + var item1 = new UiImage(CreateScannedImage()); + var item2 = new UiImage(CreateScannedImage()); + + list.Mutate(new ListMutation.Append(item1)); + list.Mutate(new ListMutation.Append(item2)); + Assert.True(list.HasUnsavedChanges); + + list.MarkAllSaved(); + Assert.False(list.HasUnsavedChanges); + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Tests/Images/UiImageTests.cs b/NAPS2.Lib.Tests/Images/UiImageTests.cs new file mode 100644 index 0000000000..fbade283bc --- /dev/null +++ b/NAPS2.Lib.Tests/Images/UiImageTests.cs @@ -0,0 +1,6 @@ +namespace NAPS2.Lib.Tests.Images; + +// TODO: Add tests +public class UiImageTests +{ +} \ No newline at end of file diff --git a/NAPS2.Lib.Tests/Images/UndoStackTests.cs b/NAPS2.Lib.Tests/Images/UndoStackTests.cs new file mode 100644 index 0000000000..91033d8bd6 --- /dev/null +++ b/NAPS2.Lib.Tests/Images/UndoStackTests.cs @@ -0,0 +1,110 @@ +using NAPS2.Sdk.Tests; +using NSubstitute; +using Xunit; + +namespace NAPS2.Lib.Tests.Images; + +public class UndoStackTests +{ + [Fact] + public void Initial_NoUndoOrRedo() + { + var stack = new UndoStack(10); + Assert.False(stack.Undo()); + Assert.False(stack.Redo()); + } + + [Fact] + public void PushUndoRedoUndo() + { + var stack = new UndoStack(10); + var element = Substitute.For(); + + Assert.True(stack.Push(element)); + Assert.True(stack.CanUndo); + Assert.False(stack.CanRedo); + element.ReceivedCallsCount(0); + + Assert.True(stack.Undo()); + Assert.False(stack.CanUndo); + Assert.True(stack.CanRedo); + element.Received().ApplyUndo(); + element.ReceivedCallsCount(1); + + Assert.True(stack.Redo()); + Assert.True(stack.CanUndo); + Assert.False(stack.CanRedo); + element.Received().ApplyRedo(); + element.ReceivedCallsCount(2); + + Assert.True(stack.Undo()); + Assert.False(stack.CanUndo); + Assert.True(stack.CanRedo); + element.ReceivedCallsCount(3); + } + + [Fact] + public void StackLimit() + { + var stack = new UndoStack(4); + for (int i = 1; i <= 10; i++) + { + stack.Push(Substitute.For()); + } + for (int i = 10; i >= 7; i--) + { + Assert.True(stack.Undo()); + } + Assert.False(stack.Undo()); + for (int i = 7; i <= 10; i++) + { + Assert.True(stack.Redo()); + } + Assert.False(stack.Redo()); + } + + [Fact] + public void ClearUndo() + { + var stack = new UndoStack(10); + for (int i = 1; i <= 5; i++) + { + stack.Push(Substitute.For()); + } + stack.ClearUndo(); + Assert.False(stack.Undo()); + } + + [Fact] + public void ClearRedo() + { + var stack = new UndoStack(10); + for (int i = 1; i <= 5; i++) + { + stack.Push(Substitute.For()); + } + for (int i = 1; i <= 5; i++) + { + stack.Undo(); + } + stack.ClearRedo(); + Assert.False(stack.Redo()); + } + + [Fact] + public void ClearBoth() + { + var stack = new UndoStack(10); + for (int i = 1; i <= 5; i++) + { + stack.Push(Substitute.For()); + } + for (int i = 1; i <= 2; i++) + { + stack.Undo(); + } + stack.ClearBoth(); + Assert.False(stack.Undo()); + Assert.False(stack.Redo()); + } +} \ No newline at end of file diff --git a/NAPS2.Lib.Tests/IsExternalInit.cs b/NAPS2.Lib.Tests/IsExternalInit.cs deleted file mode 100644 index 0993ddec14..0000000000 --- a/NAPS2.Lib.Tests/IsExternalInit.cs +++ /dev/null @@ -1,5 +0,0 @@ -// https://sergiopedri.medium.com/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb -// ReSharper disable once CheckNamespace -namespace System.Runtime.CompilerServices; - -internal static class IsExternalInit {} \ No newline at end of file diff --git a/NAPS2.Lib.Tests/NAPS2.Lib.Tests.csproj b/NAPS2.Lib.Tests/NAPS2.Lib.Tests.csproj index aeb3eba15b..dbab53ed37 100644 --- a/NAPS2.Lib.Tests/NAPS2.Lib.Tests.csproj +++ b/NAPS2.Lib.Tests/NAPS2.Lib.Tests.csproj @@ -1,29 +1,27 @@  - net6;net462 - net6 + net9-windows + net9 true NAPS2.Lib.Tests - - - ..\NAPS2.Setup\lib\PdfSharpCore.dll - - - - - - - + + + + + + + + diff --git a/NAPS2.Sdk.Tests/Ocr/TesseractLanguageManagerTests.cs b/NAPS2.Lib.Tests/Ocr/TesseractLanguageManagerTests.cs similarity index 71% rename from NAPS2.Sdk.Tests/Ocr/TesseractLanguageManagerTests.cs rename to NAPS2.Lib.Tests/Ocr/TesseractLanguageManagerTests.cs index b0560e53ec..d5e27a5943 100644 --- a/NAPS2.Sdk.Tests/Ocr/TesseractLanguageManagerTests.cs +++ b/NAPS2.Lib.Tests/Ocr/TesseractLanguageManagerTests.cs @@ -1,7 +1,8 @@ using NAPS2.Ocr; +using NAPS2.Sdk.Tests; using Xunit; -namespace NAPS2.Sdk.Tests.Ocr; +namespace NAPS2.Lib.Tests.Ocr; public class TesseractLanguageManagerTests : ContextualTests { @@ -18,47 +19,39 @@ public TesseractLanguageManagerTests() public void UsesNewBasePathOnCleanInstall() { var manager = new TesseractLanguageManager(FolderPath); - Assert.Equal(_newBasePath, manager.TessdataBasePath); } [Fact] - public void MovesToNewBasePath() + public void UsesExistingNewBasePath() { - Directory.CreateDirectory(_legacyBasePath); - + Directory.CreateDirectory(_newBasePath); var manager = new TesseractLanguageManager(FolderPath); - - Assert.False(Directory.Exists(_legacyBasePath)); - Assert.True(Directory.Exists(_newBasePath)); Assert.Equal(_newBasePath, manager.TessdataBasePath); } [Fact] - public void UsesNewBasePathWhenBothPresent() + public void UsesLegacyBasePathWhenPresent() { Directory.CreateDirectory(_legacyBasePath); - Directory.CreateDirectory(_newBasePath); var manager = new TesseractLanguageManager(FolderPath); Assert.True(Directory.Exists(_legacyBasePath)); - Assert.True(Directory.Exists(_newBasePath)); - Assert.Equal(_newBasePath, manager.TessdataBasePath); + Assert.False(Directory.Exists(_newBasePath)); + Assert.Equal(_legacyBasePath, manager.TessdataBasePath); } - // Locking only stops directory moves on Windows, there isn't an easy way to test this on Mac/Linux - // (and it's not really relevant for Mac/Linux anyway) - [PlatformFact(include: PlatformFlags.Windows)] - public void KeepsLegacyBasePathOnMoveError() + [Fact] + public void UsesNewBasePathWhenBothPresent() { Directory.CreateDirectory(_legacyBasePath); - using var lockedFile = File.OpenWrite(Path.Combine(_legacyBasePath, "blah.txt")); + Directory.CreateDirectory(_newBasePath); var manager = new TesseractLanguageManager(FolderPath); Assert.True(Directory.Exists(_legacyBasePath)); - Assert.False(Directory.Exists(_newBasePath)); - Assert.Equal(_legacyBasePath, manager.TessdataBasePath); + Assert.True(Directory.Exists(_newBasePath)); + Assert.Equal(_newBasePath, manager.TessdataBasePath); } } \ No newline at end of file diff --git a/NAPS2.Lib.Tests/Recovery/RecoveryManagerTests.cs b/NAPS2.Lib.Tests/Recovery/RecoveryManagerTests.cs index 07205ab2ea..565d2937a1 100644 --- a/NAPS2.Lib.Tests/Recovery/RecoveryManagerTests.cs +++ b/NAPS2.Lib.Tests/Recovery/RecoveryManagerTests.cs @@ -1,24 +1,23 @@ using System.Threading; -using Moq; using NAPS2.Recovery; using NAPS2.Scan; using NAPS2.Sdk.Tests; using NAPS2.Sdk.Tests.Asserts; +using NSubstitute; using Xunit; namespace NAPS2.Lib.Tests.Recovery; public class RecoveryManagerTests : ContextualTests { - private readonly string _recoveryBasePath; private readonly RecoveryManager _recoveryManager; + private readonly string _recoveryPath; public RecoveryManagerTests() { - _recoveryBasePath = Path.Combine(FolderPath, "recovery"); - ScanningContext.FileStorageManager = FileStorageManager.CreateFolder(_recoveryBasePath); - ScanningContext.RecoveryPath = _recoveryBasePath; + SetUpFileStorage(); _recoveryManager = new RecoveryManager(ScanningContext); + _recoveryPath = Path.Combine(ScanningContext.RecoveryPath!, Path.GetRandomFileName()); } [Fact] @@ -31,8 +30,7 @@ public void NoFoldersAvailable() [Fact] public void FolderWithNoImages() { - string recovery1 = Path.Combine(_recoveryBasePath, Path.GetRandomFileName()); - CreateFolderToRecoverFrom(recovery1, 0); + CreateFolderToRecoverFrom(_recoveryPath, []); var folder = _recoveryManager.GetLatestRecoverableFolder(); Assert.Null(folder); @@ -41,8 +39,7 @@ public void FolderWithNoImages() [Fact] public void FolderLocking() { - string recovery1 = Path.Combine(_recoveryBasePath, Path.GetRandomFileName()); - CreateFolderToRecoverFrom(recovery1, 1); + CreateFolderToRecoverFrom(_recoveryPath, ["a"]); using var folder = _recoveryManager.GetLatestRecoverableFolder(); Assert.NotNull(folder); @@ -58,8 +55,7 @@ public void FolderLocking() [Fact] public void FindSingleFolder() { - string recovery1 = Path.Combine(_recoveryBasePath, Path.GetRandomFileName()); - CreateFolderToRecoverFrom(recovery1, 1); + CreateFolderToRecoverFrom(_recoveryPath, ["a"]); using var folder = _recoveryManager.GetLatestRecoverableFolder(); Assert.NotNull(folder); @@ -70,47 +66,57 @@ public void FindSingleFolder() [Fact] public void DeleteFolder() { - string recovery1 = Path.Combine(_recoveryBasePath, Path.GetRandomFileName()); - CreateFolderToRecoverFrom(recovery1, 1); + CreateFolderToRecoverFrom(_recoveryPath, ["a"]); using var folder = _recoveryManager.GetLatestRecoverableFolder(); Assert.NotNull(folder); - Assert.True(Directory.Exists(recovery1)); + Assert.True(Directory.Exists(_recoveryPath)); folder.TryDelete(); - Assert.False(Directory.Exists(recovery1)); + Assert.False(Directory.Exists(_recoveryPath)); } [Fact] public void Recover() { - string recovery1 = Path.Combine(_recoveryBasePath, Path.GetRandomFileName()); - CreateFolderToRecoverFrom(recovery1, 2); + CreateFolderToRecoverFrom(_recoveryPath, ["a", "b"]); var images = new List(); void ImageCallback(ProcessedImage img) => images.Add(img); - var mockProgressCallback = new Mock(); + var mockProgressCallback = Substitute.For(); using var folder = _recoveryManager.GetLatestRecoverableFolder(); Assert.NotNull(folder); - var result = folder.TryRecover(ImageCallback, new RecoveryParams(), mockProgressCallback.Object); + var result = folder.TryRecover(ImageCallback, new RecoveryParams(), mockProgressCallback); Assert.True(result); Assert.Equal(2, images.Count); ImageAsserts.Similar(ImageResources.dog, images[0]); - mockProgressCallback.Verify(callback => callback(0, 2)); - mockProgressCallback.Verify(callback => callback(1, 2)); - mockProgressCallback.Verify(callback => callback(2, 2)); - mockProgressCallback.VerifyNoOtherCalls(); + mockProgressCallback.Received()(0, 2); + mockProgressCallback.Received()(1, 2); + mockProgressCallback.Received()(2, 2); + mockProgressCallback.ReceivedCallsCount(3); + } + + [Fact] + public void FastRecover() + { + CreateFolderToRecoverFrom(_recoveryPath, ["a", "b"]); + + using var folder = _recoveryManager.GetLatestRecoverableFolder(); + Assert.NotNull(folder); + var images = folder.FastRecover().ToList(); + + Assert.Equal(2, images.Count); + ImageAsserts.Similar(ImageResources.dog, images[0]); } [Fact] public void CancelRecover() { - string recovery1 = Path.Combine(_recoveryBasePath, Path.GetRandomFileName()); - CreateFolderToRecoverFrom(recovery1, 2); + CreateFolderToRecoverFrom(_recoveryPath, ["a", "b"]); - var mockImageCallback = new Mock>(); + var mockImageCallback = Substitute.For>(); CancellationTokenSource cts = new CancellationTokenSource(); void ProgressCallback(int current, int total) @@ -122,12 +128,14 @@ void ProgressCallback(int current, int total) using var folder = _recoveryManager.GetLatestRecoverableFolder(); Assert.NotNull(folder); - var result = folder.TryRecover(mockImageCallback.Object, new RecoveryParams(), + var result = folder.TryRecover(mockImageCallback, new RecoveryParams(), new ProgressHandler(ProgressCallback, cts.Token)); Assert.False(result); - Assert.True(Directory.Exists(recovery1)); - mockImageCallback.Verify(callback => callback(It.IsAny())); - mockImageCallback.VerifyNoOtherCalls(); + var recoveryDir = new DirectoryInfo(_recoveryPath); + Assert.True(recoveryDir.Exists); + Assert.Equal(4, recoveryDir.GetFiles().Length); + mockImageCallback.Received()(Arg.Any()); + mockImageCallback.ReceivedCallsCount(1); // After a cancelled recovery, we should be able to recover from the same folder again folder.Dispose(); @@ -138,40 +146,108 @@ void ProgressCallback(int current, int total) [Fact] public void RecoverWithMissingFile() { - string recovery1 = Path.Combine(_recoveryBasePath, Path.GetRandomFileName()); - var uiImages = CreateFolderToRecoverFrom(recovery1, 2); + var uiImages = CreateFolderToRecoverFrom(_recoveryPath, ["a", "b"]); File.Delete(((ImageFileStorage) uiImages[0].GetImageWeakReference().ProcessedImage.Storage).FullPath); var images = new List(); void ImageCallback(ProcessedImage img) => images.Add(img); - var mockProgressCallback = new Mock(); + var mockProgressCallback = Substitute.For(); using var folder = _recoveryManager.GetLatestRecoverableFolder(); Assert.NotNull(folder); - var result = folder.TryRecover(ImageCallback, new RecoveryParams(), mockProgressCallback.Object); + var result = folder.TryRecover(ImageCallback, new RecoveryParams(), mockProgressCallback); Assert.True(result); Assert.Single(images); ImageAsserts.Similar(ImageResources.dog, images[0]); - mockProgressCallback.Verify(callback => callback(0, 2)); - mockProgressCallback.Verify(callback => callback(1, 2)); - mockProgressCallback.Verify(callback => callback(2, 2)); - mockProgressCallback.VerifyNoOtherCalls(); + mockProgressCallback.Received()(0, 2); + mockProgressCallback.Received()(1, 2); + mockProgressCallback.Received()(2, 2); + mockProgressCallback.ReceivedCallsCount(3); } - private List CreateFolderToRecoverFrom(string folderPath, int imageCount) + [Fact] + public void FastRecoverWithMissingFile() + { + var uiImages = CreateFolderToRecoverFrom(_recoveryPath, ["a", "b"]); + File.Delete(((ImageFileStorage) uiImages[0].GetImageWeakReference().ProcessedImage.Storage).FullPath); + + using var folder = _recoveryManager.GetLatestRecoverableFolder(); + Assert.NotNull(folder); + var images = folder.FastRecover().ToList(); + + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + } + + [Fact] + public void RecoverWithSharedStorage() + { + CreateFolderToRecoverFrom(_recoveryPath, ["a", "a", "b", "b"]); + + var images = new List(); + void ImageCallback(ProcessedImage img) => images.Add(img); + var mockProgressCallback = Substitute.For(); + + using var folder = _recoveryManager.GetLatestRecoverableFolder(); + Assert.NotNull(folder); + var result = folder.TryRecover(ImageCallback, new RecoveryParams(), mockProgressCallback); + Assert.True(result); + + Assert.Equal(4, images.Count); + ImageAsserts.Similar(ImageResources.dog, images[0]); + ImageAsserts.Similar(ImageResources.dog, images[1]); + ImageAsserts.Similar(ImageResources.dog, images[2]); + ImageAsserts.Similar(ImageResources.dog, images[3]); + Assert.Equal(images[0].Storage, images[1].Storage); + Assert.NotEqual(images[1].Storage, images[2].Storage); + Assert.Equal(images[2].Storage, images[3].Storage); + + mockProgressCallback.Received()(0, 4); + mockProgressCallback.Received()(1, 4); + mockProgressCallback.Received()(2, 4); + mockProgressCallback.Received()(3, 4); + mockProgressCallback.Received()(4, 4); + mockProgressCallback.ReceivedCallsCount(5); + } + + [Fact] + public void FastRecoverWithSharedStorage() + { + CreateFolderToRecoverFrom(_recoveryPath, ["a", "a", "b", "b"]); + + using var folder = _recoveryManager.GetLatestRecoverableFolder(); + Assert.NotNull(folder); + var images = folder.FastRecover().ToList(); + + Assert.Equal(4, images.Count); + ImageAsserts.Similar(ImageResources.dog, images[0]); + ImageAsserts.Similar(ImageResources.dog, images[1]); + ImageAsserts.Similar(ImageResources.dog, images[2]); + ImageAsserts.Similar(ImageResources.dog, images[3]); + Assert.Equal(images[0].Storage, images[1].Storage); + Assert.NotEqual(images[1].Storage, images[2].Storage); + Assert.Equal(images[2].Storage, images[3].Storage); + } + + private List CreateFolderToRecoverFrom(string folderPath, List imageRefs) { var imageList = new UiImageList(); var rsm1 = RecoveryStorageManager.CreateFolderWithoutThrottle(folderPath, imageList); - var recoveryContext = new ScanningContext(TestImageContextFactory.Get(), new FileStorageManager(folderPath)); - var images = Enumerable.Range(0, imageCount).Select(x => new UiImage(CreateRecoveryImage(recoveryContext))) + var recoveryContext = new ScanningContext(TestImageContextFactory.Get()) + { + FileStorageManager = new FileStorageManager(folderPath) + }; + var processedImages = imageRefs.Distinct().ToDictionary(x => x, _ => CreateRecoveryImage(recoveryContext)); + var images = imageRefs.Select(x => new UiImage(processedImages[x].Clone())) .ToList(); + processedImages.Values.DisposeAll(); imageList.Mutate(new ListMutation.Append(images)); rsm1.ReleaseLockForTesting(); return images; } - + // TODO: Add tests for recovery params (i.e. thumbnail) private ProcessedImage CreateRecoveryImage(ScanningContext recoveryContext) diff --git a/NAPS2.Lib.Tests/Recovery/RecoveryStorageManagerTests.cs b/NAPS2.Lib.Tests/Recovery/RecoveryStorageManagerTests.cs index 7fbfdda4c0..bf08201151 100644 --- a/NAPS2.Lib.Tests/Recovery/RecoveryStorageManagerTests.cs +++ b/NAPS2.Lib.Tests/Recovery/RecoveryStorageManagerTests.cs @@ -12,8 +12,8 @@ public class RecoveryStorageManagerTests : ContextualTests public RecoveryStorageManagerTests() { - _recoveryFolder = Path.Combine(FolderPath, "recovery"); - ScanningContext.FileStorageManager = new FileStorageManager(_recoveryFolder); + SetUpFileStorage(); + _recoveryFolder = ScanningContext.RecoveryPath!; _imageList = new UiImageList(); _recoveryStorageManager = RecoveryStorageManager.CreateFolderWithoutThrottle(_recoveryFolder, _imageList); } @@ -52,7 +52,6 @@ public void IndexFileDefaultMetadata() Assert.Contains("00001.jpg", indexFileContent); Assert.Contains("00002.jpg", indexFileContent); - Assert.Contains("C24Bit", indexFileContent); Assert.Contains("false", indexFileContent); } @@ -79,14 +78,14 @@ public void IndexFileCustomMetadata() var image1 = new UiImage( ScanningContext.CreateProcessedImage( ImageContext.Create(100, 100, ImagePixelFormat.RGB24), - BitDepth.Grayscale, true, - -1)); + -1, + PageSize.A4)); _imageList.Mutate(new ListMutation.Append(image1)); var indexFileContent = File.ReadAllText(Path.Combine(_recoveryFolder, "index.xml")); - Assert.Contains("Grayscale", indexFileContent); Assert.Contains("true", indexFileContent); + Assert.Contains("210x297 mm", indexFileContent); } [Fact] @@ -95,7 +94,7 @@ public void IndexFileWithTransform() var image1 = new UiImage( ScanningContext.CreateProcessedImage( ImageContext.Create(100, 100, ImagePixelFormat.RGB24), - new[] {new BrightnessTransform(100)})); + transforms: [new BrightnessTransform(100)])); _imageList.Mutate(new ListMutation.Append(image1)); var indexFileContent3 = File.ReadAllText(Path.Combine(_recoveryFolder, "index.xml")); diff --git a/NAPS2.Lib.Tests/Scan/AutoSaverTests.cs b/NAPS2.Lib.Tests/Scan/AutoSaverTests.cs index 61e9cf3b2a..72a30f20fc 100644 --- a/NAPS2.Lib.Tests/Scan/AutoSaverTests.cs +++ b/NAPS2.Lib.Tests/Scan/AutoSaverTests.cs @@ -1,10 +1,11 @@ -using Moq; -using NAPS2.EtoForms; +using NAPS2.EtoForms; +using NAPS2.EtoForms.Notifications; using NAPS2.ImportExport; -using NAPS2.ImportExport.Pdf; +using NAPS2.Pdf; using NAPS2.Scan; using NAPS2.Sdk.Tests; using NAPS2.Sdk.Tests.Asserts; +using NSubstitute; using Xunit; namespace NAPS2.Lib.Tests.Scan; @@ -13,30 +14,31 @@ public class AutoSaverTests : ContextualTests { private readonly AutoSaver _autoSaver; - private readonly Mock _errorOutput; - private readonly Mock _dialogHelper; - private readonly Mock _operationProgress; - private readonly Mock _saveNotify; - private readonly Mock _overwritePrompt; + private readonly ErrorOutput _errorOutput; + private readonly DialogHelper _dialogHelper; + private readonly OperationProgress _operationProgress; + private readonly ISaveNotify _saveNotify; + private readonly IOverwritePrompt _overwritePrompt; private readonly Naps2Config _config; public AutoSaverTests() { - _errorOutput = new Mock(); - _dialogHelper = new Mock(); - _operationProgress = new Mock(); - _saveNotify = new Mock(); - _overwritePrompt = new Mock(); + _errorOutput = Substitute.For(); + _dialogHelper = Substitute.For(); + _operationProgress = Substitute.For(); + _saveNotify = Substitute.For(); + _overwritePrompt = Substitute.For(); _config = Naps2Config.Stub(); _autoSaver = new AutoSaver( - _errorOutput.Object, - _dialogHelper.Object, - _operationProgress.Object, - _saveNotify.Object, + _errorOutput, + _dialogHelper, + _operationProgress, + _saveNotify, new PdfExporter(ScanningContext), - _overwritePrompt.Object, + _overwritePrompt, _config, - ImageContext + ImageContext, + new UiImageList() ); } @@ -62,11 +64,13 @@ public async Task SinglePdf() { FilePath = Path.Combine(FolderPath, "test$(n).pdf") }; - var scanned = CreateScannedImages(ImageResources.dog).ToAsyncEnumerable(); - var output = _autoSaver.Save(settings, scanned); + var scanned = CreateScannedImages(ImageResources.dog); + var output = await _autoSaver.Save(settings, scanned.ToAsyncEnumerable()).ToListAsync(); - Assert.Single(await output.ToListAsync()); + Assert.Single(output); + Assert.False(IsDisposed(output[0])); + Assert.True(IsDisposed(scanned[0])); Assert.Single(Folder.GetFiles()); PdfAsserts.AssertImages(Path.Combine(FolderPath, "test1.pdf"), ImageResources.dog); } @@ -78,11 +82,13 @@ public async Task SingleJpeg() { FilePath = Path.Combine(FolderPath, "test$(n).jpg") }; - var scanned = CreateScannedImages(ImageResources.dog).ToAsyncEnumerable(); - var output = _autoSaver.Save(settings, scanned); + var scanned = CreateScannedImages(ImageResources.dog); + var output = await _autoSaver.Save(settings, scanned.ToAsyncEnumerable()).ToListAsync(); - Assert.Single(await output.ToListAsync()); + Assert.Single(output); + Assert.False(IsDisposed(output[0])); + Assert.True(IsDisposed(scanned[0])); Assert.Single(Folder.GetFiles()); ImageAsserts.Similar(ImageResources.dog, Path.Combine(FolderPath, "test1.jpg")); } @@ -129,7 +135,7 @@ public async Task PdfFilePerScan() // ImageAsserts.Similar(ImageResources.dog_gray, Path.Combine(FolderPath, "test2.jpg")); // } - [Fact] + [PlatformFact(exclude: PlatformFlags.ImageSharpImage)] public async Task TiffFilePerScan() { var settings = new AutoSaveSettings @@ -147,8 +153,8 @@ public async Task TiffFilePerScan() Assert.Single(Folder.GetFiles()); var frames = await ImageContext.LoadFrames(Path.Combine(FolderPath, "test1.tiff")).ToListAsync(); Assert.Equal(2, frames.Count); - ImageAsserts.Similar(ImageResources.dog, frames[0]); - ImageAsserts.Similar(ImageResources.dog_gray, frames[1]); + ImageAsserts.Similar(ImageResources.dog, frames[0], ignoreResolution: true); + ImageAsserts.Similar(ImageResources.dog_gray, frames[1], ignoreResolution: true); } [Fact] @@ -170,13 +176,84 @@ public async Task PdfFilePerPage() PdfAsserts.AssertImages(Path.Combine(FolderPath, "test2.pdf"), ImageResources.dog_gray); } + [Fact] + public async Task PdfSplitPatchT() + { + var settings = new AutoSaveSettings + { + FilePath = Path.Combine(FolderPath, "test$(n).pdf"), + Separator = SaveSeparator.PatchT + }; + var scanned = CreateScannedImages( + ImageResources.dog, + ImageResources.dog_gray, + ImageResources.patcht, + ImageResources.dog_h_n300); + scanned[2] = scanned[2].WithPostProcessingData(scanned[2].PostProcessingData with + { + Barcode = new Barcode(true, true, "PATCHT") + }, true); + var output = _autoSaver.Save(settings, scanned.ToAsyncEnumerable()); + Assert.Equal(4, (await output.ToListAsync()).Count); + Assert.Equal(2, Folder.GetFiles().Length); + PdfAsserts.AssertImages(Path.Combine(FolderPath, "test1.pdf"), ImageResources.dog, ImageResources.dog_gray); + PdfAsserts.AssertImages(Path.Combine(FolderPath, "test2.pdf"), ImageResources.dog_h_n300); + } + + [Fact] + public async Task PromptForFilePath() + { + var settings = new AutoSaveSettings + { + FilePath = Path.Combine(FolderPath, "test_a_$(n).pdf"), + PromptForFilePath = true + }; + _dialogHelper.PromptToSavePdfOrImage(Arg.Any(), out Arg.Any()).Returns(x => + { + x[1] = Path.Combine(FolderPath, "test_b_$(n).pdf"); + return true; + }); + + var scanned = CreateScannedImages(ImageResources.dog); + var output = await _autoSaver.Save(settings, scanned.ToAsyncEnumerable()).ToListAsync(); + + Assert.Single(output); + Assert.False(IsDisposed(output[0])); + Assert.True(IsDisposed(scanned[0])); + Assert.Single(Folder.GetFiles()); + PdfAsserts.AssertImages(Path.Combine(FolderPath, "test_b_1.pdf"), ImageResources.dog); + } + + [Fact] + public async Task CancelPromptForFilePath() + { + var settings = new AutoSaveSettings + { + FilePath = Path.Combine(FolderPath, "test$(n).pdf"), + PromptForFilePath = true + }; + _dialogHelper.PromptToSavePdfOrImage(Arg.Any(), out Arg.Any()).Returns(x => + { + x[1] = Path.Combine(FolderPath, "test$(n).pdf"); + return false; + }); + + var scanned = CreateScannedImages(ImageResources.dog); + var output = await _autoSaver.Save(settings, scanned.ToAsyncEnumerable()).ToListAsync(); + + Assert.Single(output); + Assert.False(IsDisposed(output[0])); + Assert.True(IsDisposed(scanned[0])); + Assert.Empty(Folder.GetFiles()); + } + // TODO: Finish out tests // // [Fact] // public async Task TwoImagesTwoPdfs() // { - // var errorOutput = new Mock(); + // var errorOutput = Substitute.For(); // var driver = Driver(errorOutput.Object, 2); // // var scanProfile = Profile(new AutoSaveSettings @@ -198,7 +275,7 @@ public async Task PdfFilePerPage() // [Fact] // public async Task TwoImagesTwoJpegs() // { - // var errorOutput = new Mock(); + // var errorOutput = Substitute.For(); // var driver = Driver(errorOutput.Object, 2); // // var scanProfile = Profile(new AutoSaveSettings @@ -217,7 +294,7 @@ public async Task PdfFilePerPage() // [Fact] // public async Task ClearAfterSaving() // { - // var errorOutput = new Mock(); + // var errorOutput = Substitute.For(); // var driver = Driver(errorOutput.Object, 2); // // var scanProfile = Profile(new AutoSaveSettings diff --git a/NAPS2.Sdk.Tests/Util/ListMutationTests.cs b/NAPS2.Lib.Tests/Util/ListMutationTests.cs similarity index 98% rename from NAPS2.Sdk.Tests/Util/ListMutationTests.cs rename to NAPS2.Lib.Tests/Util/ListMutationTests.cs index fbadb1b341..e68bae3431 100644 --- a/NAPS2.Sdk.Tests/Util/ListMutationTests.cs +++ b/NAPS2.Lib.Tests/Util/ListMutationTests.cs @@ -1,7 +1,7 @@ using NAPS2.Sdk.Tests.Asserts; using Xunit; -namespace NAPS2.Sdk.Tests.Util; +namespace NAPS2.Lib.Tests.Util; public class ListMutationTests { diff --git a/NAPS2.Lib.Tests/WinForms/DesktopControllerTests.cs b/NAPS2.Lib.Tests/WinForms/DesktopControllerTests.cs index bfbf059860..b39a778d84 100644 --- a/NAPS2.Lib.Tests/WinForms/DesktopControllerTests.cs +++ b/NAPS2.Lib.Tests/WinForms/DesktopControllerTests.cs @@ -1,13 +1,16 @@ -using Moq; using NAPS2.EtoForms; using NAPS2.EtoForms.Desktop; +using NAPS2.EtoForms.Notifications; using NAPS2.ImportExport; -using NAPS2.ImportExport.Images; using NAPS2.Platform.Windows; using NAPS2.Recovery; +using NAPS2.Remoting; +using NAPS2.Remoting.Server; +using NAPS2.Remoting.Worker; using NAPS2.Sdk.Tests; using NAPS2.Sdk.Tests.Asserts; using NAPS2.Update; +using NSubstitute; using Xunit; namespace NAPS2.Lib.Tests.WinForms; @@ -21,69 +24,73 @@ public class DesktopControllerTests : ContextualTests private readonly UiImageList _imageList; private readonly RecoveryStorageManager _recoveryStorageManager; private readonly ThumbnailRenderQueue _thumbnailRenderQueue; - private readonly Mock _operationProgress; + private readonly OperationProgress _operationProgress; private readonly Naps2Config _config; - private readonly Mock _operationFactory; + private readonly IOperationFactory _operationFactory; private readonly StillImage _stillImage; - private readonly Mock _updateChecker; - private readonly Mock _notificationManager; - private readonly ImageTransfer _imageTransfer; + private readonly IUpdateChecker _updateChecker; + private readonly INotify _notify; private readonly ImageClipboard _imageClipboard; - private readonly Mock _exportHelper; - private readonly Mock _dialogHelper; + private readonly IExportController _exportHelper; + private readonly DialogHelper _dialogHelper; private readonly DesktopImagesController _desktopImagesController; - private readonly Mock _desktopScanController; + private readonly IDesktopScanController _desktopScanController; private readonly DesktopFormProvider _desktopFormProvider; - private readonly Mock _scannedImagePrinter; + private readonly IScannedImagePrinter _scannedImagePrinter; private readonly ThumbnailController _thumbnailController; + private readonly ISharedDeviceManager _sharedDeviceManager; + private readonly ProcessCoordinator _processCoordinator; public DesktopControllerTests() { - ScanningContext.RecoveryPath = Path.Combine(FolderPath, "recovery"); - ScanningContext.FileStorageManager = new FileStorageManager(ScanningContext.RecoveryPath); + SetUpFileStorage(); _imageList = new UiImageList(); - _recoveryStorageManager = RecoveryStorageManager.CreateFolder(ScanningContext.RecoveryPath, _imageList); + _recoveryStorageManager = RecoveryStorageManager.CreateFolder(ScanningContext.RecoveryPath!, _imageList); _thumbnailRenderQueue = new ThumbnailRenderQueue(ScanningContext, new ThumbnailRenderer(ImageContext)); - _operationProgress = new Mock(); + _operationProgress = Substitute.For(); _config = Naps2Config.Stub(); - _operationFactory = new Mock(); + _operationFactory = Substitute.For(); _stillImage = new StillImage(); - _updateChecker = new Mock(); - _notificationManager = new Mock(); - _imageTransfer = new ImageTransfer(); + _updateChecker = Substitute.For(); + _notify = Substitute.For(); _imageClipboard = new ImageClipboard(); - _exportHelper = new Mock(); - _dialogHelper = new Mock(); + _exportHelper = Substitute.For(); + _dialogHelper = Substitute.For(); _desktopImagesController = new DesktopImagesController(_imageList); - _desktopScanController = new Mock(); + _desktopScanController = Substitute.For(); _desktopFormProvider = new DesktopFormProvider(); - _scannedImagePrinter = new Mock(); + _scannedImagePrinter = Substitute.For(); _thumbnailController = new ThumbnailController(_thumbnailRenderQueue, _config); + _sharedDeviceManager = Substitute.For(); + _processCoordinator = + new ProcessCoordinator(FolderPath, Guid.NewGuid().ToString("D")); + ScanningContext.WorkerFactory = Substitute.For(); _desktopController = new DesktopController( ScanningContext, _imageList, _recoveryStorageManager, _thumbnailController, - _operationProgress.Object, + _operationProgress, _config, - _operationFactory.Object, + _operationFactory, _stillImage, - _updateChecker.Object, - _notificationManager.Object, - _imageTransfer, + _updateChecker, + _notify, _imageClipboard, - new ImageListActions(_imageList, _operationFactory.Object, _operationProgress.Object, - _config, _thumbnailController, _exportHelper.Object, _notificationManager.Object), - _exportHelper.Object, - _dialogHelper.Object, + new ImageListActions(_imageList, _operationFactory, _operationProgress, + _config, _thumbnailController, _exportHelper, _notify, null!), + _dialogHelper, _desktopImagesController, - _desktopScanController.Object, + _desktopScanController, _desktopFormProvider, - _scannedImagePrinter.Object + _scannedImagePrinter, + _sharedDeviceManager, + _processCoordinator, + new RecoveryManager(ScanningContext) ); - _operationFactory.Setup(x => x.Create()).Returns( - new RecoveryOperation(new Mock().Object, new RecoveryManager(ScanningContext))); + _operationFactory.Create().Returns( + new RecoveryOperation(Substitute.For(), new RecoveryManager(ScanningContext))); } public override void Dispose() @@ -101,8 +108,8 @@ public async Task Initialize_IfNotRunBefore_SetsFirstRunDate() await _desktopController.Initialize(); Assert.True(_config.Get(c => c.HasBeenRun)); - DateAsserts.Recent(TimeSpan.FromMilliseconds(100), _config.Get(c => c.FirstRunDate)); - _notificationManager.VerifyNoOtherCalls(); + DateAsserts.Recent(TimeSpan.FromMilliseconds(1000), _config.Get(c => c.FirstRunDate)); + _notify.ReceivedCallsCount(0); } [Fact] @@ -116,7 +123,7 @@ public async Task Initialize_IfAlreadyRun_DoesntSetFirstRunDate() Assert.True(_config.Get(c => c.HasBeenRun)); Assert.Equal(firstRunDate, _config.Get(c => c.FirstRunDate)); - _notificationManager.VerifyNoOtherCalls(); + _notify.ReceivedCallsCount(0); } [Fact] @@ -128,9 +135,9 @@ public async Task Initialize_IfRun30DaysAgo_ShowsDonatePrompt() await _desktopController.Initialize(); - _notificationManager.Verify(x => x.DonatePrompt()); + _notify.Received().DonatePrompt(); Assert.True(_config.Get(c => c.HasBeenPromptedForDonation)); - DateAsserts.Recent(TimeSpan.FromMilliseconds(100), _config.Get(c => c.LastDonatePromptDate)); + DateAsserts.Recent(TimeSpan.FromMilliseconds(1000), _config.Get(c => c.LastDonatePromptDate)); } [Fact] @@ -147,7 +154,7 @@ public async Task Initialize_IfDonatePromptAlreadyShown_DoesntShowDonatePrompt() Assert.True(_config.Get(c => c.HasBeenPromptedForDonation)); Assert.Equal(donatePromptDate, _config.Get(c => c.LastDonatePromptDate)); - _notificationManager.VerifyNoOtherCalls(); + _notify.ReceivedCallsCount(0); } [Fact] @@ -157,71 +164,71 @@ public async Task Initialize_WithStillImageArgs_StartsScan() await _desktopController.Initialize(); - _desktopScanController.Verify(c => c.ScanWithDevice("abc")); - _desktopScanController.VerifyNoOtherCalls(); + _ = _desktopScanController.Received().ScanWithDevice("abc"); + _notify.ReceivedCallsCount(0); } - [Fact] + [Fact(Skip = "flaky")] public async Task Initialize_WithUpdateChecksDisabled_DoesntCheckForUpdate() { await _desktopController.Initialize(); - await Task.Delay(50); + await Task.Delay(500); Assert.False(_config.Get(c => c.HasCheckedForUpdates)); Assert.Null(_config.Get(c => c.LastUpdateCheckDate)); - _updateChecker.VerifyNoOtherCalls(); + _updateChecker.ReceivedCallsCount(0); } - [Fact] + [Fact(Skip = "flaky")] public async Task Initialize_WithNoUpdate_DoesntPromptToUpdate() { _config.User.Set(c => c.CheckForUpdates, true); await _desktopController.Initialize(); - await Task.Delay(50); + await Task.Delay(500); Assert.True(_config.Get(c => c.HasCheckedForUpdates)); - DateAsserts.Recent(TimeSpan.FromMilliseconds(100), _config.Get(c => c.LastUpdateCheckDate)); - _updateChecker.Verify(x => x.CheckForUpdates()); - _updateChecker.VerifyNoOtherCalls(); - _notificationManager.VerifyNoOtherCalls(); + DateAsserts.Recent(TimeSpan.FromMilliseconds(1000), _config.Get(c => c.LastUpdateCheckDate)); + _ = _updateChecker.Received().CheckForUpdates(); + _updateChecker.ReceivedCallsCount(1); + _notify.ReceivedCallsCount(0); } - [Fact] + [Fact(Skip = "flaky")] public async Task Initialize_WithUpdate_NotifiesOfUpdate() { _config.User.Set(c => c.CheckForUpdates, true); var mockUpdateInfo = new UpdateInfo("10.0.0", "https://www.example.com", Array.Empty(), Array.Empty()); - _updateChecker.Setup(x => x.CheckForUpdates()).ReturnsAsync(mockUpdateInfo); + _updateChecker.CheckForUpdates().Returns(Task.FromResult(mockUpdateInfo)); await _desktopController.Initialize(); - await Task.Delay(50); + await Task.Delay(500); Assert.True(_config.Get(c => c.HasCheckedForUpdates)); - DateAsserts.Recent(TimeSpan.FromMilliseconds(100), _config.Get(c => c.LastUpdateCheckDate)); - _updateChecker.Verify(x => x.CheckForUpdates()); - _notificationManager.Verify(x => x.UpdateAvailable(_updateChecker.Object, mockUpdateInfo)); - _updateChecker.VerifyNoOtherCalls(); - _notificationManager.VerifyNoOtherCalls(); + DateAsserts.Recent(TimeSpan.FromMilliseconds(1000), _config.Get(c => c.LastUpdateCheckDate)); + _ = _updateChecker.Received().CheckForUpdates(); + _notify.UpdateAvailable(_updateChecker, mockUpdateInfo); + _updateChecker.ReceivedCallsCount(1); + _notify.ReceivedCallsCount(1); } - [Fact] + [Fact(Skip = "flaky")] public async Task Initialize_WithNoUpdatePrompt_DoesntCheckForUpdate() { _config.AppDefault.Set(c => c.NoUpdatePrompt, true); _config.User.Set(c => c.CheckForUpdates, true); await _desktopController.Initialize(); - await Task.Delay(50); + await Task.Delay(500); Assert.False(_config.Get(c => c.HasCheckedForUpdates)); Assert.Null(_config.Get(c => c.LastUpdateCheckDate)); - _updateChecker.VerifyNoOtherCalls(); - _notificationManager.VerifyNoOtherCalls(); + _updateChecker.ReceivedCallsCount(0); + _notify.ReceivedCallsCount(0); } - [Fact] + [Fact(Skip = "flaky")] public async Task Initialize_WithRecentUpdateCheck_DoesntCheckForUpdate() { var updateCheckDate = DateTime.Now - TimeSpan.FromDays(6); @@ -230,15 +237,15 @@ public async Task Initialize_WithRecentUpdateCheck_DoesntCheckForUpdate() _config.User.Set(c => c.LastUpdateCheckDate, updateCheckDate); await _desktopController.Initialize(); - await Task.Delay(50); + await Task.Delay(500); Assert.True(_config.Get(c => c.HasCheckedForUpdates)); Assert.Equal(updateCheckDate, _config.Get(c => c.LastUpdateCheckDate)); - _updateChecker.VerifyNoOtherCalls(); - _notificationManager.VerifyNoOtherCalls(); + _updateChecker.ReceivedCallsCount(0); + _notify.ReceivedCallsCount(0); } - [Fact] + [Fact(Skip = "flaky")] public async Task Initialize_WithOldUpdateCheck_NotifiesOfUpdate() { var updateCheckDate = DateTime.Now - TimeSpan.FromDays(8); @@ -247,16 +254,40 @@ public async Task Initialize_WithOldUpdateCheck_NotifiesOfUpdate() _config.User.Set(c => c.LastUpdateCheckDate, updateCheckDate); var mockUpdateInfo = new UpdateInfo("10.0.0", "https://www.example.com", Array.Empty(), Array.Empty()); - _updateChecker.Setup(x => x.CheckForUpdates()).ReturnsAsync(mockUpdateInfo); + _updateChecker.CheckForUpdates().Returns(Task.FromResult(mockUpdateInfo)); await _desktopController.Initialize(); - await Task.Delay(50); + await Task.Delay(500); Assert.True(_config.Get(c => c.HasCheckedForUpdates)); - DateAsserts.Recent(TimeSpan.FromMilliseconds(100), _config.Get(c => c.LastUpdateCheckDate)); - _updateChecker.Verify(x => x.CheckForUpdates()); - _notificationManager.Verify(x => x.UpdateAvailable(_updateChecker.Object, mockUpdateInfo)); - _updateChecker.VerifyNoOtherCalls(); - _notificationManager.VerifyNoOtherCalls(); + DateAsserts.Recent(TimeSpan.FromMilliseconds(1000), _config.Get(c => c.LastUpdateCheckDate)); + _ = _updateChecker.Received().CheckForUpdates(); + _notify.Received().UpdateAvailable(_updateChecker, mockUpdateInfo); + _updateChecker.ReceivedCallsCount(1); + _notify.ReceivedCallsCount(1); + } + + [Fact] + public async Task ProcessCoordinatorOpenFile() + { + var importOp = new ImportOperation(new FileImporter(ScanningContext)); + _operationFactory.Create().Returns(importOp); + var path = CopyResourceToFile(ImageResources.dog, "test.jpg"); + + await _desktopController.Initialize(); + Assert.True(_processCoordinator.OpenFile(Process.GetCurrentProcess(), 10000, path)); + await Task.WhenAny(importOp.Success, Task.Delay(10000)); + + Assert.Single(_imageList.Images); + ImageAsserts.Similar(ImageResources.dog, _imageList.Images[0].GetClonedImage().Render()); + } + + [Fact] + public async Task ProcessCoordinatorScanWithDevice() + { + await _desktopController.Initialize(); + Assert.True(_processCoordinator.ScanWithDevice(Process.GetCurrentProcess(), 10000, "abc")); + await Task.Delay(500); + _ = _desktopScanController.Received().ScanWithDevice("abc"); } } \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/EntryPoints/WinFormsEntryPoint.cs b/NAPS2.Lib.WinForms/EntryPoints/WinFormsEntryPoint.cs index 7cba57f546..2985fa8a29 100644 --- a/NAPS2.Lib.WinForms/EntryPoints/WinFormsEntryPoint.cs +++ b/NAPS2.Lib.WinForms/EntryPoints/WinFormsEntryPoint.cs @@ -1,15 +1,6 @@ -using System.Reflection; -using System.Threading; -using Autofac; -using Eto.Forms; -using Eto.WinForms.Forms; -using NAPS2.EtoForms; -using NAPS2.EtoForms.Ui; +using NAPS2.EtoForms; using NAPS2.EtoForms.WinForms; using NAPS2.Modules; -using NAPS2.Platform.Windows; -using NAPS2.Remoting.Worker; -using wf = System.Windows.Forms; namespace NAPS2.EntryPoints; @@ -18,54 +9,16 @@ namespace NAPS2.EntryPoints; /// public static class WinFormsEntryPoint { - public static void Run(string[] args) + public static int Run(string[] args) { - // Initialize Autofac (the DI framework) - var container = AutoFacHelper.FromModules( - new CommonModule(), new GdiModule(), new WinFormsModule(), new RecoveryModule(), new ContextModule()); - - Paths.ClearTemp(); - - // Parse the command-line arguments and see if we're doing something other than displaying the main form - var lifecycle = container.Resolve(); - lifecycle.ParseArgs(args); - lifecycle.ExitIfRedundant(); - - // Start a pending worker process - container.Resolve().Init(); - - // Set up basic application configuration - container.Resolve().SetCulturesFromConfig(); - // TODO: Unify unhandled exception handling across platforms - wf.Application.ThreadException += UnhandledException; - TaskScheduler.UnobservedTaskException += UnhandledTaskException; - - // Show the main form - var application = EtoPlatform.Current.CreateApplication(); - var formFactory = container.Resolve(); - var desktop = formFactory.Create(); - - // We manually run an application rather than using eto as that lets us change the main form - // TODO: PR for eto to handle mainform changes correctly - application.MainForm = desktop; - desktop.Show(); - var appContext = new wf.ApplicationContext(desktop.ToNative()); - Invoker.Current = new WinFormsInvoker(() => appContext.MainForm!); - WinFormsDesktopForm.ApplicationContext = appContext; - var setOptionsMethod = - typeof(ApplicationHandler).GetMethod("SetOptions", BindingFlags.Instance | BindingFlags.NonPublic); - setOptionsMethod!.Invoke(application.Handler, Array.Empty()); - wf.Application.Run(appContext); - } - - private static void UnhandledTaskException(object? sender, UnobservedTaskExceptionEventArgs e) - { - Log.FatalException("An error occurred that caused the task to terminate.", e.Exception); - e.SetObserved(); - } - - private static void UnhandledException(object? sender, ThreadExceptionEventArgs e) - { - Log.FatalException("An error occurred that caused the application to close.", e.Exception); + EtoPlatform.Current = new WinFormsEtoPlatform(); + + var subArgs = args.Skip(1).ToArray(); + return args switch + { + ["worker", ..] => WindowsNativeWorkerEntryPoint.Run(subArgs), + ["server", ..] => ServerEntryPoint.Run(subArgs, new GdiModule(), new WinFormsModule()), + _ => GuiEntryPoint.Run(args, new GdiModule(), new WinFormsModule()) + }; } } \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/EntryPoints/WindowsConsoleEntryPoint.cs b/NAPS2.Lib.WinForms/EntryPoints/WindowsConsoleEntryPoint.cs index 36ca3de289..cebbbbe650 100644 --- a/NAPS2.Lib.WinForms/EntryPoints/WindowsConsoleEntryPoint.cs +++ b/NAPS2.Lib.WinForms/EntryPoints/WindowsConsoleEntryPoint.cs @@ -1,8 +1,6 @@ -using Autofac; -using CommandLine; -using NAPS2.Automation; +using NAPS2.EtoForms; +using NAPS2.EtoForms.WinForms; using NAPS2.Modules; -using NAPS2.Remoting.Worker; namespace NAPS2.EntryPoints; @@ -13,6 +11,7 @@ public static class WindowsConsoleEntryPoint { public static int Run(string[] args) { - return ConsoleEntryPoint.Run(args, new GdiModule()); + EtoPlatform.Current = new WinFormsEtoPlatform(); + return ConsoleEntryPoint.Run(args, new GdiModule(), new WinFormsModule()); } } \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/EntryPoints/WindowsNativeWorkerEntryPoint.cs b/NAPS2.Lib.WinForms/EntryPoints/WindowsNativeWorkerEntryPoint.cs new file mode 100644 index 0000000000..b1e85fd252 --- /dev/null +++ b/NAPS2.Lib.WinForms/EntryPoints/WindowsNativeWorkerEntryPoint.cs @@ -0,0 +1,33 @@ +using System.Threading; +using System.Windows.Forms; +using NAPS2.Modules; +using NAPS2.Platform.Windows; +using NAPS2.Scan.Internal.Twain; + +namespace NAPS2.EntryPoints; + +/// +/// The entry point logic for NAPS2.exe when running in worker mode. +/// +public static class WindowsNativeWorkerEntryPoint +{ + public static int Run(string[] args) + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.ThreadException += UnhandledException; + + // TODO: We don't currently do TWAIN scanning in the native worker, so maybe this can be cleaned up + var messagePump = Win32MessagePump.Create(); + // TODO: Set a logger on the message pump? + Invoker.Current = messagePump; + TwainHandleManager.Factory = () => new Win32TwainHandleManager(messagePump); + + return WorkerEntryPoint.Run(args, new GdiModule(), messagePump.RunMessageLoop, messagePump.Dispose); + } + + private static void UnhandledException(object? sender, ThreadExceptionEventArgs e) + { + Log.FatalException("An error occurred that caused the worker to close.", e.Exception); + } +} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/EntryPoints/WindowsWorkerEntryPoint.cs b/NAPS2.Lib.WinForms/EntryPoints/WindowsWorkerEntryPoint.cs deleted file mode 100644 index 86d4ba330e..0000000000 --- a/NAPS2.Lib.WinForms/EntryPoints/WindowsWorkerEntryPoint.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Threading; -using System.Windows.Forms; -using NAPS2.EtoForms.WinForms; -using NAPS2.Modules; -using NAPS2.Scan.Internal.Twain; -using NAPS2.WinForms; - -namespace NAPS2.EntryPoints; - -/// -/// The entry point for NAPS2.Worker.exe, an off-process worker. -/// -/// NAPS2.Worker.exe runs in 32-bit mode for compatibility with 32-bit TWAIN drivers. -/// -public static class WindowsWorkerEntryPoint -{ - public static int Run(string[] args) - { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.ThreadException += UnhandledException; - - // Set up a form for the worker process - // A parent form is needed for some operations, namely 64-bit TWAIN scanning - var form = new BackgroundForm(); - Invoker.Current = new WinFormsInvoker(() => form); - TwainHandleManager.Factory = () => new WinFormsTwainHandleManager(form); - - return WorkerEntryPoint.Run(args, new GdiModule(), () => Application.Run(form), () => form.Close()); - } - - private static void UnhandledException(object? sender, ThreadExceptionEventArgs e) - { - Log.FatalException("An error occurred that caused the worker to close.", e.Exception); - } -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/EtoForms/Ui/WinFormsDesktopForm.cs b/NAPS2.Lib.WinForms/EtoForms/Ui/WinFormsDesktopForm.cs index 1e54c509c5..22f77545ae 100644 --- a/NAPS2.Lib.WinForms/EtoForms/Ui/WinFormsDesktopForm.cs +++ b/NAPS2.Lib.WinForms/EtoForms/Ui/WinFormsDesktopForm.cs @@ -1,123 +1,278 @@ +using System.ComponentModel; using System.Drawing; using Eto.Forms; using Eto.WinForms; using Eto.WinForms.Forms.ToolBar; using NAPS2.EtoForms.Desktop; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Notifications; +using NAPS2.EtoForms.Widgets; using NAPS2.EtoForms.WinForms; -using NAPS2.ImportExport.Images; +using NAPS2.Scan; using NAPS2.WinForms; -using wf = System.Windows.Forms; +using WF = System.Windows.Forms; namespace NAPS2.EtoForms.Ui; public class WinFormsDesktopForm : DesktopForm { - public static wf.ApplicationContext? ApplicationContext { get; set; } - + private readonly Dictionary _menuButtons = new(); private readonly ToolbarFormatter _toolbarFormatter = new(new StringWrapper()); - private readonly wf.Form _form; - private wf.ToolStrip _toolStrip = null!; - private wf.ToolStripContainer _container = null!; + private readonly WF.Form _form; + private WF.ToolStrip _mainToolStrip = null!; + private WF.ToolStrip _profilesToolStrip = null!; + private WF.ToolStripContainer _container = null!; public WinFormsDesktopForm( Naps2Config config, DesktopKeyboardShortcuts keyboardShortcuts, - INotificationManager notify, + NotificationManager notificationManager, CultureHelper cultureHelper, + ColorScheme colorScheme, IProfileManager profileManager, UiImageList imageList, - ImageTransfer imageTransfer, ThumbnailController thumbnailController, UiThumbnailProvider thumbnailProvider, DesktopController desktopController, IDesktopScanController desktopScanController, ImageListActions imageListActions, + ImageListViewBehavior imageListViewBehavior, DesktopFormProvider desktopFormProvider, IDesktopSubFormController desktopSubFormController, - DesktopCommands commands) - : base(config, keyboardShortcuts, notify, cultureHelper, profileManager, - imageList, imageTransfer, thumbnailController, thumbnailProvider, desktopController, desktopScanController, - imageListActions, desktopFormProvider, desktopSubFormController, commands) + Lazy commands, + Sidebar sidebar, + IIconProvider iconProvider) + : base(config, keyboardShortcuts, notificationManager, cultureHelper, colorScheme, profileManager, imageList, + thumbnailController, thumbnailProvider, desktopController, desktopScanController, imageListActions, + imageListViewBehavior, desktopFormProvider, desktopSubFormController, commands, sidebar, iconProvider) { _form = this.ToNative(); + _form.FormClosing += OnFormClosing; + + // TODO: Remove this if https://github.com/picoe/Eto/issues/2601 is fixed + NativeListView.KeyDown += (_, e) => OnKeyDown(new KeyEventArgs(e.KeyData.ToEto(), KeyEventType.KeyDown)); + + Load += (_, _) => colorScheme.ColorSchemeChanged += ColorSchemeChanged; + UnLoad += (_, _) => colorScheme.ColorSchemeChanged -= ColorSchemeChanged; + } + + private void ColorSchemeChanged(object? sender, EventArgs e) + { + // WinForms dark mode is experimental +#pragma warning disable WFO5001 + WF.Application.SetColorMode(_colorScheme.DarkMode ? WF.SystemColorMode.Dark : WF.SystemColorMode.Classic); +#pragma warning restore WFO5001 + Invoker.Current.Invoke(WinFormsHacks.ClearCachedBrushesAndPens); + Invoker.Current.InvokeDispatch(() => + { + if (WF.Application.OpenForms.Count == 1) + { + // Reload the form as WinForms dark mode doesn't dynamically switch everything + SetCulture(Config.Get(c => c.Culture) ?? "en"); + } + }); + } + + protected override void OnClosing(CancelEventArgs e) + { + // Don't do anything here as we have a separate FormClosing event handler + // That allows us to check the close reason (which Eto doesn't provide) + } + + private void OnFormClosing(object? sender, WF.FormClosingEventArgs e) + { + if (!_desktopController.PrepareForClosing(e.CloseReason == WF.CloseReason.UserClosing)) + { + e.Cancel = true; + } + } + + protected override void OnClosed(EventArgs e) + { + SaveToolStripLocation(); + base.OnClosed(e); } protected override void OnLoad(EventArgs e) { base.OnLoad(e); + LoadToolStripLocation(); + NativeListView.TabIndex = 7; - NativeListView.Dock = wf.DockStyle.Fill; - // NativeListView.ContextMenuStrip = contextMenuStrip; - // NativeListView.KeyDown += ListViewKeyDown; - // NativeListView.MouseWheel += ListViewMouseWheel; + NativeListView.BorderStyle = WF.BorderStyle.None; NativeListView.Focus(); } protected override void OnShown(EventArgs e) { - _toolbarFormatter.RelayoutToolbar(_toolStrip); + EtoPlatform.Current.AttachDpiDependency(this, + scale => _toolbarFormatter.RelayoutToolbar(_mainToolStrip, scale)); base.OnShown(e); } - protected override LayoutElement GetZoomButtons() + protected override LayoutElement GetControlButtons() { // Disabled buttons don't prevent click events from being sent to the listview below the button, so without this // "mouse catcher" control you could e.g. spam click zoom out until it's maxed and then accidentally keep // clicking and change the listview selection. - var mouseCatcher = new wf.Button + var mouseCatcher = new WF.Button { BackColor = Color.White, Size = new Size(45, 23), - FlatStyle = wf.FlatStyle.Flat + FlatStyle = WF.FlatStyle.Flat }; - return L.Overlay( - mouseCatcher.ToEto(), - base.GetZoomButtons() + return L.Row( + GetSidebarButton(), + L.Overlay( + L.Row(mouseCatcher.ToEto().AlignTrailing()), + GetZoomButtons() + ) ); } - private wf.ListView NativeListView => ((WinFormsListView) _listView).NativeControl; + private WF.ListView NativeListView => ((WinFormsListView) _listView).NativeControl; + + protected override void SetCulture(string cultureId) + { + SaveToolStripLocation(); + base.SetCulture(cultureId); + } + + protected override void RecreateToolbarsAndMenus() + { + base.RecreateToolbarsAndMenus(); + _toolbarFormatter.RelayoutToolbar(_mainToolStrip, EtoPlatform.Current.GetScaleFactor(this)); + } - protected override void SetMainForm(Form newMainForm) + protected override void ConfigureToolbars() { - base.SetMainForm(newMainForm); - if (ApplicationContext == null) + _container = new WF.ToolStripContainer(); + _mainToolStrip = ((ToolBarHandler) ToolBar.Handler).Control; + _profilesToolStrip = new WF.ToolStrip(); + + _mainToolStrip.ShowItemToolTips = false; + _mainToolStrip.TabStop = true; + EtoPlatform.Current.AttachDpiDependency(this, + scale => _mainToolStrip.ImageScalingSize = new Size((int) (32 * scale), (int) (32 * scale))); + + _profilesToolStrip.ShowItemToolTips = false; + _profilesToolStrip.TabStop = true; + _profilesToolStrip.Location = new Point(0, 1000); + EtoPlatform.Current.AttachDpiDependency(this, + scale => _profilesToolStrip.ImageScalingSize = new Size((int) (16 * scale), (int) (16 * scale))); + + foreach (var panel in _container.Controls.OfType()) { - Log.Error("ApplicationContext should not be null"); - return; + // Allow tabbing through the toolbar for accessibility + WinFormsHacks.SetControlStyle(panel, WF.ControlStyles.Selectable, true); } - ApplicationContext.MainForm = newMainForm.ToSWF(); + + // We defer this to the Load event because otherwise with RTL languages we get a very weird crash (.NET bug not + // preset with .NET framework). Ideally I could file an issue but it's very hard to get a minimum reproducible + // test case. + // See https://github.com/cyanfish/naps2/issues/586 + Load += (_, _) => + { + _container.TopToolStripPanel.Controls.Add(_mainToolStrip); + _mainToolStrip.ParentChanged += (_, _) => + { + _toolbarFormatter.RelayoutToolbar(_mainToolStrip, EtoPlatform.Current.GetScaleFactor(this)); + LayoutController.Invalidate(); + }; + PlaceProfilesToolbar(); + _profilesToolStrip.ParentChanged += (_, _) => LayoutController.Invalidate(); + }; } - protected override void ConfigureToolbar() + public override void PlaceProfilesToolbar() { - _toolStrip = ((ToolBarHandler) ToolBar.Handler).Control; - _toolStrip.ShowItemToolTips = false; - _toolStrip.TabStop = true; - _toolStrip.ImageScalingSize = new Size(32, 32); - _toolStrip.ParentChanged += (_, _) => _toolbarFormatter.RelayoutToolbar(_toolStrip); + if (Config.Get(c => c.ShowProfilesToolbar) && _profilesToolStrip.Parent == null) + { + _container.TopToolStripPanel.Controls.Add(_profilesToolStrip); + } + if (!Config.Get(c => c.ShowProfilesToolbar) && _profilesToolStrip.Parent != null) + { + _profilesToolStrip.Parent.Controls.Remove(_profilesToolStrip); + } } - protected override LayoutElement GetMainContent() + protected override void UpdateProfilesToolbar() { - _container = new wf.ToolStripContainer(); - _container.TopToolStripPanel.Controls.Add(_toolStrip); - foreach (var panel in _container.Controls.OfType()) + var toolbarItems = _profilesToolStrip.Items; + var profiles = _profileManager.Profiles; + var extra = toolbarItems.Count - profiles.Count; + var missing = profiles.Count - toolbarItems.Count; + for (int i = 0; i < extra; i++) { - // Allow tabbing through the toolbar for accessibility - WinFormsHacks.SetControlStyle(panel, wf.ControlStyles.Selectable, true); + toolbarItems.RemoveAt(toolbarItems.Count - 1); + } + for (int i = 0; i < missing; i++) + { + var item = new WF.ToolStripButton + { + TextImageRelation = WF.TextImageRelation.ImageBeforeText, + ImageAlign = ContentAlignment.MiddleLeft, + TextAlign = ContentAlignment.MiddleLeft + }; + EtoPlatform.Current.AttachDpiDependency(this, + scale => item.Image = _iconProvider.GetIcon("control_play_blue_small", scale).ToSD()); + item.Click += (_, _) => _desktopScanController.ScanWithProfile((ScanProfile) item.Tag!); + toolbarItems.Add(item); } + for (int i = 0; i < profiles.Count; i++) + { + var profile = profiles[i]; + var item = toolbarItems[i]; + item.Tag = profile; + var text = profile.DisplayName.Replace("&", "&&"); + if (item.Text != text) + { + item.Text = text; + } + } + } - var wfContent = _listView.Control.ToNative(); - wfContent.Dock = wf.DockStyle.Fill; + protected override void BuildLayout() + { + base.BuildLayout(); + + var wfContent = LayoutController.Container.ToNative(); + wfContent.Dock = WF.DockStyle.Fill; + var etoContainer = _container.ToEto(); + Content = etoContainer; _container.ContentPanel.Controls.Add(wfContent); - return _container.ToEto(); + DrawContentBorders(); } - protected override void SetThumbnailSpacing(int thumbnailSize) + private void DrawContentBorders() + { + var pen = new Pen(_colorScheme.SeparatorColor.ToSD()); + + var splitter = ((LayoutLeftPanel) LayoutController.Content!).Splitter; + var panel1 = (WF.Panel) splitter.Panel1.ToNative(); + var panel2 = (WF.Panel) splitter.Panel2.ToNative(); + var split = (WF.SplitContainer) splitter.ToNative(); + // Draw horizontal lines at the top of the content (below the toolbar) and a vertical line at the sidebar split point + // TODO: Improve this for when the toolbars are in non-standard positions (i.e. not the top) + // TODO: Consider if it's worth widening the border for high-dpi + panel1.Paint += (_, args) => + { + args.Graphics.DrawLine(pen, panel1.Left, panel1.Top, panel1.Right, panel1.Top); + }; + split.Paint += (_, args) => + { + args.Graphics.DrawLine(pen, split.Left, split.Top, split.Right, split.Top); + args.Graphics.DrawLine(pen, splitter.Position + 2, split.Top, splitter.Position + 2, split.Bottom); + }; + panel2.Paint += (_, args) => + { + args.Graphics.DrawLine(pen, panel2.Left, panel2.Top, panel2.Right, panel2.Top); + }; + } + + protected override void SetThumbnailSpacing(int thumbnailSize, float scale) { const int MIN_PADDING = 6; const int MAX_PADDING = 66; @@ -126,31 +281,36 @@ protected override void SetThumbnailSpacing(int thumbnailSize) (ThumbnailSizes.MAX_SIZE - ThumbnailSizes.MIN_SIZE); int hSpacing = thumbnailSize + padding; int vSpacing = thumbnailSize + padding * 2; - WinFormsHacks.SetListSpacing(NativeListView, hSpacing, vSpacing); + WinFormsHacks.SetListSpacing(NativeListView, + (int) Math.Round(hSpacing * scale), + (int) Math.Round(vSpacing * scale)); } protected override void CreateToolbarButton(Command command) { - var item = new wf.ToolStripButton + var item = new WF.ToolStripButton { - TextImageRelation = wf.TextImageRelation.ImageAboveText + TextImageRelation = WF.TextImageRelation.ImageAboveText }; ApplyCommand(item, command); - _toolStrip.Items.Add(item); + _mainToolStrip.Items.Add(item); } - protected override void CreateToolbarButtonWithMenu(Command command, MenuProvider menu) + protected override void CreateToolbarButtonWithMenu(Command command, DesktopToolbarMenuType menuType, + MenuProvider menu) { - var item = new wf.ToolStripSplitButton + var item = new WF.ToolStripSplitButton { - TextImageRelation = wf.TextImageRelation.ImageAboveText + TextImageRelation = WF.TextImageRelation.ImageAboveText }; + EtoPlatform.Current.AttachDpiDependency(this, scale => item.DropDownButtonWidth = (int) (scale * 15)); ApplyCommand(item, command); - _toolStrip.Items.Add(item); + _mainToolStrip.Items.Add(item); menu.Handle(subItems => SetUpMenu(item, subItems)); + _menuButtons[menuType] = item; } - private void SetUpMenu(wf.ToolStripDropDownItem item, List subItems) + private void SetUpMenu(WF.ToolStripDropDownItem item, List subItems) { item.DropDownItems.Clear(); foreach (var subItem in subItems) @@ -158,16 +318,16 @@ private void SetUpMenu(wf.ToolStripDropDownItem item, List su switch (subItem) { case MenuProvider.SeparatorItem: - item.DropDownItems.Add(new wf.ToolStripSeparator()); + item.DropDownItems.Add(new WF.ToolStripSeparator()); break; case MenuProvider.CommandItem commandSubItem: - item.DropDownItems.Add(ApplyCommand(new wf.ToolStripMenuItem + item.DropDownItems.Add(ApplyCommand(new WF.ToolStripMenuItem { - ImageScaling = wf.ToolStripItemImageScaling.None + ImageScaling = WF.ToolStripItemImageScaling.None }, commandSubItem.Command)); break; case MenuProvider.SubMenuItem subMenuSubItem: - var subMenu = new wf.ToolStripMenuItem(); + var subMenu = new WF.ToolStripMenuItem(); ApplyCommand(subMenu, subMenuSubItem.Command); // TODO: If submenus are dynamic this will memory leak or something subMenuSubItem.MenuProvider.Handle(subSubItems => SetUpMenu(subMenu, subSubItems)); @@ -179,13 +339,13 @@ private void SetUpMenu(wf.ToolStripDropDownItem item, List su protected override void CreateToolbarMenu(Command command, MenuProvider menu) { - var item = new wf.ToolStripDropDownButton + var item = new WF.ToolStripDropDownButton { - TextImageRelation = wf.TextImageRelation.ImageAboveText, + TextImageRelation = WF.TextImageRelation.ImageAboveText, ShowDropDownArrow = false }; ApplyCommand(item, command); - _toolStrip.Items.Add(item); + _mainToolStrip.Items.Add(item); menu.Handle(subItems => SetUpMenu(item, subItems)); } @@ -193,27 +353,43 @@ protected override void CreateToolbarStackedButtons(Command command1, Command co { var item = new ToolStripDoubleButton { - FirstImage = command1.Image.ToSD(), FirstText = command1.ToolBarText, - SecondImage = command2.Image.ToSD(), SecondText = command2.ToolBarText }; + item.AccessibleName = string.Join(" ", command1.ToolBarText, command2.ToolBarText); + EtoPlatform.Current.AttachDpiDependency(this, scale => + { + item.FirstImage = ((ActionCommand) command1).GetIconImage(scale).ToSD(); + item.SecondImage = ((ActionCommand) command2).GetIconImage(scale).ToSD(); + }); command1.EnabledChanged += (_, _) => item.Enabled = command1.Enabled; item.FirstClick += (_, _) => command1.Execute(); item.SecondClick += (_, _) => command2.Execute(); - _toolStrip.Items.Add(item); + _mainToolStrip.Items.Add(item); } - private wf.ToolStripItem ApplyCommand(wf.ToolStripItem item, Command command) + private WF.ToolStripItem ApplyCommand(WF.ToolStripItem item, Command command) { - item.Image = command.Image.ToSD(); - item.Text = item is wf.ToolStripMenuItem ? command.MenuText : command.ToolBarText; - if (item is wf.ToolStripMenuItem menuItem) + void SetItemText() => item.Text = item is WF.ToolStripMenuItem ? command.MenuText : command.ToolBarText; + EtoPlatform.Current.AttachDpiDependency(this, + scale => item.Image = ((ActionCommand) command).GetIconImage(scale).ToSD()); + SetItemText(); + if (command is ActionCommand actionCommand) { - menuItem.ShortcutKeys = command.Shortcut.ToSWF(); + actionCommand.TextChanged += (_, _) => SetItemText(); + } + // TODO: We want a better way of determining which keyboard shortcuts are worth showing + // Ideally we could show them all, but it can be really distracting. So only showing F2/F3 etc. right now. + if (item is WF.ToolStripMenuItem menuItem && !command.Shortcut.ToString().Contains(",")) + { + var swfKeys = command.Shortcut.ToSWF(); + if (WF.ToolStripManager.IsValidShortcut(swfKeys)) + { + menuItem.ShortcutKeys = swfKeys; + } } command.EnabledChanged += (_, _) => item.Enabled = command.Enabled; - if (item is wf.ToolStripSplitButton button) + if (item is WF.ToolStripSplitButton button) { button.ButtonClick += (_, _) => command.Execute(); } @@ -226,28 +402,37 @@ private wf.ToolStripItem ApplyCommand(wf.ToolStripItem item, Command command) protected override void CreateToolbarSeparator() { - _toolStrip.Items.Add(new wf.ToolStripSeparator()); + _mainToolStrip.Items.Add(new WF.ToolStripSeparator()); } - - // TODO: Call these + public override void ShowToolbarMenu(DesktopToolbarMenuType menuType) + { + _menuButtons.Get(menuType)?.ShowDropDown(); + } private void SaveToolStripLocation() { - Config.User.Set(c => c.DesktopToolStripDock, _toolStrip.Parent.Dock.ToConfig()); + Config.User.Set(c => c.DesktopToolStripDock, _mainToolStrip.Parent!.Dock.ToConfig()); + Config.User.Set(c => c.ProfilesToolStripDock, _profilesToolStrip.Parent?.Dock.ToConfig() ?? DockStyle.Top); } private void LoadToolStripLocation() { - var dock = Config.Get(c => c.DesktopToolStripDock).ToWinForms(); - if (dock != wf.DockStyle.None) + SetDock(_mainToolStrip, Config.Get(c => c.DesktopToolStripDock)); + if (_profilesToolStrip.Parent != null) { - var panel = _container.Controls.OfType().FirstOrDefault(x => x.Dock == dock); - if (panel != null) - { - _toolStrip.Parent = panel; - } + SetDock(_profilesToolStrip, Config.Get(c => c.ProfilesToolStripDock)); + } + } + + private void SetDock(WF.ToolStrip toolStrip, DockStyle dock) + { + var wfDock = dock.ToWinForms(); + var panel = _container.Controls.OfType().FirstOrDefault(x => x.Dock == wfDock); + if (panel != null) + { + toolStrip.Parent = panel; } - _toolStrip.Parent.TabStop = true; + toolStrip.Parent!.TabStop = true; } } \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/EtoForms/Ui/WinFormsPreviewForm.cs b/NAPS2.Lib.WinForms/EtoForms/Ui/WinFormsPreviewForm.cs new file mode 100644 index 0000000000..16194d0134 --- /dev/null +++ b/NAPS2.Lib.WinForms/EtoForms/Ui/WinFormsPreviewForm.cs @@ -0,0 +1,21 @@ +using System.Drawing; +using System.Windows.Forms; + +namespace NAPS2.EtoForms.Ui; + +public class WinFormsPreviewForm : PreviewForm +{ + public WinFormsPreviewForm(Naps2Config config, DesktopCommands desktopCommands, UiImageList imageList, + IIconProvider iconProvider, ColorScheme colorScheme) : base(config, + desktopCommands, imageList, iconProvider, colorScheme) + { + } + + protected override void OnLoad(EventArgs eventArgs) + { + base.OnLoad(eventArgs); + var toolStrip = (ToolStrip) ToolBar.ControlObject; + EtoPlatform.Current.AttachDpiDependency(this, + scale => toolStrip.ImageScalingSize = new Size((int) (16 * scale), (int) (16 * scale))); + } +} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsDarkModeProvider.cs b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsDarkModeProvider.cs new file mode 100644 index 0000000000..e9ae0e9c1e --- /dev/null +++ b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsDarkModeProvider.cs @@ -0,0 +1,44 @@ +using System.Drawing; +using System.Reflection; +using System.Windows.Forms; +using Microsoft.Win32; + +namespace NAPS2.EtoForms.WinForms; + +public class WinFormsDarkModeProvider : IDarkModeProvider +{ + private bool? _value; + + public WinFormsDarkModeProvider() + { + SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged; + } + + public bool IsDarkModeEnabled => _value ??= ReadDarkMode(); + + private bool ReadDarkMode() + { + try + { + using var key = + Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize"); + return Equals(key?.GetValue("AppsUseLightTheme"), 0); + } + catch (Exception) + { + return false; + } + } + + public event EventHandler? DarkModeChanged; + + private void OnUserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e) + { + var newValue = ReadDarkMode(); + if (newValue != _value) + { + _value = newValue; + DarkModeChanged?.Invoke(this, EventArgs.Empty); + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsEtoPlatform.cs b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsEtoPlatform.cs index 10b9a9e4b9..2968033dc4 100644 --- a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsEtoPlatform.cs +++ b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsEtoPlatform.cs @@ -3,11 +3,15 @@ using Eto.Drawing; using Eto.Forms; using Eto.WinForms; +using Eto.WinForms.Forms; using Eto.WinForms.Forms.Controls; +using Eto.WinForms.Forms.Menu; +using Eto.WinForms.Forms.ToolBar; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; using NAPS2.Images.Gdi; -using sd = System.Drawing; -using wf = System.Windows.Forms; +using SD = System.Drawing; +using WF = System.Windows.Forms; namespace NAPS2.EtoForms.WinForms; @@ -19,90 +23,98 @@ public class WinFormsEtoPlatform : EtoPlatform public override bool IsWinForms => true; - public override Application CreateApplication() + public override IIconProvider IconProvider { get; } = new DefaultIconProvider(); + public override IDarkModeProvider DarkModeProvider { get; } = new WinFormsDarkModeProvider(); + + public override Application CreateApplicationCore() { - wf.Application.EnableVisualStyles(); - wf.Application.SetCompatibleTextRenderingDefault(false); + WF.Application.EnableVisualStyles(); + WF.Application.SetCompatibleTextRenderingDefault(false); + WF.Application.SetHighDpiMode(WF.HighDpiMode.PerMonitorV2); + // WinForms dark mode is experimental +#pragma warning disable WFO5001 + WF.Application.SetColorMode(ColorScheme.DarkMode ? WF.SystemColorMode.Dark : WF.SystemColorMode.Classic); +#pragma warning restore WFO5001 return new Application(Eto.Platforms.WinForms); } public override IListView CreateListView(ListViewBehavior behavior) => new WinFormsListView(behavior); - public override void ConfigureImageButton(Eto.Forms.Button button, bool big) + public override void ConfigureImageButton(Button button, ButtonFlags flags) { if (string.IsNullOrEmpty(button.Text)) { - button.MinimumSize = MinImageOnlyButtonSize; - var native = (wf.Button) button.ToNative(); - native.TextImageRelation = wf.TextImageRelation.Overlay; - native.ImageAlign = sd.ContentAlignment.MiddleCenter; + AttachDpiDependency(button, scale => button.MinimumSize = Size.Round(MinImageOnlyButtonSize * scale)); + var native = (WF.Button) button.ToNative(); + native.TextImageRelation = WF.TextImageRelation.Overlay; + native.ImageAlign = SD.ContentAlignment.MiddleCenter; return; } - button.MinimumSize = MinImageButtonSize; + bool largeText = flags.HasFlag(ButtonFlags.LargeText); + bool largeIcon = flags.HasFlag(ButtonFlags.LargeIcon); + AttachDpiDependency(button, scale => button.MinimumSize = Size.Round(MinImageButtonSize * scale)); if (button.ImagePosition == ButtonImagePosition.Left) { - var native = (wf.Button) button.ToNative(); - native.TextImageRelation = big ? wf.TextImageRelation.ImageBeforeText : wf.TextImageRelation.Overlay; - native.ImageAlign = sd.ContentAlignment.MiddleLeft; - native.TextAlign = big ? sd.ContentAlignment.MiddleLeft : sd.ContentAlignment.MiddleRight; + var native = (WF.Button) button.ToNative()!; + native.TextImageRelation = largeText ? WF.TextImageRelation.ImageBeforeText : WF.TextImageRelation.Overlay; + native.ImageAlign = SD.ContentAlignment.MiddleLeft; + native.TextAlign = largeText ? SD.ContentAlignment.MiddleLeft : SD.ContentAlignment.MiddleRight; - if (big) + if (largeText) { native.Text = @" " + native.Text; } - var imageWidth = native.Image.Width; - using var g = native.CreateGraphics(); - var textWidth = (int) g.MeasureString(native.Text, native.Font).Width; + var imageWidth = largeIcon ? 32 : 16; native.AutoSize = false; - if (big) - { - native.Padding = native.Padding with { Left = IMAGE_PADDING, Right = IMAGE_PADDING }; - } - else + AttachDpiDependency(button, scale => { - var widthWithoutRightPadding = imageWidth + textWidth + IMAGE_PADDING + 15; - native.Width = Math.Max(widthWithoutRightPadding + IMAGE_PADDING, - ButtonHandler.DefaultMinimumSize.Width); - var rightPadding = IMAGE_PADDING + (native.Width - widthWithoutRightPadding - IMAGE_PADDING) / 2; - native.Padding = native.Padding with { Left = IMAGE_PADDING, Right = rightPadding }; - } + var textWidth = WF.TextRenderer.MeasureText(native.Text, native.Font).Width; + int p = (int) Math.Round(IMAGE_PADDING * scale); + if (largeText) + { + native.Padding = native.Padding with { Left = p, Right = p }; + } + else + { + var widthWithoutRightPadding = p + textWidth + (int) Math.Round((imageWidth + 15) * scale); + var width = Math.Max(widthWithoutRightPadding + p, + (int) Math.Round(ButtonHandler.DefaultMinimumSize.Width * scale)); + button.Width = width; + var rightPadding = p + (width - widthWithoutRightPadding - p) / 2; + native.Padding = native.Padding with { Left = p, Right = rightPadding }; + } + }); } } - public override Control AccessibleImageButton(Image image, string text, Action onClick, - int xOffset = 0, int yOffset = 0) + public override void ConfigureDonateButton(Button button) { - // This works by overlaying an image on top a button. - // If the image has transparency an offset may need to be specified to keep the button hidden. - // If the text is too large relative to the button it will be impossible to hide fully. - var imageView = new ImageView { Image = image, Cursor = Eto.Forms.Cursors.Pointer }; - imageView.MouseDown += (_, _) => onClick(); - var button = new Button - { - Text = text, - Width = 0, - Height = 0, - Command = new ActionCommand(onClick) - }; - var pix = new PixelLayout(); - pix.Add(button, xOffset, yOffset); - pix.Add(imageView, 0, 0); - return pix; + var native = (WF.Button) button.ToNative(); + native.FlatStyle = WF.FlatStyle.Flat; + native.FlatAppearance.BorderColor = Color.FromRgb(0xfbad5f).ToSD(); + native.FlatAppearance.BorderSize = 2; } public override Bitmap ToBitmap(IMemoryImage image) { - return ((GdiImage) image).Bitmap.ToEto(); + var gdiImage = (GdiImage) image; + var bitmap = (SD.Bitmap) gdiImage.Bitmap.Clone(); + return bitmap.ToEto(); } - public override IMemoryImage DrawHourglass(ImageContext imageContext, IMemoryImage image) + public override IMemoryImage FromBitmap(Bitmap bitmap) + { + return new GdiImage((SD.Bitmap) bitmap.ToSD()); + } + + public override IMemoryImage DrawHourglass(IMemoryImage image) { var bitmap = new System.Drawing.Bitmap(image.Width, image.Height); - using (var g = sd.Graphics.FromImage(bitmap)) + using (var g = SD.Graphics.FromImage(bitmap)) { var attrs = new ImageAttributes(); attrs.SetColorMatrix(new ColorMatrix @@ -110,18 +122,18 @@ public override IMemoryImage DrawHourglass(ImageContext imageContext, IMemoryIma Matrix33 = 0.3f }); g.DrawImage(image.AsBitmap(), - new sd.Rectangle(0, 0, bitmap.Width, bitmap.Height), + new SD.Rectangle(0, 0, bitmap.Width, bitmap.Height), 0, 0, image.Width, image.Height, - sd.GraphicsUnit.Pixel, + SD.GraphicsUnit.Pixel, attrs); - using var hourglass = new sd.Bitmap(new MemoryStream(Icons.hourglass_grey)); - g.DrawImage(hourglass, new sd.Rectangle((bitmap.Width - 32) / 2, (bitmap.Height - 32) / 2, 32, 32)); + using var hourglass = new SD.Bitmap(new MemoryStream(Icons.hourglass_grey)); + g.DrawImage(hourglass, new SD.Rectangle((bitmap.Width - 32) / 2, (bitmap.Height - 32) / 2, 32, 32)); } image.Dispose(); - return new GdiImage(imageContext, bitmap); + return new GdiImage(bitmap); } public override void SetFrame(Control container, Control control, Point location, Size size, bool inOverlay) @@ -131,11 +143,11 @@ public override void SetFrame(Control container, Control control, Point location var y = location.Y; if (CultureInfo.CurrentCulture.TextInfo.IsRightToLeft) { - x = container.Width - x - size.Width; + x = container.ToNative().Width - x - size.Width; } - native.Location = new sd.Point(x, y); + native.Location = new SD.Point(x, y); native.AutoSize = false; - native.Size = new sd.Size(size.Width, size.Height); + native.Size = new SD.Size(size.Width, size.Height); if (inOverlay) { native.BringToFront(); @@ -146,70 +158,231 @@ public override SizeF GetPreferredSize(Control control, SizeF availableSpace) { if (control.GetType() == typeof(Panel)) { - return GetPreferredSize(((Panel) control).Content, availableSpace); + var content = ((Panel) control).Content; + if (content != null) + { + return GetPreferredSize(content, availableSpace); + } + } + var native = control.ToNative(); + if (control.GetType() == typeof(Slider)) + { + var size = (SizeF) native.PreferredSize.ToEto(); + // Work around a WinForms bug where the preferred size of a Slider/WF.TrackBar is based on the primary + // screen DPI, not the DPI of the screen it's actually on + var scaleDelta = (native.FindForm()!.DeviceDpi / 96f) / (Screen.PrimaryScreen.RealDPI / 72f); + size *= scaleDelta; + // We also want to correct for the idea that sliders should be fully scalable in orthogonal directions + // depending on the orientation + return ((Slider) control).Orientation == Orientation.Horizontal + ? new SizeF(size.Height * scaleDelta, size.Height * scaleDelta) + : new SizeF(size.Width * scaleDelta, size.Width * scaleDelta); } - return SizeF.Max( + var preferredSize = SizeF.Max( base.GetPreferredSize(control, availableSpace), - control.ToNative().PreferredSize.ToEto()); + native.PreferredSize.ToEto()); + if (control.GetType() == typeof(DropDown) && control.Height > 0) + { + // Work around a WinForms bug where the preferred height of a DropDown is incorrect + preferredSize.Height = control.Height; + } + return preferredSize; } public override SizeF GetWrappedSize(Control control, int defaultWidth) { - if (control.ControlObject is wf.Label label) + if (control.ControlObject is WF.Label label) { - using var g = label.CreateGraphics(); - return g.MeasureString(label.Text, label.Font, defaultWidth).ToEto(); + return WF.TextRenderer.MeasureText(label.Text, label.Font, new SD.Size(defaultWidth, int.MaxValue), + WF.TextFormatFlags.WordBreak).ToEto(); } return base.GetWrappedSize(control, defaultWidth); } + public override Size GetClientSize(Window window, bool excludeToolbars = false) + { + var size = window.ToNative().ClientSize.ToEto(); + if (excludeToolbars && window.Content is { ControlObject: WF.ToolStripContainer container }) + { + var top = container.TopToolStripPanel.Controls.Cast(); + var bottom = container.BottomToolStripPanel.Controls.Cast(); + var left = container.LeftToolStripPanel.Controls.Cast(); + var right = container.RightToolStripPanel.Controls.Cast(); + size -= new Size( + left.Concat(right).Sum(x => x.Width), + top.Concat(bottom).Sum(x => x.Height)); + } + else if (excludeToolbars && window.ToolBar is { ControlObject: ToolStripEx toolStripEx }) + { + size.Height -= toolStripEx.Height; + } + return size; + } + + public override void SetClientSize(Window window, Size clientSize) + { + window.ToNative().ClientSize = clientSize.ToSD(); + if (window is IFormBase { FormStateController.Loaded: false } form) + { + Invoker.Current.InvokeDispatch(() => form.LayoutController.Invalidate()); + } + } + public override Control CreateContainer() { - return new wf.Panel().ToEto(); + return new WF.Panel().ToEto(); } public override void AddToContainer(Control container, Control control, bool inOverlay) { + if (control.ToNative() is WF.TextBox textBox) + { + // WinForms textboxes behave weirdly when resized during load and can push text offscreen. This fixes that. + control.Load += (_, _) => + { + textBox.Select(0, 0); + textBox.ScrollToCaret(); + }; + } container.ToNative().Controls.Add(control.ToNative()); } + public override void RemoveFromContainer(Control container, Control control) + { + container.ToNative().Controls.Remove(control.ToNative()); + } + public override LayoutElement FormatProgressBar(ProgressBar progressBar) { return progressBar.Size(420, 40); } - public override void UpdateRtl(Window window) + public override void InitForm(Window window) { var form = window.ToNative(); + bool isRtl = CultureInfo.CurrentCulture.TextInfo.IsRightToLeft; - form.RightToLeft = isRtl ? wf.RightToLeft.Yes : wf.RightToLeft.No; + form.RightToLeft = isRtl ? WF.RightToLeft.Yes : WF.RightToLeft.No; form.RightToLeftLayout = isRtl; + + form.DpiChanged += (_, _) => (window as IFormBase)?.LayoutController.Invalidate(); } - public override void ConfigureZoomButton(Button button) + public override float GetScaleFactor(Window window) + { + var form = window.ToNative(); + return form.DeviceDpi / 96f; + } + + public override bool ScaleLayout => true; + + public override void SetImageSize(ButtonMenuItem menuItem, int size) { - button.Size = new Size(23, 23); - var wfButton = (wf.Button) button.ToNative(); + var handler = (ButtonMenuItemHandler) menuItem.Handler; + handler.ImageSize = size; + } + + public override void SetImageSize(ToolItem menuItem, int size) + { + if (menuItem.Handler is ButtonToolItemHandler buttonHandler) + { + buttonHandler.ImageSize = size; + } + if (menuItem.Handler is DropDownToolItemHandler dropDownHandler) + { + dropDownHandler.ImageSize = size; + } + } + + public override void ConfigureZoomButton(Button button, string icon) + { + AttachDpiDependency(button, scale => + { + button.Size = new Size((int) (25 * scale), (int) (25 * scale)); + button.Image = IconProvider.GetIcon(icon, scale); + }); + var wfButton = (WF.Button) button.ToNative(); wfButton.AccessibleName = button.Text; wfButton.Text = ""; - wfButton.BackColor = sd.Color.White; - wfButton.FlatStyle = wf.FlatStyle.Flat; + wfButton.BackColor = ColorScheme.BackgroundColor.ToSD(); + wfButton.FlatStyle = WF.FlatStyle.Flat; + } + + private void AttachDpiDependency(WF.Control control, Action callback) + { + void DpiChanged(object? sender, EventArgs eventArgs) + { + callback(GetScaleFactor(control.FindForm().ToEtoWindow())); + } + void Register() + { + if (control is WF.Form form) + { + form.DpiChanged += DpiChanged; + } + else + { + control.DpiChangedAfterParent += DpiChanged; + } + DpiChanged(null, EventArgs.Empty); + } + void Unregister() + { + if (control is WF.Form form) + { + form.DpiChanged -= DpiChanged; + } + else + { + control.DpiChangedAfterParent -= DpiChanged; + } + } + + if (control.IsHandleCreated) + { + Register(); + } + else + { + control.HandleCreated += (_, _) => Register(); + } + control.HandleDestroyed += (_, _) => Unregister(); } - public override void SetClipboardImage(Clipboard clipboard, Bitmap image) + public override void AttachDpiDependency(Control control, Action callback) => + AttachDpiDependency(control.ToNative(), callback); + + public override void SetClipboardImage(Clipboard clipboard, ProcessedImage processedImage, IMemoryImage memoryImage) { - var dataObj = (wf.DataObject) clipboard.ControlObject; - dataObj.SetImage(image.ToSD()); + // We also add the JPEG/PNG format to the clipboard as some applications care about the actual format + // https://github.com/cyanfish/naps2/issues/264 + var jpegOrPng = ImageExportHelper.SaveSmallestFormatToMemoryStream(memoryImage, + processedImage.Metadata.Lossless, -1, out var fileFormat); + var handler = (ClipboardHandler) clipboard.Handler; + // Note this only updates the DataObject, it doesn't set the clipboard, that's done below + handler.Control.SetData(fileFormat == ImageFileFormat.Jpeg ? "JFIF" : "PNG", jpegOrPng); + + if (memoryImage.PixelFormat is ImagePixelFormat.BW1 or ImagePixelFormat.Gray8) + { + // Storing 1bit/8bit images to the clipboard doesn't work, so we copy to 24bit if needed + using var memoryImage2 = memoryImage.CopyWithPixelFormat(ImagePixelFormat.RGB24); + handler.Control.SetImage(memoryImage2.AsBitmap()); + } + else + { + handler.Control.SetImage(memoryImage.AsBitmap()); + } + WF.Clipboard.SetDataObject(handler.Control, true); } - public override void ConfigureDropDown(DropDown dropDown) + public override void ConfigureDropDown(DropDown dropDown, bool scale) { - ((wf.ComboBox) dropDown.ControlObject).DrawMode = wf.DrawMode.Normal; + ((WF.ComboBox) dropDown.ControlObject).DrawMode = WF.DrawMode.Normal; } public override void ShowIcon(Dialog dialog) { - ((wf.Form) dialog.ControlObject).ShowIcon = true; + ((WF.Form) dialog.ControlObject).ShowIcon = true; } public override void ConfigureEllipsis(Label label) @@ -220,6 +393,73 @@ public override void ConfigureEllipsis(Label label) public override Bitmap? ExtractAssociatedIcon(string exePath) { - return sd.Icon.ExtractAssociatedIcon(exePath)?.ToBitmap().ToEto(); + return SD.Icon.ExtractAssociatedIcon(exePath)?.ToBitmap().ToEto(); + } + + public override void HandleKeyDown(Control control, Func handle) + { + control.ToNative().KeyDown += (_, args) => args.Handled = handle(args.KeyData.ToEto()); + } + + public override void AttachMouseWheelEvent(Control control, EventHandler eventHandler) + { + if (control is Scrollable scrollable) + { + var content = scrollable.Content; + var border = scrollable.Border; + var wfControl = new ScrollableWithMouseWheelEvents((ScrollableHandler) scrollable.Handler, eventHandler); + ((ScrollableHandler) control.Handler).Control = wfControl; + scrollable.Content = content; + scrollable.Border = border; + } + else + { + var wfControl = control.ToNative(); + wfControl.MouseWheel += (sender, e) => eventHandler(sender, e.ToEto(wfControl)); + } + } + + public override void AttachMouseMoveEvent(Control control, EventHandler eventHandler) + { + var wfControl = control.ToNative(); + wfControl.MouseMove += (sender, e) => eventHandler(sender, e.ToEto(wfControl)); + } + + private class ScrollableWithMouseWheelEvents : ScrollableHandler.CustomScrollable + { + private readonly EventHandler _mouseWheelHandler; + + public ScrollableWithMouseWheelEvents(ScrollableHandler handler, EventHandler mouseWheelHandler) + { + _mouseWheelHandler = mouseWheelHandler; + + // TODO: Fix this in Eto so we don't need a custom class + Handler = handler; + Size = SD.Size.Empty; + MinimumSize = SD.Size.Empty; + BorderStyle = WF.BorderStyle.Fixed3D; + AutoScroll = true; + AutoSize = true; + AutoSizeMode = WF.AutoSizeMode.GrowAndShrink; + VerticalScroll.SmallChange = 5; + VerticalScroll.LargeChange = 10; + HorizontalScroll.SmallChange = 5; + HorizontalScroll.LargeChange = 10; + Controls.Add(handler.ContainerContentControl); + } + + protected override void OnMouseWheel(WF.MouseEventArgs e) + { + var etoArgs = e.ToEto(this); + _mouseWheelHandler.Invoke(this, etoArgs); + if (e is WF.HandledMouseEventArgs handledArgs) + { + handledArgs.Handled |= etoArgs.Handled; + } + if (!etoArgs.Handled) + { + base.OnMouseWheel(e); + } + } } } \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsHacks.cs b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsHacks.cs index 43a5c7b4dd..f509f1935e 100644 --- a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsHacks.cs +++ b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsHacks.cs @@ -17,7 +17,7 @@ public static class WinFormsHacks static WinFormsHacks() { - ImageSizeField = typeof(ImageList).GetField("imageSize", BindingFlags.Instance | BindingFlags.NonPublic); + ImageSizeField = typeof(ImageList).GetField("_imageSize", BindingFlags.Instance | BindingFlags.NonPublic); PerformRecreateHandleMethod = typeof(ImageList).GetMethod("PerformRecreateHandle", BindingFlags.Instance | BindingFlags.NonPublic); if (ImageSizeField == null || PerformRecreateHandleMethod == null) @@ -32,7 +32,7 @@ public static void SetImageSize(ImageList imageList, Size size) if (ImageSizeField != null && PerformRecreateHandleMethod != null) { ImageSizeField.SetValue(imageList, size); - PerformRecreateHandleMethod.Invoke(imageList, new object[] { "ImageSize" }); + PerformRecreateHandleMethod.Invoke(imageList, []); } else { @@ -59,4 +59,23 @@ public static void SetListSpacing(ListView list, int hspacing, int vspacing) Win32.SendMessage(list.Handle, LVM_SETICONSPACING, IntPtr.Zero, (IntPtr) (int) (((ushort) hspacing) | (uint) (vspacing << 16))); } + + // Workaround for https://github.com/dotnet/winforms/issues/12027 + public static void ClearCachedBrushesAndPens() + { + var threadData = (IDictionary) typeof(SystemBrushes).Assembly.GetType("System.Drawing.Gdip")! + .GetProperty("ThreadData", BindingFlags.Static | BindingFlags.NonPublic)! + .GetValue(null)!; + + var systemBrushesKey = typeof(SystemBrushes) + .GetField("s_systemBrushesKey", BindingFlags.Static | BindingFlags.NonPublic)! + .GetValue(null)!; + + var systemPensKey = typeof(SystemPens) + .GetField("s_systemPensKey", BindingFlags.Static | BindingFlags.NonPublic)! + .GetValue(null)!; + + threadData[systemBrushesKey] = null; + threadData[systemPensKey] = null; + } } \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsImageList.cs b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsImageList.cs index a308733033..df2c638290 100644 --- a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsImageList.cs +++ b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsImageList.cs @@ -1,6 +1,7 @@ using System.Drawing; using System.Windows.Forms; using Eto.WinForms; +using NAPS2.EtoForms.Widgets; namespace NAPS2.EtoForms.WinForms; @@ -17,7 +18,7 @@ private WinFormsImageList(WinFormsListView listView, ListViewBehavior beha private Image ItemToImage(T item) { - return _behavior.GetImage(item, _listView.ImageSize).ToSD(); + return _behavior.GetImage(_listView, item).ToSD(); } public abstract void Clear(); @@ -30,7 +31,7 @@ private Image ItemToImage(T item) public class Custom : WinFormsImageList { - private readonly List _images = new(); + private readonly List _images = []; public Custom(WinFormsListView listView, ListViewBehavior behavior) : base(listView, behavior) { @@ -84,7 +85,7 @@ public class Native : WinFormsImageList public Native(WinFormsListView listView, ListViewBehavior behavior) : base(listView, behavior) { - _images = _listView.NativeControl.LargeImageList.Images; + _images = _listView.NativeControl.LargeImageList!.Images; } public override void Clear() @@ -94,7 +95,7 @@ public override void Clear() public override void Append(T item, ListViewItem listViewItem) { - _images.Add(_behavior.GetImage(item, _listView.ImageSize).ToSD()); + _images.Add(_behavior.GetImage(_listView, item).ToSD()); listViewItem.ImageIndex = _images.Count - 1; } diff --git a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsInvoker.cs b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsInvoker.cs index 49fc48d4ea..8d109aa5de 100644 --- a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsInvoker.cs +++ b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsInvoker.cs @@ -13,22 +13,49 @@ public WinFormsInvoker(Func
formFunc) public void Invoke(Action action) { - _formFunc().Invoke(action); - } - - // TODO: Maybe these can be extension methods? - public T InvokeGet(Func func) - { - T value = default!; - Invoke(() => value = func()); - return value; + try + { + Exception? error = null; + _formFunc().Invoke(() => + { + try + { + action(); + } + catch (Exception ex) + { + error = ex; + } + }); + if (error != null) + { + error.PreserveStackTrace(); + throw error; + } + } + catch (ObjectDisposedException) + { + } + catch (InvalidOperationException) + { + } } - public void SafeInvoke(Action action) + public void InvokeDispatch(Action action) { try { - Invoke(action); + _formFunc().BeginInvoke(() => + { + try + { + action(); + } + catch (Exception ex) + { + Log.ErrorException("Error in InvokeAsync action", ex); + } + }); } catch (ObjectDisposedException) { @@ -37,4 +64,27 @@ public void SafeInvoke(Action action) { } } + + public T InvokeGet(Func func) + { + T value = default!; + Exception? error = null; + _formFunc().Invoke(() => + { + try + { + value = func(); + } + catch (Exception ex) + { + error = ex; + } + if (error != null) + { + error.PreserveStackTrace(); + throw error; + } + }); + return value; + } } \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsListView.cs b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsListView.cs index 32771b12bf..a70f779e31 100644 --- a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsListView.cs +++ b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsListView.cs @@ -1,7 +1,9 @@ using System.Drawing; +using System.Runtime.InteropServices; using System.Windows.Forms; using Eto.WinForms; using Eto.WinForms.Forms.Menu; +using NAPS2.EtoForms.Widgets; using NAPS2.WinForms; using ContextMenu = Eto.Forms.ContextMenu; @@ -9,8 +11,14 @@ namespace NAPS2.EtoForms.WinForms; public class WinFormsListView : IListView where T : notnull { - private static readonly Pen DefaultPen = new(Color.Black, 1); - private static readonly Pen SelectionPen = new(Color.FromArgb(0x60, 0xa0, 0xe8), 3); + private Pen DefaultPen => new(_behavior.ColorScheme.ForegroundColor.ToSD(), 1); + private static readonly Pen BasicSelectionPen = new(Color.FromArgb(0x60, 0xa0, 0xe8), 3); + private const int PageNumberTextPadding = 6; + private const int PageNumberSelectionPadding = 3; + private SolidBrush PageNumberOutlineBrush => new(_behavior.ColorScheme.HighlightBorderColor.ToSD()); + private SolidBrush PageNumberSelectionBrush => new(_behavior.ColorScheme.HighlightBackgroundColor.ToSD()); + private static readonly StringFormat PageNumberLabelFormat = new() + { Alignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter }; private readonly ListView _view; private readonly Eto.Forms.Control _viewEtoControl; @@ -19,12 +27,15 @@ public class WinFormsListView : IListView where T : notnull private ListSelection _selection = ListSelection.Empty(); private bool _refreshing; private ContextMenu? _contextMenu; + private float _dpiScale = 1f; + private Eto.Drawing.Size _imageSize = new(48, 48); public WinFormsListView(ListViewBehavior behavior) { _behavior = behavior; _view = behavior.ScrollOnDrag ? new DragScrollListView() : new ListView(); _view.MultiSelect = behavior.MultiSelect; + if (_behavior.Checkboxes) { _view.View = View.List; @@ -45,6 +56,7 @@ public WinFormsListView(ListViewBehavior behavior) _view.SelectedIndexChanged += OnSelectedIndexChanged; } + _view.AllowDrop = _behavior.AllowDragDrop; _view.ItemActivate += OnItemActivate; _view.ItemDrag += OnItemDrag; _view.DragEnter += OnDragEnter; @@ -65,53 +77,146 @@ public WinFormsListView(ListViewBehavior behavior) _view.OwnerDraw = true; _view.DrawItem += CustomRenderItem; } + + EtoPlatform.Current.AttachDpiDependency(_viewEtoControl, scale => + { + _dpiScale = scale; + // Reset internal size based on new scale + ImageSize = ImageSize; + }); } private bool UseCustomRendering => !_behavior.ShowLabels && !_behavior.Checkboxes; private void CustomRenderItem(object? sender, DrawListViewItemEventArgs e) { - int width, height; var image = ImageList.Get(e.Item); - if (image.Width > image.Height) - { - width = ImageSize; - height = (int) Math.Round(width * (image.Height / (double) image.Width)); - } - else + int imageSizeW = (int) Math.Round(_imageSize.Width * _dpiScale); + int imageSizeH = (int) Math.Round(_imageSize.Height * _dpiScale); + if (_behavior.ShowPageNumbers) { - height = ImageSize; - width = (int) Math.Round(height * (image.Width / (double) image.Height)); - } - var x = e.Bounds.Left + (e.Bounds.Width - width) / 2; - var y = e.Bounds.Top + (e.Bounds.Height - height) / 2; - e.Graphics.DrawImage(image, new Rectangle(x, y, width, height)); + int tp = (int) Math.Round(PageNumberTextPadding * _dpiScale); + int sp = (int) Math.Round(PageNumberSelectionPadding * _dpiScale); - // Draw border - if (e.Item.Selected) - { - e.Graphics.DrawRectangle(SelectionPen, x - 2, y - 2, width + 3, height + 3); + // When page numbers are shown, we use a completely different drawing path, as we need to offset the image + // to have room for the page numbers, and the selection rectangle has a completely different style to + // encompass the page numbers too. + string label = $"{e.ItemIndex + 1} / {_view.Items.Count}"; + SizeF textSize = TextRenderer.MeasureText(label, _view.Font); + int textOffset = (int) (textSize.Height + tp); + + float scaleHeight = (float) (imageSizeH - textOffset) / image.Height; + float scaleWidth = (float) imageSizeW / image.Width; + + float scale = Math.Min(scaleWidth, scaleHeight); + int height = (int) Math.Round(image.Height * scale); + int width = (int) Math.Round(image.Width * scale); + + var x = e.Bounds.Left + (e.Bounds.Width - width) / 2; + var y = e.Bounds.Top + (e.Bounds.Height - height - textOffset) / 2; + + // Draw selection rectangle/background + if (e.Item.Selected) + { + Size intTextSize = Size.Ceiling(textSize); + + int selectionWidth = Math.Max(width, intTextSize.Width); + int selectionHeight = height + tp + intTextSize.Height; + + var selectionX = e.Bounds.Left + (e.Bounds.Width - width) / 2; + + var selectionRect = new Rectangle(selectionX, y, selectionWidth, selectionHeight); + selectionRect.Inflate(sp, sp); + + var outlineRect = selectionRect; + outlineRect.Inflate(1, 1); + e.Graphics.FillRectangle(PageNumberOutlineBrush, outlineRect); + + e.Graphics.FillRectangle(PageNumberSelectionBrush, selectionRect); + } + + // Draw image + e.Graphics.DrawImage(image, new Rectangle(x, y, width, height)); + + // Draw the text below the image + var drawBrush = new SolidBrush(_behavior.ColorScheme.ForegroundColor.ToSD()); + float x1 = x + width / 2f; + float y1 = y + height + tp; + RectangleF labelRect = new(x1, y1, 0, textSize.Height); + float maxLabelWidth = Math.Min(textSize.Width, e.Bounds.Width - 2 * tp); + labelRect.Inflate(maxLabelWidth / 2, 0); + labelRect.Width += 2; + e.Graphics.DrawString(label, _view.Font, drawBrush, labelRect, PageNumberLabelFormat); + + // Draw unselected border + if (!e.Item.Selected) + { + e.Graphics.DrawRectangle(DefaultPen, x - 1, y - 1, width + 1, height + 1); + } } else { - e.Graphics.DrawRectangle(DefaultPen, x, y, width, height); + // The basic no-page-numbers drawing path + int width, height; + if (image.Width > image.Height) + { + width = imageSizeW; + height = (int) Math.Round(width * (image.Height / (double) image.Width)); + } + else + { + height = imageSizeH; + width = (int) Math.Round(height * (image.Width / (double) image.Height)); + } + var x = e.Bounds.Left + (e.Bounds.Width - width) / 2; + var y = e.Bounds.Top + (e.Bounds.Height - height) / 2; + + // Draw image + e.Graphics.DrawImage(image, new Rectangle(x, y, width, height)); + + // Draw border + if (e.Item.Selected) + { + e.Graphics.DrawRectangle(BasicSelectionPen, x - 2, y - 2, width + 3, height + 3); + } + else + { + e.Graphics.DrawRectangle(DefaultPen, x, y, width, height); + } } } - public int ImageSize + public Eto.Drawing.Size ImageSize { - get => _view.LargeImageList.ImageSize.Width; - set => WinFormsHacks.SetImageSize(_view.LargeImageList, new Size(value, value)); + get => _imageSize; + set + { + _imageSize = value; + if (_view.LargeImageList != null) + { + int w = (int) Math.Round(_imageSize.Width * _dpiScale); + int h = (int) Math.Round(_imageSize.Height * _dpiScale); + WinFormsHacks.SetImageSize(_view.LargeImageList!, new Size(w, h)); + } + } } private void OnDragEnter(object? sender, DragEventArgs e) { var data = e.Data.ToEto(); - if (data.Contains(_behavior.CustomDragDataType) && _behavior.AllowDragDrop) + // TODO: Figure out why .Contains is not working correctly on net9 + try { - e.Effect = _behavior.GetCustomDragEffect(data.GetData(_behavior.CustomDragDataType)).ToSwf(); + if (data.Contains(_behavior.CustomDragDataType) && _behavior.AllowDragDrop) + { + e.Effect = _behavior.GetCustomDragEffect(data.GetData(_behavior.CustomDragDataType)).ToSwf(); + return; + } } - else if (data.Contains("FileDrop") && _behavior.AllowFileDrop) + catch (COMException) + { + } + if (data.Contains("FileDrop") && _behavior.AllowFileDrop) { e.Effect = DragDropEffects.Copy; } @@ -166,21 +271,27 @@ private void SetSelectedItems() { if (_behavior.Checkboxes) { - Items[i].Checked = Selection.Contains((T) Items[i].Tag); + Items[i].Checked = Selection.Contains((T) Items[i].Tag!); } else { - Items[i].Selected = Selection.Contains((T) Items[i].Tag); + Items[i].Selected = Selection.Contains((T) Items[i].Tag!); } } } - // TODO: Do we need this method? Clean up the name/doc at least public void RegenerateImages() { - if (_refreshing) + if (_refreshing || Items.Count == 0) { - throw new InvalidOperationException(); + return; + } + if (!UseCustomRendering) + { + // TODO: Not sure why but this gets glitchy unless we reset the items too + Invoker.Current.InvokeDispatch(() => + SetItems(_view.Items.Cast().Select(x => (T) x.Tag!).ToList())); + return; } _refreshing = true; _view.BeginUpdate(); @@ -189,7 +300,7 @@ public void RegenerateImages() var images = new List(); foreach (ListViewItem listViewItem in Items) { - var item = (T) listViewItem.Tag; + var item = (T) listViewItem.Tag!; images.Add(ImageList.PartialAppend(item)); } ImageList.FinishPartialAppends(images); @@ -209,7 +320,7 @@ public void ApplyDiffs(ListViewDiffs diffs) // TODO: We might want to make the differ even smarter. e.g. maybe it can generate an arbitrary order of operations that minimizes update cost // example: clear then append 1 instead of delete all but 1 - var originalItemsList = Items.OfType().Select(x => (T) x.Tag).ToList(); + var originalItemsList = Items.OfType().Select(x => (T) x.Tag!).ToList(); var originalItemsSet = new HashSet(originalItemsList); if (!diffs.AppendOperations.Any() && !diffs.ReplaceOperations.Any() && diffs.TrimOperations.Any(x => x.Count == Items.Count)) @@ -231,6 +342,7 @@ public void ApplyDiffs(ListViewDiffs diffs) } foreach (var replace in diffs.ReplaceOperations) { + // TODO: This seems to have some race condition (errors when changing languages while thumbnails render) Items[replace.Index].Tag = replace.Item; ImageList.Replace(replace.Item, replace.Index); } @@ -244,7 +356,7 @@ public void ApplyDiffs(ListViewDiffs diffs) } } SetSelectedItems(); - var newItemsList = Items.OfType().Select(x => (T) x.Tag).ToList(); + var newItemsList = Items.OfType().Select(x => (T) x.Tag!).ToList(); var newItemsSet = new HashSet(newItemsList); if (originalItemsSet.SetEquals(newItemsSet) && !originalItemsList.SequenceEqual(newItemsList)) { @@ -292,7 +404,7 @@ private void OnSelectedIndexChanged(object? sender, EventArgs e) var items = _behavior.Checkboxes ? _view.CheckedItems.Cast() : _view.SelectedItems.Cast(); - Selection = ListSelection.From(items.Select(x => (T) x.Tag)); + Selection = ListSelection.From(items.Select(x => (T) x.Tag!)); _refreshing = false; } } @@ -320,20 +432,33 @@ private void OnItemDrag(object? sender, ItemDragEventArgs e) private void OnDragDrop(object? sender, DragEventArgs e) { var index = GetDragIndex(e); + _view.InsertionMark.Index = -1; if (index != -1) { var data = e.Data.ToEto(); - if (data.Contains(_behavior.CustomDragDataType)) + // TODO: Figure out why .Contains is not working correctly on net9 + try { - Drop?.Invoke(this, new DropEventArgs(index, data.GetData(_behavior.CustomDragDataType))); + if (data.Contains(_behavior.CustomDragDataType)) + { + Drop?.Invoke(this, new DropEventArgs(index, data.GetData(_behavior.CustomDragDataType))); + } } - else if (data.Contains("FileDrop")) + catch (COMException) + { + } + try + { + if (data.Contains("FileDrop")) + { + var filePaths = (string[]) e.Data!.GetData(DataFormats.FileDrop)!; + Drop?.Invoke(this, new DropEventArgs(index, filePaths)); + } + } + catch (COMException) { - var filePaths = e.Data.ToEto().Uris.Select(uri => uri.AbsolutePath); - Drop?.Invoke(this, new DropEventArgs(index, filePaths)); } } - _view.InsertionMark.Index = -1; } private void OnDragLeave(object? sender, EventArgs e) diff --git a/NAPS2.Lib.WinForms/ImportExport/Email/Mapi/MapiEmailClients.cs b/NAPS2.Lib.WinForms/ImportExport/Email/Mapi/MapiEmailClients.cs new file mode 100644 index 0000000000..f6adefff86 --- /dev/null +++ b/NAPS2.Lib.WinForms/ImportExport/Email/Mapi/MapiEmailClients.cs @@ -0,0 +1,46 @@ +using System.Drawing; +using Microsoft.Win32; +using NAPS2.Images.Gdi; + +namespace NAPS2.ImportExport.Email.Mapi; + +internal class MapiEmailClients : ISystemEmailClients +{ + public string? GetDefaultName() + { + using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Clients\Mail", false); + return key?.GetValue(null)?.ToString(); + } + + public string[] GetNames() + { + // TODO: Swallow errors + using var clientList = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Clients\Mail", false); + return clientList?.GetSubKeyNames().Where(clientName => + { + using var clientKey = Registry.LocalMachine.OpenSubKey($@"SOFTWARE\Clients\Mail\{clientName}"); + return clientKey?.GetValue("DllPath") != null; + }).ToArray() ?? Array.Empty(); + } + + public IMemoryImage? LoadIcon(string clientName) + { + var exePath = GetExePath(clientName); + if (exePath == null) return null; + var icon = Icon.ExtractAssociatedIcon(exePath); + if (icon == null) return null; + return new GdiImage(icon.ToBitmap()); + } + + private string? GetExePath(string clientName) + { + using var command = + Registry.LocalMachine.OpenSubKey($@"SOFTWARE\Clients\Mail\{clientName}\shell\open\command", false); + string commandText = command?.GetValue(null)?.ToString() ?? ""; + if (!commandText.StartsWith("\"", StringComparison.InvariantCulture)) + { + return null; + } + return commandText.Substring(1, commandText.IndexOf("\"", 1, StringComparison.InvariantCulture) - 1); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/ImportExport/Email/Mapi/MapiEmailProvider.cs b/NAPS2.Lib.WinForms/ImportExport/Email/Mapi/MapiEmailProvider.cs similarity index 88% rename from NAPS2.Lib/ImportExport/Email/Mapi/MapiEmailProvider.cs rename to NAPS2.Lib.WinForms/ImportExport/Email/Mapi/MapiEmailProvider.cs index ec65f19ef5..da6c11fb49 100644 --- a/NAPS2.Lib/ImportExport/Email/Mapi/MapiEmailProvider.cs +++ b/NAPS2.Lib.WinForms/ImportExport/Email/Mapi/MapiEmailProvider.cs @@ -1,6 +1,6 @@ namespace NAPS2.ImportExport.Email.Mapi; -public class MapiEmailProvider : IEmailProvider +internal class MapiEmailProvider : IEmailProvider { private readonly MapiDispatcher _mapiDispatcher; private readonly Naps2Config _config; @@ -37,7 +37,7 @@ public Task SendEmail(EmailMessage message, ProgressHandler progress = def if (returnCode != MapiSendMailReturnCode.Success) { - Log.Error("Error sending email. MAPI error code: {0}", returnCode); + Log.Error($"Error sending email. MAPI error code: {returnCode}"); _errorOutput.DisplayError(MiscResources.EmailError, $"MAPI returned error code: {returnCode}"); return false; } @@ -45,4 +45,8 @@ public Task SendEmail(EmailMessage message, ProgressHandler progress = def return true; }); } + + public bool ShowInList => true; + + public bool CanSelectInList => true; } \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/ImportExport/PrintDocumentPrinter.cs b/NAPS2.Lib.WinForms/ImportExport/PrintDocumentPrinter.cs index 01a6a0c723..d68a5d3904 100644 --- a/NAPS2.Lib.WinForms/ImportExport/PrintDocumentPrinter.cs +++ b/NAPS2.Lib.WinForms/ImportExport/PrintDocumentPrinter.cs @@ -7,14 +7,8 @@ namespace NAPS2.ImportExport; public class PrintDocumentPrinter : IScannedImagePrinter { - private readonly ImageContext _imageContext; - - public PrintDocumentPrinter(ImageContext imageContext) - { - _imageContext = imageContext; - } - - public async Task PromptToPrint(IList images, IList selectedImages) + public async Task PromptToPrint( + Eto.Forms.Window parentWindow, IList images, IList selectedImages) { if (!images.Any()) { @@ -22,6 +16,8 @@ public async Task PromptToPrint(IList images, IList Print(PrinterSettings printerSettings, IList(); + imagesToPrint = []; break; } if (imagesToPrint.Count == 0) @@ -67,56 +63,45 @@ public async Task Print(PrinterSettings printerSettings, IList { - try + var printDocument = new PrintDocument(); + int i = 0; + printDocument.PrintPage += (sender, e) => { - var printDocument = new PrintDocument(); - int i = 0; - printDocument.PrintPage += (sender, e) => + var image = imagesToPrint[i].Render(); + try { - var image = imagesToPrint[i].Render(); - try + var pb = e.PageBounds; + if (Math.Sign(image.Width - image.Height) != Math.Sign(pb.Width - pb.Height)) { - var pb = e.PageBounds; - if (Math.Sign(image.Width - image.Height) != Math.Sign(pb.Width - pb.Height)) - { - // Flip portrait/landscape to match output - image = image.PerformTransform(new RotationTransform(90)); - } - - // Fit the image into the output rect while maintaining its aspect ratio - var rect = image.Width / pb.Width < image.Height / pb.Height - ? new Rectangle(pb.Left, pb.Top, image.Width * pb.Height / image.Height, pb.Height) - : new Rectangle(pb.Left, pb.Top, pb.Width, image.Height * pb.Width / image.Width); - - e.Graphics!.DrawImage(image.AsBitmap(), rect); - } - finally - { - image.Dispose(); + // Flip portrait/landscape to match output + image = image.PerformTransform(new RotationTransform(90)); } - e.HasMorePages = (++i < imagesToPrint.Count); - }; - printDocument.PrinterSettings = printerSettings; - printDocument.Print(); - - Log.Event(EventType.Print, new EventParams - { - Name = MiscResources.Print, - Pages = images.Count, - DeviceName = printDocument.PrinterSettings.PrinterName - }); + // Fit the image into the output rect while maintaining its aspect ratio + var rect = image.Width / pb.Width < image.Height / pb.Height + ? new Rectangle(pb.Left, pb.Top, image.Width * pb.Height / image.Height, pb.Height) + : new Rectangle(pb.Left, pb.Top, pb.Width, image.Height * pb.Width / image.Width); - return true; - } - finally - { - // TODO: Clone & dispose - foreach (var image in images) + e.Graphics!.DrawImage(image.AsBitmap(), rect); + } + finally { image.Dispose(); } - } + + e.HasMorePages = (++i < imagesToPrint.Count); + }; + printDocument.PrinterSettings = printerSettings; + printDocument.Print(); + + Log.Event(EventType.Print, new EventParams + { + Name = MiscResources.Print, + Pages = images.Count, + DeviceName = printDocument.PrinterSettings.PrinterName + }); + + return true; }); } } \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/Modules/WinFormsModule.cs b/NAPS2.Lib.WinForms/Modules/WinFormsModule.cs index 65904d83ca..4201974227 100644 --- a/NAPS2.Lib.WinForms/Modules/WinFormsModule.cs +++ b/NAPS2.Lib.WinForms/Modules/WinFormsModule.cs @@ -1,14 +1,9 @@ using Autofac; -using NAPS2.EtoForms; -using NAPS2.EtoForms.Desktop; using NAPS2.EtoForms.Ui; -using NAPS2.EtoForms.WinForms; using NAPS2.ImportExport; -using NAPS2.ImportExport.Pdf; -using NAPS2.Scan; -using NAPS2.Scan.Batch; -using NAPS2.Update; -using NAPS2.WinForms; +using NAPS2.ImportExport.Email; +using NAPS2.ImportExport.Email.Mapi; +using NAPS2.Platform.Windows; namespace NAPS2.Modules; @@ -16,27 +11,16 @@ public class WinFormsModule : Module { protected override void Load(ContainerBuilder builder) { - // TODO: Move common registrations (between WinForms/Mac/Gtk) to a GuiModule - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As().SingleInstance(); - builder.Register(ctx => ctx.Resolve()); + builder.RegisterType().As(); builder.RegisterType().As(); - builder.RegisterType().AsSelf().SingleInstance(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As(); + builder.RegisterType().As().WithParameter("systemDefault", true); + builder.RegisterType().As(); builder.RegisterType().As(); + builder.RegisterType().As(); - EtoPlatform.Current = new WinFormsEtoPlatform(); // TODO: Can we add a test for this? builder.RegisterBuildCallback(ctx => Log.EventLogger = ctx.Resolve()); diff --git a/NAPS2.Lib.WinForms/NAPS2.Lib.WinForms.csproj b/NAPS2.Lib.WinForms/NAPS2.Lib.WinForms.csproj index bd348b960e..5bde256229 100644 --- a/NAPS2.Lib.WinForms/NAPS2.Lib.WinForms.csproj +++ b/NAPS2.Lib.WinForms/NAPS2.Lib.WinForms.csproj @@ -1,7 +1,7 @@  - net6-windows;net462 + net9-windows enable true NAPS2 @@ -11,7 +11,6 @@ NAPS2 - Not Another PDF Scanner NAPS2 - Not Another PDF Scanner - Copyright 2009, 2012-2020 NAPS2 Contributors; Icons from http://www.fatcow.com/free-icons Debug;Release;DebugLang @@ -21,8 +20,9 @@ - - + + + diff --git a/NAPS2.Lib.WinForms/Platform/Windows/WindowsApplicationLifecycle.cs b/NAPS2.Lib.WinForms/Platform/Windows/WindowsApplicationLifecycle.cs index eac1c2a4de..4d2900daf0 100644 --- a/NAPS2.Lib.WinForms/Platform/Windows/WindowsApplicationLifecycle.cs +++ b/NAPS2.Lib.WinForms/Platform/Windows/WindowsApplicationLifecycle.cs @@ -5,33 +5,35 @@ namespace NAPS2.Platform.Windows; // TODO: Can we add tests for this somehow? -/// -/// A class to help manage the lifecycle of the NAPS2 GUI. -/// -public class WindowsApplicationLifecycle +public class WindowsApplicationLifecycle : ApplicationLifecycle { private readonly StillImage _sti; private readonly WindowsEventLogger _windowsEventLogger; - private readonly Naps2Config _config; + private readonly ProcessCoordinator _processCoordinator; private bool _shouldCreateEventSource; private int _returnCode; - public WindowsApplicationLifecycle(StillImage sti, WindowsEventLogger windowsEventLogger, Naps2Config config) + public WindowsApplicationLifecycle(StillImage sti, WindowsEventLogger windowsEventLogger, + ProcessCoordinator processCoordinator, IOsServiceManager serviceManager, Naps2Config config) + : base(processCoordinator, serviceManager, config) { _sti = sti; _windowsEventLogger = windowsEventLogger; - _config = config; + _processCoordinator = processCoordinator; } /// /// Parses the NAPS2 GUI command-line arguments. /// /// - public void ParseArgs(string[] args) + public override void ParseArgs(string[] args) { + base.ParseArgs(args); + bool silent = args.Any(x => x.Equals("/Silent", StringComparison.InvariantCultureIgnoreCase)); bool noElevation = args.Any(x => x.Equals("/NoElevation", StringComparison.InvariantCultureIgnoreCase)); + bool failedUpdate = args.Any(x => x.Equals("/FailedUpdate", StringComparison.InvariantCultureIgnoreCase)); // Utility function to send a message to the user (if /Silent is not specified) void Out(string message) @@ -61,6 +63,11 @@ bool ElevationRequired(Action action) } } + if (failedUpdate) + { + Out(MiscResources.UpdateError); + } + // Let StillImage figure out what it should do from the command-line args _sti.ParseArgs(args); @@ -98,7 +105,8 @@ bool ElevationRequired(Action action) } } - _shouldCreateEventSource = args.Any(x => x.Equals("/CreateEventSource", StringComparison.InvariantCultureIgnoreCase)); + _shouldCreateEventSource = + args.Any(x => x.Equals("/CreateEventSource", StringComparison.InvariantCultureIgnoreCase)); if (_shouldCreateEventSource) { try @@ -144,7 +152,7 @@ private void RelaunchAsElevated() /// /// May terminate the NAPS2 GUI based on the command-line arguments and running processes, sending messages to other processes if appropriate. /// - public void ExitIfRedundant() + public override void ExitIfRedundant() { if (_sti.ShouldRegister || _sti.ShouldUnregister || _shouldCreateEventSource) { @@ -159,8 +167,8 @@ public void ExitIfRedundant() foreach (var process in GetOtherNaps2Processes()) { // Another instance of NAPS2 is running, so send it the "Scan" signal - ActivateProcess(process); - if (Pipes.SendMessage(process, Pipes.MSG_SCAN_WITH_DEVICE + _sti.DeviceID!)) + SetMainWindowToForeground(process); + if (_processCoordinator.ScanWithDevice(process, 100, _sti.DeviceID!)) { // Successful, so this instance can be closed before showing any UI Environment.Exit(0); @@ -168,24 +176,10 @@ public void ExitIfRedundant() } } - // Only start one instance if configured for SingleInstance - if (_config.Get(c => c.SingleInstance)) - { - // See if there's another NAPS2 process running - foreach (var process in GetOtherNaps2Processes()) - { - // Another instance of NAPS2 is running, so send it the "Activate" signal - ActivateProcess(process); - if (Pipes.SendMessage(process, Pipes.MSG_ACTIVATE)) - { - // Successful, so this instance should be closed - Environment.Exit(0); - } - } - } + base.ExitIfRedundant(); } - private static void ActivateProcess(Process process) + protected override void SetMainWindowToForeground(Process process) { if (process.MainWindowHandle != IntPtr.Zero) { @@ -193,7 +187,7 @@ private static void ActivateProcess(Process process) } } - private static IEnumerable GetOtherNaps2Processes() + public static IEnumerable GetOtherNaps2Processes() { Process currentProcess = Process.GetCurrentProcess(); var otherProcesses = Process.GetProcessesByName(currentProcess.ProcessName) diff --git a/NAPS2.Lib.WinForms/Platform/Windows/WindowsOpenWith.cs b/NAPS2.Lib.WinForms/Platform/Windows/WindowsOpenWith.cs new file mode 100644 index 0000000000..4713a2fc9c --- /dev/null +++ b/NAPS2.Lib.WinForms/Platform/Windows/WindowsOpenWith.cs @@ -0,0 +1,88 @@ +using System.ComponentModel; +using System.Drawing; +using System.Runtime.InteropServices.ComTypes; +using System.Text; +using NAPS2.Images.Gdi; + +namespace NAPS2.Platform.Windows; + +public class WindowsOpenWith : IOpenWith +{ + private static readonly Guid BHID_DataObject = new("b8c0bd9f-ed24-455c-83e6-d5390c4fe8c4"); + + private readonly ImageContext _imageContext; + + public WindowsOpenWith(ImageContext imageContext) + { + _imageContext = imageContext; + } + + public IEnumerable GetEntries(string fileExt) + { + foreach (var handler in EnumerateHandlers(fileExt)) + { + handler.GetUIName(out var uiName); + handler.GetName(out var internalName); + handler.GetIconLocation(out var iconPath, out var iconIndex); + yield return new OpenWithEntry(internalName, uiName, iconPath, iconIndex); + } + } + + public void OpenWith(string entryId, IEnumerable filePaths) + { + var pidls = new List(); + foreach (var path in filePaths) + { + int hr = Win32.SHParseDisplayName(path, IntPtr.Zero, out var pidl, 0, out _); + if (hr != 0) throw new Win32Exception(hr); + pidls.Add(pidl); + } + try + { + int hr = Win32.SHCreateShellItemArrayFromIDLists(pidls.Count, pidls.ToArray(), out var shellItemArray); + if (hr != 0) throw new Win32Exception(hr); + + foreach (var handler in EnumerateHandlers(Path.GetExtension(filePaths.First()))) + { + handler.GetName(out var internalName); + if (internalName == entryId) + { + var dao = shellItemArray.BindToHandler(null, BHID_DataObject, typeof(IDataObject).GUID); + handler.Invoke(dao); + } + } + } + finally + { + foreach (var pidl in pidls) + { + Win32.ILFree(pidl); + } + } + } + + public IMemoryImage? LoadIcon(OpenWithEntry entry) + { + if (entry.IconPath.StartsWith("@")) + { + StringBuilder outBuff = new StringBuilder(1024); + int hr = Win32.SHLoadIndirectString(entry.IconPath, outBuff, outBuff.Capacity, IntPtr.Zero); + if (hr != 0) throw new Win32Exception(hr); + return _imageContext.Load(outBuff.ToString()); + } + var icon = Icon.ExtractIcon(entry.IconPath, entry.IconIndex); + if (icon == null) return null; + return new GdiImage(icon.ToBitmap()); + } + + private IEnumerable EnumerateHandlers(string fileExt) + { + int hr = Win32.SHAssocEnumHandlers(fileExt, Win32.ASSOC_FILTER.ASSOC_FILTER_RECOMMENDED, + out Win32.IEnumAssocHandlers eah); + if (hr != 0) throw new Win32Exception(hr); + while (eah.Next(1, out var handler, out var fetched) == 0 && fetched > 0) + { + yield return handler; + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/Platform/Windows/WindowsServiceManager.cs b/NAPS2.Lib.WinForms/Platform/Windows/WindowsServiceManager.cs new file mode 100644 index 0000000000..c2a4e05643 --- /dev/null +++ b/NAPS2.Lib.WinForms/Platform/Windows/WindowsServiceManager.cs @@ -0,0 +1,81 @@ +using System.Runtime.InteropServices.ComTypes; +using NAPS2.Remoting; + +namespace NAPS2.Platform.Windows; + +/// +/// Manages a user startup item on Windows. +/// +public class WindowsServiceManager(ProcessCoordinator processCoordinator) + : IOsServiceManager +{ + private static string ShortcutPath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Microsoft\\Windows\\Start Menu\\Programs\\Startup\\NAPS2 Scanner Sharing.lnk"); + + public bool CanRegister => true; + + public bool IsRegistered => File.Exists(ShortcutPath); + + public bool Register() + { + try + { + // ReSharper disable SuspiciousTypeConversion.Global + var shortcut = (Win32.IShellLink) new Win32.ShellLink(); + shortcut.SetWorkingDirectory(AssemblyHelper.EntryFolder); + shortcut.SetPath(Environment.ProcessPath!); + shortcut.SetArguments("server"); + shortcut.SetIconLocation(Environment.ProcessPath!, 0); + var persistFile = (IPersistFile) shortcut; + persistFile.Save(ShortcutPath, false); + } + catch (Exception ex) + { + Log.ErrorException($"Error saving startup shortcut to {ShortcutPath}", ex); + } + try + { + var process = Process.Start(new ProcessStartInfo + { + FileName = Environment.ProcessPath, + Arguments = "server" + }); + return process != null; + } + catch (Exception ex) + { + Log.ErrorException("Error starting NAPS2 server process", ex); + return false; + } + } + + public void Unregister() + { + try + { + File.Delete(ShortcutPath); + } + catch (Exception ex) + { + Log.ErrorException($"Error deleting startup shortcut at {ShortcutPath}", ex); + } + try + { + int stopped = 0; + foreach (var process in WindowsApplicationLifecycle.GetOtherNaps2Processes()) + { + if (processCoordinator.StopSharingServer(process, 100)) + { + stopped++; + } + } + Log.Debug($"Stopped {stopped} NAPS2 sharing server processes"); + } + catch (Exception ex) + { + Log.ErrorException("Error stopping NAPS2 server process", ex); + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/Scan/Twain/Legacy/DibUtils.cs b/NAPS2.Lib.WinForms/Scan/Twain/Legacy/DibUtils.cs deleted file mode 100644 index 5f0ced035e..0000000000 --- a/NAPS2.Lib.WinForms/Scan/Twain/Legacy/DibUtils.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Drawing; -using System.Runtime.InteropServices; -using NAPS2.Images.Gdi; - -namespace NAPS2.Scan.Twain.Legacy; - -internal class DibUtils -{ - [DllImport("gdi32.dll", ExactSpelling = true)] - internal static extern int SetDIBitsToDevice(IntPtr hdc, int xdst, int ydst, - int width, int height, int xsrc, int ysrc, int start, int lines, - IntPtr bitsptr, IntPtr bmiptr, int color); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern IntPtr GlobalLock(IntPtr handle); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern IntPtr GlobalFree(IntPtr handle); - - //THIS METHOD SAVES THE CONTENTS OF THE DIB POINTER INTO A BITMAP OBJECT - public static Bitmap BitmapFromDib(IntPtr pDib, out int bitdepth) - { - IntPtr dibhand = pDib; - IntPtr bmpptr = GlobalLock(dibhand); - IntPtr pixptr = GetPixelInfo(bmpptr); - BitmapInfoHeader binfo = GetDibInfo(bmpptr); - float resx = binfo.biXPelsPerMeter * 0.0254f; - float resy = binfo.biYPelsPerMeter * 0.0254f; - var scannedImage = new Bitmap(binfo.biWidth, binfo.biHeight); - Graphics scannedImageGraphics = Graphics.FromImage(scannedImage); - IntPtr hdc = scannedImageGraphics.GetHdc(); - SetDIBitsToDevice(hdc, 0, 0, binfo.biWidth, binfo.biHeight, 0, 0, 0, binfo.biHeight, pixptr, bmpptr, 0); - scannedImageGraphics.ReleaseHdc(hdc); - GlobalFree(dibhand); - scannedImageGraphics.Dispose(); - bitdepth = binfo.biBitCount; - scannedImage.SafeSetResolution(resx, resy); - return scannedImage; - } - - //THIS METHOD GETS THE POINTER TO THE BITMAP HEADER INFO - private static IntPtr GetPixelInfo(IntPtr bmpPtr) - { - var bmi = (BitmapInfoHeader)Marshal.PtrToStructure(bmpPtr, typeof(BitmapInfoHeader))!; - - if (bmi.biSizeImage == 0) - bmi.biSizeImage = (uint)(((((bmi.biWidth * bmi.biBitCount) + 31) & ~31) >> 3) * bmi.biHeight); - - var p = (int)bmi.biClrUsed; - if ((p == 0) && (bmi.biBitCount <= 8)) - p = 1 << bmi.biBitCount; - p = (p * 4) + (int)bmi.biSize + (int)bmpPtr; - return (IntPtr)p; - } - - //THIS METHOD GETS THE POINTER TO THE BITMAP HEADER INFO - private static BitmapInfoHeader GetDibInfo(IntPtr bmpPtr) - { - var bmi = (BitmapInfoHeader)Marshal.PtrToStructure(bmpPtr, typeof(BitmapInfoHeader))!; - return bmi; - } -} - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -internal struct BitmapInfoHeader -{ - public uint biSize; - public int biWidth; - public int biHeight; - public ushort biPlanes; - public ushort biBitCount; - public uint biCompression; - public uint biSizeImage; - public int biXPelsPerMeter; - public int biYPelsPerMeter; - public uint biClrUsed; - public uint biClrImportant; - - public void Init() - { - biSize = (uint)Marshal.SizeOf(this); - } -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/Scan/Twain/Legacy/TwainApi.cs b/NAPS2.Lib.WinForms/Scan/Twain/Legacy/TwainApi.cs deleted file mode 100644 index 37098dbf15..0000000000 --- a/NAPS2.Lib.WinForms/Scan/Twain/Legacy/TwainApi.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System.Collections; -using System.Drawing; -using System.Windows.Forms; -using NAPS2.Images.Gdi; -using NAPS2.Scan.Exceptions; -using NAPS2.WinForms; - -namespace NAPS2.Scan.Twain.Legacy; - -// TODO: Either make this usable without a form, or just get rid of it... -internal static class TwainApi -{ - public static ScanDevice? SelectDeviceUI() - { - var tw = new Twain(); - if (!tw.Init(Application.OpenForms[0].Handle)) - { - throw new NoDevicesFoundException(); - } - if (!tw.Select()) - { - return null; - } - string? name = tw.GetCurrentName(); - if (name == null) - { - return null; - } - return new ScanDevice(name, name); - } - - public static List GetDeviceList() - { - var tw = new Twain(); - if (!tw.Init(Application.OpenForms[0].Handle)) - { - throw new NoDevicesFoundException(); - } - var result = new List(); - if (!tw.GetFirst()) - { - return result; - } - do - { - string? name = tw.GetCurrentName(); - if (name != null) - { - result.Add(new ScanDevice(name, name)); - } - } while (tw.GetNext()); - return result; - } - - public static void Scan(ScanningContext scanningContext, ScanProfile settings, ScanDevice device, IWin32Window pForm, Action produceImage) - { - var tw = new Twain(); - if (!tw.Init(pForm.Handle)) - { - throw new DeviceNotFoundException(); - } - if (!tw.SelectByName(device.ID!)) - { - throw new DeviceNotFoundException(); - } - var form = new FTwainGui(); - var mf = new TwainMessageFilter(scanningContext, settings, tw, form); - form.ShowDialog(pForm); - foreach (var b in mf.Bitmaps) - { - produceImage(b); - } - } - - private class TwainMessageFilter : IMessageFilter - { - private readonly ScanningContext _scanningContext; - private readonly ScanProfile _settings; - private readonly Twain _tw; - private readonly FTwainGui _form; - - private bool _activated; - private bool _msgfilter; - - public TwainMessageFilter(ScanningContext scanningContext, ScanProfile settings, Twain tw, FTwainGui form) - { - _scanningContext = scanningContext; - _settings = settings; - _tw = tw; - _form = form; - Bitmaps = new List(); - form.Activated += FTwainGui_Activated; - } - - public List Bitmaps { get; } - - public bool PreFilterMessage(ref Message m) - { - TwainCommand cmd = _tw.PassMessage(ref m); - if (cmd == TwainCommand.Not) - return false; - - switch (cmd) - { - case TwainCommand.CloseRequest: - { - EndingScan(); - _tw.CloseSrc(); - _form.Close(); - break; - } - case TwainCommand.CloseOk: - { - EndingScan(); - _tw.CloseSrc(); - break; - } - case TwainCommand.DeviceEvent: - { - break; - } - case TwainCommand.TransferReady: - { - ArrayList pics = _tw.TransferPictures(); - EndingScan(); - _tw.CloseSrc(); - foreach (IntPtr img in pics) - { - int bitcount = 0; - - using Bitmap bmp = DibUtils.BitmapFromDib(img, out bitcount); - var bitDepth = bitcount == 1 ? BitDepth.BlackAndWhite : BitDepth.Color; - using var storage = new GdiImage(_scanningContext.ImageContext, bmp); - var image = _scanningContext.CreateProcessedImage( - storage, bitDepth, _settings.MaxQuality, _settings.Quality); - Bitmaps.Add(image); - } - _form.Close(); - break; - } - } - - return true; - } - private void EndingScan() - { - if (_msgfilter) - { - Application.RemoveMessageFilter(this); - _msgfilter = false; - _form.Enabled = true; - _form.Activate(); - } - } - - private void FTwainGui_Activated(object? sender, EventArgs e) - { - if (_activated) - return; - _activated = true; - if (!_msgfilter) - { - _form.Enabled = false; - _msgfilter = true; - Application.AddMessageFilter(this); - } - try - { - if (!_tw.Acquire()) - { - EndingScan(); - _form.Close(); - } - } - catch (Exception ex) - { - Log.ErrorException("An error occurred while interacting with TWAIN.", ex); - EndingScan(); - _form.Close(); - } - } - } -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/Scan/Twain/Legacy/TwainDefs.cs b/NAPS2.Lib.WinForms/Scan/Twain/Legacy/TwainDefs.cs deleted file mode 100644 index b2081229a3..0000000000 --- a/NAPS2.Lib.WinForms/Scan/Twain/Legacy/TwainDefs.cs +++ /dev/null @@ -1,299 +0,0 @@ -using System.Runtime.InteropServices; - -namespace NAPS2.Scan.Twain.Legacy; - -internal class TwProtocol -{ // TWON_PROTOCOL... - public const short MAJOR = 1; - public const short MINOR = 9; -} - -[Flags] -internal enum TwDG : short -{ // DG_..... - Control = 0x0001, - Image = 0x0002, - Audio = 0x0004 -} - -internal enum TwData : short -{ // DAT_.... - Null = 0x0000, - Capability = 0x0001, - Event = 0x0002, - Identity = 0x0003, - Parent = 0x0004, - PendingXfers = 0x0005, - SetupMemXfer = 0x0006, - SetupFileXfer = 0x0007, - Status = 0x0008, - UserInterface = 0x0009, - XferGroup = 0x000a, - TwunkIdentity = 0x000b, - CustomDSData = 0x000c, - DeviceEvent = 0x000d, - FileSystem = 0x000e, - PassThru = 0x000f, - - ImageInfo = 0x0101, - ImageLayout = 0x0102, - ImageMemXfer = 0x0103, - ImageNativeXfer = 0x0104, - ImageFileXfer = 0x0105, - CieColor = 0x0106, - GrayResponse = 0x0107, - RGBResponse = 0x0108, - JpegCompression = 0x0109, - Palette8 = 0x010a, - ExtImageInfo = 0x010b, - - SetupFileXfer2 = 0x0301 -} - -internal enum TwMessageCode : short -{ // MSG_..... - Null = 0x0000, - Get = 0x0001, - GetCurrent = 0x0002, - GetDefault = 0x0003, - GetFirst = 0x0004, - GetNext = 0x0005, - Set = 0x0006, - Reset = 0x0007, - QuerySupport = 0x0008, - - XFerReady = 0x0101, - CloseDSReq = 0x0102, - CloseDSOK = 0x0103, - DeviceEvent = 0x0104, - - CheckStatus = 0x0201, - - OpenDSM = 0x0301, - CloseDSM = 0x0302, - - OpenDS = 0x0401, - CloseDS = 0x0402, - UserSelect = 0x0403, - - DisableDS = 0x0501, - EnableDS = 0x0502, - EnableDSUIOnly = 0x0503, - - ProcessEvent = 0x0601, - - EndXfer = 0x0701, - StopFeeder = 0x0702, - - ChangeDirectory = 0x0801, - CreateDirectory = 0x0802, - Delete = 0x0803, - FormatMedia = 0x0804, - GetClose = 0x0805, - GetFirstFile = 0x0806, - GetInfo = 0x0807, - GetNextFile = 0x0808, - Rename = 0x0809, - Copy = 0x080A, - AutoCaptureDir = 0x080B, - - PassThru = 0x0901 -} - -internal enum TwReturnCode : short -{ // TWRC_.... - Success = 0x0000, - Failure = 0x0001, - CheckStatus = 0x0002, - Cancel = 0x0003, - DSEvent = 0x0004, - NotDSEvent = 0x0005, - XferDone = 0x0006, - EndOfList = 0x0007, - InfoNotSupported = 0x0008, - DataNotAvailable = 0x0009 -} - -internal enum TwConditionCode : short -{ // TWCC_.... - Success = 0x0000, - Bummer = 0x0001, - LowMemory = 0x0002, - NoDS = 0x0003, - MaxConnections = 0x0004, - OperationError = 0x0005, - BadCap = 0x0006, - BadProtocol = 0x0009, - BadValue = 0x000a, - SeqError = 0x000b, - BadDest = 0x000c, - CapUnsupported = 0x000d, - CapBadOperation = 0x000e, - CapSeqError = 0x000f, - Denied = 0x0010, - FileExists = 0x0011, - FileNotFound = 0x0012, - NotEmpty = 0x0013, - PaperJam = 0x0014, - PaperDoubleFeed = 0x0015, - FileWriteError = 0x0016, - CheckDeviceOnline = 0x0017 -} - -internal enum TwOn : short -{ // TWON_.... - Array = 0x0003, - Enum = 0x0004, - One = 0x0005, - Range = 0x0006, - DontCare = -1 -} - -internal enum TwType : short -{ // TWTY_.... - Int8 = 0x0000, - Int16 = 0x0001, - Int32 = 0x0002, - UInt8 = 0x0003, - UInt16 = 0x0004, - UInt32 = 0x0005, - Bool = 0x0006, - Fix32 = 0x0007, - Frame = 0x0008, - Str32 = 0x0009, - Str64 = 0x000a, - Str128 = 0x000b, - Str255 = 0x000c, - Str1024 = 0x000d, - Str512 = 0x000e -} - -internal enum TwCap : short -{ - XferCount = 0x0001, // CAP_XFERCOUNT - ICompression = 0x0100, // ICAP_... - IPixelType = 0x0101, - IUnits = 0x0102, - IXferMech = 0x0103 -} - -// ------------------- STRUCTS -------------------------------------------- - -[StructLayout(LayoutKind.Sequential, Pack = 2, CharSet = CharSet.Ansi)] -internal class TwIdentity -{ // TW_IDENTITY - public IntPtr Id; - public TwVersion Version; - public short ProtocolMajor; - public short ProtocolMinor; - public int SupportedGroups; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)] - public string? Manufacturer; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)] - public string? ProductFamily; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)] - public string? ProductName; -} - -[StructLayout(LayoutKind.Sequential, Pack = 2, CharSet = CharSet.Ansi)] -internal struct TwVersion -{ // TW_VERSION - public short MajorNum; - public short MinorNum; - public short Language; - public short Country; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)] - public string Info; -} - -[StructLayout(LayoutKind.Sequential, Pack = 2)] -internal class TwUserInterface -{ // TW_USERINTERFACE - public short ShowUI; // bool is strictly 32 bit, so use short - public short ModalUI; - public IntPtr ParentHand; -} - -[StructLayout(LayoutKind.Sequential, Pack = 2)] -internal class TwStatus -{ // TW_STATUS - public short ConditionCode; // TwConditionCode - public short Reserved; -} - -[StructLayout(LayoutKind.Sequential, Pack = 2)] -internal struct TwEvent -{ // TW_EVENT - public IntPtr EventPtr; - public short Message; -} - - -[StructLayout(LayoutKind.Sequential, Pack = 2)] -internal class TwImageInfo -{ // TW_IMAGEINFO - public int XResolution; - public int YResolution; - public int ImageWidth; - public int ImageLength; - public short SamplesPerPixel; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] - public short[]? BitsPerSample; - public short BitsPerPixel; - public short Planar; - public short PixelType; - public short Compression; -} - -[StructLayout(LayoutKind.Sequential, Pack = 2)] -internal class TwPendingXfers -{ // TW_PENDINGXFERS - public short Count; - public int EOJ; -} - -[StructLayout(LayoutKind.Sequential, Pack = 2)] -internal struct TwFix32 -{ // TW_FIX32 - public short Whole; - public ushort Frac; - - public float ToFloat() - { - return Whole + (Frac / 65536.0f); - } - public void FromFloat(float f) - { - var i = (int)((f * 65536.0f) + 0.5f); - Whole = (short)(i >> 16); - Frac = (ushort)(i & 0x0000ffff); - } -} - -[StructLayout(LayoutKind.Sequential, Pack = 2)] -internal class TwCapability -{ // TW_CAPABILITY - public TwCapability(TwCap cap) - { - Cap = (short)cap; - ConType = -1; - } - public TwCapability(TwCap cap, short sval) - { - Cap = (short)cap; - ConType = (short)TwOn.One; - Handle = Twain.GlobalAlloc(0x42, 6); - IntPtr pv = Twain.GlobalLock(Handle); - Marshal.WriteInt16(pv, 0, (short)TwType.Int16); - Marshal.WriteInt32(pv, 2, sval); - Twain.GlobalUnlock(Handle); - } - ~TwCapability() - { - if (Handle != IntPtr.Zero) - Twain.GlobalFree(Handle); - } - public short Cap; - public short ConType; - public IntPtr Handle; -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/Scan/Twain/Legacy/TwainLib.cs b/NAPS2.Lib.WinForms/Scan/Twain/Legacy/TwainLib.cs deleted file mode 100644 index e3af6450d6..0000000000 --- a/NAPS2.Lib.WinForms/Scan/Twain/Legacy/TwainLib.cs +++ /dev/null @@ -1,341 +0,0 @@ -using System.Collections; -using System.Runtime.InteropServices; -using System.Windows.Forms; - -namespace NAPS2.Scan.Twain.Legacy; - -internal enum TwainCommand -{ - Not = -1, - Null = 0, - TransferReady = 1, - CloseRequest = 2, - CloseOk = 3, - DeviceEvent = 4 -} - -internal class Twain -{ - private const short COUNTRY_USA = 1; - private const short LANGUAGE_USA = 13; - private readonly TwIdentity _appid; - private readonly TwIdentity _srcds; - private TwEvent _evtmsg; - private IntPtr _hwnd; - private WINMSG _winmsg; - - public Twain() - { - _appid = new TwIdentity - { - Id = IntPtr.Zero, - Version = - { - MajorNum = 1, - MinorNum = 1, - Language = LANGUAGE_USA, - Country = COUNTRY_USA, - Info = "Hack 1" - }, - ProtocolMajor = TwProtocol.MAJOR, - ProtocolMinor = TwProtocol.MINOR, - SupportedGroups = (int)(TwDG.Image | TwDG.Control), - Manufacturer = "NETMaster", - ProductFamily = "Freeware", - ProductName = "Hack" - }; - - _srcds = new TwIdentity { Id = IntPtr.Zero }; - - _evtmsg.EventPtr = Marshal.AllocHGlobal(Marshal.SizeOf(_winmsg)); - } - - public static int ScreenBitDepth - { - get - { - IntPtr screenDC = CreateDC("DISPLAY", null, null, IntPtr.Zero); - int bitDepth = GetDeviceCaps(screenDC, 12); - bitDepth *= GetDeviceCaps(screenDC, 14); - DeleteDC(screenDC); - return bitDepth; - } - } - - ~Twain() - { - Marshal.FreeHGlobal(_evtmsg.EventPtr); - } - - public bool Init(IntPtr hwndp) - { - Finish(); - TwReturnCode returnCode = DSMparent(_appid, IntPtr.Zero, TwDG.Control, TwData.Parent, TwMessageCode.OpenDSM, ref hwndp); - if (returnCode == TwReturnCode.Success) - { - returnCode = DSMident(_appid, IntPtr.Zero, TwDG.Control, TwData.Identity, TwMessageCode.GetFirst, _srcds); - if (returnCode == TwReturnCode.Success) - { - _hwnd = hwndp; - return true; - } - else - { - returnCode = DSMparent(_appid, IntPtr.Zero, TwDG.Control, TwData.Parent, TwMessageCode.CloseDSM, ref hwndp); - return false; - } - } - return false; - } - - public bool GetFirst() - { - TwReturnCode returnCode; - CloseSrc(); - if (_appid.Id == IntPtr.Zero) - { - Init(_hwnd); - if (_appid.Id == IntPtr.Zero) - return false; - } - returnCode = DSMident(_appid, IntPtr.Zero, TwDG.Control, TwData.Identity, TwMessageCode.GetFirst, _srcds); - return returnCode == TwReturnCode.Success; - } - - public bool GetNext() - { - TwReturnCode returnCode = DSMident(_appid, IntPtr.Zero, TwDG.Control, TwData.Identity, TwMessageCode.GetNext, _srcds); - return returnCode == TwReturnCode.Success; - } - - public bool Select() - { - TwReturnCode returnCode; - CloseSrc(); - if (_appid.Id == IntPtr.Zero) - { - Init(_hwnd); - if (_appid.Id == IntPtr.Zero) - return false; - } - returnCode = DSMident(_appid, IntPtr.Zero, TwDG.Control, TwData.Identity, TwMessageCode.UserSelect, _srcds); - return returnCode == TwReturnCode.Success; - } - - public bool SelectByName(string name) - { - if (_srcds.ProductName == name) - { - return true; - } - var rc = TwReturnCode.Success; - while (rc == TwReturnCode.Success) - { - rc = DSMident(_appid, IntPtr.Zero, TwDG.Control, TwData.Identity, TwMessageCode.GetNext, _srcds); - if (_srcds.ProductName == name) - { - return true; - } - } - return false; - } - - public string? GetCurrentName() => _srcds.ProductName; - - public bool Acquire() - { - TwReturnCode returnCode; - CloseSrc(); - if (_appid.Id == IntPtr.Zero) - { - Init(_hwnd); - if (_appid.Id == IntPtr.Zero) - throw new InvalidOperationException("Init call falied"); - } - returnCode = DSMident(_appid, IntPtr.Zero, TwDG.Control, TwData.Identity, TwMessageCode.OpenDS, _srcds); - if (returnCode != TwReturnCode.Success) - throw new InvalidOperationException("DSMident call falied"); - - var guif = new TwUserInterface - { - ShowUI = 1, - ModalUI = 1, - ParentHand = _hwnd - }; - returnCode = DSuserif(_appid, _srcds, TwDG.Control, TwData.UserInterface, TwMessageCode.EnableDS, guif); - if (returnCode != TwReturnCode.Success) - { - CloseSrc(); - if (returnCode == TwReturnCode.Cancel) - { - return false; - } - throw new InvalidOperationException("DSuserif call falied"); - } - return true; - } - - public ArrayList TransferPictures() - { - var pics = new ArrayList(); - if (_srcds.Id == IntPtr.Zero) - return pics; - - TwReturnCode returnCode; - IntPtr hbitmap = IntPtr.Zero; - var pxfr = new TwPendingXfers(); - - do - { - pxfr.Count = 0; - hbitmap = IntPtr.Zero; - - var iinf = new TwImageInfo(); - returnCode = DSiinf(_appid, _srcds, TwDG.Image, TwData.ImageInfo, TwMessageCode.Get, iinf); - if (returnCode != TwReturnCode.Success) - { - CloseSrc(); - return pics; - } - - returnCode = DSixfer(_appid, _srcds, TwDG.Image, TwData.ImageNativeXfer, TwMessageCode.Get, ref hbitmap); - if (returnCode != TwReturnCode.XferDone) - { - CloseSrc(); - return pics; - } - - returnCode = DSpxfer(_appid, _srcds, TwDG.Control, TwData.PendingXfers, TwMessageCode.EndXfer, pxfr); - if (returnCode != TwReturnCode.Success) - { - CloseSrc(); - return pics; - } - - pics.Add(hbitmap); - } - while (pxfr.Count != 0); - - returnCode = DSpxfer(_appid, _srcds, TwDG.Control, TwData.PendingXfers, TwMessageCode.Reset, pxfr); - return pics; - } - - public TwainCommand PassMessage(ref Message m) - { - if (_srcds.Id == IntPtr.Zero) - return TwainCommand.Not; - - int pos = GetMessagePos(); - - _winmsg.hwnd = m.HWnd; - _winmsg.message = m.Msg; - _winmsg.wParam = m.WParam; - _winmsg.lParam = m.LParam; - _winmsg.time = GetMessageTime(); - _winmsg.x = (short)pos; - _winmsg.y = (short)(pos >> 16); - - Marshal.StructureToPtr(_winmsg, _evtmsg.EventPtr, false); - _evtmsg.Message = 0; - TwReturnCode returnCode = DSevent(_appid, _srcds, TwDG.Control, TwData.Event, TwMessageCode.ProcessEvent, ref _evtmsg); - if (returnCode == TwReturnCode.NotDSEvent) - return TwainCommand.Not; - if (_evtmsg.Message == (short)TwMessageCode.XFerReady) - return TwainCommand.TransferReady; - if (_evtmsg.Message == (short)TwMessageCode.CloseDSReq) - return TwainCommand.CloseRequest; - if (_evtmsg.Message == (short)TwMessageCode.CloseDSOK) - return TwainCommand.CloseOk; - if (_evtmsg.Message == (short)TwMessageCode.DeviceEvent) - return TwainCommand.DeviceEvent; - - return TwainCommand.Null; - } - - public void CloseSrc() - { - TwReturnCode returnCode; - if (_srcds.Id != IntPtr.Zero) - { - var guif = new TwUserInterface(); - returnCode = DSuserif(_appid, _srcds, TwDG.Control, TwData.UserInterface, TwMessageCode.DisableDS, guif); - returnCode = DSMident(_appid, IntPtr.Zero, TwDG.Control, TwData.Identity, TwMessageCode.CloseDS, _srcds); - } - } - - public void Finish() - { - TwReturnCode returnCode; - CloseSrc(); - if (_appid.Id != IntPtr.Zero) - returnCode = DSMparent(_appid, IntPtr.Zero, TwDG.Control, TwData.Parent, TwMessageCode.CloseDSM, ref _hwnd); - _appid.Id = IntPtr.Zero; - } - - // ------ DSM entry point DAT_ variants: - [DllImport("twain_32.dll", EntryPoint = "#1")] - private static extern TwReturnCode DSMparent([In, Out] TwIdentity origin, IntPtr zeroptr, TwDG dg, TwData data, TwMessageCode messageCode, ref IntPtr refptr); - - [DllImport("twain_32.dll", EntryPoint = "#1")] - private static extern TwReturnCode DSMident([In, Out] TwIdentity origin, IntPtr zeroptr, TwDG dg, TwData data, TwMessageCode messageCode, [In, Out] TwIdentity idds); - - [DllImport("twain_32.dll", EntryPoint = "#1")] - private static extern TwReturnCode DSMstatus([In, Out] TwIdentity origin, IntPtr zeroptr, TwDG dg, TwData data, TwMessageCode messageCode, [In, Out] TwStatus dsmstat); - - // ------ DSM entry point DAT_ variants to DS: - [DllImport("twain_32.dll", EntryPoint = "#1")] - private static extern TwReturnCode DSuserif([In, Out] TwIdentity origin, [In, Out] TwIdentity dest, TwDG dg, TwData data, TwMessageCode messageCode, TwUserInterface guif); - - [DllImport("twain_32.dll", EntryPoint = "#1")] - private static extern TwReturnCode DSevent([In, Out] TwIdentity origin, [In, Out] TwIdentity dest, TwDG dg, TwData data, TwMessageCode messageCode, ref TwEvent evt); - - [DllImport("twain_32.dll", EntryPoint = "#1")] - private static extern TwReturnCode DSstatus([In, Out] TwIdentity origin, [In] TwIdentity dest, TwDG dg, TwData data, TwMessageCode messageCode, [In, Out] TwStatus dsmstat); - - [DllImport("twain_32.dll", EntryPoint = "#1")] - private static extern TwReturnCode DScap([In, Out] TwIdentity origin, [In] TwIdentity dest, TwDG dg, TwData data, TwMessageCode messageCode, [In, Out] TwCapability capa); - - [DllImport("twain_32.dll", EntryPoint = "#1")] - private static extern TwReturnCode DSiinf([In, Out] TwIdentity origin, [In] TwIdentity dest, TwDG dg, TwData data, TwMessageCode messageCode, [In, Out] TwImageInfo imginf); - - [DllImport("twain_32.dll", EntryPoint = "#1")] - private static extern TwReturnCode DSixfer([In, Out] TwIdentity origin, [In] TwIdentity dest, TwDG dg, TwData data, TwMessageCode messageCode, ref IntPtr hbitmap); - - [DllImport("twain_32.dll", EntryPoint = "#1")] - private static extern TwReturnCode DSpxfer([In, Out] TwIdentity origin, [In] TwIdentity dest, TwDG dg, TwData data, TwMessageCode messageCode, [In, Out] TwPendingXfers pxfr); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern IntPtr GlobalAlloc(int flags, int size); - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern IntPtr GlobalLock(IntPtr handle); - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern bool GlobalUnlock(IntPtr handle); - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern IntPtr GlobalFree(IntPtr handle); - - [DllImport("user32.dll", ExactSpelling = true)] - private static extern int GetMessagePos(); - [DllImport("user32.dll", ExactSpelling = true)] - private static extern int GetMessageTime(); - - [DllImport("gdi32.dll", ExactSpelling = true)] - private static extern int GetDeviceCaps(IntPtr hDC, int nIndex); - - [DllImport("gdi32.dll", CharSet = CharSet.Auto)] - private static extern IntPtr CreateDC(string szdriver, string? szdevice, string? szoutput, IntPtr devmode); - - [DllImport("gdi32.dll", ExactSpelling = true)] - private static extern bool DeleteDC(IntPtr hdc); - - [StructLayout(LayoutKind.Sequential, Pack = 4)] - internal struct WINMSG - { - public IntPtr hwnd; - public int message; - public IntPtr wParam; - public IntPtr lParam; - public int time; - public int x; - public int y; - } -} // class Twain \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/Util/CompilerAttributes.cs b/NAPS2.Lib.WinForms/Util/CompilerAttributes.cs deleted file mode 100644 index 8230a289b1..0000000000 --- a/NAPS2.Lib.WinForms/Util/CompilerAttributes.cs +++ /dev/null @@ -1,61 +0,0 @@ -// https://sergiopedri.medium.com/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb -// ReSharper disable once CheckNamespace - -namespace System.Runtime.CompilerServices -{ - internal static class IsExternalInit - { - } - - /// Specifies that a type has required members or that a member is required. - [AttributeUsage( - AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, - AllowMultiple = false, Inherited = false)] - internal sealed class RequiredMemberAttribute : Attribute - { - } - - /// - /// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. - /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] - internal sealed class CompilerFeatureRequiredAttribute : Attribute - { - public CompilerFeatureRequiredAttribute(string featureName) - { - FeatureName = featureName; - } - - /// - /// The name of the compiler feature. - /// - public string FeatureName { get; } - - /// - /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . - /// - public bool IsOptional { get; init; } - - /// - /// The used for the ref structs C# feature. - /// - public const string RefStructs = nameof(RefStructs); - - /// - /// The used for the required members C# feature. - /// - public const string RequiredMembers = nameof(RequiredMembers); - } -} - -namespace System.Diagnostics.CodeAnalysis -{ - /// - /// Specifies that this constructor sets all required members for the current type, and callers - /// do not need to set any required members themselves. - /// - [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] - internal sealed class SetsRequiredMembersAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/Util/StringWrapper.cs b/NAPS2.Lib.WinForms/Util/StringWrapper.cs index 305ca9ac23..e30eda98bd 100644 --- a/NAPS2.Lib.WinForms/Util/StringWrapper.cs +++ b/NAPS2.Lib.WinForms/Util/StringWrapper.cs @@ -1,5 +1,6 @@ using System.Drawing; using System.Text; +using System.Windows.Forms; namespace NAPS2.Util; @@ -8,7 +9,7 @@ namespace NAPS2.Util; /// public class StringWrapper { - public string Wrap(string text, int maxWidth, Graphics drawingGraphics, Font drawingFont) + public string Wrap(string text, int maxWidth, Font drawingFont) { var result = new StringBuilder(); var parts = new Queue(text.Split(' ')); @@ -19,7 +20,7 @@ public string Wrap(string text, int maxWidth, Graphics drawingGraphics, Font dra { nextParts.Add(parts.Dequeue()); } while ( - parts.Count > 0 && drawingGraphics.MeasureString(string.Join(" ", nextParts.Concat(new[] { parts.Peek() })).Replace("&", ""), + parts.Count > 0 && TextRenderer.MeasureText(string.Join(" ", nextParts.Concat(new[] { parts.Peek() })).Replace("&", ""), drawingFont).Width < maxWidth); result.Append(string.Join(" ", nextParts)); if (parts.Count > 0) diff --git a/NAPS2.Lib.WinForms/WinForms/BackgroundForm.cs b/NAPS2.Lib.WinForms/WinForms/BackgroundForm.cs index 13a697ad48..2417814112 100644 --- a/NAPS2.Lib.WinForms/WinForms/BackgroundForm.cs +++ b/NAPS2.Lib.WinForms/WinForms/BackgroundForm.cs @@ -11,7 +11,9 @@ public BackgroundForm() { ShowInTaskbar = false; FormBorderStyle = FormBorderStyle.FixedToolWindow; - WindowState = FormWindowState.Minimized; + // Even though the form is invisible, we want to make sure it's positioned inside + // its parent (if present) so any child forms are also inside the parent. + StartPosition = FormStartPosition.CenterParent; } protected override void SetVisibleCore(bool value) @@ -19,8 +21,8 @@ protected override void SetVisibleCore(bool value) if (!IsHandleCreated) { CreateHandle(); - value = false; } + value = false; base.SetVisibleCore(value); } } \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/DonatePromptNotifyWidget.cs b/NAPS2.Lib.WinForms/WinForms/DonatePromptNotifyWidget.cs deleted file mode 100644 index 2ef087a660..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/DonatePromptNotifyWidget.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace NAPS2.WinForms; - -public class DonatePromptNotifyWidget : NotifyWidget -{ - public DonatePromptNotifyWidget() - : base(MiscResources.DonatePrompt, MiscResources.Donate, "https://www.naps2.com/donate", null) - { - hideTimer.Interval = 60 * 1000; - } - - public override NotifyWidgetBase Clone() => new DonatePromptNotifyWidget(); -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/DragScrollListView.cs b/NAPS2.Lib.WinForms/WinForms/DragScrollListView.cs index acd6c2e633..0b8b1aa1d8 100644 --- a/NAPS2.Lib.WinForms/WinForms/DragScrollListView.cs +++ b/NAPS2.Lib.WinForms/WinForms/DragScrollListView.cs @@ -22,6 +22,7 @@ public DragScrollListView() _tmrLvScroll = new Timer(components); SuspendLayout(); _tmrLvScroll.Tick += tmrLVScroll_Tick; + HandleDestroyed += (_, _) => _tmrLvScroll.Dispose(); DragOver += ListViewBase_DragOver; ResumeLayout(false); } diff --git a/NAPS2.Lib.WinForms/WinForms/ImagesSavedNotifyWidget.cs b/NAPS2.Lib.WinForms/WinForms/ImagesSavedNotifyWidget.cs deleted file mode 100644 index b882917297..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/ImagesSavedNotifyWidget.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace NAPS2.WinForms; - -public class ImagesSavedNotifyWidget : NotifyWidget -{ - private readonly int _imageCount; - private readonly string _path; - - public ImagesSavedNotifyWidget(int imageCount, string path) - : base(string.Format(MiscResources.ImagesSaved, imageCount), Path.GetFileName(path), path, Path.GetDirectoryName(path)) - { - _imageCount = imageCount; - _path = path; - } - - public override NotifyWidgetBase Clone() => new ImagesSavedNotifyWidget(_imageCount, _path); -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/NotificationManager.cs b/NAPS2.Lib.WinForms/WinForms/NotificationManager.cs deleted file mode 100644 index 73bf878db7..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/NotificationManager.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System.Drawing; -using Eto.Forms; -using NAPS2.EtoForms; -using NAPS2.EtoForms.Desktop; -using NAPS2.Update; -using wf = System.Windows.Forms; - -namespace NAPS2.WinForms; - -public class NotificationManager : INotificationManager -{ - private const int PADDING_X = 25, PADDING_Y = 25; - private const int SPACING_Y = 20; - - private readonly Naps2Config _config; - private readonly List _slots = new(); - private wf.Form? _parentForm; - - public NotificationManager(Naps2Config config, DesktopFormProvider desktopFormProvider) - { - _config = config; - desktopFormProvider.DesktopFormChanged += (_, _) => - { - if (_parentForm != null) - { - _parentForm.Resize -= parentForm_Resize; - } - _parentForm = desktopFormProvider.DesktopForm.ToNative(); - _parentForm.Resize += parentForm_Resize; - }; - } - - public void PdfSaved(string path) - { - Show(new PdfSavedNotifyWidget(path)); - } - - public void ImagesSaved(int imageCount, string path) - { - if (imageCount == 1) - { - Show(new OneImageSavedNotifyWidget(path)); - } - else if (imageCount > 1) - { - Show(new ImagesSavedNotifyWidget(imageCount, path)); - } - } - - public void DonatePrompt() - { - Show(new DonatePromptNotifyWidget()); - } - - public void OperationProgress(OperationProgress opModalProgress, IOperation op) - { - Show(new OperationProgressNotifyWidget(opModalProgress, op)); - } - - public void UpdateAvailable(IUpdateChecker updateChecker, UpdateInfo update) - { - Show(new UpdateAvailableNotifyWidget(updateChecker, update)); - } - - public void Rebuild() - { - var old = _slots.ToList(); - _slots.Clear(); - for (int i = 0; i < old.Count; i++) - { - var slot = old[i]; - if (slot != null) - { - Show(slot.Clone()); - } - } - } - - private void Show(NotifyWidgetBase n) - { - if (_config.Get(c => c.DisableSaveNotifications) && n is NotifyWidget) - { - return; - } - - Invoker.Current.SafeInvoke(() => - { - int slot = FillNextSlot(n); - n.Location = GetPosition(n, slot); - n.Resize += parentForm_Resize; - n.BringToFront(); - n.HideNotify += (sender, args) => ClearSlot(n); - n.ShowNotify(); - }); - } - - private void parentForm_Resize(object? sender, EventArgs e) - { - for (int i = 0; i < _slots.Count; i++) - { - var slot = _slots[i]; - if (slot != null) - { - slot.Location = GetPosition(slot, i); - } - } - } - - private void ClearSlot(NotifyWidgetBase n) - { - var index = _slots.IndexOf(n); - if (index != -1) - { - _parentForm!.Controls.Remove(n); - _slots[index] = null; - } - } - - private int FillNextSlot(NotifyWidgetBase n) - { - var index = _slots.IndexOf(null); - if (index == -1) - { - index = _slots.Count; - _slots.Add(n); - } - else - { - _slots[index] = n; - } - _parentForm!.Controls.Add(n); - return index; - } - - private Point GetPosition(NotifyWidgetBase n, int slot) - { - return new Point(_parentForm!.ClientSize.Width - n.Width - PADDING_X, - _parentForm.ClientSize.Height - n.Height - PADDING_Y - (n.Height + SPACING_Y) * slot); - } -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/NotifyWidget.Designer.cs b/NAPS2.Lib.WinForms/WinForms/NotifyWidget.Designer.cs deleted file mode 100644 index 4ed36d375b..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/NotifyWidget.Designer.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Eto.WinForms; -using NAPS2.EtoForms; - -namespace NAPS2.WinForms -{ - partial class NotifyWidget - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.components = new System.ComponentModel.Container(); - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(NotifyWidget)); - this.lblTitle = new System.Windows.Forms.Label(); - this.linkLabel1 = new System.Windows.Forms.LinkLabel(); - this.btnClose = new System.Windows.Forms.Button(); - this.hideTimer = new System.Windows.Forms.Timer(this.components); - this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components); - this.openFolderToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.contextMenuStrip1.SuspendLayout(); - this.SuspendLayout(); - // - // lblTitle - // - resources.ApplyResources(this.lblTitle, "lblTitle"); - this.lblTitle.Name = "lblTitle"; - // - // linkLabel1 - // - resources.ApplyResources(this.linkLabel1, "linkLabel1"); - this.linkLabel1.AutoEllipsis = true; - this.linkLabel1.Name = "linkLabel1"; - this.linkLabel1.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabel1_LinkClicked); - // - // btnClose - // - resources.ApplyResources(this.btnClose, "btnClose"); - this.btnClose.Cursor = System.Windows.Forms.Cursors.Hand; - this.btnClose.FlatAppearance.BorderSize = 0; - this.btnClose.Image = global::NAPS2.Icons.close.ToBitmap(); - this.btnClose.Name = "btnClose"; - this.btnClose.UseVisualStyleBackColor = true; - this.btnClose.Click += new System.EventHandler(this.btnClose_Click); - // - // hideTimer - // - this.hideTimer.Interval = 5000; - this.hideTimer.Tick += new System.EventHandler(this.hideTimer_Tick); - // - // contextMenuStrip1 - // - this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.openFolderToolStripMenuItem}); - this.contextMenuStrip1.Name = "contextMenuStrip1"; - resources.ApplyResources(this.contextMenuStrip1, "contextMenuStrip1"); - // - // openFolderToolStripMenuItem - // - this.openFolderToolStripMenuItem.Name = "openFolderToolStripMenuItem"; - resources.ApplyResources(this.openFolderToolStripMenuItem, "openFolderToolStripMenuItem"); - this.openFolderToolStripMenuItem.Click += new System.EventHandler(this.openFolderToolStripMenuItem_Click); - // - // NotifyWidget - // - resources.ApplyResources(this, "$this"); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(234)))), ((int)(((byte)(234)))), ((int)(((byte)(234))))); - this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; - this.Controls.Add(this.btnClose); - this.Controls.Add(this.linkLabel1); - this.Controls.Add(this.lblTitle); - this.Name = "NotifyWidget"; - this.MouseEnter += new System.EventHandler(this.NotifyWidget_MouseEnter); - this.MouseLeave += new System.EventHandler(this.NotifyWidget_MouseLeave); - this.contextMenuStrip1.ResumeLayout(false); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - protected System.Windows.Forms.Label lblTitle; - private System.Windows.Forms.LinkLabel linkLabel1; - private System.Windows.Forms.Button btnClose; - protected System.Windows.Forms.Timer hideTimer; - private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; - private System.Windows.Forms.ToolStripMenuItem openFolderToolStripMenuItem; - } -} diff --git a/NAPS2.Lib.WinForms/WinForms/NotifyWidget.cs b/NAPS2.Lib.WinForms/WinForms/NotifyWidget.cs deleted file mode 100644 index 7319d4e9b6..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/NotifyWidget.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Windows.Forms; - -namespace NAPS2.WinForms; - -public partial class NotifyWidget : NotifyWidgetBase -{ - private readonly string? _linkTarget; - private readonly string? _folderTarget; - - public NotifyWidget(string title, string linkLabel, string? linkTarget, string? folderTarget) - { - _linkTarget = linkTarget; - _folderTarget = folderTarget; - InitializeComponent(); - - lblTitle.Text = title; - linkLabel1.Text = linkLabel; - - if (lblTitle.Width > Width - 35) - { - Width = lblTitle.Width + 35; - } - if (lblTitle.Height > Height - 35) - { - Height = lblTitle.Height + 35; - } - - if (folderTarget == null) - { - contextMenuStrip1.Enabled = false; - } - - openFolderToolStripMenuItem.Text = UiStrings.OpenFolder; - } - - private void hideTimer_Tick(object sender, EventArgs e) - { - DoHideNotify(); - } - - protected void DoHideNotify() - { - InvokeHideNotify(); - hideTimer.Stop(); - } - - private void btnClose_Click(object sender, EventArgs e) - { - DoHideNotify(); - } - - private void NotifyWidget_MouseEnter(object sender, EventArgs e) - { - hideTimer.Stop(); - } - - private void NotifyWidget_MouseLeave(object sender, EventArgs e) - { - hideTimer.Start(); - } - - public override void ShowNotify() - { - hideTimer.Start(); - } - - protected virtual void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) - { - if (_linkTarget == null) - { - Log.Error("Link target should not be null"); - return; - } - if (e.Button == MouseButtons.Right) - { - contextMenuStrip1.Show(linkLabel1, linkLabel1.Location); - } - else - { - Process.Start(new ProcessStartInfo - { - UseShellExecute = true, - FileName = _linkTarget, - Verb = "open" - }); - } - } - - private void openFolderToolStripMenuItem_Click(object sender, EventArgs e) - { - Process.Start(new ProcessStartInfo - { - UseShellExecute = true, - FileName = _folderTarget, - Verb = "open" - }); - } -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/NotifyWidget.resx b/NAPS2.Lib.WinForms/WinForms/NotifyWidget.resx deleted file mode 100644 index c0a183547e..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/NotifyWidget.resx +++ /dev/null @@ -1,261 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - True - - - - Microsoft Sans Serif, 8.25pt, style=Bold - - - - NoControl - - - 7, 8 - - - 200, 0 - - - 0, 13 - - - 0 - - - lblTitle - - - System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - $this - - - 3 - - - Bottom, Left, Right - - - 7, 27 - - - 150, 13 - - - 1 - - - linkLabel1 - - - System.Windows.Forms.LinkLabel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - $this - - - 2 - - - Top, Right - - - Flat - - - Microsoft Sans Serif, 12pt - - - 132, 4 - - - 0, 0, 1, 1 - - - 20, 20 - - - 2 - - - btnClose - - - System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - $this - - - 1 - - - 17, 17 - - - 123, 17 - - - 140, 26 - - - contextMenuStrip1 - - - System.Windows.Forms.ContextMenuStrip, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 139, 22 - - - Open Folder - - - True - - - 6, 13 - - - 156, 46 - - - hideTimer - - - System.Windows.Forms.Timer, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - openFolderToolStripMenuItem - - - System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - NotifyWidget - - - System.Windows.Forms.UserControl, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/NotifyWidgetBase.cs b/NAPS2.Lib.WinForms/WinForms/NotifyWidgetBase.cs deleted file mode 100644 index ad6f7ced95..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/NotifyWidgetBase.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Windows.Forms; - -namespace NAPS2.WinForms; - -public class NotifyWidgetBase : UserControl -{ - public event EventHandler? HideNotify; - - protected void InvokeHideNotify() - { - Invoker.Current.SafeInvoke(() => HideNotify?.Invoke(this, EventArgs.Empty)); - } - - public virtual void ShowNotify() - { - } - - public virtual NotifyWidgetBase Clone() => throw new NotImplementedException(); -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/OneImageSavedNotifyWidget.cs b/NAPS2.Lib.WinForms/WinForms/OneImageSavedNotifyWidget.cs deleted file mode 100644 index 9cff30d026..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/OneImageSavedNotifyWidget.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace NAPS2.WinForms; - -public class OneImageSavedNotifyWidget : NotifyWidget -{ - private readonly string _path; - - public OneImageSavedNotifyWidget(string path) - : base(MiscResources.ImageSaved, Path.GetFileName(path), path, Path.GetDirectoryName(path)) - { - _path = path; - } - - public override NotifyWidgetBase Clone() => new OneImageSavedNotifyWidget(_path); -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/OperationProgressNotifyWidget.Designer.cs b/NAPS2.Lib.WinForms/WinForms/OperationProgressNotifyWidget.Designer.cs deleted file mode 100644 index d47d56982e..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/OperationProgressNotifyWidget.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -namespace NAPS2.WinForms -{ - partial class OperationProgressNotifyWidget - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.components = new System.ComponentModel.Container(); - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(OperationProgressNotifyWidget)); - this.lblTitle = new System.Windows.Forms.Label(); - this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components); - this.cancelToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.progressBar = new System.Windows.Forms.ProgressBar(); - this.lblNumber = new System.Windows.Forms.Label(); - this.contextMenuStrip1.SuspendLayout(); - this.SuspendLayout(); - // - // lblTitle - // - resources.ApplyResources(this.lblTitle, "lblTitle"); - this.lblTitle.Name = "lblTitle"; - this.lblTitle.MouseClick += new System.Windows.Forms.MouseEventHandler(this.OperationProgressNotifyWidget_Click); - // - // contextMenuStrip1 - // - this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.cancelToolStripMenuItem}); - this.contextMenuStrip1.Name = "contextMenuStrip1"; - resources.ApplyResources(this.contextMenuStrip1, "contextMenuStrip1"); - // - // cancelToolStripMenuItem - // - this.cancelToolStripMenuItem.Name = "cancelToolStripMenuItem"; - resources.ApplyResources(this.cancelToolStripMenuItem, "cancelToolStripMenuItem"); - this.cancelToolStripMenuItem.Click += new System.EventHandler(this.cancelToolStripMenuItem_Click); - // - // progressBar - // - resources.ApplyResources(this.progressBar, "progressBar"); - this.progressBar.MarqueeAnimationSpeed = 50; - this.progressBar.Name = "progressBar"; - this.progressBar.Style = System.Windows.Forms.ProgressBarStyle.Continuous; - this.progressBar.MouseClick += new System.Windows.Forms.MouseEventHandler(this.OperationProgressNotifyWidget_Click); - // - // lblNumber - // - resources.ApplyResources(this.lblNumber, "lblNumber"); - this.lblNumber.Name = "lblNumber"; - // - // OperationProgressNotifyWidget - // - resources.ApplyResources(this, "$this"); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(234)))), ((int)(((byte)(234)))), ((int)(((byte)(234))))); - this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; - this.ContextMenuStrip = this.contextMenuStrip1; - this.Controls.Add(this.lblNumber); - this.Controls.Add(this.progressBar); - this.Controls.Add(this.lblTitle); - this.Name = "OperationProgressNotifyWidget"; - this.MouseClick += new System.Windows.Forms.MouseEventHandler(this.OperationProgressNotifyWidget_Click); - this.contextMenuStrip1.ResumeLayout(false); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - protected System.Windows.Forms.Label lblTitle; - private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; - private System.Windows.Forms.ToolStripMenuItem cancelToolStripMenuItem; - private System.Windows.Forms.ProgressBar progressBar; - private System.Windows.Forms.Label lblNumber; - } -} diff --git a/NAPS2.Lib.WinForms/WinForms/OperationProgressNotifyWidget.cs b/NAPS2.Lib.WinForms/WinForms/OperationProgressNotifyWidget.cs deleted file mode 100644 index 474590b5c7..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/OperationProgressNotifyWidget.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Windows.Forms; - -namespace NAPS2.WinForms; - -public partial class OperationProgressNotifyWidget : NotifyWidgetBase -{ - private readonly OperationProgress _operationProgress; - private readonly IOperation _op; - - public OperationProgressNotifyWidget(OperationProgress operationProgress, IOperation op) - { - InitializeComponent(); - - _operationProgress = operationProgress; - _op = op; - - cancelToolStripMenuItem.Text = UiStrings.Cancel; - cancelToolStripMenuItem.Visible = op.AllowCancel; - op.StatusChanged += Op_StatusChanged; - op.Finished += Op_Finished; - } - - public override void ShowNotify() => DisplayProgress(); - - public override NotifyWidgetBase Clone() => new OperationProgressNotifyWidget(_operationProgress, _op); - - private void DisplayProgress() - { - var lblNumberRight = lblNumber.Right; - WinFormsOperationProgress.RenderStatus(_op, lblTitle, lblNumber, progressBar); - if (_op.Status?.IndeterminateProgress != true) - { - // Don't display the number if the progress bar is precise - // Otherwise, the widget will be too cluttered - // The number is only shown for OcrOperation at the moment - lblNumber.Text = ""; - } - lblNumber.Left = lblNumberRight - lblNumber.Width; - Width = Math.Max(Width, lblTitle.Width + lblNumber.Width + 22); - Height = Math.Max(Height, lblTitle.Height + 35); - } - - private void DoHideNotify() - { - _op.StatusChanged -= Op_StatusChanged; - _op.Finished -= Op_Finished; - InvokeHideNotify(); - } - - private void Op_StatusChanged(object? sender, EventArgs e) - { - Invoker.Current.SafeInvoke(DisplayProgress); - } - - private void Op_Finished(object? sender, EventArgs e) - { - DoHideNotify(); - } - - private void cancelToolStripMenuItem_Click(object? sender, EventArgs e) - { - _op.Cancel(); - cancelToolStripMenuItem.Enabled = false; - } - - private void OperationProgressNotifyWidget_Click(object? sender, MouseEventArgs e) - { - if (e.Button == MouseButtons.Left) - { - DoHideNotify(); - _operationProgress.ShowModalProgress(_op); - } - } -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/OperationProgressNotifyWidget.resx b/NAPS2.Lib.WinForms/WinForms/OperationProgressNotifyWidget.resx deleted file mode 100644 index b70d105acd..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/OperationProgressNotifyWidget.resx +++ /dev/null @@ -1,246 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - True - - - - NoControl - - - - 7, 8 - - - 200, 0 - - - 0, 13 - - - 0 - - - lblTitle - - - System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - $this - - - 3 - - - 123, 17 - - - 110, 22 - - - Cancel - - - 111, 26 - - - contextMenuStrip1 - - - System.Windows.Forms.ContextMenuStrip, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Bottom, Left, Right - - - 7, 26 - - - 138, 11 - - - 3 - - - progressBar - - - System.Windows.Forms.ProgressBar, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - $this - - - 2 - - - Top, Right - - - True - - - 145, 8 - - - 0, 13 - - - 4 - - - TopRight - - - lblNumber - - - System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - $this - - - 0 - - - True - - - 6, 13 - - - 150, 40 - - - cancelToolStripMenuItem - - - System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - OperationProgressNotifyWidget - - - NAPS2.WinForms.NotifyWidgetBase, NAPS2.Core, Version=6.0.1.27018, Culture=neutral, PublicKeyToken=null - - \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/PdfSavedNotifyWidget.cs b/NAPS2.Lib.WinForms/WinForms/PdfSavedNotifyWidget.cs deleted file mode 100644 index 1d0c6160b1..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/PdfSavedNotifyWidget.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace NAPS2.WinForms; - -public class PdfSavedNotifyWidget : NotifyWidget -{ - private readonly string _path; - - public PdfSavedNotifyWidget(string path) - : base(MiscResources.PdfSaved, Path.GetFileName(path), path, Path.GetDirectoryName(path)) - { - _path = path; - } - - public override NotifyWidgetBase Clone() => new PdfSavedNotifyWidget(_path); -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/SelectableListView.cs b/NAPS2.Lib.WinForms/WinForms/SelectableListView.cs index 3119ded8ff..1b8a5afb00 100644 --- a/NAPS2.Lib.WinForms/WinForms/SelectableListView.cs +++ b/NAPS2.Lib.WinForms/WinForms/SelectableListView.cs @@ -21,7 +21,7 @@ private void ListViewOnSelectedIndexChanged(object? sender, EventArgs e) if (!_refreshing) { _refreshing = true; - Selection = ListSelection.From(_listView.SelectedItems.Cast().Select(x => (T) x.Tag)); + Selection = ListSelection.From(_listView.SelectedItems.Cast().Select(x => (T) x.Tag!)); _refreshing = false; } } @@ -37,7 +37,7 @@ public ListSelection Selection _refreshing = true; for (int i = 0; i < _listView.Items.Count; i++) { - _listView.Items[i].Selected = _selection.Contains((T) _listView.Items[i].Tag); + _listView.Items[i].Selected = _selection.Contains((T) _listView.Items[i].Tag!); } _refreshing = false; } @@ -56,7 +56,7 @@ public void RefreshItems(IEnumerable items, Func labelFunc, Func 0 && width > MaxTextWidth) { var words = text.Split(' '); @@ -46,7 +51,7 @@ private int MeasureTextWidth(string text, ref bool wrap) { var left = string.Join(" ", words.Take(words.Length - i)); var right = string.Join(" ", words.Skip(words.Length - i)); - var wrappedWidth = (int) Math.Ceiling(g.MeasureString(left + "\n" + right, Font).Width); + var wrappedWidth = TextRenderer.MeasureText(left + "\n" + right, Font).Width; if (wrappedWidth < width) { width = wrappedWidth; @@ -59,7 +64,7 @@ private int MeasureTextWidth(string text, ref bool wrap) protected override void OnPaint(PaintEventArgs e) { - if (Owner == null) + if (Owner == null || FirstImage == null || SecondImage == null) return; ToolStripRenderer renderer = ToolStripManager.Renderer; diff --git a/NAPS2.Lib.WinForms/WinForms/ToolbarFormatter.cs b/NAPS2.Lib.WinForms/WinForms/ToolbarFormatter.cs index a5d49aa859..593320db48 100644 --- a/NAPS2.Lib.WinForms/WinForms/ToolbarFormatter.cs +++ b/NAPS2.Lib.WinForms/WinForms/ToolbarFormatter.cs @@ -1,4 +1,3 @@ -using System.Drawing; using System.Windows.Forms; using DockStyle = System.Windows.Forms.DockStyle; @@ -11,27 +10,37 @@ public class ToolbarFormatter { private const int FORM_PIXEL_BUFFER = 30; private readonly StringWrapper _stringWrapper; + private readonly HashSet _charsUsedForAltHotkeys = []; public ToolbarFormatter(StringWrapper stringWrapper) { _stringWrapper = stringWrapper; } - public void RelayoutToolbar(ToolStrip tStrip) + public void RelayoutToolbar(ToolStrip tStrip, float scaleFactor) { if (tStrip.Parent == null) return; // Resize and wrap text as necessary - using (var g = tStrip.CreateGraphics()) + foreach (var btn in tStrip.Items.OfType()) { - foreach (var btn in tStrip.Items.OfType()) + btn.Text = _stringWrapper.Wrap(btn.Text ?? "", (int) (80 * scaleFactor), btn.Font); + if (!btn.Text.Contains("&")) { - btn.Text = _stringWrapper.Wrap(btn.Text ?? "", 80, g, btn.Font); + var charToUse = btn.Text.Where(char.IsLetter) + .FirstOrDefault(c => !_charsUsedForAltHotkeys.Contains(char.ToLowerInvariant(c))); + if (charToUse != default) + { + _charsUsedForAltHotkeys.Add(char.ToLowerInvariant(charToUse)); + var index = btn.Text.IndexOf(charToUse); + btn.Text = btn.Text.Substring(0, index) + @"&" + btn.Text.Substring(index); + } } } - ResetToolbarMargin(tStrip); + ResetToolbarMargin(tStrip, scaleFactor); // Recalculate sizes - tStrip.BeginInvoke(() => + Invoker.Current.InvokeDispatch(() => { + if (tStrip.Parent == null) return; if (tStrip.Parent.Dock == DockStyle.Top || tStrip.Parent.Dock == DockStyle.Bottom) { // TODO: If we cache the used width, this check doesn't require any layout - so we can run it on form resize to see if we need to relayout @@ -39,51 +48,58 @@ public void RelayoutToolbar(ToolStrip tStrip) var usedWidth = tStrip.Items.OfType().Select(btn => btn.Width + btn.Margin.Horizontal) .Sum(); var form = tStrip.FindForm(); - if (form != null && usedWidth > form.Width - FORM_PIXEL_BUFFER) + if (form != null && usedWidth > form.Width - (int) (FORM_PIXEL_BUFFER * scaleFactor)) { - ShrinkToolbarMargin(tStrip); + ShrinkToolbarMargin(tStrip, scaleFactor); } } }); } - private void ResetToolbarMargin(ToolStrip tStrip) + private void ResetToolbarMargin(ToolStrip tStrip, float scaleFactor) { + int s1 = (int) Math.Round(1 * scaleFactor); + int s2 = (int) Math.Round(2 * scaleFactor); + int s5 = (int) Math.Round(5 * scaleFactor); + int s10 = (int) Math.Round(10 * scaleFactor); foreach (var btn in tStrip.Items.OfType()) { if (btn is ToolStripSplitButton) { - if (tStrip.Parent.Dock == DockStyle.Left || tStrip.Parent.Dock == DockStyle.Right) + if (tStrip.Parent!.Dock == DockStyle.Left || tStrip.Parent.Dock == DockStyle.Right) { - btn.Margin = new Padding(10, 1, 5, 2); + btn.Margin = new Padding(s10, s1, s5, s2); } else { - btn.Margin = new Padding(5, 1, 5, 2); + btn.Margin = new Padding(s5, s1, s5, s2); } } else if (btn is ToolStripDoubleButton) { - btn.Padding = new Padding(5, 0, 5, 0); + btn.Padding = new Padding(s5, 0, s5, 0); } - else if (tStrip.Parent.Dock == DockStyle.Left || tStrip.Parent.Dock == DockStyle.Right) + else if (tStrip.Parent!.Dock == DockStyle.Left || tStrip.Parent.Dock == DockStyle.Right) { - btn.Margin = new Padding(0, 1, 5, 2); + btn.Margin = new Padding(0, s1, s5, s2); } else { - btn.Padding = new Padding(10, 0, 10, 0); + btn.Padding = new Padding(s10, 0, s10, 0); } } } - private void ShrinkToolbarMargin(ToolStrip tStrip) + private void ShrinkToolbarMargin(ToolStrip tStrip, float scaleFactor) { + int s1 = (int) Math.Round(1 * scaleFactor); + int s2 = (int) Math.Round(2 * scaleFactor); + int s5 = (int) Math.Round(5 * scaleFactor); foreach (var btn in tStrip.Items.OfType()) { if (btn is ToolStripSplitButton) { - btn.Margin = new Padding(0, 1, 0, 2); + btn.Margin = new Padding(0, s1, 0, s2); } else if (btn is ToolStripDoubleButton) { @@ -91,7 +107,7 @@ private void ShrinkToolbarMargin(ToolStrip tStrip) } else { - btn.Padding = new Padding(5, 0, 5, 0); + btn.Padding = new Padding(s5, 0, s5, 0); } } } diff --git a/NAPS2.Lib.WinForms/WinForms/UpdateAvailableNotifyWidget.cs b/NAPS2.Lib.WinForms/WinForms/UpdateAvailableNotifyWidget.cs deleted file mode 100644 index 23ac8130cc..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/UpdateAvailableNotifyWidget.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Windows.Forms; -using NAPS2.Update; - -namespace NAPS2.WinForms; - -public class UpdateAvailableNotifyWidget : NotifyWidget -{ - private readonly IUpdateChecker _updateChecker; - private readonly UpdateInfo _update; - - public UpdateAvailableNotifyWidget(IUpdateChecker updateChecker, UpdateInfo update) - : base(MiscResources.UpdateAvailable, string.Format(MiscResources.Install, update.Name), null, null) - { - _updateChecker = updateChecker; - _update = update; - - hideTimer.Interval = 60 * 1000; - } - - public override NotifyWidgetBase Clone() => new UpdateAvailableNotifyWidget(_updateChecker, _update); - - protected override void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) - { - _updateChecker.StartUpdate(_update); - DoHideNotify(); - } -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/WinFormsExtensions.cs b/NAPS2.Lib.WinForms/WinForms/WinFormsExtensions.cs index 82829a5418..b423ede4fe 100644 --- a/NAPS2.Lib.WinForms/WinForms/WinFormsExtensions.cs +++ b/NAPS2.Lib.WinForms/WinForms/WinFormsExtensions.cs @@ -1,37 +1,37 @@ namespace NAPS2.WinForms; -using wf = System.Windows.Forms; +using WF = System.Windows.Forms; public static class WinFormsExtensions { - public static wf.MessageBoxIcon ToWinForms(this MessageBoxIcon icon) + public static WF.MessageBoxIcon ToWinForms(this MessageBoxIcon icon) { return icon switch { - MessageBoxIcon.Information => wf.MessageBoxIcon.Information, - MessageBoxIcon.Warning => wf.MessageBoxIcon.Warning, - _ => wf.MessageBoxIcon.None + MessageBoxIcon.Information => WF.MessageBoxIcon.Information, + MessageBoxIcon.Warning => WF.MessageBoxIcon.Warning, + _ => WF.MessageBoxIcon.None }; } - public static wf.DockStyle ToWinForms(this DockStyle dock) + public static WF.DockStyle ToWinForms(this DockStyle dock) { return dock switch { - DockStyle.Bottom => wf.DockStyle.Bottom, - DockStyle.Left => wf.DockStyle.Left, - DockStyle.Right => wf.DockStyle.Right, - _ => wf.DockStyle.Top + DockStyle.Bottom => WF.DockStyle.Bottom, + DockStyle.Left => WF.DockStyle.Left, + DockStyle.Right => WF.DockStyle.Right, + _ => WF.DockStyle.Top }; } - public static DockStyle ToConfig(this wf.DockStyle dock) + public static DockStyle ToConfig(this WF.DockStyle dock) { return dock switch { - wf.DockStyle.Bottom => DockStyle.Bottom, - wf.DockStyle.Left => DockStyle.Left, - wf.DockStyle.Right => DockStyle.Right, + WF.DockStyle.Bottom => DockStyle.Bottom, + WF.DockStyle.Left => DockStyle.Left, + WF.DockStyle.Right => DockStyle.Right, _ => DockStyle.Top }; } diff --git a/NAPS2.Lib.WinForms/WinForms/WinFormsOperationProgress.cs b/NAPS2.Lib.WinForms/WinForms/WinFormsOperationProgress.cs deleted file mode 100644 index 09faf7c437..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/WinFormsOperationProgress.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Collections.Immutable; -using System.Windows.Forms; -using NAPS2.EtoForms; -using NAPS2.EtoForms.Ui; - -namespace NAPS2.WinForms; - -public class WinFormsOperationProgress : OperationProgress -{ - private readonly IFormFactory _formFactory; - private readonly INotificationManager _notificationManager; - private readonly Naps2Config _config; - - private readonly HashSet _activeOperations = new HashSet(); - - public WinFormsOperationProgress(IFormFactory formFactory, INotificationManager notificationManager, Naps2Config config) - { - _formFactory = formFactory; - _notificationManager = notificationManager; - _config = config; - } - - public override void Attach(IOperation op) - { - lock (this) - { - if (!_activeOperations.Contains(op)) - { - _activeOperations.Add(op); - op.Finished += (sender, args) => _activeOperations.Remove(op); - if (op.IsFinished) _activeOperations.Remove(op); - } - } - } - - public override void ShowProgress(IOperation op) - { - if (_config.Get(c => c.BackgroundOperations).Contains(op.GetType().Name)) - { - ShowBackgroundProgress(op); - } - else - { - ShowModalProgress(op); - } - } - - public override void ShowModalProgress(IOperation op) - { - Attach(op); - - var bgOps = _config.Get(c => c.BackgroundOperations) ?? ImmutableHashSet.Empty; - bgOps = bgOps.Remove(op.GetType().Name); - _config.User.Set(c => c.BackgroundOperations, bgOps); - - if (!op.IsFinished) - { - var form = _formFactory.Create(); - form.Operation = op; - form.ShowModal(); - } - - if (!op.IsFinished) - { - ShowBackgroundProgress(op); - } - } - - public override void ShowBackgroundProgress(IOperation op) - { - Attach(op); - - var bgOps = _config.Get(c => c.BackgroundOperations) ?? ImmutableHashSet.Empty; - bgOps = bgOps.Add(op.GetType().Name); - _config.User.Set(c => c.BackgroundOperations, bgOps); - - if (!op.IsFinished) - { - Invoker.Current.SafeInvoke(() => _notificationManager.OperationProgress(this, op)); - } - } - - public static void RenderStatus(IOperation op, Label textLabel, Label numberLabel, ProgressBar progressBar) - { - var status = op.Status ?? new OperationStatus(); - textLabel.Text = status.StatusText; - progressBar.Style = status.MaxProgress == 1 || status.IndeterminateProgress - ? ProgressBarStyle.Marquee - : ProgressBarStyle.Continuous; - if (status.MaxProgress == 1 || status.ProgressType == OperationProgressType.None) - { - numberLabel.Text = ""; - } - else if (status.MaxProgress == 0) - { - numberLabel.Text = ""; - progressBar.Maximum = 1; - progressBar.Value = 0; - } - else if (status.ProgressType == OperationProgressType.BarOnly) - { - numberLabel.Text = ""; - progressBar.Maximum = status.MaxProgress; - progressBar.Value = status.CurrentProgress; - } - else - { - numberLabel.Text = status.ProgressType == OperationProgressType.MB - ? string.Format(MiscResources.SizeProgress, (status.CurrentProgress / 1000000.0).ToString("f1"), (status.MaxProgress / 1000000.0).ToString("f1")) - : string.Format(MiscResources.ProgressFormat, status.CurrentProgress, status.MaxProgress); - progressBar.Maximum = status.MaxProgress; - progressBar.Value = status.CurrentProgress; - } - // Force the progress bar to render immediately - if (progressBar.Value < progressBar.Maximum) - { - progressBar.Value += 1; - progressBar.Value -= 1; - } - } - - public override List ActiveOperations - { - get - { - lock (_activeOperations) - { - return _activeOperations.ToList(); - } - } - } -} \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/WinForms/WinFormsTwainHandleManager.cs b/NAPS2.Lib.WinForms/WinForms/WinFormsTwainHandleManager.cs deleted file mode 100644 index dc03e00254..0000000000 --- a/NAPS2.Lib.WinForms/WinForms/WinFormsTwainHandleManager.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Windows.Forms; -using NAPS2.Platform.Windows; -using NAPS2.Scan.Internal.Twain; - -namespace NAPS2.WinForms; - -public class WinFormsTwainHandleManager : TwainHandleManager -{ - private readonly Form _baseForm; - private Form? _parentForm; - private IntPtr _disabledWindow; - private bool _disposed; - - public WinFormsTwainHandleManager(Form baseForm) - { - _baseForm = baseForm; - } - - public override IntPtr GetDsmHandle(IntPtr dialogParent, bool useNativeUi) - { - return _baseForm.Handle; - } - - public override IntPtr GetEnableHandle(IntPtr dialogParent, bool useNativeUi) - { - if (!useNativeUi || dialogParent == IntPtr.Zero) - { - return _baseForm.Handle; - } - _parentForm = new BackgroundForm(); - // At the Windows API level, a modal window is implemented by doing two things: - // 1. Setting the parent on the child window - // 2. Disabling the parent window - // We do this rather than calling ShowDialog to avoid blocking the thread. - _parentForm.Show(new Win32Window(dialogParent)); - Win32.EnableWindow(dialogParent, false); - _disabledWindow = dialogParent; - return _parentForm.Handle; - } - - public override void Dispose() - { - if (_disposed) return; - _disposed = true; - _parentForm?.Close(); - if (_disabledWindow != IntPtr.Zero) - { - Win32.EnableWindow(_disabledWindow, true); - } - } -} \ No newline at end of file diff --git a/NAPS2.Lib/AutofacEmailProviderFactory.cs b/NAPS2.Lib/AutofacEmailProviderFactory.cs index 3f51df4b51..42a22509ec 100644 --- a/NAPS2.Lib/AutofacEmailProviderFactory.cs +++ b/NAPS2.Lib/AutofacEmailProviderFactory.cs @@ -1,11 +1,10 @@ using Autofac; using NAPS2.ImportExport.Email; -using NAPS2.ImportExport.Email.Mapi; using NAPS2.ImportExport.Email.Oauth; namespace NAPS2; -public class AutofacEmailProviderFactory : IEmailProviderFactory +internal class AutofacEmailProviderFactory : IEmailProviderFactory { private readonly IComponentContext _container; @@ -20,10 +19,16 @@ public IEmailProvider Create(EmailProviderType type) { case EmailProviderType.Gmail: return _container.Resolve(); + case EmailProviderType.OutlookNew: + return _container.Resolve(); case EmailProviderType.OutlookWeb: return _container.Resolve(); + case EmailProviderType.Thunderbird: + return _container.Resolve(); + case EmailProviderType.AppleMail: + return _container.Resolve(); default: - return _container.Resolve(); + return _container.Resolve(new NamedParameter("systemDefault", true)); } } diff --git a/NAPS2.Lib/Automation/AutomatedScanning.cs b/NAPS2.Lib/Automation/AutomatedScanning.cs index a35f36a39e..708b8e2c1c 100644 --- a/NAPS2.Lib/Automation/AutomatedScanning.cs +++ b/NAPS2.Lib/Automation/AutomatedScanning.cs @@ -1,24 +1,27 @@ using System.Threading; +using NAPS2.Dependencies; using NAPS2.EtoForms; using NAPS2.ImportExport; using NAPS2.ImportExport.Email; using NAPS2.ImportExport.Images; -using NAPS2.ImportExport.Pdf; using NAPS2.Lang.ConsoleResources; using NAPS2.Ocr; +using NAPS2.Pdf; using NAPS2.Recovery; using NAPS2.Scan; +using NAPS2.Scan.Exceptions; +using NAPS2.Scan.Internal; using NAPS2.Serialization; namespace NAPS2.Automation; -public class AutomatedScanning +internal class AutomatedScanning { private readonly ImageContext _imageContext; private readonly IEmailProviderFactory _emailProviderFactory; private readonly IScanPerformer _scanPerformer; private readonly ErrorOutput _errorOutput; - private readonly IScannedImageImporter _scannedImageImporter; + private readonly FileImporter _fileImporter; private readonly IOperationFactory _operationFactory; private readonly TesseractLanguageManager _tesseractLanguageManager; private readonly IFormFactory _formFactory; @@ -26,9 +29,11 @@ public class AutomatedScanning private readonly IProfileManager _profileManager; private readonly RecoveryStorageManager _recoveryStorageManager; private readonly ScanningContext _scanningContext; + private readonly IScanBridgeFactory _scanBridgeFactory; private readonly ConsoleOutput _output; private readonly AutomatedScanningOptions _options; + private readonly CancellationTokenSource _cts = new(); private PdfEncryption _parsedEncryptConfigOption = null!; private List> _scanList = null!; private int _pagesScanned; @@ -36,12 +41,14 @@ public class AutomatedScanning private Placeholders _placeholders = null!; private List _actualOutputPaths = null!; private OcrParams _ocrParams = null!; + private PageDimensions? _pageDimensions; public AutomatedScanning(ConsoleOutput output, AutomatedScanningOptions options, ImageContext imageContext, IScanPerformer scanPerformer, ErrorOutput errorOutput, IEmailProviderFactory emailProviderFactory, - IScannedImageImporter scannedImageImporter, IOperationFactory operationFactory, + FileImporter fileImporter, IOperationFactory operationFactory, TesseractLanguageManager tesseractLanguageManager, IFormFactory formFactory, Naps2Config config, - IProfileManager profileManager, RecoveryStorageManager recoveryStorageManager, ScanningContext scanningContext) + IProfileManager profileManager, RecoveryStorageManager recoveryStorageManager, ScanningContext scanningContext, + IScanBridgeFactory scanBridgeFactory) { _output = output; _options = options; @@ -49,7 +56,7 @@ public AutomatedScanning(ConsoleOutput output, AutomatedScanningOptions options, _scanPerformer = scanPerformer; _errorOutput = errorOutput; _emailProviderFactory = emailProviderFactory; - _scannedImageImporter = scannedImageImporter; + _fileImporter = fileImporter; _operationFactory = operationFactory; _tesseractLanguageManager = tesseractLanguageManager; _formFactory = formFactory; @@ -57,6 +64,7 @@ public AutomatedScanning(ConsoleOutput output, AutomatedScanningOptions options, _profileManager = profileManager; _recoveryStorageManager = recoveryStorageManager; _scanningContext = scanningContext; + _scanBridgeFactory = scanBridgeFactory; } public IEnumerable AllImages => _scanList.SelectMany(x => x); @@ -71,6 +79,15 @@ private void OutputVerbose(string value, params object[] args) public async Task Execute() { + Console.CancelKeyPress += (_, e) => + { + if (!_cts.IsCancellationRequested) + { + Console.WriteLine(ConsoleResources.Cancelling); + _cts.Cancel(); + e.Cancel = true; + } + }; bool hasUnexpectedException = false; try { @@ -83,25 +100,33 @@ public async Task Execute() if (_options.Install != null) { - InstallComponents(); + await InstallComponents(); if (_options.OutputPath == null && _options.EmailFileName == null && !_options.AutoSave) { return; } } + if (_options.ListDevices) + { + await ListDevices(); + return; + } + if (!PreCheckOverwriteFile()) { return; } - _scanList = new List>(); + _scanList = []; if (_options.ImportPath != null) { await ImportImages(); } + if (_cts.IsCancellationRequested) return; + ConfigureOcr(); if (_options.Number > 0) @@ -110,10 +135,15 @@ public async Task Execute() { return; } - + if (!await SetProfileOverrides(profile)) + { + return; + } await PerformScan(profile); } + if (_cts.IsCancellationRequested) return; + ReorderScannedImages(); if (_options.OutputPath != null) @@ -126,11 +156,20 @@ public async Task Execute() await EmailScannedImages(); } } + catch (ScanDriverException ex) when (ex is not ScanDriverUnknownException) + { + _output.Writer.WriteLine(ex.Message); + } catch (Exception ex) { hasUnexpectedException = true; Log.FatalException("An error occurred that caused the console application to close.", ex); _output.Writer.WriteLine(ConsoleResources.UnexpectedError); + _output.Writer.WriteLine(ex.Message); + if (ex.InnerException != null) + { + _output.Writer.WriteLine(ex.InnerException.Message); + } } finally { @@ -148,6 +187,11 @@ public async Task Execute() private void ConfigureOcr() { + if (_options.NoProfile) + { + // Don't use OCR-enabled settings from the GUI if --noprofile is set + _config.Run.Set(c => c.EnableOcr, false); + } bool canUseOcr = IsPdfFile(_options.OutputPath) || IsPdfFile(_options.EmailFileName); if (!canUseOcr) { @@ -161,48 +205,64 @@ private void ConfigureOcr() { _config.Run.Set(c => c.EnableOcr, true); } - _config.Run.Set(c => c.OcrLanguageCode, _options.OcrLang); + if (!string.IsNullOrEmpty(_options.OcrLang)) + { + _config.Run.Set(c => c.OcrLanguageCode, _options.OcrLang); + } _ocrParams = _config.DefaultOcrParams(); } - private void InstallComponents() + private async Task InstallComponents() + { + var availableComponents = new List(); + availableComponents.AddRange(_tesseractLanguageManager.LanguageComponents); + + var componentDict = availableComponents.ToDictionary(x => x.Id.ToLowerInvariant()); + var installId = _options.Install!.ToLowerInvariant(); + if (!componentDict.TryGetValue(installId, out var toInstall)) + { + _output.Writer.WriteLine(ConsoleResources.ComponentNotAvailable); + return; + } + if (toInstall.IsInstalled) + { + _output.Writer.WriteLine(ConsoleResources.ComponentAlreadyInstalled); + return; + } + var downloadController = new DownloadController(_scanningContext); + if (toInstall.Id.StartsWith("ocr-", StringComparison.InvariantCulture) && + componentDict.TryGetValue("ocr", out var ocrExe) && !ocrExe.IsInstalled) + { + downloadController.QueueFile(ocrExe); + if (_options.Verbose) + { + _output.Writer.WriteLine(ConsoleResources.Installing, ocrExe.Id); + } + } + downloadController.QueueFile(toInstall); + if (_options.Verbose) + { + _output.Writer.WriteLine(ConsoleResources.Installing, toInstall.Id); + } + downloadController.DownloadError += (_, _) => + { + _errorOutput.DisplayError(MiscResources.FilesCouldNotBeDownloaded); + }; + await downloadController.StartDownloadsAsync(); + } + + private async Task ListDevices() { - // TODO: Extract out a DownloadController from FDownloadProgress and use that - throw new NotImplementedException(); - - // var availableComponents = new List(); - // availableComponents.AddRange(_tesseractLanguageManager.LanguageComponents); - // - // var componentDict = availableComponents.ToDictionary(x => x.Id.ToLowerInvariant()); - // var installId = _options.Install!.ToLowerInvariant(); - // if (!componentDict.TryGetValue(installId, out var toInstall)) - // { - // _output.Writer.WriteLine(ConsoleResources.ComponentNotAvailable); - // return; - // } - // if (toInstall.IsInstalled) - // { - // _output.Writer.WriteLine(ConsoleResources.ComponentAlreadyInstalled); - // return; - // } - // // Using a form here is not ideal (since this is supposed to be a console app), but good enough for now - // // Especially considering wia/twain often show forms anyway - // var progressForm = _formFactory.Create(); - // if (toInstall.Id.StartsWith("ocr-", StringComparison.InvariantCulture) && - // componentDict.TryGetValue("ocr", out var ocrExe) && !ocrExe.IsInstalled) - // { - // progressForm.QueueFile(ocrExe); - // if (_options.Verbose) - // { - // _output.Writer.WriteLine(ConsoleResources.Installing, ocrExe.Id); - // } - // } - // progressForm.QueueFile(toInstall); - // if (_options.Verbose) - // { - // _output.Writer.WriteLine(ConsoleResources.Installing, toInstall.Id); - // } - // progressForm.ShowDialog(); + var profile = new ScanProfile + { + DriverName = ScanPerformer.SystemDefaultDriverName + }; + await SetProfileOverrides(profile); + + await foreach (var device in _scanPerformer.GetDevices(profile, _cts.Token)) + { + _output.Writer.WriteLine(device.Name); + } } private void ReorderScannedImages() @@ -216,7 +276,7 @@ private void ReorderScannedImages() foreach (var scan in _scanList) { // To take advantage of the existing mutation logic we wrap the scan in a UiImageList then copy it back - var imageList = new UiImageList(scan.Select(x => new UiImage(x)).ToList()); + var imageList = UiImageList.FromImages(scan.Select(x => new UiImage(x)).ToList()); if (_options.AltDeinterleave) { @@ -284,9 +344,9 @@ private async Task ImportImages() PatchTOnly = true } }; - await foreach (var image in _scannedImageImporter.Import(actualPath, importParams)) + await foreach (var image in _fileImporter.Import(actualPath, importParams, _cts.Token)) { - scan.Add(image); + scan.Add(PostProcessImportedImage(image)); } _scanList.Add(scan); } @@ -306,6 +366,22 @@ private async Task ImportImages() } } + private ProcessedImage PostProcessImportedImage(ProcessedImage image) + { + // We separate post-processing for scanned images (as part of the scanning pipeline) and imported images (here). + // The reason is that post-processing affects OCR, which runs as part of the scanning pipeline. + if (_options.RotateDegrees != null) + { + image = image.WithTransform(new RotationTransform(_options.RotateDegrees.Value), true); + } + if (_options.Deskew) + { + using var rendered = image.Render(); + image = image.WithTransform(Deskewer.GetDeskewTransform(rendered), true); + } + return image; + } + private async Task EmailScannedImages() { if (_scanList.Count == 0) @@ -329,6 +405,7 @@ private async Task EmailScannedImages() message.Recipients.AddRange(EmailRecipient.FromText(EmailRecipientType.Cc, _options.EmailCc)); message.Recipients.AddRange(EmailRecipient.FromText(EmailRecipientType.Bcc, _options.EmailBcc)); + // TODO: We may want to normalize this to use SavePdfOperation's email functionality var tempFolder = new DirectoryInfo(Path.Combine(Paths.Temp, Path.GetRandomFileName())); tempFolder.Create(); try @@ -346,7 +423,11 @@ private async Task EmailScannedImages() { string attachmentName = _placeholders.Substitute(_options.EmailFileName!, false, i++, _scanList.Count > 1 ? digits : 0); - message.Attachments.Add(new EmailAttachment(path, attachmentName)); + message.Attachments.Add(new EmailAttachment + { + FilePath = path, + AttachmentName = attachmentName + }); } } else @@ -374,7 +455,7 @@ private async Task EmailScannedImages() } OutputVerbose(ConsoleResources.SendingEmail); - if (await _emailProviderFactory.Default.SendEmail(message)) + if (await _emailProviderFactory.Default.SendEmail(message, _cts.Token)) { OutputVerbose(ConsoleResources.EmailSent); } @@ -391,10 +472,14 @@ private async Task EmailScannedImages() private void AttachFilesInFolder(DirectoryInfo folder, EmailMessage message) { - foreach (var file in folder.EnumerateFiles()) + foreach (var file in folder.EnumerateFiles().OrderBy(x => x.Name)) { OutputVerbose(ConsoleResources.Attaching, file.Name); - message.Attachments.Add(new EmailAttachment(file.FullName, file.Name)); + message.Attachments.Add(new EmailAttachment + { + FilePath = file.FullName, + AttachmentName = file.Name + }); } } @@ -402,7 +487,7 @@ public bool ValidateOptions() { // Most validation is done by the CommandLineParser library, but some constraints that can't be represented by that API need to be checked here if (_options.OutputPath == null && _options.EmailFileName == null && _options.Install == null && - !_options.AutoSave) + !_options.AutoSave && !_options.ListDevices) { _errorOutput.DisplayError(ConsoleResources.OutputOrEmailRequired); return false; @@ -413,6 +498,37 @@ public bool ValidateOptions() return false; } + if ((_options.Device != null || _options.ListDevices) && _options.Driver == null) + { + _errorOutput.DisplayError(string.Format(ConsoleResources.DriverRequired, SupportedDriversString)); + return false; + } + + if (_options.Driver != null) + { + if (ScanPerformer.ParseDriver(_options.Driver) == Driver.Default) + { + _errorOutput.DisplayError(string.Format(ConsoleResources.InvalidDriver, SupportedDriversString)); + return false; + } + } + + if (_options.PageSize != null) + { + var pageSize = PageSize.Parse(_options.PageSize); + if (pageSize == null) + { + _errorOutput.DisplayError(ConsoleResources.CouldntParsePageSize); + return false; + } + _pageDimensions = new PageDimensions + { + Width = pageSize.Width, + Height = pageSize.Height, + Unit = (LocalizedPageSizeUnit) pageSize.Unit + }; + } + if (new[] { _options.Interleave, _options.Deinterleave, _options.AltInterleave, _options.AltDeinterleave } .Count(x => x) > 1) { @@ -437,6 +553,9 @@ public bool ValidateOptions() return true; } + private string SupportedDriversString => string.Join(", ", + ScanOptionsValidator.SupportedDrivers.Select(x => x.ToString().ToLowerInvariant())); + private async Task ExportScannedImages() { if (_scanList.Count == 0) @@ -483,6 +602,7 @@ private async Task DoExportToImageFiles(string outputPath) foreach (var scan in _scanList) { var op = _operationFactory.Create(); + _cts.Token.Register(op.Cancel); int i = -1; op.StatusChanged += (sender, args) => { @@ -506,6 +626,7 @@ private async Task DoExportToPdf(string path, bool email) { var defaults = InternalDefaults.GetCommonConfig(); + _config.Run.Set(c => c.PdfSettings.SinglePagePdfs, false); if (!_options.UseSavedMetadata) { _config.Run.Set(c => c.PdfSettings.Metadata, defaults.PdfSettings.Metadata); @@ -561,14 +682,15 @@ private async Task DoExportToPdf(string path, bool email) _config.Run.Set(c => c.PdfSettings.Compat, compat); int scanIndex = 0; - _actualOutputPaths = new List(); + _actualOutputPaths = []; foreach (var fileContents in _scanList) { var op = _operationFactory.Create(); + _cts.Token.Register(op.Cancel); int i = -1; op.StatusChanged += (sender, args) => { - if (op.Status.CurrentProgress > i) + if (op.Status.CurrentProgress > i && op.Status.CurrentProgress < op.Status.MaxProgress) { OutputVerbose(ConsoleResources.ExportingPage, op.Status.CurrentProgress + 1, fileContents.Count); i = op.Status.CurrentProgress; @@ -576,7 +698,7 @@ private async Task DoExportToPdf(string path, bool email) }; int digits = (int) Math.Floor(Math.Log10(_scanList.Count)) + 1; string actualPath = _placeholders.Substitute(path, true, scanIndex++, _scanList.Count > 1 ? digits : 0); - op.Start(actualPath, _placeholders, fileContents, _config.Get(c => c.PdfSettings), _ocrParams, email, null); + op.Start(actualPath, _placeholders, fileContents, _config.Get(c => c.PdfSettings), _ocrParams); if (!await op.Success) { return false; @@ -608,14 +730,22 @@ private async Task PerformScan(ScanProfile profile) _totalPagesScanned = 0; foreach (int i in Enumerable.Range(1, _options.Number)) { - if (_options.Delay > 0) + if (i > 1 || !_options.FirstNow) { - OutputVerbose(ConsoleResources.Waiting, _options.Delay); - Thread.Sleep(_options.Delay); + if (_options.WaitScan) + { + OutputVerbose(ConsoleResources.PressEnterToScan); + Console.ReadLine(); + } + if (_options.Delay > 0) + { + OutputVerbose(ConsoleResources.Waiting, _options.Delay); + Thread.Sleep(_options.Delay); + } } OutputVerbose(ConsoleResources.StartingScan, i, _options.Number); _pagesScanned = 0; - _scanList.Add(new List()); + _scanList.Add([]); var scanParams = new ScanParams { NoUI = !_options.Progress, @@ -623,7 +753,7 @@ private async Task PerformScan(ScanProfile profile) DetectPatchT = _options.SplitPatchT, OcrParams = _ocrParams }; - await foreach (var image in _scanPerformer.PerformScan(profile, scanParams)) + await foreach (var image in _scanPerformer.PerformScan(profile, scanParams, cancelToken: _cts.Token)) { ReceiveScannedImage(image); } @@ -635,18 +765,27 @@ private bool GetProfile(out ScanProfile profile) { try { - if (_options.ProfileName == null) + if (_options.NoProfile) { - // If no profile is specified, use the default (if there is one) - profile = _profileManager.Profiles.Single(x => x.IsDefault); + profile = new ScanProfile { Version = ScanProfile.CURRENT_VERSION }; } - else + else if (_options.ProfileName != null) { // Use the profile with the specified name (try case-sensitive first, then case-insensitive) profile = _profileManager.Profiles.FirstOrDefault(x => x.DisplayName == _options.ProfileName) ?? _profileManager.Profiles.First(x => x.DisplayName.ToLower() == _options.ProfileName.ToLower()); } + else + { + // If no profile is specified, use the default (if there is one) + profile = _profileManager.Profiles.FirstOrDefault(x => x.IsDefault) ?? + new ScanProfile { Version = ScanProfile.CURRENT_VERSION }; + } + // If we have any profile overrides we do not want to risk persisting changes back to the main profile. + // It shouldn't happen anyway as we shouldn't call ProfileManager.Save, but this provides a level of + // protection against that. + profile = profile.Clone(); } catch (InvalidOperationException) { @@ -657,6 +796,75 @@ private bool GetProfile(out ScanProfile profile) return true; } + private async Task SetProfileOverrides(ScanProfile profile) + { + if (!string.IsNullOrEmpty(_options.Driver)) + { + var driver = ScanPerformer.ParseDriver(_options.Driver); + profile.DriverName = driver.ToString().ToLowerInvariant(); + } + if (_options.Source != null) + { + profile.PaperSource = _options.Source.Value; + } + if (_pageDimensions != null) + { + profile.PageSize = ScanPageSize.Custom; + profile.CustomPageSize = _pageDimensions; + } + if (_options.Dpi != null) + { + profile.Resolution.Dpi = _options.Dpi.Value; + } + if (_options.BitDepth != null) + { + profile.BitDepth = _options.BitDepth switch + { + ConsoleBitDepth.Color => ScanBitDepth.C24Bit, + ConsoleBitDepth.Gray => ScanBitDepth.Grayscale, + ConsoleBitDepth.Bw => ScanBitDepth.BlackWhite, + _ => ScanBitDepth.C24Bit + }; + } + if (_options.Deskew) + { + profile.AutoDeskew = true; + } + if (_options.RotateDegrees != null) + { + profile.RotateDegrees = _options.RotateDegrees.Value; + } + + if (!string.IsNullOrEmpty(_options.Device)) + { + var cts = new CancellationTokenSource(); + bool foundDevice = false; + await foreach (var device in _scanPerformer.GetDevices(profile, cts.Token)) + { + if (device.Name.ContainsInvariantIgnoreCase(_options.Device!)) + { + cts.Cancel(); + profile.Device = new ScanProfileDevice(device.ID, device.Name); + foundDevice = true; + break; + } + } + if (!foundDevice) + { + _errorOutput.DisplayError(SdkResources.DeviceNotFound); + return false; + } + } + + if (!_options.ListDevices && profile.Device == null) + { + _errorOutput.DisplayError(ConsoleResources.NoDeviceSpecified); + return false; + } + + return true; + } + public void ReceiveScannedImage(ProcessedImage scannedImage) { _scanList.Last().Add(scannedImage); diff --git a/NAPS2.Lib/Automation/AutomatedScanningOptions.cs b/NAPS2.Lib/Automation/AutomatedScanningOptions.cs index bc51d54f9f..3366022fad 100644 --- a/NAPS2.Lib/Automation/AutomatedScanningOptions.cs +++ b/NAPS2.Lib/Automation/AutomatedScanningOptions.cs @@ -1,4 +1,5 @@ using CommandLine; +using NAPS2.Scan; namespace NAPS2.Automation; @@ -15,13 +16,10 @@ public class AutomatedScanningOptions " Only works if the profile has Auto Save enabled.")] public bool AutoSave { get; set; } - [Option("install", HelpText = "Use this option to download and install optional components (e.g. \"ocr-eng\", \"generic-import\").")] + [Option("install", HelpText = "Use this option to download and install optional components" + + " (e.g. \"ocr-eng\", \"generic-import\").")] public string? Install { get; set; } - [Option('p', "profile", HelpText = "The name of the profile to use for scanning." + - " If not specified, the most-recently-used profile from the GUI is selected.")] - public string? ProfileName { get; set; } - [Option('i', "import", HelpText = "The name and path of one or more pdf/image files to import." + " Imported files are prepended to the output in the order they are specified." + " Multiple files are separated by a semicolon (\";\")." + @@ -30,7 +28,7 @@ public class AutomatedScanningOptions [Option("importpassword", HelpText = "The password to use to import one or more encrypted PDF files.")] public string? ImportPassword { get; set; } - + [Option("progress", HelpText = "Display a graphical window for scanning progress.")] public bool Progress { get; set; } @@ -41,9 +39,15 @@ public class AutomatedScanningOptions [Option('n', "number", Default = 1, HelpText = "The number of scans to perform.")] public int Number { get; set; } = 1; - [Option('d', "delay", Default = 0, HelpText = "The delay (in milliseconds) between each scan.")] + [Option('d', "delay", Default = 0, HelpText = "The delay (in milliseconds) before each scan.")] public int Delay { get; set; } + [Option("waitscan", HelpText = "Wait for user input (enter/return) before each scan.")] + public bool WaitScan { get; set; } + + [Option("firstnow", HelpText = "When using --delay or --waitscan in combination with multiple scans (-n), don't wait before the first scan.")] + public bool FirstNow { get; set; } + [Option('f', "force", HelpText = "Overwrite existing files." + " If not specified, any files that already exist will not be changed.")] public bool ForceOverwrite { get; set; } @@ -53,6 +57,53 @@ public class AutomatedScanningOptions #endregion + #region Profile Options + + [Option('p', "profile", HelpText = "The name of the profile to use for scanning." + + " If not specified, the most-recently-used profile from the GUI is selected.")] + public string? ProfileName { get; set; } + + [Option("noprofile", HelpText = "Use default profile settings instead of a GUI profile.")] + public bool NoProfile { get; set; } + + [Option("driver", HelpText = "Scanning driver (wia/twain/escl/sane/apple).")] + public string? Driver { get; set; } + + [Option("device", HelpText = "Scanning device name (can be inexact).")] + public string? Device { get; set; } + + [Option("listdevices", HelpText = "Instead of scanning, list available devices.")] + public bool ListDevices { get; set; } + + #endregion + + #region Scan Options + + [Option("source", HelpText = "The paper source (glass/feeder/duplex) to use when scanning.")] + public ScanSource? Source { get; set; } + + [Option("pagesize", HelpText = "The page size to use when scanning. Either a well-known size" + + " (\"letter\", \"legal\", \"a4\", etc.) or a dimension (e.g. \"8.5x11in\").")] + public string? PageSize { get; set; } + + [Option("dpi", HelpText = "The resolution (dots per inch) to use when scanning.")] + public int? Dpi { get; set; } + + [Option("bitdepth", HelpText = "The bit depth (color/gray/bw) to use when scanning.")] + public ConsoleBitDepth? BitDepth { get; set; } + + #endregion + + #region Post-Processing Options + + [Option("deskew", HelpText = "Automatically deskew scanned pages.")] + public bool Deskew { get; set; } + + [Option("rotate", HelpText = "Rotates pages clockwise by the specified number of degrees.")] + public double? RotateDegrees { get; set; } + + #endregion + #region Order Options [Option("interleave", HelpText = "Interleave pages before saving.")] @@ -83,7 +134,8 @@ public class AutomatedScanningOptions [Option("splitpatcht", HelpText = "Split the pages into multiple PDF/TIFF files, separating by Patch-T.")] public bool SplitPatchT { get; set; } - [Option("splitsize", HelpText = "Split the pages into multiple PDF/TIFF files with the given number of pages per file.")] + [Option("splitsize", + HelpText = "Split the pages into multiple PDF/TIFF files with the given number of pages per file.")] public int SplitSize { get; set; } #endregion @@ -102,16 +154,20 @@ public class AutomatedScanningOptions [Option("pdfkeywords", HelpText = "The keywords for generated PDF metadata.")] public string? PdfKeywords { get; set; } - [Option("usesavedmetadata", HelpText = "Use the metadata (title, author, subject, keywords) configured in the GUI, if any, for the generated PDF.")] + [Option("usesavedmetadata", HelpText = "Use the metadata (title, author, subject, keywords) configured" + + " in the GUI, if any, for the generated PDF.")] public bool UseSavedMetadata { get; set; } - [Option("encryptconfig", HelpText = "The name and path of an XML file to configure encryption for the generated PDF.")] + [Option("encryptconfig", + HelpText = "The name and path of an XML file to configure encryption for the generated PDF.")] public string? EncryptConfig { get; set; } - [Option("usesavedencryptconfig", HelpText = "Use the encryption configured in the GUI, if any, for the generated PDF.")] + [Option("usesavedencryptconfig", + HelpText = "Use the encryption configured in the GUI, if any, for the generated PDF.")] public bool UseSavedEncryptConfig { get; set; } - [Option("pdfcompat", HelpText = "The standard to use for the generated PDF. Possible values: default, A1-b, A2-b, A3-b, A3-u")] + [Option("pdfcompat", + HelpText = "The standard to use for the generated PDF. Possible values: default, A1-b, A2-b, A3-b, A3-u")] public string? PdfCompat { get; set; } #endregion @@ -124,7 +180,8 @@ public class AutomatedScanningOptions [Option("disableocr", HelpText = "Disable OCR for generated PDFs. Overrides --enableocr.")] public bool DisableOcr { get; set; } - [Option("ocrlang", HelpText = "The three-letter code for the language used for OCR (e.g. 'eng' for English, 'fra' for French, etc.). Implies --enableocr.")] + [Option("ocrlang", HelpText = "The three-letter code for the language used for OCR (e.g. 'eng' for" + + " English, 'fra' for French, etc.). Implies --enableocr.")] public string? OcrLang { get; set; } #endregion @@ -158,10 +215,11 @@ public class AutomatedScanningOptions " Requires -e/--email.")] public string? EmailBcc { get; set; } - [Option("autosend", HelpText = "Actually send the email immediately after scanning completes without prompting the user for changes." + - " However, this may prompt the user to login. To avoid that, use --silentsend." + - " Note that Outlook may still require user interaction to send an email, regardless of --autosend or --silentsend options." + - " Requires -e/--email.")] + [Option("autosend", HelpText = + "Actually send the email immediately after scanning completes without prompting the user for changes." + + " However, this may prompt the user to login. To avoid that, use --silentsend." + + " Note that Outlook may still require user interaction to send an email, regardless of --autosend or --silentsend options." + + " Requires -e/--email.")] public bool EmailAutoSend { get; set; } [Option("silentsend", HelpText = "Doesn't prompt the user to login when --autosend is specified." + @@ -179,6 +237,6 @@ public class AutomatedScanningOptions [Option("tiffcomp", HelpText = "The compression to use for TIFF files. Possible values: auto, lzw, ccitt4, none")] public string? TiffComp { get; set; } - + #endregion } \ No newline at end of file diff --git a/NAPS2.Lib/Automation/ConsoleBitDepth.cs b/NAPS2.Lib/Automation/ConsoleBitDepth.cs new file mode 100644 index 0000000000..4e95807ced --- /dev/null +++ b/NAPS2.Lib/Automation/ConsoleBitDepth.cs @@ -0,0 +1,8 @@ +namespace NAPS2.Automation; + +public enum ConsoleBitDepth +{ + Color, + Gray, + Bw +} \ No newline at end of file diff --git a/NAPS2.Lib/Automation/ConsoleDevicePrompt.cs b/NAPS2.Lib/Automation/ConsoleDevicePrompt.cs index 9f32a5b24f..d7747be45e 100644 --- a/NAPS2.Lib/Automation/ConsoleDevicePrompt.cs +++ b/NAPS2.Lib/Automation/ConsoleDevicePrompt.cs @@ -4,9 +4,9 @@ namespace NAPS2.Automation; public class ConsoleDevicePrompt : IDevicePrompt { - public ScanDevice? PromptForDevice(List deviceList, IntPtr dialogParent) + public Task PromptForDevice(ScanOptions options, bool allowAlwaysAsk) { // TODO: What's best to do here? Use a GUI prompt? A console prompt? Or just do nothing like this? - return null; + return Task.FromResult(DeviceChoice.None); } } \ No newline at end of file diff --git a/NAPS2.Lib/Automation/ConsoleOperationProgress.cs b/NAPS2.Lib/Automation/ConsoleOperationProgress.cs index 67496979eb..f4cd85164a 100644 --- a/NAPS2.Lib/Automation/ConsoleOperationProgress.cs +++ b/NAPS2.Lib/Automation/ConsoleOperationProgress.cs @@ -22,15 +22,8 @@ public override void ShowProgress(IOperation op) public override void ShowModalProgress(IOperation op) { - // TODO: We might want to use an eto-based progress form, or at least show some kind of indicator + // TODO: We might want to show some kind of indicator // Where is this method called from anyway? - - // if (!op.IsFinished) - // { - // var form = _formFactory.Create(); - // form.Operation = op; - // form.ShowDialog(); - // } op.Wait(); } diff --git a/NAPS2.Lib/Automation/ConsolePdfPasswordProvider.cs b/NAPS2.Lib/Automation/ConsolePdfPasswordProvider.cs index c70db1383b..6e214c81b0 100644 --- a/NAPS2.Lib/Automation/ConsolePdfPasswordProvider.cs +++ b/NAPS2.Lib/Automation/ConsolePdfPasswordProvider.cs @@ -1,5 +1,5 @@ -using NAPS2.ImportExport.Pdf; -using NAPS2.Lang.ConsoleResources; +using NAPS2.Lang.ConsoleResources; +using NAPS2.Pdf; namespace NAPS2.Automation; diff --git a/NAPS2.Lib/Config/CommonConfig.cs b/NAPS2.Lib/Config/CommonConfig.cs index 7ee5beea72..5e719d33cd 100644 --- a/NAPS2.Lib/Config/CommonConfig.cs +++ b/NAPS2.Lib/Config/CommonConfig.cs @@ -1,16 +1,15 @@ using System.Collections.Immutable; using NAPS2.Config.Model; +using NAPS2.Escl; using NAPS2.ImportExport.Email; using NAPS2.ImportExport.Images; -using NAPS2.ImportExport.Pdf; +using NAPS2.Pdf; using NAPS2.Ocr; using NAPS2.Scan; using NAPS2.Scan.Batch; namespace NAPS2.Config; -// TODO: Remove all unnecessary nullables -// TODO: Maybe have this serialize with the root node named as AppConfig/UserConfig somehow? [Config] public class CommonConfig { @@ -32,7 +31,7 @@ public class CommonConfig public ImmutableList CustomPageSizePresets { get; set; } = ImmutableList.Empty; [User] - public ImmutableList? SavedProxies { get; set; } + public int LastCustomResolution { get; set; } [App] public string? StartupMessageTitle { get; set; } @@ -43,6 +42,18 @@ public class CommonConfig [App] public MessageBoxIcon StartupMessageIcon { get; set; } + [Common] + public Theme Theme { get; set; } + + [Common] + public bool ScanChangesDefaultProfile { get; set; } + + [Common] + public bool ShowProfilesToolbar { get; set; } + + [Common] + public ScanButtonDefaultAction ScanButtonDefaultAction { get; set; } + [Common] public SaveButtonDefaultAction SaveButtonDefaultAction { get; set; } @@ -64,8 +75,8 @@ public class CommonConfig [Common] public bool AlwaysRememberDevice { get; set; } - [App] - public bool DisableGenericPdfImport { get; set; } + [Common] + public bool DeviceListAsTextOnly { get; set; } [Common] public bool NoUpdatePrompt { get; set; } @@ -91,9 +102,18 @@ public class CommonConfig [User] public DateTime? LastDonatePromptDate { get; set; } + [User] + public bool HasBeenPromptedForReview { get; set; } + + [User] + public DateTime? LastReviewPromptDate { get; set; } + [Common] public bool DeleteAfterSaving { get; set; } + [Common] + public bool KeepSession { get; set; } + [Common] public bool DisableSaveNotifications { get; set; } @@ -115,21 +135,39 @@ public class CommonConfig [Common] public string? OcrLanguageCode { get; set; } + [Common] + public string? LastOcrMultiLangCode { get; set; } + [Common] public LocalizedOcrMode OcrMode { get; set; } + [Common] + public bool OcrPreProcessing { get; set; } + [Common] public bool OcrAfterScanning { get; set; } [User] public string? LastImageExt { get; set; } + [User] + public string? LastPdfOrImageExt { get; set; } + [Common] public int ThumbnailSize { get; set; } [Common] public DockStyle DesktopToolStripDock { get; set; } + [Common] + public DockStyle ProfilesToolStripDock { get; set; } + + [Common] + public EsclSecurityPolicy EsclSecurityPolicy { get; set; } + + [Common] + public string? EsclServerCertificatePath { get; set; } + [App] public EventType EventLogging { get; set; } @@ -168,4 +206,28 @@ public class CommonConfig [Common] public ScanProfile? DefaultProfileSettings { get; set; } + + [Common] + public bool EnableDebugLogging { get; set; } + + [User] + public bool ShowPageNumbers { get; set; } + + [Common] + public bool DisableScannerSharing { get; set; } + + [User] + public bool SidebarVisible { get; set; } + + [User] + public int SidebarWidth { get; set; } + + [User] + public string? EditWithAppPath { get; set; } + + [User] + public string? EditWithAppName { get; set; } + + [User] + public bool ApplyToAllSelected { get; set; } } \ No newline at end of file diff --git a/NAPS2.Lib/Config/ConfigExtensions.cs b/NAPS2.Lib/Config/ConfigExtensions.cs index 2f003376c9..73a2bf0da7 100644 --- a/NAPS2.Lib/Config/ConfigExtensions.cs +++ b/NAPS2.Lib/Config/ConfigExtensions.cs @@ -13,21 +13,20 @@ public static OcrParams DefaultOcrParams(this Naps2Config config) } return new OcrParams( config.Get(c => c.OcrLanguageCode), - MapOcrMode(config.Get(c => c.OcrMode)), + MapOcrMode(config.Get(c => c.OcrMode), config.Get(c => c.OcrPreProcessing)), config.Get(c => c.OcrTimeoutInSeconds)); } - private static OcrMode MapOcrMode(LocalizedOcrMode ocrMode) + private static OcrMode MapOcrMode(LocalizedOcrMode ocrMode, bool preProcessing) { - switch (ocrMode) + return (ocrMode, preProcessing) switch { - case LocalizedOcrMode.Fast: - return OcrMode.Fast; - case LocalizedOcrMode.Best: - return OcrMode.Best; - default: - return OcrMode.Default; - } + (LocalizedOcrMode.Fast, false) => OcrMode.Fast, + (LocalizedOcrMode.Fast, true) => OcrMode.FastWithPreProcess, + (LocalizedOcrMode.Best, false) => OcrMode.Best, + (LocalizedOcrMode.Best, true) => OcrMode.BestWithPreProcess, + _ => OcrMode.Default + }; } public static OcrParams OcrAfterScanningParams(this Naps2Config config) diff --git a/NAPS2.Lib/Config/ConfigSerializer.cs b/NAPS2.Lib/Config/ConfigSerializer.cs index fb2058cd41..31022c706b 100644 --- a/NAPS2.Lib/Config/ConfigSerializer.cs +++ b/NAPS2.Lib/Config/ConfigSerializer.cs @@ -1,9 +1,11 @@ using System.Collections.Immutable; +using System.Linq.Expressions; using System.Text; using System.Xml; using NAPS2.Config.Model; using NAPS2.Config.ObsoleteTypes; -using NAPS2.ImportExport.Pdf; +using NAPS2.Escl; +using NAPS2.Pdf; using NAPS2.Serialization; namespace NAPS2.Config; @@ -37,15 +39,18 @@ protected override ConfigStorage InternalDeserialize(Stream stream if (_mode == ConfigReadMode.DefaultOnly) { var oldAppConfig = new XmlSerializer().Deserialize(stream); - return AppConfigV0ToCommonConfigDefault(oldAppConfig ?? throw new InvalidOperationException("Couldn't parse app config")); + return AppConfigV0ToCommonConfigDefault( + oldAppConfig ?? throw new InvalidOperationException("Couldn't parse app config")); } if (_mode == ConfigReadMode.LockedOnly) { var oldAppConfig = new XmlSerializer().Deserialize(stream); - return AppConfigV0ToCommonConfigLocked(oldAppConfig ?? throw new InvalidOperationException("Couldn't parse app config")); + return AppConfigV0ToCommonConfigLocked( + oldAppConfig ?? throw new InvalidOperationException("Couldn't parse app config"), doc); } var oldUserConfig = new XmlSerializer().Deserialize(stream); - return UserConfigV0ToCommonConfig(oldUserConfig ?? throw new InvalidOperationException("Couldn't parse user config")); + return UserConfigV0ToCommonConfig(oldUserConfig ?? + throw new InvalidOperationException("Couldn't parse user config")); } if (_mode == ConfigReadMode.DefaultOnly) { @@ -63,7 +68,9 @@ protected override ConfigStorage InternalDeserialize(Stream stream private ConfigStorage DeserializeXDoc(XDocument doc) { var filteredStream = new MemoryStream(); - doc.WriteTo(new XmlTextWriter(filteredStream, Encoding.UTF8)); + var xmlWriter = new XmlTextWriter(filteredStream, Encoding.UTF8); + doc.WriteTo(xmlWriter); + xmlWriter.Flush(); filteredStream.Seek(0, SeekOrigin.Begin); return _storageSerializer.Deserialize(filteredStream); } @@ -77,60 +84,99 @@ private void FilterProperties(XElement node, string target, string current) { FilterProperties(child, target, childMode); } - else if(childMode != target) + else if (childMode != target) { child.Remove(); } } } - private ConfigStorage AppConfigV0ToCommonConfigDefault(AppConfigV0 c) => - new(new CommonConfig - { - // TODO: This initializes all properties, we need to specify exactly what's set - // Could maybe move the app-only properties to Locked scope, but it really shouldn't matter - Version = CommonConfig.CURRENT_VERSION, - Culture = c.DefaultCulture, - StartupMessageTitle = c.StartupMessageTitle, - StartupMessageText = c.StartupMessageText, - StartupMessageIcon = c.StartupMessageIcon, - DefaultProfileSettings = c.DefaultProfileSettings, - SaveButtonDefaultAction = c.SaveButtonDefaultAction, - HiddenButtons = GetHiddenButtonFlags(c), - DisableAutoSave = c.DisableAutoSave, - LockSystemProfiles = c.LockSystemProfiles, - LockUnspecifiedDevices = c.LockUnspecifiedDevices, - NoUserProfiles = c.NoUserProfiles, - AlwaysRememberDevice = c.AlwaysRememberDevice, - DisableGenericPdfImport = c.DisableGenericPdfImport, - NoUpdatePrompt = c.NoUpdatePrompt, - DeleteAfterSaving = c.DeleteAfterSaving, - DisableSaveNotifications = c.DisableSaveNotifications, - SingleInstance = c.SingleInstance, - ComponentsPath = c.ComponentsPath, - OcrTimeoutInSeconds = c.OcrTimeoutInSeconds, - OcrLanguageCode = c.OcrDefaultLanguage, - OcrMode = c.OcrDefaultMode, - OcrAfterScanning = c.OcrDefaultAfterScanning, - EventLogging = c.EventLogging, - KeyboardShortcuts = c.KeyboardShortcuts ?? new KeyboardShortcuts() - }); + private ConfigStorage AppConfigV0ToCommonConfigDefault(AppConfigV0 c) + { + var storage = new ConfigStorage(); + storage.Set(x => x.Version, CommonConfig.CURRENT_VERSION); + storage.Set(x => x.Culture, c.DefaultCulture); + storage.Set(x => x.StartupMessageTitle, c.StartupMessageTitle); + storage.Set(x => x.StartupMessageText, c.StartupMessageText); + storage.Set(x => x.StartupMessageIcon, c.StartupMessageIcon); + storage.Set(x => x.DefaultProfileSettings, c.DefaultProfileSettings); + storage.Set(x => x.Theme, c.Theme); + storage.Set(x => x.ShowPageNumbers, c.ShowPageNumbers); + storage.Set(x => x.ShowProfilesToolbar, c.ShowProfilesToolbar); + storage.Set(x => x.ScanChangesDefaultProfile, c.ScanChangesDefaultProfile); + storage.Set(x => x.ScanButtonDefaultAction, c.ScanButtonDefaultAction); + storage.Set(x => x.SaveButtonDefaultAction, c.SaveButtonDefaultAction); + storage.Set(x => x.DeleteAfterSaving, c.DeleteAfterSaving); + storage.Set(x => x.KeepSession, c.KeepSession); + storage.Set(x => x.SingleInstance, c.SingleInstance); + storage.Set(x => x.HiddenButtons, GetHiddenButtonFlags(c)); + storage.Set(x => x.DisableAutoSave, c.DisableAutoSave); + storage.Set(x => x.LockSystemProfiles, c.LockSystemProfiles); + storage.Set(x => x.LockUnspecifiedDevices, c.LockUnspecifiedDevices); + storage.Set(x => x.NoUserProfiles, c.NoUserProfiles); + storage.Set(x => x.AlwaysRememberDevice, c.AlwaysRememberDevice); + storage.Set(x => x.NoUpdatePrompt, c.NoUpdatePrompt); + storage.Set(x => x.DisableSaveNotifications, c.DisableSaveNotifications); + storage.Set(x => x.ComponentsPath, c.ComponentsPath); + storage.Set(x => x.OcrTimeoutInSeconds, c.OcrTimeoutInSeconds); + storage.Set(x => x.OcrLanguageCode, c.OcrDefaultLanguage); + storage.Set(x => x.OcrMode, c.OcrDefaultMode); + storage.Set(x => x.OcrAfterScanning, c.OcrDefaultAfterScanning); + storage.Set(x => x.EsclSecurityPolicy, c.EsclSecurityPolicy); + storage.Set(x => x.EsclServerCertificatePath, c.EsclServerCertificatePath); + storage.Set(x => x.EventLogging, c.EventLogging); + storage.Set(x => x.KeyboardShortcuts, MapKeyboardShortcuts(c.KeyboardShortcuts ?? new KeyboardShortcuts())); + return storage; + } - private ConfigStorage AppConfigV0ToCommonConfigLocked(AppConfigV0 c) + private ConfigStorage AppConfigV0ToCommonConfigLocked(AppConfigV0 c, XDocument doc) { var storage = new ConfigStorage(); if (c.OcrState == OcrState.Enabled) { - storage.Set(c => c.EnableOcr, true); + storage.Set(x => x.EnableOcr, true); } if (c.OcrState == OcrState.Disabled) { - storage.Set(c => c.EnableOcr, false); + storage.Set(x => x.EnableOcr, false); } if (c.ForcePdfCompat != PdfCompat.Default) { - storage.Set(c => c.PdfSettings.Compat, c.ForcePdfCompat); + storage.Set(x => x.PdfSettings.Compat, c.ForcePdfCompat); } + if (c.NoDebugLogging) + { + storage.Set(x => x.EnableDebugLogging, false); + } + if (c.NoScannerSharing) + { + storage.Set(x => x.DisableScannerSharing, true); + } + if (c.EsclSecurityPolicy != EsclSecurityPolicy.None) + { + storage.Set(x => x.EsclSecurityPolicy, c.EsclSecurityPolicy); + } + + void SetIfLocked(Expression> accessor, T value, string name) + { + var element = doc.Root!.Element(name); + bool isLocked = element?.Attribute("mode")?.Value == "lock"; + if (isLocked) + { + storage.Set(accessor, value); + } + } + SetIfLocked(x => x.Theme, c.Theme, nameof(c.Theme)); + SetIfLocked(x => x.ShowPageNumbers, c.ShowPageNumbers, nameof(c.ShowPageNumbers)); + SetIfLocked(x => x.ShowProfilesToolbar, c.ShowProfilesToolbar, nameof(c.ShowProfilesToolbar)); + SetIfLocked(x => x.ScanChangesDefaultProfile, c.ScanChangesDefaultProfile, nameof(c.ScanChangesDefaultProfile)); + SetIfLocked(x => x.ScanButtonDefaultAction, c.ScanButtonDefaultAction, nameof(c.ScanButtonDefaultAction)); + SetIfLocked(x => x.SaveButtonDefaultAction, c.SaveButtonDefaultAction, nameof(c.SaveButtonDefaultAction)); + SetIfLocked(x => x.DeleteAfterSaving, c.DeleteAfterSaving, nameof(c.DeleteAfterSaving)); + SetIfLocked(x => x.KeepSession, c.KeepSession, nameof(c.KeepSession)); + SetIfLocked(x => x.SingleInstance, c.SingleInstance, nameof(c.SingleInstance)); + SetIfLocked(x => x.KeyboardShortcuts, MapKeyboardShortcuts(c.KeyboardShortcuts ?? new KeyboardShortcuts()), nameof(c.KeyboardShortcuts)); + return storage; } @@ -143,36 +189,62 @@ private ToolbarButtons GetHiddenButtonFlags(AppConfigV0 c) if (c.HideSaveImagesButton) flags |= ToolbarButtons.SaveImages; if (c.HideEmailButton) flags |= ToolbarButtons.EmailPdf; if (c.HidePrintButton) flags |= ToolbarButtons.Print; + if (c.HideSettingsButton) flags |= ToolbarButtons.Settings; if (c.HideDonateButton) flags |= ToolbarButtons.Donate; + if (c.HideSidebar) flags |= ToolbarButtons.Sidebar; return flags; } - private static ConfigStorage UserConfigV0ToCommonConfig(UserConfigV0 c) => - new(new CommonConfig // TODO: We don't want new config fields to be considered specified - { - Version = CommonConfig.CURRENT_VERSION, - Culture = c.Culture, - // The new form states have different values (client size instead of form size), so best to go with defaults - FormStates = ImmutableList.Empty, // ImmutableList.CreateRange(c.FormStates), - BackgroundOperations = ImmutableHashSet.CreateRange(c.BackgroundOperations), - CheckForUpdates = c.CheckForUpdates, - LastUpdateCheckDate = c.LastUpdateCheckDate, - FirstRunDate = c.FirstRunDate, - LastDonatePromptDate = c.LastDonatePromptDate, - EnableOcr = c.EnableOcr, - OcrLanguageCode = c.OcrLanguageCode, - OcrMode = c.OcrMode, - OcrAfterScanning = c.OcrAfterScanning, - LastImageExt = c.LastImageExt, - PdfSettings = c.PdfSettings, - ImageSettings = c.ImageSettings, - EmailSettings = c.EmailSettings, - EmailSetup = c.EmailSetup, - ThumbnailSize = c.ThumbnailSize, - BatchSettings = c.LastBatchSettings ?? new(), // TODO: This is broken - DesktopToolStripDock = c.DesktopToolStripDock, - KeyboardShortcuts = c.KeyboardShortcuts, - CustomPageSizePresets = ImmutableList.CreateRange(c.CustomPageSizePresets), - SavedProxies = ImmutableList.CreateRange(c.SavedProxies) - }); + private KeyboardShortcuts MapKeyboardShortcuts(KeyboardShortcuts keyboardShortcuts) + { + // To be platform-independent, replace "Ctrl+" with "Mod+", which means "Ctrl" on Window/Linux and "Cmd" on Mac + var serializer = new XmlSerializer(); + var doc = serializer.SerializeToXDocument(keyboardShortcuts); + foreach (var node in doc.Root!.Elements()) + { + node.Value = node.Value.Replace("Ctrl+", "Mod+"); + } + return serializer.DeserializeFromXDocument(doc)!; + } + + private static ConfigStorage UserConfigV0ToCommonConfig(UserConfigV0 c) + { + var storage = new ConfigStorage(); + storage.Set(x => x.Version, CommonConfig.CURRENT_VERSION); + storage.Set(x => x.Culture, c.Culture); + // The new form states have different values (client size instead of form size), so best to go with defaults + storage.Set(x => x.FormStates, ImmutableList.Empty); + if (c.BackgroundOperations != null) + { + storage.Set(x => x.BackgroundOperations, ImmutableHashSet.CreateRange(c.BackgroundOperations)); + } + storage.Set(x => x.CheckForUpdates, c.CheckForUpdates); + storage.Set(x => x.LastUpdateCheckDate, c.LastUpdateCheckDate); + storage.Set(x => x.FirstRunDate, c.FirstRunDate); + storage.Set(x => x.LastDonatePromptDate, c.LastDonatePromptDate); + storage.Set(x => x.EnableOcr, c.EnableOcr); + storage.Set(x => x.OcrLanguageCode, c.OcrLanguageCode); + storage.Set(x => x.OcrMode, c.OcrMode); + storage.Set(x => x.OcrAfterScanning, c.OcrAfterScanning); + storage.Set(x => x.LastImageExt, c.LastImageExt); + storage.Set(x => x.PdfSettings, c.PdfSettings); + storage.Set(x => x.ImageSettings, c.ImageSettings); + storage.Set(x => x.EmailSettings, c.EmailSettings); + storage.Set(x => x.EmailSetup, c.EmailSetup); + storage.Set(x => x.ThumbnailSize, c.ThumbnailSize); + if (c.LastBatchSettings != null) + { + storage.Set(x => x.BatchSettings, c.LastBatchSettings); + } + storage.Set(x => x.DesktopToolStripDock, c.DesktopToolStripDock); + if (c.KeyboardShortcuts != null) + { + storage.Set(x => x.KeyboardShortcuts, c.KeyboardShortcuts); + } + if (c.CustomPageSizePresets != null) + { + storage.Set(x => x.CustomPageSizePresets, ImmutableList.CreateRange(c.CustomPageSizePresets)); + } + return storage; + } } \ No newline at end of file diff --git a/NAPS2.Lib/Config/InternalDefaults.cs b/NAPS2.Lib/Config/InternalDefaults.cs index f8cfdf7ae7..354eac1b76 100644 --- a/NAPS2.Lib/Config/InternalDefaults.cs +++ b/NAPS2.Lib/Config/InternalDefaults.cs @@ -3,8 +3,8 @@ using NAPS2.ImportExport.Email; using NAPS2.ImportExport.Email.Oauth; using NAPS2.ImportExport.Images; -using NAPS2.ImportExport.Pdf; using NAPS2.Ocr; +using NAPS2.Pdf; using NAPS2.Scan; using NAPS2.Scan.Batch; @@ -20,10 +20,12 @@ public static CommonConfig GetCommonConfig() => FormStates = ImmutableList.Empty, BackgroundOperations = ImmutableHashSet.Empty, CustomPageSizePresets = ImmutableList.Empty, - SavedProxies = ImmutableList.Empty, StartupMessageTitle = "", StartupMessageText = "", StartupMessageIcon = MessageBoxIcon.None, + Theme = Theme.Default, + ScanChangesDefaultProfile = true, + ScanButtonDefaultAction = ScanButtonDefaultAction.ScanWithDefaultProfile, SaveButtonDefaultAction = SaveButtonDefaultAction.SaveAll, HiddenButtons = ToolbarButtons.None, DisableAutoSave = false, @@ -31,7 +33,6 @@ public static CommonConfig GetCommonConfig() => LockUnspecifiedDevices = false, NoUserProfiles = false, AlwaysRememberDevice = false, - DisableGenericPdfImport = false, NoUpdatePrompt = false, CheckForUpdates = false, HasCheckedForUpdates = false, @@ -40,6 +41,8 @@ public static CommonConfig GetCommonConfig() => FirstRunDate = null, HasBeenPromptedForDonation = false, LastDonatePromptDate = null, + HasBeenPromptedForReview = false, + LastReviewPromptDate = null, DeleteAfterSaving = false, DisableSaveNotifications = false, DisableExitConfirmation = false, @@ -48,12 +51,21 @@ public static CommonConfig GetCommonConfig() => OcrTimeoutInSeconds = 10 * 60, // 10 minutes EnableOcr = false, OcrLanguageCode = "", + LastOcrMultiLangCode = "", OcrMode = LocalizedOcrMode.Fast, + OcrPreProcessing = false, OcrAfterScanning = true, LastImageExt = "", + LastPdfOrImageExt = "", ThumbnailSize = ThumbnailSizes.DEFAULT_SIZE, DesktopToolStripDock = DockStyle.Top, EventLogging = EventType.None, + ShowPageNumbers = false, + SidebarVisible = true, + SidebarWidth = 230, + EditWithAppPath = "", + EditWithAppName = "", + ApplyToAllSelected = false, PdfSettings = new PdfSettings { Metadata = new PdfMetadata @@ -125,7 +137,7 @@ public static CommonConfig GetCommonConfig() => }, KeyboardShortcuts = new KeyboardShortcuts { - ScanDefault = "Ctrl+Enter", + ScanDefault = "Mod+Enter", ScanProfile1 = "F2", ScanProfile2 = "F3", ScanProfile3 = "F4", @@ -138,21 +150,25 @@ public static CommonConfig GetCommonConfig() => ScanProfile10 = "F11", ScanProfile11 = "F12", ScanProfile12 = "", - NewProfile = "", - BatchScan = "Ctrl+B", - Profiles = "", - Ocr = "", - Import = "Ctrl+O", - SavePDF = "Ctrl+S", - SavePDFAll = "", - SavePDFSelected = "", + NewProfile = "Mod+N", + BatchScan = "Mod+B", + Profiles = "Mod+L", + ScannerSharing = "Mod+G", + Ocr = "Mod+Alt+O", + Import = "Mod+O", + SavePDF = "", + SavePDFAll = "Mod+S", + SavePDFSelected = "Mod+Shift+S", + PDFSettings = "Mod+Alt+P", SaveImages = "", - SaveImagesAll = "", - SaveImagesSelected = "", + SaveImagesAll = "Mod+I", + SaveImagesSelected = "Mod+Shift+I", + ImageSettings = "Mod+Alt+I", EmailPDF = "", - EmailPDFAll = "", - EmailPDFSelected = "", - Print = "Ctrl+P", + EmailPDFAll = "Mod+E", + EmailPDFSelected = "Mod+Shift+E", + EmailSettings = "Mod+Alt+E", + Print = "Mod+P", ImageView = "", ImageBlackWhite = "", ImageBrightness = "", @@ -162,12 +178,17 @@ public static CommonConfig GetCommonConfig() => ImageReset = "", ImageSaturation = "", ImageSharpen = "", - RotateLeft = "", - RotateRight = "", - RotateFlip = "", + ImageDocumentCorrection = "", + ImageSplit = "", + ImageCombine = "", + ImageEditWith = "", + RotateLeft = "Mod+Shift+Left", + RotateRight = "Mod+Shift+Right", + RotateFlip = "Mod+Shift+Down", RotateCustom = "", - MoveUp = "Ctrl+Up", - MoveDown = "Ctrl+Down", + RotateDeskew = "", + MoveUp = "Mod+Up", + MoveDown = "Mod+Down", ReorderInterleave = "", ReorderDeinterleave = "", ReorderAltInterleave = "", @@ -175,10 +196,11 @@ public static CommonConfig GetCommonConfig() => ReorderReverseAll = "", ReorderReverseSelected = "", Delete = "", - Clear = "Ctrl+Shift+Del", + Clear = "Mod+Shift+Del", + Settings = "", About = "F1", - ZoomIn = "Ctrl+Oemplus", - ZoomOut = "Ctrl+OemMinus" + ZoomIn = "Mod+Oemplus", + ZoomOut = "Mod+OemMinus" }, DefaultProfileSettings = new ScanProfile { Version = ScanProfile.CURRENT_VERSION } }; diff --git a/NAPS2.Lib/Config/KeyboardShortcuts.cs b/NAPS2.Lib/Config/KeyboardShortcuts.cs index f490be871e..6a1a631539 100644 --- a/NAPS2.Lib/Config/KeyboardShortcuts.cs +++ b/NAPS2.Lib/Config/KeyboardShortcuts.cs @@ -19,22 +19,24 @@ public class KeyboardShortcuts public string? BatchScan { get; set; } public string? Profiles { get; set; } - + public string? ScannerSharing { get; set; } public string? Ocr { get; set; } - public string? Import { get; set; } public string? SavePDF { get; set; } public string? SavePDFAll { get; set; } public string? SavePDFSelected { get; set; } + public string? PDFSettings { get; set; } public string? SaveImages { get; set; } public string? SaveImagesAll { get; set; } public string? SaveImagesSelected { get; set; } + public string? ImageSettings { get; set; } public string? EmailPDF { get; set; } public string? EmailPDFAll { get; set; } public string? EmailPDFSelected { get; set; } + public string? EmailSettings { get; set; } public string? Print { get; set; } @@ -46,11 +48,16 @@ public class KeyboardShortcuts public string? ImageHue { get; set; } public string? ImageSaturation { get; set; } public string? ImageSharpen { get; set; } + public string? ImageDocumentCorrection { get; set; } + public string? ImageSplit { get; set; } + public string? ImageCombine { get; set; } + public string? ImageEditWith { get; set; } public string? ImageReset { get; set; } public string? RotateLeft { get; set; } public string? RotateRight { get; set; } public string? RotateFlip { get; set; } + public string? RotateDeskew { get; set; } public string? RotateCustom { get; set; } public string? MoveUp { get; set; } @@ -64,9 +71,8 @@ public class KeyboardShortcuts public string? ReorderReverseSelected { get; set; } public string? Delete { get; set; } - public string? Clear { get; set; } - + public string? Settings { get; set; } public string? About { get; set; } public string? ZoomIn { get; set; } diff --git a/NAPS2.Lib/Config/Model/ConfigScope.cs b/NAPS2.Lib/Config/Model/ConfigScope.cs index dcea6b434f..1e5159f9fb 100644 --- a/NAPS2.Lib/Config/Model/ConfigScope.cs +++ b/NAPS2.Lib/Config/Model/ConfigScope.cs @@ -22,6 +22,8 @@ protected ConfigScope(ConfigScopeMode mode) public ConfigScopeMode Mode { get; } + public bool Has(Expression> accessor) => TryGet(accessor, out _); + public bool TryGet(Expression> accessor, out T value) { var result = TryGet(ConfigLookup.ExpandExpression(accessor), out var obj); diff --git a/NAPS2.Lib/Config/Model/ConfigStorage.cs b/NAPS2.Lib/Config/Model/ConfigStorage.cs index 06606045a5..43bbe1b53c 100644 --- a/NAPS2.Lib/Config/Model/ConfigStorage.cs +++ b/NAPS2.Lib/Config/Model/ConfigStorage.cs @@ -212,8 +212,14 @@ private void CopyXElementToNode(XElement src, StorageNode dst, UntypedXmlSeriali var propDataDict = ConfigLookup.GetPropertyData(dst.ValueType).ToDictionary(x => x.Name); foreach (var childElement in src.Elements()) { - // TODO: Handle errors - var propData = propDataDict[childElement.Name.ToString()]; + var key = childElement.Name.ToString(); + if (!propDataDict.ContainsKey(key)) + { + // Unexpected element; best to ignore rather than blow up the config (e.g. after a version downgrade) + // TODO: Log? + continue; + } + var propData = propDataDict[key]; var childNode = dst.Children.GetOrSet(propData.Name, () => new StorageNode(propData.Type) { Key = propData.Name, @@ -235,8 +241,10 @@ public void SerializeTo(XDocument doc, string? customRootElementName) } var serializer = new UntypedXmlSerializer(); var rootElementName = customRootElementName ?? serializer.GetDefaultElementName(typeof(TConfig)); - doc.Add(new XElement(rootElementName)); - CopyNodeToXElement(_root, doc.Root!, serializer); + var root = new XElement(rootElementName); + doc.Add(root); + root.SetAttributeValue(XNamespace.Xmlns + "xsi", "http://www.w3.org/2001/XMLSchema-instance"); + CopyNodeToXElement(_root, root, serializer); } } diff --git a/NAPS2.Lib/Config/Model/FileConfigScope.cs b/NAPS2.Lib/Config/Model/FileConfigScope.cs index 4ece67e50c..b64dcdfde9 100644 --- a/NAPS2.Lib/Config/Model/FileConfigScope.cs +++ b/NAPS2.Lib/Config/Model/FileConfigScope.cs @@ -5,19 +5,20 @@ namespace NAPS2.Config.Model; public class FileConfigScope : ConfigScope { - private static readonly TimeSpan READ_INTERVAL = TimeSpan.FromMilliseconds(100); - + private static readonly TimeSpan READ_INTERVAL = TimeSpan.FromMilliseconds(5000); + private readonly string _filePath; private readonly ISerializer> _serializer; private ConfigStorage _cache = new(); private ConfigStorage _changes = new(); private readonly TimedThrottle _readHandshakeThrottle; - public FileConfigScope(string filePath, ISerializer> serializer, ConfigScopeMode mode) : base(mode) + public FileConfigScope(string filePath, ISerializer> serializer, ConfigScopeMode mode, + TimeSpan? readInterval = null) : base(mode) { _filePath = filePath; _serializer = serializer; - _readHandshakeThrottle = new TimedThrottle(ReadHandshake, READ_INTERVAL); + _readHandshakeThrottle = new TimedThrottle(ReadHandshake, readInterval ?? READ_INTERVAL); } protected override bool TryGetInternal(ConfigLookup lookup, out object? value) @@ -100,7 +101,8 @@ private void WriteHandshake() catch (Exception) { // Failed to load. Since we're using FileShare.None, it can't be concurrent modification. - // Either the file is newly created, or it was corrupted. In either case we can ignore and overwrite. + // Either the file is newly created, or it was corrupted. In either case we can backup and overwrite. + Backup(stream); } // Merge the cache and our changes into a local copy (so in case of exceptions nothing is changed) copy = new ConfigStorage(); @@ -120,4 +122,19 @@ private void WriteHandshake() Log.ErrorException($"Error writing {_filePath}", ex); } } + + private void Backup(FileStream stream) + { + try + { + using var backupStream = new FileStream(_filePath + ".bak", FileMode.Create); + backupStream.SetLength(0); + stream.Seek(0, SeekOrigin.Begin); + stream.CopyTo(backupStream); + } + catch (Exception) + { + // Ignore, we did our best + } + } } \ No newline at end of file diff --git a/NAPS2.Lib/Config/ObsoleteTypes/AppConfigV0.cs b/NAPS2.Lib/Config/ObsoleteTypes/AppConfigV0.cs index 78fab1db6b..97ec0f14a7 100644 --- a/NAPS2.Lib/Config/ObsoleteTypes/AppConfigV0.cs +++ b/NAPS2.Lib/Config/ObsoleteTypes/AppConfigV0.cs @@ -1,6 +1,7 @@ using System.Xml.Serialization; -using NAPS2.ImportExport.Pdf; +using NAPS2.Escl; using NAPS2.Ocr; +using NAPS2.Pdf; using NAPS2.Scan; namespace NAPS2.Config.ObsoleteTypes; @@ -20,8 +21,24 @@ public class AppConfigV0 public ScanProfile? DefaultProfileSettings { get; set; } + public Theme Theme { get; set; } + + public bool ShowPageNumbers { get; set; } + + public bool ShowProfilesToolbar { get; set; } + + public bool ScanChangesDefaultProfile { get; set; } + + public ScanButtonDefaultAction ScanButtonDefaultAction { get; set; } + public SaveButtonDefaultAction SaveButtonDefaultAction { get; set; } + public bool DeleteAfterSaving { get; set; } + + public bool KeepSession { get; set; } + + public bool SingleInstance { get; set; } + public bool HideOcrButton { get; set; } public bool HideImportButton { get; set; } @@ -34,8 +51,12 @@ public class AppConfigV0 public bool HidePrintButton { get; set; } + public bool HideSettingsButton { get; set; } + public bool HideDonateButton { get; set; } + public bool HideSidebar { get; set; } + public bool DisableAutoSave { get; set; } public bool LockSystemProfiles { get; set; } @@ -46,15 +67,13 @@ public class AppConfigV0 public bool AlwaysRememberDevice { get; set; } - public bool DisableGenericPdfImport { get; set; } - public bool NoUpdatePrompt { get; set; } - public bool DeleteAfterSaving { get; set; } + public bool NoDebugLogging { get; set; } - public bool DisableSaveNotifications { get; set; } + public bool NoScannerSharing { get; set; } - public bool SingleInstance { get; set; } + public bool DisableSaveNotifications { get; set; } public string? ComponentsPath { get; set; } @@ -70,6 +89,10 @@ public class AppConfigV0 public PdfCompat ForcePdfCompat { get; set; } + public EsclSecurityPolicy EsclSecurityPolicy { get; set; } + + public string? EsclServerCertificatePath { get; set; } + public EventType EventLogging { get; set; } public KeyboardShortcuts? KeyboardShortcuts { get; set; } diff --git a/NAPS2.Lib/Config/ObsoleteTypes/ExtendedScanSettingsV0.cs b/NAPS2.Lib/Config/ObsoleteTypes/ExtendedScanSettingsV0.cs index 59573a1467..5ebd3a0c17 100644 --- a/NAPS2.Lib/Config/ObsoleteTypes/ExtendedScanSettingsV0.cs +++ b/NAPS2.Lib/Config/ObsoleteTypes/ExtendedScanSettingsV0.cs @@ -15,7 +15,7 @@ public ExtendedScanSettingsV0() BitDepth = ScanBitDepth.C24Bit; PageAlign = ScanHorizontalAlign.Left; PageSize = ScanPageSize.Letter; - Resolution = ScanDpi.Dpi200; + Resolution.Dpi = 200; PaperSource = ScanSource.Glass; } @@ -37,7 +37,7 @@ public ExtendedScanSettingsV0() public PageDimensions? CustomPageSize { get; set; } - public ScanDpi Resolution { get; set; } + public ScanResolution Resolution { get; set; } = new(); public ScanSource PaperSource { get; set; } } \ No newline at end of file diff --git a/NAPS2.Lib/Config/ObsoleteTypes/UserConfigV0.cs b/NAPS2.Lib/Config/ObsoleteTypes/UserConfigV0.cs index c2e80eea9e..cf0fc8d00b 100644 --- a/NAPS2.Lib/Config/ObsoleteTypes/UserConfigV0.cs +++ b/NAPS2.Lib/Config/ObsoleteTypes/UserConfigV0.cs @@ -1,7 +1,7 @@ using System.Xml.Serialization; using NAPS2.ImportExport.Email; using NAPS2.ImportExport.Images; -using NAPS2.ImportExport.Pdf; +using NAPS2.Pdf; using NAPS2.Ocr; using NAPS2.Scan; using NAPS2.Scan.Batch; @@ -15,9 +15,9 @@ public class UserConfigV0 public string? Culture { get; set; } - public List FormStates { get; set; } = new(); + public List? FormStates { get; set; } - public HashSet BackgroundOperations { get; set; } = new(); + public HashSet? BackgroundOperations { get; set; } public bool CheckForUpdates { get; set; } @@ -47,13 +47,11 @@ public class UserConfigV0 public int ThumbnailSize { get; set; } = ThumbnailSizes.DEFAULT_SIZE; - public BatchSettings? LastBatchSettings { get; set; } // TODO: Handle this being null. + public BatchSettings? LastBatchSettings { get; set; } public DockStyle DesktopToolStripDock { get; set; } - public KeyboardShortcuts KeyboardShortcuts { get; set; } = new(); + public KeyboardShortcuts? KeyboardShortcuts { get; set; } - public List CustomPageSizePresets { get; set; } = new(); - - public List SavedProxies { get; set; } = new(); + public List? CustomPageSizePresets { get; set; } } \ No newline at end of file diff --git a/NAPS2.Lib/Config/ProfileManager.cs b/NAPS2.Lib/Config/ProfileManager.cs index cf1d1163c0..a888710003 100644 --- a/NAPS2.Lib/Config/ProfileManager.cs +++ b/NAPS2.Lib/Config/ProfileManager.cs @@ -4,7 +4,6 @@ namespace NAPS2.Config; -// TODO: Fix cross-instance contention public class ProfileManager : IProfileManager { private readonly ProfileSerializer _serializer = new(); @@ -106,7 +105,7 @@ public void Save() { _userScope.Set(c => c, ImmutableList.CreateRange(_profiles!)); } - ProfilesUpdated?.Invoke(this, EventArgs.Empty); + Invoker.Current.InvokeDispatch(() => ProfilesUpdated?.Invoke(this, EventArgs.Empty)); } private List GetProfiles() @@ -152,7 +151,7 @@ private void MergeUserProfilesIntoSystemProfiles(List userProfiles, } var systemProfileNames = new HashSet(systemProfiles.Select(x => x.DisplayName)); - foreach (var profile in userProfiles) + foreach (var profile in userProfiles.ToList()) { if (systemProfileNames.Contains(profile.DisplayName)) { diff --git a/NAPS2.Lib/Config/ProfileSerializer.cs b/NAPS2.Lib/Config/ProfileSerializer.cs index 6f64c89cc9..52c7885eea 100644 --- a/NAPS2.Lib/Config/ProfileSerializer.cs +++ b/NAPS2.Lib/Config/ProfileSerializer.cs @@ -84,7 +84,7 @@ private ImmutableList ReadOldProfiles(Stream configFileStream) { Version = ScanProfile.CURRENT_VERSION, UpgradedFrom = profile.Version, - Device = profile.Device is { ID: { }, Name: { } } ? new ScanDevice(profile.Device.ID, profile.Device.Name) : null, + Device = profile.Device is { ID: { }, Name: { } } ? new ScanProfileDevice(profile.Device.ID, profile.Device.Name) : null, DriverName = profile.DriverName, DisplayName = profile.DisplayName ?? "", MaxQuality = profile.MaxQuality, @@ -123,7 +123,7 @@ private ImmutableList ReadVeryOldProfiles(Stream configFileStream) { Version = ScanProfile.CURRENT_VERSION, UpgradedFrom = 0, - Device = profile.Device is { ID: { }, Name: { } } ? new ScanDevice(profile.Device.ID, profile.Device.Name) : null, + Device = profile.Device is { ID: { }, Name: { } } ? new ScanProfileDevice(profile.Device.ID, profile.Device.Name) : null, DriverName = profile.DriverName, DisplayName = profile.DisplayName ?? "", MaxQuality = profile.MaxQuality, diff --git a/NAPS2.Lib/Config/SaveButtonDefaultAction.cs b/NAPS2.Lib/Config/SaveButtonDefaultAction.cs index 17e1c1fa66..5499350b04 100644 --- a/NAPS2.Lib/Config/SaveButtonDefaultAction.cs +++ b/NAPS2.Lib/Config/SaveButtonDefaultAction.cs @@ -1,9 +1,15 @@ -namespace NAPS2.Config; +using NAPS2.Scan; + +namespace NAPS2.Config; public enum SaveButtonDefaultAction { + [LocalizedDescription(typeof(SettingsResources), "SaveButtonDefaultAction_SaveAll")] SaveAll, + [LocalizedDescription(typeof(SettingsResources), "SaveButtonDefaultAction_SaveSelected")] SaveSelected, + [LocalizedDescription(typeof(SettingsResources), "SaveButtonDefaultAction_AlwaysPrompt")] AlwaysPrompt, + [LocalizedDescription(typeof(SettingsResources), "SaveButtonDefaultAction_PromptIfSelected")] PromptIfSelected } \ No newline at end of file diff --git a/NAPS2.Lib/Config/ScanButtonDefaultAction.cs b/NAPS2.Lib/Config/ScanButtonDefaultAction.cs new file mode 100644 index 0000000000..deb738f025 --- /dev/null +++ b/NAPS2.Lib/Config/ScanButtonDefaultAction.cs @@ -0,0 +1,11 @@ +using NAPS2.Scan; + +namespace NAPS2.Config; + +public enum ScanButtonDefaultAction +{ + [LocalizedDescription(typeof(SettingsResources), "ScanButtonDefaultAction_ScanWithDefaultProfile")] + ScanWithDefaultProfile, + [LocalizedDescription(typeof(SettingsResources), "ScanButtonDefaultAction_AlwaysPrompt")] + AlwaysPrompt +} \ No newline at end of file diff --git a/NAPS2.Lib/Config/StubProfileManager.cs b/NAPS2.Lib/Config/StubProfileManager.cs index a5cebb02a6..5cf56300db 100644 --- a/NAPS2.Lib/Config/StubProfileManager.cs +++ b/NAPS2.Lib/Config/StubProfileManager.cs @@ -5,7 +5,7 @@ namespace NAPS2.Config; public class StubProfileManager : IProfileManager { - private readonly List _profiles = new List(); + private readonly List _profiles = []; public ImmutableList Profiles => ImmutableList.CreateRange(_profiles); diff --git a/NAPS2.Lib/Config/Theme.cs b/NAPS2.Lib/Config/Theme.cs new file mode 100644 index 0000000000..645cb6dec1 --- /dev/null +++ b/NAPS2.Lib/Config/Theme.cs @@ -0,0 +1,13 @@ +using NAPS2.Scan; + +namespace NAPS2.Config; + +public enum Theme +{ + [LocalizedDescription(typeof(SettingsResources), "Theme_Default")] + Default, + [LocalizedDescription(typeof(SettingsResources), "Theme_Light")] + Light, + [LocalizedDescription(typeof(SettingsResources), "Theme_Dark")] + Dark +} \ No newline at end of file diff --git a/NAPS2.Lib/Config/ToolbarButtons.cs b/NAPS2.Lib/Config/ToolbarButtons.cs index 560fa71587..9e833218be 100644 --- a/NAPS2.Lib/Config/ToolbarButtons.cs +++ b/NAPS2.Lib/Config/ToolbarButtons.cs @@ -22,5 +22,6 @@ public enum ToolbarButtons Language = 1 << 15, Settings = 1 << 16, About = 1 << 17, - Donate = 1 << 18 + Donate = 1 << 18, + Sidebar = 1 << 19 } \ No newline at end of file diff --git a/NAPS2.Lib/Dependencies/DownloadController.cs b/NAPS2.Lib/Dependencies/DownloadController.cs new file mode 100644 index 0000000000..1b7e90f3e8 --- /dev/null +++ b/NAPS2.Lib/Dependencies/DownloadController.cs @@ -0,0 +1,178 @@ +using System.Net.Http; +using System.Security.Cryptography; +using Microsoft.Extensions.Logging; +using NAPS2.Scan; + +namespace NAPS2.Dependencies; + +public class DownloadController +{ + private static readonly HttpClient DefaultHttpClient = new(); + + private readonly ScanningContext _scanningContext; + private readonly ILogger _logger; + + private readonly HttpClient _client; + private readonly List _filesToDownload = new(); + private bool _cancel; + + public DownloadController(ScanningContext scanningContext, HttpClient? client = null) + { + _scanningContext = scanningContext; + _logger = scanningContext.Logger; + _client = client ?? DefaultHttpClient; + } + + public int FilesDownloaded { get; private set; } + + public int TotalFiles => _filesToDownload.Count; + + public long CurrentFileSize { get; private set; } + + public long CurrentFileProgress { get; private set; } + + public void QueueFile(DownloadInfo downloadInfo, Action fileCallback) + { + _filesToDownload.Add(new QueueItem(downloadInfo, fileCallback)); + } + + public void QueueFile(IExternalComponent component) + { + _filesToDownload.Add(new QueueItem(component.DownloadInfo, component.Install)); + } + + public void Stop() + { + _cancel = true; + _client.CancelPendingRequests(); + } + + public event EventHandler? DownloadError; + + public event EventHandler? DownloadComplete; + + public event EventHandler? DownloadProgress; + + private async Task TryDownloadFromUrlAsync(string filename, string url) + { + CurrentFileProgress = 0; + CurrentFileSize = 0; + DownloadProgress?.Invoke(this, EventArgs.Empty); + try + { + var response = await _client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + CurrentFileSize = response.Content.Headers.ContentLength.GetValueOrDefault(); + + using var contentStream = await response.Content.ReadAsStreamAsync(); + + var result = new MemoryStream(); + long previousLength; + byte[] buffer = new byte[1024 * 40]; + do + { + previousLength = result.Length; + int length = await contentStream.ReadAsync(buffer, 0, buffer.Length); + if (length > 0) + { + result.Write(buffer, 0, length); + CurrentFileProgress = result.Length; + DownloadProgress?.Invoke(this, EventArgs.Empty); + } + if (_cancel) + { + throw new OperationCanceledException(); + } + } + while (previousLength < result.Length); + return result; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error downloading file: {FileName}", filename); + return null; + } + } + + private async Task TryDownloadQueueItemAsync(QueueItem fileToDownload) + { + foreach (var url in fileToDownload.DownloadInfo.Urls) + { + var result = await TryDownloadFromUrlAsync(fileToDownload.DownloadInfo.FileName, url); + if (result != null) + { + result.Position = 0; + if (fileToDownload.DownloadInfo.Sha256 == CalculateSha256(result)) + { + return result; + } + _logger.LogError("Error downloading file (invalid checksum): {FileName}", fileToDownload.DownloadInfo.FileName); + } + } + return null; + } + + private async Task InternalStartDownloadsAsync() + { + FilesDownloaded = 0; + foreach (var fileToDownload in _filesToDownload) + { + MemoryStream? result; + try + { + result = await TryDownloadQueueItemAsync(fileToDownload); + } + catch (OperationCanceledException) + { + return false; + } + + if (result == null) + { + DownloadComplete?.Invoke(this, EventArgs.Empty); + DownloadError?.Invoke(this, EventArgs.Empty); + return false; + } + + string tempFolder = Path.Combine(_scanningContext.TempFolderPath, Path.GetRandomFileName()); + Directory.CreateDirectory(tempFolder); + string p = Path.Combine(tempFolder, fileToDownload.DownloadInfo.FileName); + try + { + result.Position = 0; + var preparedFilePath = fileToDownload.DownloadInfo.Format.Prepare(result, p); + fileToDownload.FileCallback(preparedFilePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error preparing downloaded file"); + DownloadError?.Invoke(this, EventArgs.Empty); + } + FilesDownloaded++; + Directory.Delete(tempFolder, true); + } + + DownloadComplete?.Invoke(this, EventArgs.Empty); + return true; + } + + private string CalculateSha256(Stream stream) + { + using var sha = SHA256.Create(); + byte[] checksum = sha.ComputeHash(stream); + string str = BitConverter.ToString(checksum).Replace("-", String.Empty).ToLowerInvariant(); + return str; + } + + private record QueueItem(DownloadInfo DownloadInfo, Action FileCallback); + + public async Task StartDownloadsAsync() + { + DownloadProgress?.Invoke(this, EventArgs.Empty); + return await InternalStartDownloadsAsync(); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Dependencies/DownloadFormat.cs b/NAPS2.Lib/Dependencies/DownloadFormat.cs new file mode 100644 index 0000000000..2e7a29ba88 --- /dev/null +++ b/NAPS2.Lib/Dependencies/DownloadFormat.cs @@ -0,0 +1,45 @@ +using System.IO.Compression; + +namespace NAPS2.Dependencies; + +public abstract class DownloadFormat +{ + public static readonly DownloadFormat Gzip = new GzipDownloadFormat(); + + public static readonly DownloadFormat Zip = new ZipDownloadFormat(); + + public abstract string Prepare(MemoryStream stream, string tempFilePath); + + private class GzipDownloadFormat : DownloadFormat + { + private const string FileExtension = ".gz"; + + public override string Prepare(MemoryStream stream, string tempFilePath) + { + if (tempFilePath.EndsWith(FileExtension, StringComparison.InvariantCultureIgnoreCase)) + { + tempFilePath = tempFilePath.Substring(0, tempFilePath.Length - 3); + } + Extract(stream, tempFilePath); + return tempFilePath; + } + + private static void Extract(MemoryStream stream, string destPath) + { + using FileStream outFile = File.Create(destPath); + using GZipStream decompress = new(stream, CompressionMode.Decompress); + decompress.CopyTo(outFile); + } + } + + private class ZipDownloadFormat : DownloadFormat + { + public override string Prepare(MemoryStream stream, string tempFilePath) + { + var tempDir = Path.GetDirectoryName(tempFilePath) ?? throw new ArgumentException("Path was a root path", nameof(tempFilePath)); + ZipArchive archive = new(stream); + archive.ExtractToDirectory(tempDir); + return tempDir; + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Dependencies/DownloadInfo.cs b/NAPS2.Lib/Dependencies/DownloadInfo.cs similarity index 78% rename from NAPS2.Sdk/Dependencies/DownloadInfo.cs rename to NAPS2.Lib/Dependencies/DownloadInfo.cs index 84685ad02a..47af0ac0b9 100644 --- a/NAPS2.Sdk/Dependencies/DownloadInfo.cs +++ b/NAPS2.Lib/Dependencies/DownloadInfo.cs @@ -2,12 +2,12 @@ public class DownloadInfo { - public DownloadInfo(string fileName, List mirrors, double size, string sha1, DownloadFormat format) + public DownloadInfo(string fileName, List mirrors, double size, string sha256, DownloadFormat format) { FileName = fileName; Urls = mirrors.Select(x => x.Url(fileName)).ToList(); Size = size; - Sha1 = sha1; + Sha256 = sha256; Format = format; } @@ -19,5 +19,5 @@ public DownloadInfo(string fileName, List mirrors, double size, public double Size { get; } - public string Sha1 { get; } + public string Sha256 { get; } } \ No newline at end of file diff --git a/NAPS2.Sdk/Dependencies/DownloadMirror.cs b/NAPS2.Lib/Dependencies/DownloadMirror.cs similarity index 100% rename from NAPS2.Sdk/Dependencies/DownloadMirror.cs rename to NAPS2.Lib/Dependencies/DownloadMirror.cs diff --git a/NAPS2.Sdk/Dependencies/ExternalComponent.cs b/NAPS2.Lib/Dependencies/ExternalComponent.cs similarity index 100% rename from NAPS2.Sdk/Dependencies/ExternalComponent.cs rename to NAPS2.Lib/Dependencies/ExternalComponent.cs diff --git a/NAPS2.Sdk/Dependencies/IExternalComponent.cs b/NAPS2.Lib/Dependencies/IExternalComponent.cs similarity index 100% rename from NAPS2.Sdk/Dependencies/IExternalComponent.cs rename to NAPS2.Lib/Dependencies/IExternalComponent.cs diff --git a/NAPS2.Sdk/Dependencies/MultiFileExternalComponent.cs b/NAPS2.Lib/Dependencies/MultiFileExternalComponent.cs similarity index 100% rename from NAPS2.Sdk/Dependencies/MultiFileExternalComponent.cs rename to NAPS2.Lib/Dependencies/MultiFileExternalComponent.cs diff --git a/NAPS2.Lib/EntryPoints/ConsoleEntryPoint.cs b/NAPS2.Lib/EntryPoints/ConsoleEntryPoint.cs index 0fc72b18ff..ccf7fe200b 100644 --- a/NAPS2.Lib/EntryPoints/ConsoleEntryPoint.cs +++ b/NAPS2.Lib/EntryPoints/ConsoleEntryPoint.cs @@ -1,8 +1,10 @@ using Autofac; using CommandLine; using NAPS2.Automation; +using NAPS2.EtoForms; using NAPS2.Modules; using NAPS2.Remoting.Worker; +using NAPS2.Scan; namespace NAPS2.EntryPoints; @@ -11,27 +13,48 @@ namespace NAPS2.EntryPoints; /// public static class ConsoleEntryPoint { - public static int Run(string[] args, Module imageModule) + public static int Run(string[] args, Module imageModule, Module platformModule) { // Parse the command-line arguments (and display help text if appropriate) - var options = Parser.Default.ParseArguments(args).Value; + var options = new Parser(settings => + { + settings.HelpWriter = Console.Error; + settings.CaseInsensitiveEnumValues = true; + }).ParseArguments(args).Value; if (options == null) { return 0; } // Initialize Autofac (the DI framework) - var container = AutoFacHelper.FromModules( - new CommonModule(), imageModule, new ConsoleModule(options), new RecoveryModule(), new ContextModule()); + var container = AutoFacHelper.FromModules(new CommonModule(), imageModule, platformModule, + new ConsoleModule(options), new RecoveryModule(), new StaticInitModule()); Paths.ClearTemp(); // Start a pending worker process - container.Resolve().Init(); + container.Resolve().Init( + container.Resolve(), + new WorkerFactoryInitOptions { StartSpareWorkers = false }); // Run the scan automation logic var scanning = container.Resolve(); - scanning.Execute().Wait(); + + if (options.Progress) + { + // We need to set up an Eto application in order to be able to display a progress GUI + EtoPlatform.Current.InitializeForegroundApp(); + container.Resolve().SetCulturesFromConfig(); + + var application = EtoPlatform.Current.CreateApplication(); + application.Initialized += (_, _) => scanning.Execute().ContinueWith(_ => application.Quit()); + Invoker.Current = new EtoInvoker(application); + application.Run(); + } + else + { + scanning.Execute().Wait(); + } return ((ConsoleErrorOutput) container.Resolve()).HasError ? 1 : 0; } diff --git a/NAPS2.Lib/EntryPoints/CoreWorkerEntryPoint.cs b/NAPS2.Lib/EntryPoints/CoreWorkerEntryPoint.cs new file mode 100644 index 0000000000..b9e479308a --- /dev/null +++ b/NAPS2.Lib/EntryPoints/CoreWorkerEntryPoint.cs @@ -0,0 +1,82 @@ +using System.Threading; +using GrpcDotNetNamedPipes; +using Microsoft.Extensions.Logging; +using NAPS2.Remoting.Worker; + +namespace NAPS2.EntryPoints; + +/// +/// A stripped-down version of WorkerEntryPoint with limited dependencies. +/// +public static class CoreWorkerEntryPoint +{ + private static readonly TimeSpan ParentCheckInterval = TimeSpan.FromSeconds(10); + + internal static int Run(string[] args, ILogger logger, WorkerServiceImpl serviceImpl, Action? run = null, Action? stop = null) + { + try + { + // Expect a single argument, the parent process id + if (args.Length != 1 || !int.TryParse(args[0], out int procId) || !IsProcessRunning(procId)) + { + return 1; + } + + void UnhandledTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + { + logger.LogError(e.Exception, "An error occurred that caused the worker task to terminate."); + e.SetObserved(); + } + TaskScheduler.UnobservedTaskException += UnhandledTaskException; + + var flag = new ManualResetEvent(false); + run ??= () => flag.WaitOne(); + stop ??= () => flag.Set(); + + // Connect to the main NAPS2 process and listen for assigned work + var server = + new NamedPipeServer(string.Format(WorkerFactory.PIPE_NAME_FORMAT, Process.GetCurrentProcess().Id)); + serviceImpl.OnStop += (_, _) => stop(); + using var parentCheckTimer = new Timer(_ => + { + // The Job object created by the parent is supposed to kill the child processes, + // but it can have issues (especially on Windows 7). This is a backup to avoid leftover workers. + if (!IsProcessRunning(procId)) + { + serviceImpl.Stop(); + } + }, null, TimeSpan.Zero, ParentCheckInterval); + WorkerService.BindService(server.ServiceBinder, serviceImpl); + server.Start(); + try + { + Console.WriteLine(@"ready"); + run(); + } + finally + { + server.Kill(); + } + return 0; + } + catch (Exception ex) + { + Console.Write(@"error"); + logger.LogError(ex, "An error occurred that caused the worker application to close."); + return 1; + } + } + + private static bool IsProcessRunning(int procId) + { + try + { + var proc = Process.GetProcessById(procId); + return !proc.HasExited; + } + catch (ArgumentException) + { + return false; + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EntryPoints/GuiEntryPoint.cs b/NAPS2.Lib/EntryPoints/GuiEntryPoint.cs new file mode 100644 index 0000000000..d6fa69b57d --- /dev/null +++ b/NAPS2.Lib/EntryPoints/GuiEntryPoint.cs @@ -0,0 +1,46 @@ +using Autofac; +using NAPS2.EtoForms; +using NAPS2.EtoForms.Ui; +using NAPS2.Modules; +using NAPS2.Remoting.Worker; +using NAPS2.Scan; + +namespace NAPS2.EntryPoints; + +/// +/// The entry point for the main NAPS2 executable. +/// +public static class GuiEntryPoint +{ + public static int Run(string[] args, Module imageModule, Module platformModule) + { + // Initialize Autofac (the DI framework) + var container = AutoFacHelper.FromModules( + new CommonModule(), imageModule, platformModule, new GuiModule(), new RecoveryModule(), new StaticInitModule()); + + Paths.ClearTemp(); + + // Parse the command-line arguments and see if we're doing something other than displaying the main form + var lifecycle = container.Resolve(); + lifecycle.ParseArgs(args); + lifecycle.ExitIfRedundant(); + + EtoPlatform.Current.InitializeForegroundApp(); + + // Set up basic application configuration + container.Resolve().SetCulturesFromConfig(); + Trace.Listeners.Add(new ConsoleTraceListener()); + + // Start a pending worker process + container.Resolve().Init(container.Resolve()); + + // Show the main form + var application = EtoPlatform.Current.CreateApplication(); + Invoker.Current = new EtoInvoker(application); + var formFactory = container.Resolve(); + var desktop = formFactory.Create(); + + application.Run(desktop); + return 0; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EntryPoints/ServerEntryPoint.cs b/NAPS2.Lib/EntryPoints/ServerEntryPoint.cs new file mode 100644 index 0000000000..aeae1c825c --- /dev/null +++ b/NAPS2.Lib/EntryPoints/ServerEntryPoint.cs @@ -0,0 +1,82 @@ +using Autofac; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using NAPS2.EtoForms; +using NAPS2.EtoForms.Ui; +using NAPS2.Modules; +using NAPS2.Remoting; +using NAPS2.Remoting.Server; +using NAPS2.Remoting.Worker; +using NAPS2.Scan; + +namespace NAPS2.EntryPoints; + +/// +/// The entry point for NAPS2 running as a server for scanner sharing. +/// +public static class ServerEntryPoint +{ + public static int Run(string[] args, Module imageModule, Module platformModule) + { + // Initialize Autofac (the DI framework) + var container = + AutoFacHelper.FromModules(new CommonModule(), imageModule, platformModule, new StaticInitModule()); + + // Start a pending worker process + container.Resolve().Init(container.Resolve()); + + container.Resolve().SetCulturesFromConfig(); + + // We need to set up an Eto application in order to display a tray indicator + var application = EtoPlatform.Current.CreateApplication(); + application.Initialized += (_, _) => + { + var sharedDeviceManager = container.Resolve(); + var processCoordinator = container.Resolve(); + var trayIndicator = container.Resolve(); + + void Stop(bool unregister = false) + { + processCoordinator.KillServer(); + sharedDeviceManager.StopSharing(); + if (unregister) + { + // If the user manually stops the background app, treat it the same as if they unchecked + // "Share even when NAPS2 is closed". + // We need to do this in a separate process as on Mac/Linux this process will die as soon as the service + // is unregistered and the full cleanup won't happen. + Process.Start(AssemblyHelper.EntryFile, "/UnregisterSharingService").WaitForExit(5000); + } + application.Quit(); + } + + // Start the actual sharing server + sharedDeviceManager.StartSharing(); + // Listen for the StopSharingServer event, which is sent if the user unchecks + // "Share even when NAPS2 is closed" + processCoordinator.StartServer(new ProcessCoordinatorServiceImpl(() => Stop())); + + // Set up and show the tray indicator, which has a single "Stop Scanner Sharing" menu item + trayIndicator.StopClicked += (_, _) => Stop(true); + trayIndicator.Show(); + + // If the server was started on the command-line, allow Ctrl+C to terminate it + Console.CancelKeyPress += (_, _) => Stop(); + }; + Invoker.Current = new EtoInvoker(application); + application.Run(); + + return 0; + } + + private class ProcessCoordinatorServiceImpl(Action stop) + : ProcessCoordinatorService.ProcessCoordinatorServiceBase + { + public override Task StopSharingServer(StopSharingServerRequest request, + ServerCallContext context) + { + stop(); + return Task.FromResult(new StopSharingServerResponse { Stopped = true }); + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EntryPoints/WorkerEntryPoint.cs b/NAPS2.Lib/EntryPoints/WorkerEntryPoint.cs index e898b9c694..bcdabddb33 100644 --- a/NAPS2.Lib/EntryPoints/WorkerEntryPoint.cs +++ b/NAPS2.Lib/EntryPoints/WorkerEntryPoint.cs @@ -1,7 +1,5 @@ -using System.Text; -using System.Threading; -using Autofac; -using GrpcDotNetNamedPipes; +using Autofac; +using Microsoft.Extensions.Logging; using NAPS2.Modules; using NAPS2.Remoting.Worker; @@ -14,89 +12,15 @@ namespace NAPS2.EntryPoints; /// public static class WorkerEntryPoint { - private static readonly TimeSpan ParentCheckInterval = TimeSpan.FromSeconds(10); - public static int Run(string[] args, Module imageModule, Action? run = null, Action? stop = null) { - try - { -#if DEBUG - // Debugger.Launch(); -#endif - // Initialize Autofac (the DI framework) var container = AutoFacHelper.FromModules( - new CommonModule(), imageModule, new WorkerModule(), new ContextModule()); - - // Expect a single argument, the parent process id - if (args.Length != 1 || !int.TryParse(args[0], out int procId) || !IsProcessRunning(procId)) - { - return 1; - } - - TaskScheduler.UnobservedTaskException += UnhandledTaskException; + new CommonModule(), imageModule, new WorkerModule(), new StaticInitModule()); - var flag = new ManualResetEvent(false); - run ??= () => flag.WaitOne(); - stop ??= () => flag.Set(); - - // Connect to the main NAPS2 process and listen for assigned work - var server = - new NamedPipeServer(string.Format(WorkerFactory.PIPE_NAME_FORMAT, Process.GetCurrentProcess().Id)); + var logger = container.Resolve(); var serviceImpl = container.Resolve(); - serviceImpl.OnStop += (_, _) => stop(); - using var parentCheckTimer = new Timer(_ => - { - // The Job object created by the parent is supposed to kill the child processes, - // but it can have issues (especially on Windows 7). This is a backup to avoid leftover workers. - if (!IsProcessRunning(procId)) - { - serviceImpl.Stop(); - } - }, null, TimeSpan.Zero, ParentCheckInterval); - WorkerService.BindService(server.ServiceBinder, serviceImpl); - server.Start(); - try - { - Console.WriteLine(@"ready"); - run(); - } - finally - { - server.Kill(); - } - return 0; - } - catch (Exception ex) - { - Console.Write(@"error"); - Log.FatalException("An error occurred that caused the worker application to close.", ex); - return 1; - } - } - private static string ReadEncodedString() - { - string input = Console.ReadLine() ?? throw new InvalidOperationException("No input"); - return Encoding.UTF8.GetString(Convert.FromBase64String(input)); - } - - private static bool IsProcessRunning(int procId) - { - try - { - var proc = Process.GetProcessById(procId); - return !proc.HasExited; - } - catch (ArgumentException) - { - return false; - } - } - - private static void UnhandledTaskException(object? sender, UnobservedTaskExceptionEventArgs e) - { - Log.FatalException("An error occurred that caused the worker task to terminate.", e.Exception); - e.SetObserved(); + return CoreWorkerEntryPoint.Run(args, logger, serviceImpl, run, stop); } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/ActionCommand.cs b/NAPS2.Lib/EtoForms/ActionCommand.cs index 2595bfcbf6..744ad15e28 100644 --- a/NAPS2.Lib/EtoForms/ActionCommand.cs +++ b/NAPS2.Lib/EtoForms/ActionCommand.cs @@ -1,3 +1,4 @@ +using Eto.Drawing; using Eto.Forms; namespace NAPS2.EtoForms; @@ -23,6 +24,14 @@ public string Text { ToolBarText = value; MenuText = value; + TextChanged?.Invoke(this, EventArgs.Empty); } } + + public event EventHandler? TextChanged; + + public string? IconName { get; set; } + + public Image? GetIconImage(float scale) => + IconName != null ? EtoPlatform.Current.IconProvider.GetIcon(IconName, scale) : null; } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/ButtonFlags.cs b/NAPS2.Lib/EtoForms/ButtonFlags.cs new file mode 100644 index 0000000000..08335e49f6 --- /dev/null +++ b/NAPS2.Lib/EtoForms/ButtonFlags.cs @@ -0,0 +1,9 @@ +namespace NAPS2.EtoForms; + +[Flags] +public enum ButtonFlags +{ + None, + LargeIcon, + LargeText +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/ColorScheme.cs b/NAPS2.Lib/EtoForms/ColorScheme.cs new file mode 100644 index 0000000000..ffbf7b1d53 --- /dev/null +++ b/NAPS2.Lib/EtoForms/ColorScheme.cs @@ -0,0 +1,59 @@ +using Eto.Drawing; + +namespace NAPS2.EtoForms; + +public class ColorScheme +{ + private static readonly Color VeryDarkGray = Color.FromRgb(0x262626); + private static readonly Color MidGray = Color.FromRgb(0x606060); + private static readonly Color LightGray = Color.FromRgb(0xdddddd); + private static readonly Color HighlightBlue = Color.FromRgb(0x007bff); + private static readonly Color MidBlue = Color.FromRgb(0x60a0e8); + private static readonly Color PaleBlue = Color.FromRgb(0xcce8ff); + private static readonly Color DarkGrayBlue = Color.FromRgb(0x28445b); + private static readonly Color DarkOutlineBlue = Color.FromRgb(0x0078d4); + + private readonly IDarkModeProvider _darkModeProvider; + + public ColorScheme(IDarkModeProvider darkModeProvider) + { + _darkModeProvider = darkModeProvider; + _darkModeProvider.DarkModeChanged += (_, _) => ColorSchemeChanged?.Invoke(this, EventArgs.Empty); + } + + public bool DarkMode => (Config ?? throw new InvalidOperationException()).Get(c => c.Theme) switch + { + Theme.Light => false, + Theme.Dark => true, + _ => _darkModeProvider.IsDarkModeEnabled, + }; + + public Naps2Config? Config { get; set; } + + public void UserThemeChanged() + { + ColorSchemeChanged?.Invoke(this, EventArgs.Empty); + } + + public Color ForegroundColor => DarkMode ? Colors.White : Colors.Black; + + public Color BackgroundColor => DarkMode ? VeryDarkGray : Colors.White; + + public Color SeparatorColor => DarkMode ? MidGray : LightGray; + + public Color BorderColor => DarkMode ? LightGray : Colors.Black; + + public Color CropColor => DarkMode ? HighlightBlue : Colors.Black; + + public Color HighlightBorderColor => DarkMode ? DarkOutlineBlue : MidBlue; + + public Color HighlightBackgroundColor => DarkMode ? DarkGrayBlue : PaleBlue; + + public Color NotificationBackgroundColor => DarkMode ? Color.FromRgb(0x323232) : Color.FromRgb(0xf2f2f2); + + public Color NotificationBorderColor => DarkMode ? Color.FromRgb(0x606060) : Color.FromRgb(0xb2b2b2); + + public Color LinkColor => DarkMode ? Color.FromRgb(0x60cdff) : Color.FromRgb(0x0000ff); + + public event EventHandler? ColorSchemeChanged; +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/DefaultIconProvider.cs b/NAPS2.Lib/EtoForms/DefaultIconProvider.cs index 04ca31e011..8fc5bebe0d 100644 --- a/NAPS2.Lib/EtoForms/DefaultIconProvider.cs +++ b/NAPS2.Lib/EtoForms/DefaultIconProvider.cs @@ -4,10 +4,54 @@ namespace NAPS2.EtoForms; public class DefaultIconProvider : IIconProvider { - public Image? GetIcon(string name) + public Bitmap? GetIcon(string name, float scale = 1f, bool oversized = false) { + if (scale > 1) + { + // TODO: Maybe generalize everything with a numeric pixel size suffix? + if (name.EndsWith("_small")) + { + var norm = (byte[]?) Icons.ResourceManager.GetObject(name.Substring(0, name.Length - 6)); + if (norm != null) + { + return new Bitmap(norm).ResizeTo((int) (16 * scale)); + } + } + else if (name.EndsWith("_48")) + { + var hires = (byte[]?) Icons.ResourceManager.GetObject(name.Substring(0, name.Length - 3) + "_96"); + if (hires != null) + { + return new Bitmap(hires).ResizeTo((int) (48 * scale)); + } + } + else + { + var hires = (byte[]?) Icons.ResourceManager.GetObject(name + "_hires"); + if (hires != null) + { + return new Bitmap(hires).ResizeTo((int) (32 * scale)); + } + } + } + var data = (byte[]?) Icons.ResourceManager.GetObject(name); - if (data == null) return null; - return new Bitmap(data); + if (data != null) + { + var bitmap = new Bitmap(data); + if (scale > 1) + { + return bitmap.ResizeTo((int) (bitmap.Width * scale), (int) (bitmap.Height * scale)); + } + return bitmap; + } + + return null; + } + + public Icon? GetFormIcon(string name, float scale = 1f) + { + var icon = GetIcon(name, scale); + return icon != null ? new Icon(1f, icon) : null; } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Desktop/DesktopController.cs b/NAPS2.Lib/EtoForms/Desktop/DesktopController.cs index 163051c0ac..2048650d9e 100644 --- a/NAPS2.Lib/EtoForms/Desktop/DesktopController.cs +++ b/NAPS2.Lib/EtoForms/Desktop/DesktopController.cs @@ -1,21 +1,20 @@ using System.Threading; +using Eto.Drawing; using Eto.Forms; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using NAPS2.EtoForms.Notifications; using NAPS2.ImportExport; using NAPS2.ImportExport.Images; using NAPS2.Platform.Windows; using NAPS2.Recovery; using NAPS2.Remoting; +using NAPS2.Remoting.Server; using NAPS2.Scan; using NAPS2.Update; namespace NAPS2.EtoForms.Desktop; -// TODO: We undoubtedly want to decompose this file even further. -// We almost certainly want a DesktopScanController for the scanning-related logic. -// We could have a DesktopPipesController that depends on DesktopScanController. -// Specifically each line in Initialize might make sense as a sub-controller. -// We also need to think about how to pass the Form instance around as needed. (e.g. to Activate it). Maybe this should be something injectable, and could also be used by UpdateOperation instead of searching through open forms. -// i.e. (I)DesktopFormProvider public class DesktopController { private readonly ScanningContext _scanningContext; @@ -27,18 +26,21 @@ public class DesktopController private readonly IOperationFactory _operationFactory; private readonly StillImage _stillImage; private readonly IUpdateChecker _updateChecker; - private readonly INotificationManager _notify; - private readonly ImageTransfer _imageTransfer; + private readonly INotify _notify; private readonly ImageClipboard _imageClipboard; private readonly ImageListActions _imageListActions; - private readonly IExportController _exportController; private readonly DialogHelper _dialogHelper; private readonly DesktopImagesController _desktopImagesController; private readonly IDesktopScanController _desktopScanController; private readonly DesktopFormProvider _desktopFormProvider; private readonly IScannedImagePrinter _scannedImagePrinter; + private readonly ISharedDeviceManager _sharedDeviceManager; + private readonly ProcessCoordinator _processCoordinator; + private readonly RecoveryManager _recoveryManager; + private readonly ImageTransfer _imageTransfer = new(); private bool _closed; + private bool _preInitialized; private bool _initialized; private bool _suspended; @@ -46,11 +48,13 @@ public DesktopController(ScanningContext scanningContext, UiImageList imageList, RecoveryStorageManager recoveryStorageManager, ThumbnailController thumbnailController, OperationProgress operationProgress, Naps2Config config, IOperationFactory operationFactory, StillImage stillImage, - IUpdateChecker updateChecker, INotificationManager notify, ImageTransfer imageTransfer, - ImageClipboard imageClipboard, ImageListActions imageListActions, IExportController exportController, + IUpdateChecker updateChecker, INotify notify, + ImageClipboard imageClipboard, ImageListActions imageListActions, DialogHelper dialogHelper, DesktopImagesController desktopImagesController, IDesktopScanController desktopScanController, - DesktopFormProvider desktopFormProvider, IScannedImagePrinter scannedImagePrinter) + DesktopFormProvider desktopFormProvider, IScannedImagePrinter scannedImagePrinter, + ISharedDeviceManager sharedDeviceManager, ProcessCoordinator processCoordinator, + RecoveryManager recoveryManager) { _scanningContext = scanningContext; _imageList = imageList; @@ -62,35 +66,58 @@ public DesktopController(ScanningContext scanningContext, UiImageList imageList, _stillImage = stillImage; _updateChecker = updateChecker; _notify = notify; - _imageTransfer = imageTransfer; _imageClipboard = imageClipboard; _imageListActions = imageListActions; - _exportController = exportController; _dialogHelper = dialogHelper; _desktopImagesController = desktopImagesController; _desktopScanController = desktopScanController; _desktopFormProvider = desktopFormProvider; _scannedImagePrinter = scannedImagePrinter; + _sharedDeviceManager = sharedDeviceManager; + _processCoordinator = processCoordinator; + _recoveryManager = recoveryManager; } public bool SkipRecoveryCleanup { get; set; } + public void PreInitialize() + { + if (_preInitialized) return; + _preInitialized = true; + RestoreSession(); + } + public async Task Initialize() { if (_initialized) return; _initialized = true; - StartPipesServer(); + _sharedDeviceManager.StartSharing(); + StartProcessCoordinator(); ShowStartupMessages(); ShowRecoveryPrompt(); + ImportFilesFromCommandLine(); InitThumbnailRendering(); await RunStillImageEvents(); SetFirstRunDate(); - ShowDonationPrompt(); + ShowDonationOrReviewPrompt(); ShowUpdatePrompt(); } - private void ShowDonationPrompt() + private void ShowDonationOrReviewPrompt() { + // Show a review prompt after a month of using the Microsoft Store msix version +#if MSI + if (WindowsEnvironment.IsRunningAsMsix && + !_config.Get(c => c.HasBeenPromptedForReview) && + DateTime.Now - _config.Get(c => c.FirstRunDate) > TimeSpan.FromDays(30)) + { + var transact = _config.User.BeginTransaction(); + transact.Set(c => c.HasBeenPromptedForReview, true); + transact.Set(c => c.LastReviewPromptDate, DateTime.Now); + transact.Commit(); + _notify.ReviewPrompt(); + } +#endif // Show a donation prompt after a month of use #if !MSI if (!_config.Get(c => c.HiddenButtons).HasFlag(ToolbarButtons.Donate) && @@ -160,14 +187,15 @@ private async Task RunStillImageEvents() public void Cleanup() { if (_suspended) return; - Pipes.KillServer(); - if (!SkipRecoveryCleanup) + _processCoordinator.KillServer(); + _sharedDeviceManager.StopSharing(); + if (!SkipRecoveryCleanup && !_config.Get(c => c.KeepSession)) { try { - // TODO: Figure out and fix undisposed processed images _scanningContext.Dispose(); _recoveryStorageManager.Dispose(); + _imageList.Images.DisposeAll(); } catch (Exception ex) { @@ -176,11 +204,12 @@ public void Cleanup() } _closed = true; _thumbnailController.Dispose(); + _scanningContext.WorkerFactory!.StopSpareWorkers(); } public bool PrepareForClosing(bool userClosing) { - if (_closed) return true; + if (_suspended || _closed) return true; if (_operationProgress.ActiveOperations.Any()) { @@ -203,9 +232,9 @@ public bool PrepareForClosing(bool userClosing) SkipRecoveryCleanup = true; } } - else if (_imageList.Images.Any() && _imageList.SavedState != _imageList.CurrentState) + else if (_imageList.Images.Any() && _imageList.HasUnsavedChanges) { - if (userClosing && !SkipRecoveryCleanup) + if (userClosing && !SkipRecoveryCleanup && !_config.Get(c => c.KeepSession)) { var result = MessageBox.Show(_desktopFormProvider.DesktopForm, MiscResources.ExitWithUnsavedChanges, MiscResources.UnsavedChanges, @@ -214,7 +243,7 @@ public bool PrepareForClosing(bool userClosing) { return false; } - _imageList.SavedState = _imageList.CurrentState; + _imageList.MarkAllSaved(); } else { @@ -239,7 +268,7 @@ public bool PrepareForClosing(bool userClosing) { } _closed = true; - Invoker.Current.SafeInvoke(_desktopFormProvider.DesktopForm.Close); + Invoker.Current.Invoke(_desktopFormProvider.DesktopForm.Close); }); return false; } @@ -247,44 +276,10 @@ public bool PrepareForClosing(bool userClosing) return true; } - private void StartPipesServer() + private void StartProcessCoordinator() { - // Receive messages from other processes - Pipes.StartServer(msg => - { - if (msg.StartsWith(Pipes.MSG_SCAN_WITH_DEVICE, StringComparison.InvariantCulture)) - { - Invoker.Current.SafeInvoke(async () => - await _desktopScanController.ScanWithDevice(msg.Substring(Pipes.MSG_SCAN_WITH_DEVICE.Length))); - } - if (msg.Equals(Pipes.MSG_ACTIVATE)) - { - Invoker.Current.SafeInvoke(() => - { - // TODO: xplat - var formOnTop = Application.Instance.Windows.Last(); - if (formOnTop.WindowState == WindowState.Minimized) - { - Win32.ShowWindow(formOnTop.NativeHandle, Win32.ShowWindowCommands.Restore); - } - formOnTop.BringToFront(); - }); - } - if (msg.Equals(Pipes.MSG_CLOSE_WINDOW)) - { - Invoker.Current.SafeInvoke(() => - { - _desktopFormProvider.DesktopForm.Close(); -#if NET6_0_OR_GREATER - if (OperatingSystem.IsMacOS()) - { - // Closing the main window isn't enough to quit the app on Mac - Application.Instance.Quit(); - } -#endif - }); - } - }); + // Receive messages from other NAPS2 processes + _processCoordinator.StartServer(new ProcessCoordinatorServiceImpl(this)); } private void ShowStartupMessages() @@ -298,29 +293,71 @@ private void ShowStartupMessages() } } + private void RestoreSession() + { + // In case the user has the "Keep images across sessions" option, this is similar to the RecoveryOperation in + // ShowRecoveryPrompt, but designed to be faster and more seamless. This means a few things: + // - Image files are moved instead of copied. This is destructive (higher risk of data loss) but fast. + // - Thumbnail rendering is deferred. + // - No operation progress is displayed and the recovery happens synchronously during OnLoad instead of + // asynchronously after OnShown. + // - Images are sent back to the UI as a single batch. + if (!_config.Get(c => c.KeepSession)) + { + return; + } + using var recoverableFolder = _recoveryManager.GetLatestRecoverableFolder(); + if (recoverableFolder != null) + { + _desktopImagesController.AppendImageBatch(recoverableFolder.FastRecover()); + } + } + private void ShowRecoveryPrompt() { + if (_config.Get(c => c.KeepSession)) + { + return; + } // Allow scanned images to be recovered in case of an unexpected close var op = _operationFactory.Create(); - if (op.Start(_desktopImagesController.ReceiveScannedImage(), - new RecoveryParams { ThumbnailSize = _thumbnailController.RenderSize })) + var recoveryParams = new RecoveryParams + { + ThumbnailSize = _thumbnailController.RenderSize + }; + if (op.Start(_desktopImagesController.ReceiveScannedImage(), recoveryParams)) { _operationProgress.ShowProgress(op); } } + private void ImportFilesFromCommandLine() + { + if (Environment.GetCommandLineArgs() is [_, var arg] && File.Exists(arg)) + { + ImportFiles([arg]); + } + } + private void InitThumbnailRendering() { _thumbnailController.Init(_imageList); } - public void ImportFiles(IEnumerable files) + public void ImportFiles(ICollection files, bool background = false) { var op = _operationFactory.Create(); if (op.Start(OrderFiles(files), _desktopImagesController.ReceiveScannedImage(), new ImportParams { ThumbnailSize = _thumbnailController.RenderSize })) { - _operationProgress.ShowProgress(op); + if (background) + { + _operationProgress.ShowBackgroundProgress(op); + } + else + { + _operationProgress.ShowProgress(op); + } } } @@ -332,7 +369,7 @@ private List OrderFiles(IEnumerable files) return filesList; } - public void ImportDirect(ImageTransferData data, bool copy) + internal void ImportDirect(ImageTransferData data, bool copy) { var op = _operationFactory.Create(); if (op.Start(data, copy, _desktopImagesController.ReceiveScannedImage(), @@ -348,6 +385,18 @@ public void Paste() { ImportDirect(_imageTransfer.GetFromClipboard(), true); } + else if (Clipboard.Instance.ContainsImage) + { + var etoBitmap = (Bitmap) Clipboard.Instance.Image; + Task.Run(() => + { + var image = EtoPlatform.Current.FromBitmap(etoBitmap); + var processedImage = _scanningContext.CreateProcessedImage(image); + processedImage = ImportPostProcessor.AddPostProcessingData(processedImage, image, + _thumbnailController.RenderSize, new BarcodeDetectionOptions(), true); + _desktopImagesController.ReceiveScannedImage()(processedImage); + }); + } } public async Task Copy() @@ -364,16 +413,16 @@ public void Clear() if (MessageBox.Show(_desktopFormProvider.DesktopForm, string.Format(MiscResources.ConfirmClearItems, _imageList.Images.Count), MiscResources.Clear, MessageBoxButtons.OKCancel, - MessageBoxType.Question) == DialogResult.Ok) + MessageBoxType.Question, MessageBoxDefaultButton.OK) == DialogResult.Ok) { _imageListActions.DeleteAll(); + GC.Collect(); } } } public void Delete() { - // TODO: Move to ImageListActions and use a null parent form if (_imageList.Selection.Any()) { if (MessageBox.Show(_desktopFormProvider.DesktopForm, @@ -382,18 +431,11 @@ public void Delete() MessageBoxType.Question, MessageBoxDefaultButton.OK) == DialogResult.Ok) { _imageListActions.DeleteSelected(); + GC.Collect(); } } } - public void RunDocumentCorrection() - { - foreach (var image in _imageList.Selection) - { - image.AddTransform(new CorrectionTransform(CorrectionMode.Document)); - } - } - public void ResetImage() { if (_imageList.Selection.Any()) @@ -415,8 +457,7 @@ public async Task SavePdf() if (action == SaveButtonDefaultAction.AlwaysPrompt || action == SaveButtonDefaultAction.PromptIfSelected && _imageList.Selection.Any()) { - // TODO - // tsdSavePDF.ShowDropDown(); + _desktopFormProvider.DesktopForm.ShowToolbarMenu(DesktopToolbarMenuType.SavePdf); } else if (action == SaveButtonDefaultAction.SaveSelected && _imageList.Selection.Any()) { @@ -435,8 +476,7 @@ public async Task SaveImages() if (action == SaveButtonDefaultAction.AlwaysPrompt || action == SaveButtonDefaultAction.PromptIfSelected && _imageList.Selection.Any()) { - // TODO - // tsdSaveImages.ShowDropDown(); + _desktopFormProvider.DesktopForm.ShowToolbarMenu(DesktopToolbarMenuType.SaveImages); } else if (action == SaveButtonDefaultAction.SaveSelected && _imageList.Selection.Any()) { @@ -455,8 +495,7 @@ public async Task EmailPdf() if (action == SaveButtonDefaultAction.AlwaysPrompt || action == SaveButtonDefaultAction.PromptIfSelected && _imageList.Selection.Any()) { - // TODO - // tsdEmailPDF.ShowDropDown(); + _desktopFormProvider.DesktopForm.ShowToolbarMenu(DesktopToolbarMenuType.EmailPdf); } else if (action == SaveButtonDefaultAction.SaveSelected && _imageList.Selection.Any()) { @@ -473,9 +512,12 @@ public async Task Print() var state = _imageList.CurrentState; using var allImages = _imageList.Images.Select(x => x.GetClonedImage()).ToDisposableList(); using var selectedImages = _imageList.Selection.Select(x => x.GetClonedImage()).ToDisposableList(); - if (await _scannedImagePrinter.PromptToPrint(allImages.InnerList, selectedImages.InnerList)) + if (await _scannedImagePrinter.PromptToPrint( + _desktopFormProvider.DesktopForm, allImages.InnerList, selectedImages.InnerList)) { - _imageList.SavedState = state; + // Ideally we would know the exact images saved but it's not a big deal to get it wrong for printing which + // is pretty uncommon. + _imageList.MarkSaved(state, allImages); } } @@ -496,4 +538,65 @@ public void Resume() { _suspended = false; } + + private class ProcessCoordinatorServiceImpl(DesktopController controller) + : ProcessCoordinatorService.ProcessCoordinatorServiceBase + { + public override Task Activate(ActivateRequest request, ServerCallContext context) + { + Invoker.Current.InvokeDispatch(() => + { + var formOnTop = Application.Instance.Windows.Last(); + if (PlatformCompat.System.CanUseWin32) + { + if (formOnTop.WindowState == WindowState.Minimized) + { + Win32.ShowWindow(formOnTop.NativeHandle, Win32.ShowWindowCommands.Restore); + } + Win32.SetForegroundWindow(formOnTop.NativeHandle); + } + else + { + formOnTop.BringToFront(); + } + }); + return Task.FromResult(new Empty()); + } + + public override Task CloseWindow(CloseWindowRequest request, ServerCallContext context) + { + Invoker.Current.InvokeDispatch(() => + { + controller._desktopFormProvider.DesktopForm.Close(); +#if NET6_0_OR_GREATER + if (OperatingSystem.IsMacOS()) + { + // Closing the main window isn't enough to quit the app on Mac + Application.Instance.Quit(); + } +#endif + }); + return Task.FromResult(new Empty()); + } + + public override Task OpenFile(OpenFileRequest request, ServerCallContext context) + { + Invoker.Current.InvokeDispatch(() => controller.ImportFiles(request.Path, true)); + return Task.FromResult(new Empty()); + } + + public override Task ScanWithDevice(ScanWithDeviceRequest request, ServerCallContext context) + { + Invoker.Current.InvokeDispatch(async () => + await controller._desktopScanController.ScanWithDevice(request.Device)); + return Task.FromResult(new Empty()); + } + + public override Task StopSharingServer(StopSharingServerRequest request, + ServerCallContext context) + { + controller._sharedDeviceManager.InvokeSharingServerStopped(); + return Task.FromResult(new StopSharingServerResponse()); + } + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Desktop/DesktopFormProvider.cs b/NAPS2.Lib/EtoForms/Desktop/DesktopFormProvider.cs index 54b6ced6cc..2d4abc8fa0 100644 --- a/NAPS2.Lib/EtoForms/Desktop/DesktopFormProvider.cs +++ b/NAPS2.Lib/EtoForms/Desktop/DesktopFormProvider.cs @@ -1,10 +1,12 @@ +using NAPS2.EtoForms.Ui; + namespace NAPS2.EtoForms.Desktop; public class DesktopFormProvider { - private EtoFormBase? _desktopForm; + private DesktopForm? _desktopForm; - public EtoFormBase DesktopForm + public DesktopForm DesktopForm { get => _desktopForm ?? throw new InvalidOperationException(); set diff --git a/NAPS2.Lib/EtoForms/Desktop/DesktopImagesController.cs b/NAPS2.Lib/EtoForms/Desktop/DesktopImagesController.cs index c28c3e4c3a..b794ba6fe4 100644 --- a/NAPS2.Lib/EtoForms/Desktop/DesktopImagesController.cs +++ b/NAPS2.Lib/EtoForms/Desktop/DesktopImagesController.cs @@ -28,4 +28,11 @@ public Action ReceiveScannedImage() } }; } + + public void AppendImageBatch(IEnumerable images) + { + _imageList.Mutate( + new ImageListMutation.Append(images.Select(image => new UiImage(image))), + isPassiveInteraction: true); + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Desktop/DesktopKeyboardShortcuts.cs b/NAPS2.Lib/EtoForms/Desktop/DesktopKeyboardShortcuts.cs index 5c85a823cf..0ac9d383d9 100644 --- a/NAPS2.Lib/EtoForms/Desktop/DesktopKeyboardShortcuts.cs +++ b/NAPS2.Lib/EtoForms/Desktop/DesktopKeyboardShortcuts.cs @@ -16,29 +16,26 @@ public DesktopKeyboardShortcuts(KeyboardShortcutManager ksm, Naps2Config config) public void Assign(DesktopCommands commands) { - // Defaults - _ksm.Assign("Ctrl+Enter", commands.Scan); - _ksm.Assign("Ctrl+B", commands.BatchScan); - _ksm.Assign("Ctrl+O", commands.Import); - _ksm.Assign("Ctrl+S", commands.SavePdf); - _ksm.Assign("Ctrl+P", commands.Print); - _ksm.Assign("Ctrl+Up", commands.MoveUp); - _ksm.Assign("Ctrl+Left", commands.MoveUp); - _ksm.Assign("Ctrl+Down", commands.MoveDown); - _ksm.Assign("Ctrl+Right", commands.MoveDown); - _ksm.Assign("Ctrl+Shift+Del", commands.ClearAll); - _ksm.Assign("F1", commands.About); - _ksm.Assign("Ctrl+OemMinus", commands.ZoomOut); - _ksm.Assign("Ctrl+Oemplus", commands.ZoomIn); + _ksm.Clear(); + + // Unconfigurable defaults + _ksm.Assign("Mod+.", commands.Scan); + _ksm.Assign("Mod+Up", commands.MoveUp); + _ksm.Assign("Mod+Left", commands.MoveUp); + _ksm.Assign("Mod+Down", commands.MoveDown); + _ksm.Assign("Mod+Right", commands.MoveDown); _ksm.Assign("Del", commands.Delete); - _ksm.Assign("Ctrl+A", commands.SelectAll); - _ksm.Assign("Ctrl+C", commands.Copy); - _ksm.Assign("Ctrl+V", commands.Paste); + _ksm.Assign("Mod+A", commands.SelectAll); + _ksm.Assign("Mod+C", commands.Copy); + _ksm.Assign("Mod+V", commands.Paste); + _ksm.Assign("Mod+Z", commands.Undo); + _ksm.Assign(EtoPlatform.Current.IsWinForms ? "Mod+Y" : "Mod+Shift+Z", commands.Redo); - // Configured + // Configured defaults var ks = _config.Get(c => c.KeyboardShortcuts); + _ksm.Assign(ks.Settings, commands.Settings); _ksm.Assign(ks.About, commands.About); _ksm.Assign(ks.BatchScan, commands.BatchScan); _ksm.Assign(ks.Clear, commands.ClearAll); @@ -48,6 +45,7 @@ public void Assign(DesktopCommands commands) _ksm.Assign(ks.EmailPDF, commands.EmailPdf); _ksm.Assign(ks.EmailPDFAll, commands.EmailAll); _ksm.Assign(ks.EmailPDFSelected, commands.EmailSelected); + _ksm.Assign(ks.EmailSettings, commands.EmailSettings); } _ksm.Assign(ks.ImageBlackWhite, commands.BlackWhite); _ksm.Assign(ks.ImageBrightness, commands.BrightCont); @@ -56,6 +54,10 @@ public void Assign(DesktopCommands commands) _ksm.Assign(ks.ImageHue, commands.HueSat); _ksm.Assign(ks.ImageSaturation, commands.HueSat); _ksm.Assign(ks.ImageSharpen, commands.Sharpen); + _ksm.Assign(ks.ImageDocumentCorrection, commands.DocumentCorrection); + _ksm.Assign(ks.ImageSplit, commands.Split); + _ksm.Assign(ks.ImageCombine, commands.Combine); + _ksm.Assign(ks.ImageEditWith, commands.EditWithApp); _ksm.Assign(ks.ImageReset, commands.ResetImage); _ksm.Assign(ks.ImageView, commands.ViewImage); _ksm.Assign(ks.Import, commands.Import); @@ -76,16 +78,28 @@ public void Assign(DesktopCommands commands) _ksm.Assign(ks.ReorderReverseAll, commands.ReverseAll); _ksm.Assign(ks.ReorderReverseSelected, commands.ReverseSelected); _ksm.Assign(ks.RotateCustom, commands.CustomRotate); + _ksm.Assign(ks.RotateDeskew, commands.Deskew); _ksm.Assign(ks.RotateFlip, commands.Flip); _ksm.Assign(ks.RotateLeft, commands.RotateLeft); _ksm.Assign(ks.RotateRight, commands.RotateRight); - _ksm.Assign(ks.SaveImages, commands.SaveImages); - _ksm.Assign(ks.SaveImagesAll, commands.SaveAllImages); - _ksm.Assign(ks.SaveImagesSelected, commands.SaveSelectedImages); - _ksm.Assign(ks.SavePDF, commands.SavePdf); - _ksm.Assign(ks.SavePDFAll, commands.SaveAllPdf); - _ksm.Assign(ks.SavePDFSelected, commands.SaveSelectedPdf); + if (PlatformCompat.System.CombinedPdfAndImageSaving) + { + _ksm.Assign(ks.SavePDFAll, commands.SaveAll); + _ksm.Assign(ks.SavePDFSelected, commands.SaveSelected); + } + else + { + _ksm.Assign(ks.SaveImages, commands.SaveImages); + _ksm.Assign(ks.SaveImagesAll, commands.SaveAllImages); + _ksm.Assign(ks.SaveImagesSelected, commands.SaveSelectedImages); + _ksm.Assign(ks.SavePDF, commands.SavePdf); + _ksm.Assign(ks.SavePDFAll, commands.SaveAllPdf); + _ksm.Assign(ks.SavePDFSelected, commands.SaveSelectedPdf); + } + _ksm.Assign(ks.PDFSettings, commands.PdfSettings); + _ksm.Assign(ks.ImageSettings, commands.ImageSettings); _ksm.Assign(ks.ScanDefault, commands.Scan); + _ksm.Assign(ks.ScannerSharing, commands.ScannerSharing); _ksm.Assign(ks.ZoomIn, commands.ZoomIn); _ksm.Assign(ks.ZoomOut, commands.ZoomOut); @@ -103,7 +117,6 @@ public void AssignProfileShortcut(int i, Command command) private string? GetProfileShortcut(int i) { - // TODO: Granular var ks = _config.Get(c => c.KeyboardShortcuts); switch (i) { diff --git a/NAPS2.Lib/EtoForms/Desktop/DesktopScanController.cs b/NAPS2.Lib/EtoForms/Desktop/DesktopScanController.cs index 94a300ebed..2cf4329df6 100644 --- a/NAPS2.Lib/EtoForms/Desktop/DesktopScanController.cs +++ b/NAPS2.Lib/EtoForms/Desktop/DesktopScanController.cs @@ -64,18 +64,26 @@ public async Task ScanWithDevice(string deviceID) // No profile for the device we're scanning with, so prompt to create one var editSettingsForm = _formFactory.Create(); + editSettingsForm.NewProfile = true; editSettingsForm.ScanProfile = _config.DefaultProfileSettings(); #if !MAC - try - { - // Populate the device field automatically (because we can do that!) - using var deviceManager = new WiaDeviceManager(); - using var device = deviceManager.FindDevice(deviceID); - editSettingsForm.CurrentDevice = new ScanDevice(deviceID, device.Name()); - } - catch (WiaException) +#if NET6_0_OR_GREATER + if (OperatingSystem.IsWindows()) { +#endif + try + { + // Populate the device field automatically (because we can do that!) + using var deviceManager = new WiaDeviceManager(); + using var device = deviceManager.FindDevice(deviceID); + editSettingsForm.SetDevice(new ScanDevice(Driver.Wia, deviceID, device.Name())); + } + catch (WiaException) + { + } +#if NET6_0_OR_GREATER } +#endif #endif editSettingsForm.ShowModal(); if (!editSettingsForm.Result) @@ -85,7 +93,7 @@ public async Task ScanWithDevice(string deviceID) profile = editSettingsForm.ScanProfile; _profileManager.Mutate(new ListMutation.Append(profile), ListSelection.Empty()); - _profileManager.DefaultProfile = profile; + MaybeSetDefaultProfile(profile); } // We got a profile, yay, so we can actually do the scan now @@ -94,7 +102,13 @@ public async Task ScanWithDevice(string deviceID) public async Task ScanDefault() { - if (_profileManager.DefaultProfile != null) + var action = _config.Get(c => c.ScanButtonDefaultAction); + + if (action == ScanButtonDefaultAction.AlwaysPrompt) + { + _desktopFormProvider.DesktopForm.ShowToolbarMenu(DesktopToolbarMenuType.Scan); + } + else if (_profileManager.DefaultProfile != null) { await DoScan(_profileManager.DefaultProfile); } @@ -111,6 +125,7 @@ public async Task ScanDefault() public async Task ScanWithNewProfile() { var editSettingsForm = _formFactory.Create(); + editSettingsForm.NewProfile = true; editSettingsForm.ScanProfile = _config.DefaultProfileSettings(); editSettingsForm.ShowModal(); if (!editSettingsForm.Result) @@ -119,17 +134,25 @@ public async Task ScanWithNewProfile() } _profileManager.Mutate(new ListMutation.Append(editSettingsForm.ScanProfile), ListSelection.Empty()); - _profileManager.DefaultProfile = editSettingsForm.ScanProfile; + MaybeSetDefaultProfile(editSettingsForm.ScanProfile); await DoScan(editSettingsForm.ScanProfile); } public async Task ScanWithProfile(ScanProfile profile) { - _profileManager.DefaultProfile = profile; + MaybeSetDefaultProfile(profile); await DoScan(profile); } + private void MaybeSetDefaultProfile(ScanProfile profile) + { + if (_config.Get(c => c.ScanChangesDefaultProfile) || _profileManager.DefaultProfile == null) + { + _profileManager.DefaultProfile = profile; + } + } + private async Task DoScan(ScanProfile profile) { var images = diff --git a/NAPS2.Lib/EtoForms/Desktop/DesktopSubFormController.cs b/NAPS2.Lib/EtoForms/Desktop/DesktopSubFormController.cs index 7941778324..7391a3c187 100644 --- a/NAPS2.Lib/EtoForms/Desktop/DesktopSubFormController.cs +++ b/NAPS2.Lib/EtoForms/Desktop/DesktopSubFormController.cs @@ -1,5 +1,4 @@ using NAPS2.EtoForms.Ui; -using NAPS2.Images; using NAPS2.Ocr; namespace NAPS2.EtoForms.Desktop; @@ -20,16 +19,36 @@ public DesktopSubFormController(IFormFactory formFactory, UiImageList imageList, _tesseractLanguageManager = tesseractLanguageManager; } + private Func>? SelectionFunc { get; init; } + + private ListSelection Selection => SelectionFunc?.Invoke() ?? _imageList.Selection; + + public IDesktopSubFormController WithSelection(Func> selectionFunc) + { + return new DesktopSubFormController(_formFactory, _imageList, _desktopImagesController, + _tesseractLanguageManager) + { + SelectionFunc = selectionFunc + }; + } + public void ShowCropForm() => ShowImageForm(); public void ShowBrightnessContrastForm() => ShowImageForm(); public void ShowHueSaturationForm() => ShowImageForm(); public void ShowBlackWhiteForm() => ShowImageForm(); public void ShowSharpenForm() => ShowImageForm(); + public void ShowSplitForm() => ShowImageForm(); public void ShowRotateForm() => ShowImageForm(); + public void ShowCombineForm() + { + if (_imageList.Images.Count < 2) return; + ShowImageForm(); + } + private void ShowImageForm() where T : ImageFormBase { - var selection = _imageList.Selection; + var selection = Selection; if (selection.Any()) { var form = _formFactory.Create(); @@ -69,9 +88,15 @@ public void ShowBatchScanForm() form.ShowModal(); } + public void ShowScannerSharingForm() + { + var form = _formFactory.Create(); + form.ShowModal(); + } + public void ShowViewerForm() { - var selected = _imageList.Selection.FirstOrDefault(); + var selected = Selection.FirstOrDefault(); if (selected != null) { using var viewer = _formFactory.Create(); @@ -95,13 +120,13 @@ public void ShowEmailSettingsForm() _formFactory.Create().ShowModal(); } - public void ShowAboutForm() + public void ShowSettingsForm() { - _formFactory.Create().ShowModal(); + _formFactory.Create().ShowModal(); } - public void ShowSettingsForm() + public void ShowAboutForm() { - // FormFactory.Create().ShowDialog(); + _formFactory.Create().ShowModal(); } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Desktop/DesktopToolbarMenuType.cs b/NAPS2.Lib/EtoForms/Desktop/DesktopToolbarMenuType.cs new file mode 100644 index 0000000000..76cd8e0557 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Desktop/DesktopToolbarMenuType.cs @@ -0,0 +1,9 @@ +namespace NAPS2.EtoForms.Desktop; + +public enum DesktopToolbarMenuType +{ + Scan, + SavePdf, + SaveImages, + EmailPdf +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Desktop/EditWithController.cs b/NAPS2.Lib/EtoForms/Desktop/EditWithController.cs new file mode 100644 index 0000000000..72108aa512 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Desktop/EditWithController.cs @@ -0,0 +1,63 @@ +using NAPS2.EtoForms.Ui; +using NAPS2.Scan; + +namespace NAPS2.EtoForms.Desktop; + +public class EditWithController( + ScanningContext scanningContext, + UiImageList imageList, + Naps2Config config, + IOpenWith openWith, + ErrorOutput errorOutput, + IFormFactory formFactory, + DesktopFormProvider desktopFormProvider) +{ + public void EditWithApp(IEnumerable selection) + { + string? appId = config.Get(c => c.EditWithAppPath); + if (string.IsNullOrEmpty(appId)) + { + EditWithPick(selection); + return; + } + EditWithPath(appId, selection); + } + + public void EditWithPick(IEnumerable selection) + { + var pickForm = formFactory.Create(); + pickForm.ShowModal(); + var entry = pickForm.Result; + if (entry != null) + { + var transact = config.User.BeginTransaction(); + transact.Set(c => c.EditWithAppPath, entry.Id); + transact.Set(c => c.EditWithAppName, entry.Name); + transact.Commit(); + desktopFormProvider.DesktopForm.EditWithAppChanged(); + EditWithPath(entry.Id, selection); + } + } + + private void EditWithPath(string appPath, IEnumerable selection) + { + var tempFilePaths = new List(); + foreach (var uiImage in selection) + { + if (!uiImage.IsDisposed) + { + var editorSession = new ExternalEditorSession(scanningContext, imageList, uiImage); + uiImage.EditorSessions.Add(editorSession); + tempFilePaths.Add(editorSession.TempFilePath); + } + } + try + { + openWith.OpenWith(appPath, tempFilePaths); + } + catch (Exception ex) + { + errorOutput.DisplayError(string.Format(UiStrings.ErrorStartingApplication, appPath), ex); + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Desktop/IDesktopSubFormController.cs b/NAPS2.Lib/EtoForms/Desktop/IDesktopSubFormController.cs index c10aa2790d..e50d3e2488 100644 --- a/NAPS2.Lib/EtoForms/Desktop/IDesktopSubFormController.cs +++ b/NAPS2.Lib/EtoForms/Desktop/IDesktopSubFormController.cs @@ -2,15 +2,19 @@ namespace NAPS2.EtoForms.Desktop; public interface IDesktopSubFormController { + IDesktopSubFormController WithSelection(Func> selectionFunc); void ShowCropForm(); void ShowBrightnessContrastForm(); void ShowHueSaturationForm(); void ShowBlackWhiteForm(); void ShowSharpenForm(); + void ShowSplitForm(); + void ShowCombineForm(); void ShowRotateForm(); void ShowProfilesForm(); void ShowOcrForm(); void ShowBatchScanForm(); + void ShowScannerSharingForm(); void ShowViewerForm(); void ShowPdfSettingsForm(); void ShowImageSettingsForm(); diff --git a/NAPS2.Lib/EtoForms/ImageListActions.cs b/NAPS2.Lib/EtoForms/Desktop/ImageListActions.cs similarity index 74% rename from NAPS2.Lib/EtoForms/ImageListActions.cs rename to NAPS2.Lib/EtoForms/Desktop/ImageListActions.cs index 688234e61b..5e77d647eb 100644 --- a/NAPS2.Lib/EtoForms/ImageListActions.cs +++ b/NAPS2.Lib/EtoForms/Desktop/ImageListActions.cs @@ -1,4 +1,8 @@ -namespace NAPS2.EtoForms; +using NAPS2.EtoForms.Notifications; +using NAPS2.EtoForms.Widgets; +using NAPS2.ImportExport; + +namespace NAPS2.EtoForms.Desktop; public class ImageListActions { @@ -8,11 +12,12 @@ public class ImageListActions private readonly Naps2Config _config; private readonly ThumbnailController _thumbnailController; private readonly IExportController _exportController; - private readonly INotificationManager _notify; + private readonly INotify _notify; + private readonly EditWithController _editWithController; public ImageListActions(UiImageList imageList, IOperationFactory operationFactory, OperationProgress operationProgress, Naps2Config config, ThumbnailController thumbnailController, - IExportController exportController, INotificationManager notify) + IExportController exportController, INotify notify, EditWithController editWithController) { _imageList = imageList; _operationFactory = operationFactory; @@ -21,16 +26,19 @@ public ImageListActions(UiImageList imageList, IOperationFactory operationFactor _thumbnailController = thumbnailController; _exportController = exportController; _notify = notify; + _editWithController = editWithController; } - private ListSelection? Selection { get; init; } + private Func>? SelectionFunc { get; init; } + + private ListSelection? Selection => SelectionFunc?.Invoke(); - public ImageListActions WithSelection(ListSelection selection) + public ImageListActions WithSelection(Func> selectionFunc) { return new ImageListActions(_imageList, _operationFactory, _operationProgress, _config, _thumbnailController, - _exportController, _notify) + _exportController, _notify, _editWithController) { - Selection = selection + SelectionFunc = selectionFunc }; } @@ -64,6 +72,10 @@ public async Task RotateRight() => public async Task Flip() => await _imageList.MutateAsync(new ImageListMutation.RotateFlip(180), Selection); + public void DocumentCorrection() => + _imageList.Mutate(new ImageListMutation.AddTransforms([new CorrectionTransform(CorrectionMode.Document)]), + Selection); + // TODO: Does it make sense to move this method somewhere else? public void Deskew() { @@ -74,7 +86,7 @@ public void Deskew() } var op = _operationFactory.Create(); - if (op.Start(images, new DeskewParams { ThumbnailSize = _thumbnailController.RenderSize })) + if (op.Start(_imageList, images.ToList(), new DeskewParams { ThumbnailSize = _thumbnailController.RenderSize })) { _operationProgress.ShowProgress(op); } @@ -87,6 +99,10 @@ public async Task RotateFlip(double angle) => public void SelectAll() => _imageList.UpdateSelection(ListSelection.From(_imageList.Images)); + public async Task Undo() => await _imageList.Undo(); + + public async Task Redo() => await _imageList.Redo(); + public Task SaveAllAsPdf() => _exportController.SavePdf(_imageList.Images, _notify); public Task SaveSelectedAsPdf() => _exportController.SavePdf(_imageList.Selection, _notify); public Task SaveAllAsImages() => _exportController.SaveImages(_imageList.Images, _notify); @@ -95,4 +111,7 @@ public async Task RotateFlip(double angle) => public Task SaveSelectedAsPdfOrImages() => _exportController.SavePdfOrImages(_imageList.Selection, _notify); public Task EmailAllAsPdf() => _exportController.EmailPdf(_imageList.Images); public Task EmailSelectedAsPdf() => _exportController.EmailPdf(_imageList.Selection); + + public void EditWithApp() => _editWithController.EditWithApp(Selection ?? _imageList.Selection); + public void EditWithPick() => _editWithController.EditWithPick(Selection ?? _imageList.Selection); } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Desktop/Sidebar.cs b/NAPS2.Lib/EtoForms/Desktop/Sidebar.cs new file mode 100644 index 0000000000..d8c8d5e477 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Desktop/Sidebar.cs @@ -0,0 +1,252 @@ +using System.Globalization; +using Eto.Forms; +using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Ui; +using NAPS2.EtoForms.Widgets; +using NAPS2.Scan; +using NAPS2.Scan.Internal; + +namespace NAPS2.EtoForms.Desktop; + +public class Sidebar +{ + private readonly IScanPerformer _scanPerformer; + private readonly DeviceCapsCache _deviceCapsCache; + private readonly IProfileManager _profileManager; + private readonly Naps2Config _config; + private readonly IIconProvider _iconProvider; + private readonly IFormFactory _formFactory; + private readonly IDesktopScanController _desktopScanController; + + private readonly LayoutVisibility _sidebarVis = new(true); + private readonly LayoutVisibility _onboardingVis = new(false); + private readonly LayoutVisibility _predefinedVis = new(true); + private readonly DropDownWidget _profile = new(); + private readonly EnumDropDownWidget _paperSource = new(); + private readonly EnumDropDownWidget _bitDepth = new(); + + private DeviceSelectorWidget? _deviceSelectorWidget; + private PageSizeDropDownWidget? _pageSize; + private ResolutionDropDownWidget? _resolution; + + public Sidebar(IScanPerformer scanPerformer, DeviceCapsCache deviceCapsCache, IProfileManager profileManager, + Naps2Config config, IIconProvider iconProvider, IFormFactory formFactory, + IDesktopScanController desktopScanController) + { + _scanPerformer = scanPerformer; + _deviceCapsCache = deviceCapsCache; + _profileManager = profileManager; + _config = config; + _iconProvider = iconProvider; + _formFactory = formFactory; + _desktopScanController = desktopScanController; + _profile.Format = x => x.DisplayName; + _profileManager.ProfilesUpdated += (_, _) => UpdateProfilesDropdown(); + UpdateProfilesDropdown(); + + EditProfileCommand = new ActionCommand(EditProfile) + { + ToolTip = UiStrings.Edit, + IconName = "pencil_small" + }; + NewProfileCommand = new ActionCommand(NewProfile) + { + Text = UiStrings.NewProfile, + ToolTip = UiStrings.New, + IconName = "add_small" + }; + ScanCommand = new ActionCommand(DoScan) + { + Text = UiStrings.Scan, + IconName = "control_play_blue_small" + }; + } + + private ActionCommand NewProfileCommand { get; } + private ActionCommand EditProfileCommand { get; } + private ActionCommand ScanCommand { get; } + + private void UpdateProfilesDropdown() + { + _profile.Items = _profileManager.Profiles; + _onboardingVis.IsVisible = _profileManager.Profiles.Count == 0; + } + + private void EditProfile() + { + var originalProfile = _profile.SelectedItem; + if (originalProfile != null) + { + var fedit = _formFactory.Create(); + fedit.ScanProfile = originalProfile; + fedit.ShowModal(); + if (fedit.Result) + { + _profile.SelectedItem = fedit.ScanProfile; + _profileManager.Mutate(new ListMutation.ReplaceWith(fedit.ScanProfile), + ListSelection.Of(originalProfile)); + } + } + } + + private void NewProfile() + { + if (!(_config.Get(c => c.NoUserProfiles) && _profileManager.Profiles.Any(x => x.IsLocked))) + { + var fedit = _formFactory.Create(); + fedit.NewProfile = true; + fedit.ScanProfile = _config.DefaultProfileSettings(); + fedit.ShowModal(); + if (fedit.Result) + { + _profile.SelectedItem = fedit.ScanProfile; + _profileManager.Mutate(new ListMutation.Append(fedit.ScanProfile), + ListSelection.Empty()); + } + } + } + + private void DoScan() + { + var profile = _profile.SelectedItem!; + var pageSize = _pageSize!.SelectedItem!; + + profile.PaperSource = _paperSource.SelectedItem; + profile.PageSize = pageSize.Type; + profile.CustomPageSizeName = pageSize.CustomName; + profile.CustomPageSize = pageSize.CustomDimens; + profile.Resolution = new ScanResolution { Dpi = _resolution?.SelectedItem?.Dpi ?? 0 }; + profile.BitDepth = _bitDepth.SelectedItem; + _profileManager.Save(); + + _desktopScanController.ScanWithProfile(profile); + } + + public LayoutElement CreateView(IFormBase parentWindow) + { + _sidebarVis.IsVisible = _config.Get(c => c.SidebarVisible); + _profile.SelectedItem = _profileManager.DefaultProfile ?? _profileManager.Profiles.FirstOrDefault(); + + _deviceSelectorWidget = new DeviceSelectorWidget(_scanPerformer, _deviceCapsCache, _iconProvider, parentWindow) + { + ShowChooseDevice = false, + ProfileFunc = () => _profile.SelectedItem! + }; + _pageSize = new PageSizeDropDownWidget(parentWindow); + _resolution = new ResolutionDropDownWidget(parentWindow); + _profile.SelectedItemChanged += (_, _) => + { + if (_config.Get(c => c.ScanChangesDefaultProfile)) + { + _profileManager.DefaultProfile = _profile.SelectedItem; + } + UpdateUiForProfile(); + }; + _profileManager.ProfilesUpdated += (_, _) => UpdateUiForProfile(); + + UpdateUiForProfile(); + + return L.Column( + C.Filler().NaturalWidth(100), + L.Column( + C.Button(NewProfileCommand, ButtonImagePosition.Left).Height(30).AlignCenter() + ).Visible(_onboardingVis), + L.Column( + L.Row( + C.Label(UiStrings.ProfileLabel).AlignTrailing(), + C.Filler(), + // On Mac we set an explicit height as for some reason it fixes the button style after hide+show + C.Button(EditProfileCommand, ButtonImagePosition.Overlay) + .Height(EtoPlatform.Current.IsMac ? 20 : null).Width(30), + C.Button(NewProfileCommand, ButtonImagePosition.Overlay) + .Height(EtoPlatform.Current.IsMac ? 20 : null).Width(30) + ), + _profile.AsControl(), + C.Spacer(), + _deviceSelectorWidget, + L.Column( + C.Spacer(), + C.Label(UiStrings.PaperSourceLabel), + _paperSource, + C.Label(UiStrings.PageSizeLabel), + _pageSize, + C.Label(UiStrings.ResolutionLabel), + _resolution, + C.Label(UiStrings.BitDepthLabel), + _bitDepth + ).Visible(_predefinedVis), + C.Spacer(), + C.Button(ScanCommand, ButtonImagePosition.Left).AlignCenter().Height(30) + ).Visible(!_onboardingVis), + C.Filler() + ).Padding(left: parentWindow.LayoutController.DefaultSpacing + 10, right: 10).Visible(_sidebarVis); + } + + private void UpdateUiForProfile() + { + var profile = _profile.SelectedItem; + if (profile == null) return; + + var deviceDriver = new ScanOptionsValidator().ValidateDriver( + Enum.TryParse(profile.DriverName, true, out var driver) + ? driver + : Driver.Default); + + var device = profile.Device?.ToScanDevice(deviceDriver); + if (device != null) + { + _deviceSelectorWidget!.Choice = DeviceChoice.ForDevice(device); + } + else + { + _deviceSelectorWidget!.Choice = DeviceChoice.ForAlwaysAsk(deviceDriver); + } + + _predefinedVis.IsVisible = !profile.UseNativeUI; + + if (profile.PageSize == ScanPageSize.Custom && profile.CustomPageSize != null) + { + _pageSize!.SetCustom(profile.CustomPageSizeName, profile.CustomPageSize); + } + else + { + _pageSize!.SetPreset(profile.PageSize); + } + + _paperSource.SelectedItem = profile.PaperSource; + _bitDepth.SelectedItem = profile.BitDepth; + _resolution!.SetDpi(profile.Resolution.Dpi); + + _paperSource.Items = profile.Caps?.PaperSources?.Values is [_, ..] paperSources + ? paperSources + : EnumDropDownWidget.DefaultItems; + + var selectedSource = _paperSource.SelectedItem; + var perSource = selectedSource switch + { + ScanSource.Glass => profile.Caps?.Glass, + ScanSource.Feeder => profile.Caps?.Feeder, + ScanSource.Duplex => profile.Caps?.Duplex, + _ => null + }; + + var validResolutions = perSource?.Resolutions; + _resolution.VisiblePresets = validResolutions is [_, ..] + ? validResolutions + : EnumDropDownWidget.DefaultItems.Select(x => x.ToIntDpi()); + + var scanArea = perSource?.ScanArea; + var sizeCaps = new PageSizeCaps { ScanArea = scanArea }; + + var allPresets = EnumDropDownWidget.DefaultItems.SkipLast(2).ToList(); + var conditionalPresets = new[] { ScanPageSize.A3, ScanPageSize.B4 }; + _pageSize.VisiblePresets = allPresets.Where(preset => + !conditionalPresets.Contains(preset) || sizeCaps.Fits(preset.PageDimensions()!.ToPageSize())); + } + + public void ToggleVisibility() + { + _sidebarVis.IsVisible = !_sidebarVis.IsVisible; + _config.User.Set(c => c.SidebarVisible, _sidebarVis.IsVisible); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/EtoDevicePrompt.cs b/NAPS2.Lib/EtoForms/EtoDevicePrompt.cs index 3a4ef7e859..b3cbf24f1f 100644 --- a/NAPS2.Lib/EtoForms/EtoDevicePrompt.cs +++ b/NAPS2.Lib/EtoForms/EtoDevicePrompt.cs @@ -12,11 +12,16 @@ public EtoDevicePrompt(IFormFactory formFactory) _formFactory = formFactory; } - public ScanDevice? PromptForDevice(List deviceList, IntPtr dialogParent) + public Task PromptForDevice(ScanOptions options, bool allowAlwaysAsk) { - var deviceForm = _formFactory.Create(); - deviceForm.DeviceList = deviceList; - deviceForm.ShowModal(); - return deviceForm.SelectedDevice; + // TODO: Extension method or something to turn InvokeGet into Task? + return Task.FromResult(Invoker.Current.InvokeGet(() => + { + var deviceForm = _formFactory.Create(); + deviceForm.ScanOptions = options; + deviceForm.AllowAlwaysAsk = allowAlwaysAsk; + deviceForm.ShowModal(); + return deviceForm.Choice; + })); } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/EtoDialogBase.cs b/NAPS2.Lib/EtoForms/EtoDialogBase.cs index cd39d41353..4b3c2addf1 100644 --- a/NAPS2.Lib/EtoForms/EtoDialogBase.cs +++ b/NAPS2.Lib/EtoForms/EtoDialogBase.cs @@ -1,4 +1,3 @@ -using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; @@ -7,16 +6,21 @@ namespace NAPS2.EtoForms; public abstract class EtoDialogBase : Dialog, IFormBase { private IFormFactory? _formFactory; - + protected EtoDialogBase(Naps2Config config) { - EtoPlatform.Current.UpdateRtl(this); Config = config; FormStateController = new FormStateController(this, config); Resizable = true; ShowInTaskbar = true; LayoutController.Bind(this); LayoutController.Invalidated += (_, _) => FormStateController.UpdateLayoutSize(LayoutController); + if (EtoPlatform.Current.IsMac) + { + // Always have a basic menu on Mac, otherwise system keyboard shortcuts like Copy/Paste don't work + Menu = new MenuBar(); + } + EtoPlatform.Current.InitForm(this); } protected abstract void BuildLayout(); @@ -27,17 +31,6 @@ protected override void OnPreLoad(EventArgs e) base.OnPreLoad(e); } - // TODO: PR for Eto to integrate this - public new Icon Icon - { - get => base.Icon; - set - { - base.Icon = value; - EtoPlatform.Current.ShowIcon(this); - } - } - public FormStateController FormStateController { get; } public LayoutController LayoutController { get; } = new(); @@ -49,4 +42,13 @@ public IFormFactory FormFactory } public Naps2Config Config { get; set; } + + public string IconName + { + set + { + EtoPlatform.Current.AttachDpiDependency(this, + scale => Icon = EtoPlatform.Current.IconProvider.GetFormIcon(value, scale)); + } + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/EtoDialogHelper.cs b/NAPS2.Lib/EtoForms/EtoDialogHelper.cs index e3f979a0d7..66df31721f 100644 --- a/NAPS2.Lib/EtoForms/EtoDialogHelper.cs +++ b/NAPS2.Lib/EtoForms/EtoDialogHelper.cs @@ -1,4 +1,5 @@ using Eto.Forms; +using NAPS2.ImportExport; namespace NAPS2.EtoForms; @@ -6,6 +7,7 @@ public class EtoDialogHelper : DialogHelper { private readonly Naps2Config _config; private readonly FileFilters _fileFilters; + private bool _addExt = EtoPlatform.Current.IsGtk; public EtoDialogHelper(Naps2Config config, FileFilters fileFilters) { @@ -15,18 +17,22 @@ public EtoDialogHelper(Naps2Config config, FileFilters fileFilters) public override bool PromptToSavePdfOrImage(string? defaultPath, out string? savePath) { + var lastExt = _config.Get(c => c.LastPdfOrImageExt)?.ToLowerInvariant(); + if (string.IsNullOrEmpty(lastExt)) + { + lastExt = "pdf"; + } var sd = new SaveFileDialog { - CheckFileExists = false, - // TODO - // AddExtension = true, - FileName = Path.IsPathRooted(defaultPath) ? Path.GetFileName(defaultPath) : null + FileName = GetDefaultFileName(defaultPath, lastExt!) }; - _fileFilters.Set(sd, FileFilterGroup.Pdf | FileFilterGroup.Image); + _fileFilters.Set(sd, FileFilterGroup.Pdf | FileFilterGroup.Image, lastExt); SetDir(sd, defaultPath); + EtoPlatform.Current.ConfigureFileDialog(sd); if (sd.ShowDialog(null) == DialogResult.Ok) { savePath = sd.FileName; + _config.User.Set(c => c.LastPdfOrImageExt, (Path.GetExtension(sd.FileName) ?? "").Replace(".", "")); return true; } savePath = null; @@ -37,12 +43,11 @@ public override bool PromptToSavePdf(string? defaultPath, out string? savePath) { var sd = new SaveFileDialog { - CheckFileExists = false, - // AddExtension = true, - FileName = Path.IsPathRooted(defaultPath) ? Path.GetFileName(defaultPath) : null + FileName = GetDefaultFileName(defaultPath, "pdf") }; _fileFilters.Set(sd, FileFilterGroup.Pdf); SetDir(sd, defaultPath); + EtoPlatform.Current.ConfigureFileDialog(sd); if (sd.ShowDialog(null) == DialogResult.Ok) { savePath = sd.FileName; @@ -54,15 +59,21 @@ public override bool PromptToSavePdf(string? defaultPath, out string? savePath) public override bool PromptToSaveImage(string? defaultPath, out string? savePath) { + var lastExt = _config.Get(c => c.LastImageExt)?.ToLowerInvariant(); + if (string.IsNullOrEmpty(lastExt)) + { + lastExt = "jpg"; + } var sd = new SaveFileDialog { - CheckFileExists = false, - // AddExtension = true, - FileName = Path.IsPathRooted(defaultPath) ? Path.GetFileName(defaultPath) : null + FileName = GetDefaultFileName(defaultPath, lastExt!) }; - var lastExt = _config.Get(c => c.LastImageExt)?.ToLowerInvariant(); - _fileFilters.Set(sd, FileFilterGroup.Image, lastExt ?? "jpg"); + var filterGroups = EtoPlatform.Current.IsGtk + ? FileFilterGroup.AllImages | FileFilterGroup.Image + : FileFilterGroup.Image; + _fileFilters.Set(sd, filterGroups, lastExt); SetDir(sd, defaultPath); + EtoPlatform.Current.ConfigureFileDialog(sd); if (sd.ShowDialog(null) == DialogResult.Ok) { savePath = sd.FileName; @@ -73,6 +84,18 @@ public override bool PromptToSaveImage(string? defaultPath, out string? savePath return false; } + private string? GetDefaultFileName(string? defaultPath, string ext) + { + if (string.IsNullOrEmpty(defaultPath)) + { + return _addExt ? $".{ext}" : null; + } + var normPath = NormalizePath(defaultPath); + return _addExt && !Path.HasExtension(normPath) + ? $"{normPath}.{ext}" + : normPath; + } + private void SetDir(SaveFileDialog dialog, string? defaultPath) { string? path = null; @@ -81,16 +104,26 @@ private void SetDir(SaveFileDialog dialog, string? defaultPath) // For UI test automation we choose the appdata folder for test isolation and consistency path = Paths.AppData; } - else + else if (Path.IsPathRooted(defaultPath)) { - path = Path.IsPathRooted(defaultPath) - ? Path.GetDirectoryName(defaultPath) - : null; + path = Path.GetDirectoryName(NormalizePath(defaultPath)); } if (path != null) { - dialog.Directory = new Uri(Path.GetFullPath(path)); + dialog.Directory = UriHelper.FilePathToFileUri(Path.GetFullPath(path)); + } + } + + private static string NormalizePath(string path) + { + string normPath = Placeholders.NonNumeric.Substitute(path); + // If the path points to a directory, it should end in a trailing slash. + // Otherwise, path functions will assume that the directory name is a file name. + if (Directory.Exists(normPath) && !normPath.EndsWith(Path.DirectorySeparatorChar)) + { + normPath += Path.DirectorySeparatorChar; } + return normPath; } public override bool PromptToImport(out string[]? filePaths) @@ -105,8 +138,9 @@ public override bool PromptToImport(out string[]? filePaths) if (Paths.IsTestAppDataPath) { // For UI test automation we choose the appdata folder to find the prepared files to import - ofd.Directory = new Uri(Path.GetFullPath(Paths.AppData)); + ofd.Directory = UriHelper.FilePathToFileUri(Path.GetFullPath(Paths.AppData)); } + EtoPlatform.Current.ConfigureFileDialog(ofd); if (ofd.ShowDialog(null) == DialogResult.Ok) { filePaths = ofd.Filenames.ToArray(); diff --git a/NAPS2.Lib/EtoForms/EtoExtensions.cs b/NAPS2.Lib/EtoForms/EtoExtensions.cs index 70537777e2..92f81466ab 100644 --- a/NAPS2.Lib/EtoForms/EtoExtensions.cs +++ b/NAPS2.Lib/EtoForms/EtoExtensions.cs @@ -1,5 +1,6 @@ using Eto.Drawing; using Eto.Forms; +using NAPS2.Images.Bitwise; namespace NAPS2.EtoForms; @@ -10,6 +11,8 @@ public static class EtoExtensions public static Bitmap ToEtoImage(this byte[] bytes) => new(bytes); public static Bitmap ToEtoImage(this IMemoryImage image) => EtoPlatform.Current.ToBitmap(image); + public static Bitmap Clone(this Image image) => new(image); + public static MessageBoxType ToEto(this MessageBoxIcon icon) { return icon switch @@ -40,4 +43,43 @@ public static bool IsChecked(this CheckBox checkBox) { return checkBox.Checked == true; } + + public static void Fill(this IMemoryImage image, Color color) + { + new FillColorImageOp((byte) color.Rb, (byte) color.Gb, (byte) color.Bb, (byte) color.Ab).Perform(image); + } + + public static Image PadTo(this Image image, Size size) + { + bool fits = image.Width <= size.Width && image.Height <= size.Height; + bool needsPadding = image.Height < size.Height || image.Width < size.Width; + if (fits && needsPadding) + { + var newImage = new Bitmap(size.Width, size.Height, PixelFormat.Format32bppRgba); + using var graphics = new Graphics(newImage); + graphics.Clear(Colors.Transparent); + graphics.DrawImage(image, (size.Width - image.Width) / 2f, (size.Height - image.Height) / 2f); + image.Dispose(); + return newImage; + } + return image; + } + + public static Bitmap ResizeTo(this Bitmap image, int size) => ResizeTo(image, new Size(size, size)); + + public static Bitmap ResizeTo(this Bitmap image, int width, int height) => ResizeTo(image, new Size(width, height)); + + public static Bitmap ResizeTo(this Bitmap image, Size size) + { + if (image.Width != size.Width || image.Height != size.Height) + { + var newImage = new Bitmap(size.Width, size.Height, PixelFormat.Format32bppRgba); + using var graphics = new Graphics(newImage); + graphics.Clear(Colors.Transparent); + graphics.DrawImage(image, 0, 0, size.Width, size.Height); + image.Dispose(); + return newImage; + } + return image; + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/EtoFormBase.cs b/NAPS2.Lib/EtoForms/EtoFormBase.cs index c9d8423b72..920a93de69 100644 --- a/NAPS2.Lib/EtoForms/EtoFormBase.cs +++ b/NAPS2.Lib/EtoForms/EtoFormBase.cs @@ -9,12 +9,12 @@ public abstract class EtoFormBase : Form, IFormBase protected EtoFormBase(Naps2Config config) { - EtoPlatform.Current.UpdateRtl(this); Config = config; FormStateController = new FormStateController(this, config); Resizable = true; LayoutController.Bind(this); LayoutController.Invalidated += (_, _) => FormStateController.UpdateLayoutSize(LayoutController); + EtoPlatform.Current.InitForm(this); } protected abstract void BuildLayout(); @@ -36,4 +36,13 @@ public IFormFactory FormFactory } public Naps2Config Config { get; set; } + + public string IconName + { + set + { + EtoPlatform.Current.AttachDpiDependency(this, + scale => Icon = EtoPlatform.Current.IconProvider.GetFormIcon(value, scale)); + } + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/EtoInvoker.cs b/NAPS2.Lib/EtoForms/EtoInvoker.cs new file mode 100644 index 0000000000..2d3fd9eaa7 --- /dev/null +++ b/NAPS2.Lib/EtoForms/EtoInvoker.cs @@ -0,0 +1,30 @@ +using Eto.Forms; + +namespace NAPS2.EtoForms; + +public class EtoInvoker : IInvoker +{ + private readonly Application _application; + + public EtoInvoker(Application application) + { + _application = application; + } + + public void Invoke(Action action) + { + EtoPlatform.Current.Invoke(_application, action); + } + + public void InvokeDispatch(Action action) + { + EtoPlatform.Current.AsyncInvoke(_application, action); + } + + public T InvokeGet(Func func) + { + T value = default!; + EtoPlatform.Current.Invoke(_application, () => value = func()); + return value; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/EtoOperationProgress.cs b/NAPS2.Lib/EtoForms/EtoOperationProgress.cs index 59bf1c8937..c83da2354a 100644 --- a/NAPS2.Lib/EtoForms/EtoOperationProgress.cs +++ b/NAPS2.Lib/EtoForms/EtoOperationProgress.cs @@ -1,4 +1,5 @@ using Eto.Forms; +using NAPS2.EtoForms.Notifications; using NAPS2.EtoForms.Ui; namespace NAPS2.EtoForms; @@ -6,15 +7,15 @@ namespace NAPS2.EtoForms; public class EtoOperationProgress : OperationProgress { private readonly IFormFactory _formFactory; - private readonly INotificationManager _notificationManager; + private readonly INotify _notify; private readonly Naps2Config _config; - private readonly HashSet _activeOperations = new(); + private readonly HashSet _activeOperations = []; - public EtoOperationProgress(IFormFactory formFactory, INotificationManager notificationManager, Naps2Config config) + public EtoOperationProgress(IFormFactory formFactory, INotify notify, Naps2Config config) { _formFactory = formFactory; - _notificationManager = notificationManager; + _notify = notify; _config = config; } @@ -33,7 +34,8 @@ public override void Attach(IOperation op) public override void ShowProgress(IOperation op) { - if (_config.Get(c => c.BackgroundOperations).Contains(op.GetType().Name)) + if (PlatformCompat.System.ShouldRememberBackgroundOperations && + _config.Get(c => c.BackgroundOperations).Contains(op.GetType().Name)) { ShowBackgroundProgress(op); } @@ -47,15 +49,14 @@ public override void ShowModalProgress(IOperation op) { Attach(op); - var bgOps = _config.Get(c => c.BackgroundOperations); - bgOps = bgOps.Remove(op.GetType().Name); - _config.User.Set(c => c.BackgroundOperations, bgOps); - if (!op.IsFinished) { - var form = _formFactory.Create(); - form.Operation = op; - form.ShowModal(); + Invoker.Current.Invoke(() => + { + var form = _formFactory.Create(); + form.Operation = op; + form.ShowModal(); + }); } if (!op.IsFinished) @@ -68,13 +69,9 @@ public override void ShowBackgroundProgress(IOperation op) { Attach(op); - var bgOps = _config.Get(c => c.BackgroundOperations); - bgOps = bgOps.Add(op.GetType().Name); - _config.User.Set(c => c.BackgroundOperations, bgOps); - if (!op.IsFinished) { - Invoker.Current.SafeInvoke(() => _notificationManager.OperationProgress(this, op)); + Invoker.Current.Invoke(() => _notify.OperationProgress(this, op)); } } @@ -103,7 +100,8 @@ public static void RenderStatus(IOperation op, Label textLabel, Label numberLabe else { numberLabel.Text = status.ProgressType == OperationProgressType.MB - ? string.Format(MiscResources.SizeProgress, (status.CurrentProgress / 1000000.0).ToString("f1"), (status.MaxProgress / 1000000.0).ToString("f1")) + ? string.Format(MiscResources.SizeProgress, (status.CurrentProgress / 1000000.0).ToString("f1"), + (status.MaxProgress / 1000000.0).ToString("f1")) : string.Format(MiscResources.ProgressFormat, status.CurrentProgress, status.MaxProgress); progressBar.MaxValue = status.MaxProgress; progressBar.Value = status.CurrentProgress; diff --git a/NAPS2.Lib/EtoForms/EtoOverwritePrompt.cs b/NAPS2.Lib/EtoForms/EtoOverwritePrompt.cs index 776bdc9fdb..48d3b02d77 100644 --- a/NAPS2.Lib/EtoForms/EtoOverwritePrompt.cs +++ b/NAPS2.Lib/EtoForms/EtoOverwritePrompt.cs @@ -9,7 +9,8 @@ public OverwriteResponse ConfirmOverwrite(string path) string fileName = Path.GetFileName(path); var dialogResult = Invoker.Current.InvokeGet(() => MessageBox.Show(string.Format(MiscResources.ConfirmOverwriteFile, fileName), - MiscResources.OverwriteFile, MessageBoxButtons.YesNoCancel, MessageBoxType.Warning)); + MiscResources.OverwriteFile, MessageBoxButtons.YesNoCancel, MessageBoxType.Warning, + MessageBoxDefaultButton.Yes)); return dialogResult switch { DialogResult.Yes => OverwriteResponse.Yes, diff --git a/NAPS2.Lib/EtoForms/EtoPlatform.cs b/NAPS2.Lib/EtoForms/EtoPlatform.cs index 3be152b9e1..28a07e2ecc 100644 --- a/NAPS2.Lib/EtoForms/EtoPlatform.cs +++ b/NAPS2.Lib/EtoForms/EtoPlatform.cs @@ -1,6 +1,7 @@ using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; namespace NAPS2.EtoForms; @@ -8,27 +9,66 @@ public abstract class EtoPlatform { private static EtoPlatform? _current; + public static bool HasCurrent => _current != null; + public static EtoPlatform Current { get => _current ?? throw new InvalidOperationException(); set => _current = value ?? throw new ArgumentNullException(nameof(value)); } + protected EtoPlatform() + { + ColorScheme = new ColorScheme(DarkModeProvider); + } + public virtual bool IsGtk => false; public virtual bool IsMac => false; public virtual bool IsWinForms => false; - public abstract Application CreateApplication(); + public abstract IIconProvider IconProvider { get; } + public abstract IDarkModeProvider DarkModeProvider { get; } + public ColorScheme ColorScheme { get; } + + public Application CreateApplication() + { + var app = CreateApplicationCore(); + app.UnhandledException += UnhandledException; + return app; + } + + private static void UnhandledException(object? sender, Eto.UnhandledExceptionEventArgs e) + { + Log.FatalException("An error occurred that caused the application to close.", + e.ExceptionObject as Exception ?? new Exception()); + } + + public abstract Application CreateApplicationCore(); public abstract IListView CreateListView(ListViewBehavior behavior) where T : notnull; - public abstract void ConfigureImageButton(Button button, bool big); + public abstract void ConfigureImageButton(Button button, ButtonFlags flags); public abstract Bitmap ToBitmap(IMemoryImage image); - public abstract IMemoryImage DrawHourglass(ImageContext imageContext, IMemoryImage thumb); + public abstract IMemoryImage FromBitmap(Bitmap bitmap); + public abstract IMemoryImage DrawHourglass(IMemoryImage thumb); public abstract void SetFrame(Control container, Control control, Point location, Size size, bool inOverlay); public abstract Control CreateContainer(); public abstract void AddToContainer(Control container, Control control, bool inOverlay); + public abstract void RemoveFromContainer(Control container, Control control); - public abstract Control AccessibleImageButton(Image image, string text, Action onClick, - int xOffset = 0, int yOffset = 0); + public abstract void ConfigureDonateButton(Button button); + + public virtual void InitializeForegroundApp() + { + } + + public virtual void Invoke(Application application, Action action) + { + application.Invoke(action); + } + + public virtual void AsyncInvoke(Application application, Action action) + { + application.AsyncInvoke(action); + } public virtual void SetContainerSize(Window window, Control container, Size size, int padding) { @@ -44,7 +84,7 @@ public virtual void SetFormSize(Window window, Size size) window.Size = size; } - public virtual Size GetClientSize(Window window) + public virtual Size GetClientSize(Window window, bool excludeToolbars = false) { return window.ClientSize; } @@ -75,32 +115,36 @@ public virtual void SetFormLocation(Window window, Point location) window.Location = location; } - public virtual void UpdateRtl(Window window) + public virtual void InitForm(Window window) { } - public virtual void ConfigureZoomButton(Button button) + public virtual void ConfigureZoomButton(Button button, string icon) { } + public virtual void AttachDpiDependency(Control control, Action callback) => + callback(GetScaleFactor(control.ParentWindow)); + public virtual SizeF GetWrappedSize(Control control, int defaultWidth) { - return control.GetPreferredSize(new SizeF(defaultWidth, LayoutController.MAX_SIZE)); + return GetPreferredSize(control, new SizeF(defaultWidth, LayoutController.MAX_SIZE)); } - public virtual void SetClipboardImage(Clipboard clipboard, Bitmap image) + public virtual void SetClipboardImage(Clipboard clipboard, ProcessedImage processedImage, IMemoryImage memoryImage) { - clipboard.Image = image; + using var etoBitmap = memoryImage.ToEtoImage(); + clipboard.Image = etoBitmap; } - public virtual void ConfigureDropDown(DropDown dropDown) + public virtual void ConfigureDropDown(DropDown dropDown, bool scale) { } public virtual LayoutElement CreateGroupBox(string title, LayoutElement content) { var groupBox = new GroupBox { Text = title }; - return L.Overlay(groupBox, L.Buffer(content, 6, 18, 6, 6)); + return L.Overlay(groupBox, L.Buffer(content, 6, IsGtk ? 21 : 18, 6, 6)); } public virtual void ShowIcon(Dialog dialog) @@ -109,7 +153,47 @@ public virtual void ShowIcon(Dialog dialog) public virtual void ConfigureEllipsis(Label label) { + // TODO: Maybe implement our own ellipsis logic that uses text-measuring to strip trailing characters and add "..."? } public virtual Bitmap? ExtractAssociatedIcon(string exePath) => throw new NotSupportedException(); + + public virtual void HandleKeyDown(Control control, Func handle) + { + control.KeyDown += (_, args) => args.Handled = handle(args.KeyData); + } + + public virtual void AttachMouseWheelEvent(Control control, EventHandler eventHandler) + { + control.MouseWheel += eventHandler; + } + + public virtual void AttachMouseMoveEvent(Control control, EventHandler eventHandler) + { + control.MouseMove += eventHandler; + } + + public virtual float GetScaleFactor(Window window) => 1f; + + public virtual bool ScaleLayout => false; + + public float GetLayoutScaleFactor(Window window) => ScaleLayout ? GetScaleFactor(window) : 1f; + + public virtual void SetImageSize(ButtonMenuItem menuItem, int size) + { + } + + public virtual void SetImageSize(ToolItem toolItem, int size) + { + } + + public virtual Control? MaybeCreateOverlayContainer() => null; + + public virtual Control? GetOverlayContainer(Control? container, bool inOverlay) => null; + + public virtual void SetSplitterPosition(Splitter splitter, int pos) => splitter.Position = pos; + + public virtual void ConfigureFileDialog(FileDialog fileDialog) + { + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/FileFilters.cs b/NAPS2.Lib/EtoForms/FileFilters.cs index b706ef0149..c36ab74934 100644 --- a/NAPS2.Lib/EtoForms/FileFilters.cs +++ b/NAPS2.Lib/EtoForms/FileFilters.cs @@ -25,14 +25,11 @@ public void Set(FileDialog fileDialog, FileFilterGroup groups, string? selectedE if (groups.HasFlag(FileFilterGroup.AllImages)) { filters.Add(new FileFilter(MiscResources.FileTypeImageFiles, - ".bmp", ".emf", ".exif", ".gif", "jpg", ".jpeg", ".png", ".tiff", ".tif")); + ".bmp", "jpg", ".jpeg", ".png", ".tiff", ".tif")); } if (groups.HasFlag(FileFilterGroup.Image)) { filters.Add(new FileFilter(MiscResources.FileTypeBmp, ".bmp")); - filters.Add(new FileFilter(MiscResources.FileTypeEmf, ".emf")); - filters.Add(new FileFilter(MiscResources.FileTypeExif, ".exif")); - filters.Add(new FileFilter(MiscResources.FileTypeGif, ".gif")); filters.Add(new FileFilter(MiscResources.FileTypeJpeg, ".jpg", ".jpeg")); if (_imageContext.SupportsFormat(ImageFileFormat.Jpeg2000)) { @@ -41,7 +38,8 @@ public void Set(FileDialog fileDialog, FileFilterGroup groups, string? selectedE filters.Add(new FileFilter(MiscResources.FileTypePng, ".png")); filters.Add(new FileFilter(MiscResources.FileTypeTiff, ".tiff", ".tif")); } - if (selectedExt != null) + // TODO: Fix setting current filter on Eto GTK + if (selectedExt != null && !EtoPlatform.Current.IsGtk) { selectedExt = selectedExt.Replace(".", ""); foreach (var filter in filters) diff --git a/NAPS2.Lib/EtoForms/FormStateController.cs b/NAPS2.Lib/EtoForms/FormStateController.cs index d087585636..295bd29e39 100644 --- a/NAPS2.Lib/EtoForms/FormStateController.cs +++ b/NAPS2.Lib/EtoForms/FormStateController.cs @@ -4,13 +4,11 @@ namespace NAPS2.EtoForms; -// TODO: Verify all the size handling works correctly with maximized forms. public class FormStateController { private readonly Window _window; private readonly Naps2Config _config; private FormState? _formState; - private bool _loaded; private bool _hasSetSize; private Size _minimumClientSize; private Size _maximumClientSize; @@ -32,27 +30,32 @@ public FormStateController(Window window, Naps2Config config) public bool AutoLayoutSize { get; set; } = true; - public Size DefaultExtraLayoutSize { get; set; } + public SizeF DefaultExtraLayoutSize { get; set; } - public Size DefaultClientSize { get; set; } + public SizeF DefaultClientSize { get; set; } public bool FixedHeightLayout { get; set; } public bool Resizable { get; set; } = true; + public bool Loaded { get; private set; } + + public bool Shown { get; private set; } + public string FormName => _window.GetType().Name; public void UpdateLayoutSize(LayoutController layoutController) { if (AutoLayoutSize) { - _minimumClientSize = layoutController.GetLayoutSize(false); + var scale = EtoPlatform.Current.GetLayoutScaleFactor(_window); + _minimumClientSize = layoutController.GetLayoutSize(); var oldDefaultClientSize = DefaultClientSize; var oldMaximumClientSize = _maximumClientSize; - DefaultClientSize = layoutController.GetLayoutSize(true) + DefaultExtraLayoutSize; - _maximumClientSize = FixedHeightLayout ? new Size(0, _minimumClientSize.Height) : Size.Empty; + DefaultClientSize = (SizeF) _minimumClientSize / scale + DefaultExtraLayoutSize; + _maximumClientSize = FixedHeightLayout || !Resizable ? new Size(0, _minimumClientSize.Height) : Size.Empty; - if (_loaded) + if (Loaded) { // Dynamic re-sizing because the layout contents have changed (not just a normal resize/maximize etc). var size = EtoPlatform.Current.GetClientSize(_window); @@ -60,7 +63,11 @@ public void UpdateLayoutSize(LayoutController layoutController) { size.Height = Math.Min(size.Height, oldMaximumClientSize.Height); } - size += DefaultClientSize - oldDefaultClientSize; + // TODO: Maybe we can add a flag to do this behavior? It makes it so that changes to the layout size + // after the form is loaded cause the form size to change proportionally (even if we're still within + // our min/max bounds). This is causing problems for dynamically sized images/labels etc. but maybe + // there's a world where we want to re-enable this for some forms. + // size += DefaultClientSize - oldDefaultClientSize; size = Size.Max(size, _minimumClientSize); if (_maximumClientSize.Height > 0) { @@ -77,7 +84,7 @@ private void OnLoadInternal(object? sender, EventArgs eventArgs) if (RestoreFormState || SaveFormState) { var formStates = _config.Get(c => c.FormStates); - _formState = formStates.SingleOrDefault(x => x.Name == FormName) ?? new FormState {Name = FormName}; + _formState = formStates.SingleOrDefault(x => x.Name == FormName) ?? new FormState { Name = FormName }; } if (RestoreFormState) @@ -87,9 +94,10 @@ private void OnLoadInternal(object? sender, EventArgs eventArgs) if (!_hasSetSize && !DefaultClientSize.IsEmpty) { // TODO: Use size delta to re-center - EtoPlatform.Current.SetClientSize(_window, DefaultClientSize); + var scale = EtoPlatform.Current.GetLayoutScaleFactor(_window); + EtoPlatform.Current.SetClientSize(_window, Size.Round(DefaultClientSize * scale)); } - _loaded = true; + Loaded = true; } private void OnShownInternal(object? sender, EventArgs e) @@ -99,6 +107,7 @@ private void OnShownInternal(object? sender, EventArgs e) EtoPlatform.Current.SetMinimumClientSize(_window, _minimumClientSize); } _window.Resizable = Resizable; + Shown = true; } protected void DoRestoreFormState() @@ -108,17 +117,20 @@ protected void DoRestoreFormState() throw new InvalidOperationException(); } var location = new Point(_formState.Location.X, _formState.Location.Y); - var size = new Size(_formState.Size.Width, _formState.Size.Height); - if (!location.IsZero) + if (!location.IsZero && + // Restoring location causes DPI mismatches if there are multiple monitors with different DPI (WinForms bug) + !FixWinFormsDpiIssues && + // Only move to the specified location if it's onscreen + // It might be offscreen if the user has disconnected a monitor + Screen.Screens.Any(x => x.WorkingArea.Contains(location))) { - if (Screen.Screens.Any(x => x.WorkingArea.Contains(location))) - { - // Only move to the specified location if it's onscreen - // It might be offscreen if the user has disconnected a monitor - EtoPlatform.Current.SetFormLocation(_window, location); - } + EtoPlatform.Current.SetFormLocation(_window, location); } - if (!size.IsEmpty && _window.Resizable) + var scale = FixWinFormsDpiIssues ? 1f : EtoPlatform.Current.GetLayoutScaleFactor(_window); + var size = new Size( + (int) Math.Round(_formState.Size.Width * scale), + (int) Math.Round(_formState.Size.Height * scale)); + if (!size.IsEmpty && Resizable) { if (!_minimumClientSize.IsEmpty) { @@ -134,25 +146,38 @@ protected void DoRestoreFormState() if (_formState.Maximized && _window.Resizable) { _window.WindowState = WindowState.Maximized; + // TODO: Setting WindowState immediately doesn't work on WinForms when we've already created a window handle + // Theoretically we could move this code around earlier before the handle is created (e.g. when we're + // doing layouting to determine the width of the sidebar), but it's hard to get all the interdependencies + // between layouting and form state working correctly. This workaround of setting it again later works but + // results in an animation for the maximization. + Invoker.Current.InvokeDispatch(() => _window.WindowState = WindowState.Maximized); } } + private bool FixWinFormsDpiIssues => EtoPlatform.Current.IsWinForms && DifferentMonitorScales; + + private bool DifferentMonitorScales => Screen.Screens.Select(x => x.RealDPI).Distinct().Count() > 1; + private void OnResize(object? sender, EventArgs eventArgs) { - if (_loaded && _formState != null && SaveFormState) + if (Shown && _formState != null && SaveFormState) { _formState.Maximized = (_window.WindowState == WindowState.Maximized); if (_window.WindowState == WindowState.Normal) { var size = EtoPlatform.Current.GetClientSize(_window); - _formState.Size = new FormState.FormSize(size.Width, size.Height); + var scale = FixWinFormsDpiIssues ? 1f : EtoPlatform.Current.GetLayoutScaleFactor(_window); + _formState.Size = new FormState.FormSize( + (int) Math.Round(size.Width / scale), + (int) Math.Round(size.Height / scale)); } } } private void OnMove(object? sender, EventArgs eventArgs) { - if (_loaded && _formState != null && SaveFormState) + if (Shown && _formState != null && SaveFormState) { if (_window.WindowState == WindowState.Normal) { diff --git a/NAPS2.Lib/EtoForms/IDarkModeProvider.cs b/NAPS2.Lib/EtoForms/IDarkModeProvider.cs new file mode 100644 index 0000000000..12684c5f73 --- /dev/null +++ b/NAPS2.Lib/EtoForms/IDarkModeProvider.cs @@ -0,0 +1,8 @@ +namespace NAPS2.EtoForms; + +public interface IDarkModeProvider +{ + bool IsDarkModeEnabled { get; } + + event EventHandler? DarkModeChanged; +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/IFormBase.cs b/NAPS2.Lib/EtoForms/IFormBase.cs index d6d25bd9e3..dbfdc78d0b 100644 --- a/NAPS2.Lib/EtoForms/IFormBase.cs +++ b/NAPS2.Lib/EtoForms/IFormBase.cs @@ -1,11 +1,16 @@ +using NAPS2.EtoForms.Layout; + namespace NAPS2.EtoForms; public interface IFormBase { FormStateController FormStateController { get; } - // TODO: Make these constructor injected, Eto requires things to be defined in the constructor so property injection is error-prone IFormFactory FormFactory { get; set; } Naps2Config Config { get; set; } + + LayoutController LayoutController { get; } + + IntPtr NativeHandle { get; } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/IIconProvider.cs b/NAPS2.Lib/EtoForms/IIconProvider.cs index 3eece1c539..e6a34bacf2 100644 --- a/NAPS2.Lib/EtoForms/IIconProvider.cs +++ b/NAPS2.Lib/EtoForms/IIconProvider.cs @@ -4,5 +4,7 @@ namespace NAPS2.EtoForms; public interface IIconProvider { - Image? GetIcon(string name); + Bitmap? GetIcon(string name, float scale = 1f, bool oversized = false); + + Icon? GetFormIcon(string name, float scale = 1f); } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/INotificationManager.cs b/NAPS2.Lib/EtoForms/INotificationManager.cs deleted file mode 100644 index fb47fdf8ce..0000000000 --- a/NAPS2.Lib/EtoForms/INotificationManager.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NAPS2.Update; - -namespace NAPS2.EtoForms; - -public interface INotificationManager : ISaveNotify -{ - void DonatePrompt(); - void OperationProgress(OperationProgress opModalProgress, IOperation op); - void UpdateAvailable(IUpdateChecker updateChecker, UpdateInfo update); - void Rebuild(); -} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/KeyboardShortcutManager.cs b/NAPS2.Lib/EtoForms/KeyboardShortcutManager.cs index 290b2404cb..173a31ce18 100644 --- a/NAPS2.Lib/EtoForms/KeyboardShortcutManager.cs +++ b/NAPS2.Lib/EtoForms/KeyboardShortcutManager.cs @@ -1,4 +1,5 @@ -using Eto.Forms; +using System.Text; +using Eto.Forms; namespace NAPS2.EtoForms; @@ -10,15 +11,71 @@ public class KeyboardShortcutManager private readonly Dictionary _dict = new(); private readonly Dictionary _commandDict = new(); - private readonly Dictionary _customMap = new() + private readonly Dictionary _parseMap = new() { + { "mod", Application.Instance.CommonModifier }, + { "cmd", Keys.Application }, { "ctrl", Keys.Control }, { "del", Keys.Delete }, { "ins", Keys.Insert }, { "break", Keys.Pause }, { "oemplus", Keys.Equal }, { "oemminus", Keys.Minus }, - }; + { "esc", Keys.Escape }, + { "\\", Keys.Backslash }, + { ",", Keys.Comma }, + { "=", Keys.Equal }, + { "`", Keys.Grave }, + { "-", Keys.Minus }, + { ".", Keys.Period }, + { "\"", Keys.Quote }, + { ";", Keys.Semicolon }, + { "/", Keys.Slash }, + { "[", Keys.LeftBracket }, + { "]", Keys.RightBracket }, + { "0", Keys.D0 }, + { "1", Keys.D1 }, + { "2", Keys.D2 }, + { "3", Keys.D3 }, + { "4", Keys.D4 }, + { "5", Keys.D5 }, + { "6", Keys.D6 }, + { "7", Keys.D7 }, + { "8", Keys.D8 }, + { "9", Keys.D9 } + }; + + private readonly Dictionary _stringifyMap = new() + { + { Keys.Escape, "Esc" }, + { Keys.Backslash, "\\" }, + { Keys.Comma, "," }, + { Keys.Equal, "=" }, + { Keys.Grave, "`" }, + { Keys.Minus, "-" }, + { Keys.Period, "." }, + { Keys.Quote, "\"" }, + { Keys.Semicolon, ";" }, + { Keys.Slash, "/" }, + { Keys.LeftBracket, "[" }, + { Keys.RightBracket, "]" }, + { Keys.D0, "0" }, + { Keys.D1, "1" }, + { Keys.D2, "2" }, + { Keys.D3, "3" }, + { Keys.D4, "4" }, + { Keys.D5, "5" }, + { Keys.D6, "6" }, + { Keys.D7, "7" }, + { Keys.D8, "8" }, + { Keys.D9, "9" } + }; + + public void Clear() + { + _dict.Clear(); + _commandDict.Clear(); + } public Keys Parse(string? value) { @@ -29,9 +86,9 @@ public Keys Parse(string? value) var keys = Keys.None; foreach (var part in value!.Split('+').Select(x => x.Trim().ToLowerInvariant())) { - if (_customMap.ContainsKey(part)) + if (_parseMap.ContainsKey(part)) { - keys |= _customMap[part]; + keys |= _parseMap[part]; } else { @@ -48,6 +105,34 @@ public Keys Parse(string? value) return Keys.None; } + public string? Stringify(Keys keys) + { + if (keys == Keys.None) + { + return null; + } + var sb = new StringBuilder(); + if (keys.HasFlag(Keys.Application)) + { + sb.Append(EtoPlatform.Current.IsMac ? "Cmd + " : "Win + "); + } + if (keys.HasFlag(Keys.Control)) + { + sb.Append("Ctrl + "); + } + if (keys.HasFlag(Keys.Shift)) + { + sb.Append("Shift + "); + } + if (keys.HasFlag(Keys.Alt)) + { + sb.Append("Alt + "); + } + var keyWithoutMods = keys & ~Keys.ModifierMask; + sb.Append(_stringifyMap.Get(keyWithoutMods) ?? keyWithoutMods.ToString()); + return sb.ToString(); + } + public bool Assign(string? value, Action action) { var keys = Parse(value); diff --git a/NAPS2.Lib/EtoForms/Layout/BufferLayoutElement.cs b/NAPS2.Lib/EtoForms/Layout/BufferLayoutElement.cs index 430f2ec78b..c7567ca20e 100644 --- a/NAPS2.Lib/EtoForms/Layout/BufferLayoutElement.cs +++ b/NAPS2.Lib/EtoForms/Layout/BufferLayoutElement.cs @@ -19,18 +19,25 @@ public BufferLayoutElement(LayoutElement element, int left, int top, int right, public int Right { get; } public int Bottom { get; } + public override void Materialize(LayoutContext context) => Element.Materialize(context); + public override void DoLayout(LayoutContext context, RectangleF bounds) { - Element.DoLayout(context, - new RectangleF(bounds.X + Left, bounds.Top + Top, bounds.Width - Left - Right, - bounds.Height - Top - Bottom)); + Element.DoLayout(context, new RectangleF( + bounds.X + Left * context.Scale, + bounds.Y + Top * context.Scale, + bounds.Width - (Left + Right) * context.Scale, + bounds.Height - (Top + Bottom) * context.Scale)); } - public override SizeF GetPreferredSize(LayoutContext context, RectangleF parentBounds) + protected override SizeF GetPreferredSizeCore(LayoutContext context, RectangleF parentBounds) { return Element.GetPreferredSize(context, - new RectangleF(parentBounds.X + Left, parentBounds.Top + Top, parentBounds.Width - Left - Right, - parentBounds.Height - Top - Bottom)) - + new SizeF(Left + Right, Top + Bottom); + new RectangleF( + parentBounds.X + Left * context.Scale, + parentBounds.Y + Top * context.Scale, + parentBounds.Width - (Left + Right) * context.Scale, + parentBounds.Height - (Top + Bottom) * context.Scale)) + + new SizeF(Left + Right, Top + Bottom) * context.Scale; } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/C.cs b/NAPS2.Lib/EtoForms/Layout/C.cs index 1dfb096b3b..2e6d15eb2b 100644 --- a/NAPS2.Lib/EtoForms/Layout/C.cs +++ b/NAPS2.Lib/EtoForms/Layout/C.cs @@ -1,7 +1,6 @@ using System.Windows.Input; using Eto.Drawing; using Eto.Forms; -using NAPS2.Scan; namespace NAPS2.EtoForms.Layout; @@ -16,44 +15,44 @@ public static Label NoWrap(string text) => new Label { Text = text, Wrap = WrapMode.None }; /// - /// Creates a link button with the given URL as both text and click action. + /// Creates a link button with the specified text. /// - /// - /// + /// /// - public static LinkButton UrlLink(string url, string? label = null) + public static LinkButton Link(string text) { - void OnClick() => Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); - return new LinkButton + var link = new LinkButton { Text = text }; + if (EtoPlatform.Current.IsWinForms) { - Text = label ?? url, - Command = new ActionCommand(OnClick) - }; + // TODO: Remove this when https://github.com/dotnet/winforms/issues/11935 is fixed + link.TextColor = EtoPlatform.Current.ColorScheme.LinkColor; + } + return link; } /// - /// Creates a link button with the specified text. + /// Creates a link button with the specified text and action. /// /// + /// /// - public static LinkButton Link(string text) + public static LinkButton Link(string text, Action onClick) { - return new LinkButton { Text = text }; + var link = Link(text); + link.Command = new ActionCommand(onClick); + return link; } /// - /// Creates a link button with the specified text and action. + /// Creates a link button with the given URL as both text and click action. /// - /// - /// + /// + /// /// - public static LinkButton Link(string text, Action onClick) + public static LinkButton UrlLink(string url, string? label = null) { - return new LinkButton - { - Text = text, - Command = new ActionCommand(onClick) - }; + void OnClick() => ProcessHelper.OpenUrl(url); + return Link(label ?? url, OnClick); } /// @@ -78,31 +77,54 @@ public static Button Button(string text, Action onClick) => /// /// /// - public static Button Button(string text, ICommand command) => - new Button + public static Button Button(string text, ActionCommand command) + { + var button = new Button { Text = text, Command = command }; + if (string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(command.ToolTip)) + { + button.ToolTip = command.ToolTip; + } + return button; + } - public static Button Button(Command command) => - Button(command.MenuText, command); + public static Button Button(ActionCommand command) => Button(command.Text, command); - public static Button Button(Command command, ButtonImagePosition imagePosition, bool big = false) + public static Button Button(ActionCommand command, ButtonImagePosition imagePosition, ButtonFlags flags = default) { - return Button(command, command.Image, imagePosition, big); + return Button(command, command.IconName, imagePosition, flags); } - public static Button Button(Command command, Image image, ButtonImagePosition imagePosition = default, bool big = false) + public static Button Button(ActionCommand command, string? iconName, ButtonImagePosition imagePosition = default, + ButtonFlags flags = default) { - var button = Button(command); - button.Image = image; + var button = Button(imagePosition == ButtonImagePosition.Overlay ? "" : command.MenuText, command); + if (command.Image != null) + { + EtoPlatform.Current.AttachDpiDependency(button, + scale => + { + int targetSize = flags.HasFlag(ButtonFlags.LargeIcon) ? 32 : 16; + button.Image = command.Image.Clone().ResizeTo((int) (targetSize * scale)); + }); + } + else if (iconName != null) + { + bool oversized = imagePosition == ButtonImagePosition.Above && flags.HasFlag(ButtonFlags.LargeIcon); + EtoPlatform.Current.AttachDpiDependency(button, + scale => button.Image = EtoPlatform.Current.IconProvider.GetIcon(iconName, scale, oversized)); + } button.ImagePosition = imagePosition; - if (big) + if (flags.HasFlag(ButtonFlags.LargeText)) { - button.Font = new Font(button.Font.Family, 12); + var baseFontSize = button.Font.Size; + EtoPlatform.Current.AttachDpiDependency(button, + _ => button.Font = new Font(button.Font.Family, baseFontSize * 4 / 3)); } - EtoPlatform.Current.ConfigureImageButton(button, big); + EtoPlatform.Current.ConfigureImageButton(button, flags); return button; } @@ -119,15 +141,15 @@ public static Button ImageButton(Command command) => /// Creates a null placeholder for Eto layouts that absorbs scaling. /// /// - public static ControlWithLayoutAttributes Filler() => - new ControlWithLayoutAttributes(null).Scale(); + public static LayoutControl Filler() => + new LayoutControl(null).Scale(); /// /// Creates a null placeholder for Eto layouts. /// /// - public static ControlWithLayoutAttributes Spacer() => - new ControlWithLayoutAttributes(null); + public static LayoutControl Spacer() => + new LayoutControl(null); /// /// Creates an label of default height to be used as a visual paragraph separator. @@ -137,45 +159,10 @@ public static ControlWithLayoutAttributes Spacer() => public static Label Label(string text) => new() { Text = text }; - public static DropDown DropDown() + public static DropDown DropDown(bool scale = true) { var dropDown = new DropDown(); - EtoPlatform.Current.ConfigureDropDown(dropDown); - return dropDown; - } - - public static DropDown EnumDropDown(params T[] values) where T : Enum - { - var dropDown = new DropDown(); - EtoPlatform.Current.ConfigureDropDown(dropDown); - foreach (var item in values) - { - dropDown.Items.Add(new ListItem - { - Key = item.ToString(), - Text = item.Description() - }); - } - return dropDown; - } - - public static DropDown EnumDropDown() where T : Enum - { - return EnumDropDown(x => x.Description()); - } - - public static DropDown EnumDropDown(Func format) where T : Enum - { - var dropDown = new DropDown(); - EtoPlatform.Current.ConfigureDropDown(dropDown); - foreach (var item in Enum.GetValues(typeof(T))) - { - dropDown.Items.Add(new ListItem - { - Key = item.ToString(), - Text = format((T) item) - }); - } + EtoPlatform.Current.ConfigureDropDown(dropDown, scale); return dropDown; } @@ -223,4 +210,33 @@ public static LayoutElement None() { return new SkipLayoutElement(); } + + public static LayoutControl IconButton(string iconName, Action onClick) + { + var button = new Button + { + ImagePosition = ButtonImagePosition.Overlay + }; + EtoPlatform.Current.AttachDpiDependency(button, scale => + { + var icon = EtoPlatform.Current.IconProvider.GetIcon(iconName, scale)!; + button.Image = icon; + button.MinimumSize = new Size(icon.Width + 30, 0); + }); + button.Click += (_, _) => onClick(); + return button.Width(40); + } + + public static MenuItem ButtonMenuItem(Window window, ActionCommand command) + { + var menuItem = new ButtonMenuItem(command); + // TODO: Can we fix this memory leak? + command.TextChanged += (_, _) => menuItem.Text = command.MenuText; + EtoPlatform.Current.AttachDpiDependency(window, scale => + { + EtoPlatform.Current.SetImageSize(menuItem, (int) (16 * scale)); + menuItem.Image = command.GetIconImage(scale); + }); + return menuItem; + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/EtoLayoutExtensions.cs b/NAPS2.Lib/EtoForms/Layout/EtoLayoutExtensions.cs index b10ceb64a6..8322cc027d 100644 --- a/NAPS2.Lib/EtoForms/Layout/EtoLayoutExtensions.cs +++ b/NAPS2.Lib/EtoForms/Layout/EtoLayoutExtensions.cs @@ -31,131 +31,230 @@ public static DockPosition ToEto(this DockStyle dock) }; } - public static ControlWithLayoutAttributes Width(this Control control, int width) => - new ControlWithLayoutAttributes(control, width: width); - public static ControlWithLayoutAttributes MinWidth(this Control control, int minWidth) => - new ControlWithLayoutAttributes(control, minWidth: minWidth); - public static ControlWithLayoutAttributes MaxWidth(this Control control, int maxWidth) => - new ControlWithLayoutAttributes(control, maxWidth: maxWidth); - public static ControlWithLayoutAttributes Height(this Control control, int height) => - new ControlWithLayoutAttributes(control, height: height); - public static ControlWithLayoutAttributes MinHeight(this Control control, int minHeight) => - new ControlWithLayoutAttributes(control, minHeight: minHeight); - public static ControlWithLayoutAttributes MaxHeight(this Control control, int maxHeight) => - new ControlWithLayoutAttributes(control, maxHeight: maxHeight); - public static ControlWithLayoutAttributes Size(this Control control, int width, int height) => - new ControlWithLayoutAttributes(control, width: width, height: height); - public static ControlWithLayoutAttributes NaturalSize(this Control control, int width, int height) => - new ControlWithLayoutAttributes(control, naturalWidth: width, naturalHeight: height); - public static ControlWithLayoutAttributes NaturalWidth(this Control control, int width) => - new ControlWithLayoutAttributes(control, naturalWidth: width); - public static ControlWithLayoutAttributes NaturalHeight(this Control control, int height) => - new ControlWithLayoutAttributes(control, naturalHeight: height); - public static ControlWithLayoutAttributes Scale(this Control control) => - new ControlWithLayoutAttributes(control, scale: true); - public static ControlWithLayoutAttributes SpacingAfter(this Control control, int spacingAfter) => - new ControlWithLayoutAttributes(control, spacingAfter: spacingAfter); - public static ControlWithLayoutAttributes AlignCenter(this Control control) => - new ControlWithLayoutAttributes(control, alignment: LayoutAlignment.Center); - public static ControlWithLayoutAttributes AlignLeading(this Control control) => - new ControlWithLayoutAttributes(control, alignment: LayoutAlignment.Leading); - public static ControlWithLayoutAttributes AlignTrailing(this Control control) => - new ControlWithLayoutAttributes(control, alignment: LayoutAlignment.Trailing); - public static ControlWithLayoutAttributes Align(this Control control, LayoutAlignment alignment) => - new ControlWithLayoutAttributes(control, alignment: alignment); - public static ControlWithLayoutAttributes Padding(this Control control, Padding padding) => - new ControlWithLayoutAttributes(control, padding: padding); - public static ControlWithLayoutAttributes Padding(this Control control, int all) => - new ControlWithLayoutAttributes(control, padding: new Padding(all)); - public static ControlWithLayoutAttributes Padding(this Control control, int left = 0, int top = 0, int right = 0, int bottom = 0) => - new ControlWithLayoutAttributes(control, padding: new Padding(left, top, right, bottom)); - public static ControlWithLayoutAttributes Visible(this Control control, LayoutVisibility? visibility) => - new ControlWithLayoutAttributes(control, visibility: visibility); - - public static ControlWithLayoutAttributes Wrap(this Label label, int defaultWidth) + public static LayoutControl Width(this Control control, int? width) => + new LayoutControl(control, width: width); + + public static LayoutControl MinWidth(this Control control, int minWidth) => + new LayoutControl(control, minWidth: minWidth); + + public static LayoutControl MaxWidth(this Control control, int maxWidth) => + new LayoutControl(control, maxWidth: maxWidth); + + public static LayoutControl Height(this Control control, int? height) => + new LayoutControl(control, height: height); + + public static LayoutControl MinHeight(this Control control, int minHeight) => + new LayoutControl(control, minHeight: minHeight); + + public static LayoutControl MaxHeight(this Control control, int maxHeight) => + new LayoutControl(control, maxHeight: maxHeight); + + public static LayoutControl Size(this Control control, int width, int height) => + new LayoutControl(control, width: width, height: height); + + public static LayoutControl NaturalSize(this Control control, int width, int height) => + new LayoutControl(control, naturalWidth: width, naturalHeight: height); + + public static LayoutControl NaturalWidth(this Control control, int width) => + new LayoutControl(control, naturalWidth: width); + + public static LayoutControl NaturalHeight(this Control control, int height) => + new LayoutControl(control, naturalHeight: height); + + public static LayoutControl Scale(this Control control) => + new LayoutControl(control, scale: true); + + public static LayoutControl SpacingAfter(this Control control, int spacingAfter) => + new LayoutControl(control, spacingAfter: spacingAfter); + + public static LayoutControl AlignCenter(this Control control) => + new LayoutControl(control, alignment: LayoutAlignment.Center); + + public static LayoutControl AlignLeading(this Control control) => + new LayoutControl(control, alignment: LayoutAlignment.Leading); + + public static LayoutControl AlignTrailing(this Control control) => + new LayoutControl(control, alignment: LayoutAlignment.Trailing); + + public static LayoutControl Align(this Control control, LayoutAlignment alignment) => + new LayoutControl(control, alignment: alignment); + + public static LayoutColumn AlignCenter(this LayoutColumn column) => + new LayoutColumn(column, alignment: LayoutAlignment.Center); + + public static LayoutColumn AlignLeading(this LayoutColumn column) => + new LayoutColumn(column, alignment: LayoutAlignment.Leading); + + public static LayoutColumn AlignTrailing(this LayoutColumn column) => + new LayoutColumn(column, alignment: LayoutAlignment.Trailing); + + public static LayoutColumn Align(this LayoutColumn column, LayoutAlignment alignment) => + new LayoutColumn(column, alignment: alignment); + + public static LayoutRow AlignCenter(this LayoutRow row) => + new LayoutRow(row, alignment: LayoutAlignment.Center); + + public static LayoutRow AlignLeading(this LayoutRow row) => + new LayoutRow(row, alignment: LayoutAlignment.Leading); + + public static LayoutRow AlignTrailing(this LayoutRow row) => + new LayoutRow(row, alignment: LayoutAlignment.Trailing); + + public static LayoutRow Align(this LayoutRow row, LayoutAlignment alignment) => + new LayoutRow(row, alignment: alignment); + + public static LayoutControl Padding(this Control control, Padding padding) => + new LayoutControl(control, padding: padding); + + public static LayoutControl Padding(this Control control, int all) => + new LayoutControl(control, padding: new Padding(all)); + + public static LayoutControl Padding(this Control control, int left = 0, int top = 0, int right = 0, + int bottom = 0) => + new LayoutControl(control, padding: new Padding(left, top, right, bottom)); + + public static LayoutControl Visible(this Control control, LayoutVisibility? visibility) => + new LayoutControl(control, visibility: visibility); + + public static Label Ellipsize(this Label label) + { + EtoPlatform.Current.ConfigureEllipsis(label); + return label; + } + + /// + /// Wraps the given label. The way this works is that is uses the specified width to calculate the allocated height, + /// which remains static (so e.g. if you're resizing a form, the form height isn't dependent on its width). If + /// wrapping happens at that width, it becomes the minimum width (as otherwise the label could exceed its vertical + /// bounds). If wrapping doesn't happen, the text width is the minimum width. Note that depending on how the control + /// is laid out in the form, you might need to set a Width or MaxWidth constraint for wrapping to actually happen. + /// + /// + /// + /// + public static LayoutControl DynamicWrap(this Label label, int defaultWidth) { + if (defaultWidth == 0) + { + return label; + } label.Wrap = WrapMode.Word; - return new ControlWithLayoutAttributes(label, wrapDefaultWidth: defaultWidth);; + return new LayoutControl(label, wrapDefaultWidth: defaultWidth); } - public static ControlWithLayoutAttributes Width(this ControlWithLayoutAttributes control, int width) => - new ControlWithLayoutAttributes(control, width: width); - public static ControlWithLayoutAttributes MinWidth(this ControlWithLayoutAttributes control, int minWidth) => - new ControlWithLayoutAttributes(control, minWidth: minWidth); - public static ControlWithLayoutAttributes MaxWidth(this ControlWithLayoutAttributes control, int maxWidth) => - new ControlWithLayoutAttributes(control, maxWidth: maxWidth); - public static ControlWithLayoutAttributes Height(this ControlWithLayoutAttributes control, int height) => - new ControlWithLayoutAttributes(control, height: height); - public static ControlWithLayoutAttributes MinHeight(this ControlWithLayoutAttributes control, int minHeight) => - new ControlWithLayoutAttributes(control, minHeight: minHeight); - public static ControlWithLayoutAttributes MaxHeight(this ControlWithLayoutAttributes control, int maxHeight) => - new ControlWithLayoutAttributes(control, maxHeight: maxHeight); - public static ControlWithLayoutAttributes NaturalSize(this ControlWithLayoutAttributes control, int width, int height) => - new ControlWithLayoutAttributes(control, naturalWidth: width, naturalHeight: height); - public static ControlWithLayoutAttributes NaturalWidth(this ControlWithLayoutAttributes control, int width) => - new ControlWithLayoutAttributes(control, naturalWidth: width); - public static ControlWithLayoutAttributes NaturalHeight(this ControlWithLayoutAttributes control, int height) => - new ControlWithLayoutAttributes(control, naturalHeight: height); - public static ControlWithLayoutAttributes Scale(this ControlWithLayoutAttributes control) => - new ControlWithLayoutAttributes(control, scale: true); - public static ControlWithLayoutAttributes SpacingAfter(this ControlWithLayoutAttributes control, int spacingAfter) => - new ControlWithLayoutAttributes(control, spacingAfter: spacingAfter); - public static ControlWithLayoutAttributes AlignCenter(this ControlWithLayoutAttributes control) => - new ControlWithLayoutAttributes(control, alignment: LayoutAlignment.Center); - public static ControlWithLayoutAttributes AlignLeading(this ControlWithLayoutAttributes control) => - new ControlWithLayoutAttributes(control, alignment: LayoutAlignment.Leading); - public static ControlWithLayoutAttributes AlignTrailing(this ControlWithLayoutAttributes control) => - new ControlWithLayoutAttributes(control, alignment: LayoutAlignment.Trailing); - public static ControlWithLayoutAttributes Align(this ControlWithLayoutAttributes control, LayoutAlignment alignment) => - new ControlWithLayoutAttributes(control, alignment: alignment); - public static ControlWithLayoutAttributes Padding(this ControlWithLayoutAttributes control, Padding padding) => - new ControlWithLayoutAttributes(control, padding: padding); - public static ControlWithLayoutAttributes Padding(this ControlWithLayoutAttributes control, int all) => - new ControlWithLayoutAttributes(control, padding: new Padding(all)); - public static ControlWithLayoutAttributes Padding(this ControlWithLayoutAttributes control, int left = 0, int top = 0, int right = 0, int bottom = 0) => - new ControlWithLayoutAttributes(control, padding: new Padding(left, top, right, bottom)); - public static ControlWithLayoutAttributes Visible(this ControlWithLayoutAttributes control, LayoutVisibility? visibility) => - new ControlWithLayoutAttributes(control, visibility: visibility); + public static LayoutControl Width(this LayoutControl control, int width) => + new LayoutControl(control, width: width); + + public static LayoutControl MinWidth(this LayoutControl control, int minWidth) => + new LayoutControl(control, minWidth: minWidth); + + public static LayoutControl MaxWidth(this LayoutControl control, int maxWidth) => + new LayoutControl(control, maxWidth: maxWidth); + + public static LayoutControl Height(this LayoutControl control, int height) => + new LayoutControl(control, height: height); + + public static LayoutControl MinHeight(this LayoutControl control, int minHeight) => + new LayoutControl(control, minHeight: minHeight); + + public static LayoutControl MaxHeight(this LayoutControl control, int maxHeight) => + new LayoutControl(control, maxHeight: maxHeight); + + public static LayoutControl NaturalSize(this LayoutControl control, int width, int height) => + new LayoutControl(control, naturalWidth: width, naturalHeight: height); + + public static LayoutControl NaturalWidth(this LayoutControl control, int width) => + new LayoutControl(control, naturalWidth: width); + + public static LayoutControl NaturalHeight(this LayoutControl control, int height) => + new LayoutControl(control, naturalHeight: height); + + public static LayoutControl Scale(this LayoutControl control) => + new LayoutControl(control, scale: true); + + public static LayoutControl SpacingAfter(this LayoutControl control, int spacingAfter) => + new LayoutControl(control, spacingAfter: spacingAfter); + + public static LayoutControl AlignCenter(this LayoutControl control) => + new LayoutControl(control, alignment: LayoutAlignment.Center); + + public static LayoutControl AlignLeading(this LayoutControl control) => + new LayoutControl(control, alignment: LayoutAlignment.Leading); + + public static LayoutControl AlignTrailing(this LayoutControl control) => + new LayoutControl(control, alignment: LayoutAlignment.Trailing); + + public static LayoutControl Align(this LayoutControl control, LayoutAlignment alignment) => + new LayoutControl(control, alignment: alignment); + + public static LayoutControl Padding(this LayoutControl control, Padding padding) => + new LayoutControl(control, padding: padding); + + public static LayoutControl Padding(this LayoutControl control, int all) => + new LayoutControl(control, padding: new Padding(all)); + + public static LayoutControl Padding(this LayoutControl control, int left = 0, int top = 0, int right = 0, + int bottom = 0) => + new LayoutControl(control, padding: new Padding(left, top, right, bottom)); + + public static LayoutControl Visible(this LayoutControl control, LayoutVisibility? visibility) => + new LayoutControl(control, visibility: visibility); public static LayoutColumn Padding(this LayoutColumn column, Padding padding) => new LayoutColumn(column, padding: padding); + public static LayoutColumn Padding(this LayoutColumn column, int all) => new LayoutColumn(column, padding: new Padding(all)); - public static LayoutColumn Padding(this LayoutColumn column, int left = 0, int top = 0, int right = 0, int bottom = 0) => + + public static LayoutColumn Padding(this LayoutColumn column, int left = 0, int top = 0, int right = 0, + int bottom = 0) => new LayoutColumn(column, padding: new Padding(left, top, right, bottom)); + public static LayoutColumn Spacing(this LayoutColumn column, int spacing) => new LayoutColumn(column, spacing: spacing); + public static LayoutColumn SpacingAfter(this LayoutColumn column, int spacingAfter) => new LayoutColumn(column, spacingAfter: spacingAfter); + public static LayoutColumn Scale(this LayoutColumn column) => new LayoutColumn(column, scale: true); + public static LayoutColumn Aligned(this LayoutColumn column) => new LayoutColumn(column, aligned: true); - // public static LayoutColumn Visible(this LayoutColumn column, LayoutVisibility visibility) => - // new LayoutColumn(column, visibility: visibility); + + public static LayoutColumn Visible(this LayoutColumn column, LayoutVisibility visibility) => + new LayoutColumn(column, visibility: visibility); public static LayoutRow Padding(this LayoutRow row, Padding padding) => new LayoutRow(row, padding: padding); + public static LayoutRow Padding(this LayoutRow row, int all) => new LayoutRow(row, padding: new Padding(all)); + public static LayoutRow Padding(this LayoutRow row, int left = 0, int top = 0, int right = 0, int bottom = 0) => new LayoutRow(row, padding: new Padding(left, top, right, bottom)); + public static LayoutRow Spacing(this LayoutRow row, int spacing) => new LayoutRow(row, spacing: spacing); + public static LayoutRow SpacingAfter(this LayoutRow row, int spacingAfter) => new LayoutRow(row, spacingAfter: spacingAfter); + public static LayoutRow Scale(this LayoutRow row) => new LayoutRow(row, scale: true); + public static LayoutRow Aligned(this LayoutRow row) => new LayoutRow(row, aligned: true); - // public static LayoutRow Visible(this LayoutRow row, LayoutVisibility visibility) => - // new LayoutRow(row, visibility: visibility); + + public static LayoutRow Visible(this LayoutRow row, LayoutVisibility visibility) => + new LayoutRow(row, visibility: visibility); public static LayoutOverlay Scale(this LayoutOverlay overlay) => new LayoutOverlay(overlay, scale: true); public static LayoutElement Expand(this IEnumerable elements) => new ExpandLayoutElement(elements.ToArray()); + public static LayoutElement Expand(this IEnumerable elements) => new ExpandLayoutElement(elements.Select(x => (LayoutElement) x).ToArray()); diff --git a/NAPS2.Lib/EtoForms/Layout/ExpandLayoutElement.cs b/NAPS2.Lib/EtoForms/Layout/ExpandLayoutElement.cs index b7181cd964..effc0c2712 100644 --- a/NAPS2.Lib/EtoForms/Layout/ExpandLayoutElement.cs +++ b/NAPS2.Lib/EtoForms/Layout/ExpandLayoutElement.cs @@ -11,8 +11,10 @@ public ExpandLayoutElement(params LayoutElement[] children) public LayoutElement[] Children { get; } + public override void Materialize(LayoutContext context) => throw new NotSupportedException(); + public override void DoLayout(LayoutContext context, RectangleF bounds) => throw new NotSupportedException(); - public override SizeF GetPreferredSize(LayoutContext context, RectangleF parentBounds) => + protected override SizeF GetPreferredSizeCore(LayoutContext context, RectangleF parentBounds) => throw new NotSupportedException(); } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/L.cs b/NAPS2.Lib/EtoForms/Layout/L.cs index 05aeeaa588..c492aa9efc 100644 --- a/NAPS2.Lib/EtoForms/Layout/L.cs +++ b/NAPS2.Lib/EtoForms/Layout/L.cs @@ -1,3 +1,4 @@ +using Eto.Drawing; using Eto.Forms; namespace NAPS2.EtoForms.Layout; @@ -54,4 +55,9 @@ public static LayoutElement Buffer(LayoutElement element, int left, int top, int { return new BufferLayoutElement(element, left, top, right, bottom); } + + public static LayoutLeftPanel LeftPanel(LayoutElement left, LayoutElement right) + { + return new LayoutLeftPanel(left, right); + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutColumn.cs b/NAPS2.Lib/EtoForms/Layout/LayoutColumn.cs index a502523c2d..e686df9057 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutColumn.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutColumn.cs @@ -3,14 +3,15 @@ namespace NAPS2.EtoForms.Layout; -public class LayoutColumn : LayoutLine +public class LayoutColumn : LayoutLine { public LayoutColumn(LayoutElement[] children) : base(children) { } public LayoutColumn(LayoutColumn original, Padding? padding = null, int? spacing = null, int? labelSpacing = null, - int? spacingAfter = null, bool? scale = null, bool? aligned = null, LayoutVisibility? visibility = null) + int? spacingAfter = null, bool? scale = null, bool? aligned = null, LayoutAlignment? alignment = null, + LayoutVisibility? visibility = null) : base(original.Children) { Padding = padding ?? original.Padding; @@ -19,11 +20,16 @@ public LayoutColumn(LayoutColumn original, Padding? padding = null, int? spacing SpacingAfter = spacingAfter ?? original.SpacingAfter; Scale = scale ?? original.Scale; Aligned = aligned ?? original.Aligned; + Alignment = alignment ?? original.Alignment; Visibility = visibility ?? original.Visibility; + Width = original.Width; + Height = original.Height; } protected int? LabelSpacing { get; init; } + protected override bool IsOrthogonalTo(LayoutLine other) => other is LayoutRow; + protected override PointF UpdatePosition(PointF position, float delta) { position.Y += delta; @@ -45,7 +51,7 @@ protected override SizeF UpdateTotalSize(SizeF size, SizeF childSize, int spacin protected override int GetSpacingCore(int i, LayoutContext context) { - if (i < Children.Length - 1 && Children[i] is ControlWithLayoutAttributes { Control: Label }) + if (i < Children.Count - 1 && Children[i] is LayoutControl { Control: Label }) { return Children[i].SpacingAfter ?? LabelSpacing ?? context.DefaultLabelSpacing; } diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutContainer.cs b/NAPS2.Lib/EtoForms/Layout/LayoutContainer.cs index 01b74d9448..e16d3bcaea 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutContainer.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutContainer.cs @@ -1,19 +1,19 @@ -using Eto.Drawing; - namespace NAPS2.EtoForms.Layout; public abstract class LayoutContainer : LayoutElement { - protected LayoutContainer(LayoutElement[] children) + protected LayoutContainer(IEnumerable children) { Children = ExpandChildren(children); } - protected internal LayoutElement[] Children { get; } - - protected internal bool Aligned { get; set; } + protected internal List Children { get; } - protected abstract float GetBreadth(SizeF size); - protected abstract float GetLength(SizeF size); - protected abstract SizeF GetSize(float length, float breadth); + public override void Materialize(LayoutContext context) + { + foreach (var child in Children) + { + child.Materialize(context); + } + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutContext.cs b/NAPS2.Lib/EtoForms/Layout/LayoutContext.cs index b5a29c8b80..1e712079bc 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutContext.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutContext.cs @@ -2,8 +2,10 @@ namespace NAPS2.EtoForms.Layout; -public record LayoutContext(Control Layout) +public record LayoutContext { + public required Control Container { get; init; } + public int DefaultSpacing { get; init; } public int DefaultLabelSpacing { get; init; } @@ -14,10 +16,6 @@ public record LayoutContext(Control Layout) public bool IsLayout { get; init; } - public bool IsFirstLayout { get; init; } - - public bool IsNaturalSizeQuery { get; init; } - public bool IsCellLengthQuery { get; set; } public int Depth { get; init; } @@ -26,5 +24,13 @@ public record LayoutContext(Control Layout) public bool InOverlay { get; init; } + public LayoutVisibility? ParentVisibility { get; init; } + public required Action Invalidate { get; init; } + + public bool UseCache { get; init; } = true; + + public Dictionary PreferredSizeCache { get; } = new(); + + public float Scale { get; init; } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/ControlWithLayoutAttributes.cs b/NAPS2.Lib/EtoForms/Layout/LayoutControl.cs similarity index 56% rename from NAPS2.Lib/EtoForms/Layout/ControlWithLayoutAttributes.cs rename to NAPS2.Lib/EtoForms/Layout/LayoutControl.cs index 10ea87d21c..052aa829c9 100644 --- a/NAPS2.Lib/EtoForms/Layout/ControlWithLayoutAttributes.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutControl.cs @@ -6,24 +6,27 @@ namespace NAPS2.EtoForms.Layout; // Ignore unreachable code for DEBUG_LAYOUT #pragma warning disable CS0162 -public class ControlWithLayoutAttributes : LayoutElement +public class LayoutControl : LayoutElement { private static readonly FieldInfo VisualParentField = typeof(Control).GetField("VisualParent_Key", BindingFlags.NonPublic | BindingFlags.Static)!; - + private static readonly MethodInfo TriggerPreLoadMethod = + typeof(Control).GetMethod("TriggerPreLoad", BindingFlags.NonPublic | BindingFlags.Instance)!; private static readonly MethodInfo TriggerLoadMethod = typeof(Control).GetMethod("TriggerLoad", BindingFlags.NonPublic | BindingFlags.Instance)!; + private static readonly MethodInfo TriggerLoadCompleteMethod = + typeof(Control).GetMethod("TriggerLoadComplete", BindingFlags.NonPublic | BindingFlags.Instance)!; private bool _isAdded; private bool _isWindowSet; - public ControlWithLayoutAttributes(Control? control) + public LayoutControl(Control? control) { Control = control; } - public ControlWithLayoutAttributes( - ControlWithLayoutAttributes control, + public LayoutControl( + LayoutControl control, bool? scale = null, Padding? padding = null, int? spacingAfter = null, int? width = null, int? minWidth = null, int? maxWidth = null, int? naturalWidth = null, int? height = null, int? minHeight = null, int? maxHeight = null, int? naturalHeight = null, @@ -46,16 +49,14 @@ public ControlWithLayoutAttributes( Visibility = visibility ?? control.Visibility; } - public static implicit operator ControlWithLayoutAttributes(Control control) => - new ControlWithLayoutAttributes(control); + public static implicit operator LayoutControl(Control control) => + new LayoutControl(control); internal Control? Control { get; } private Padding Padding { get; } - private int? Width { get; } private int? MinWidth { get; } private int? MaxWidth { get; } private int? NaturalWidth { get; } - private int? Height { get; } private int? MinHeight { get; } private int? MaxHeight { get; } private int? NaturalHeight { get; } @@ -70,32 +71,36 @@ public override void DoLayout(LayoutContext context, RectangleF bounds) $"{new string(' ', context.Depth)}{text}{Control?.GetType().Name ?? "ZeroSpace"} layout with bounds {bounds}"); } bounds.Size = UpdateFixedDimensions(context, bounds.Size); - if (Control != null) - { - var location = new PointF(bounds.X + Padding.Left, bounds.Y + Padding.Top); - var size = new SizeF(bounds.Width - Padding.Horizontal, bounds.Height - Padding.Vertical); - size = SizeF.Max(SizeF.Empty, size); - EnsureIsAdded(context); - EtoPlatform.Current.SetFrame( - context.Layout, - Control, - Point.Round(location), - Size.Round(size), - context.InOverlay); - } + UpdateVisibility(context); + UpdateFrame(context, bounds); + } + + private void UpdateFrame(LayoutContext context, RectangleF bounds) + { + if (Control == null) return; + var location = new PointF(bounds.X + Padding.Left * context.Scale, bounds.Y + Padding.Top * context.Scale); + var size = new SizeF( + bounds.Width - Padding.Horizontal * context.Scale, + bounds.Height - Padding.Vertical * context.Scale); + size = SizeF.Max(SizeF.Empty, size); + EtoPlatform.Current.SetFrame( + context.Container, + Control, + Point.Round(location), + Size.Round(size), + context.InOverlay); } - public override SizeF GetPreferredSize(LayoutContext context, RectangleF parentBounds) + protected override SizeF GetPreferredSizeCore(LayoutContext context, RectangleF parentBounds) { var size = SizeF.Empty; if (!IsVisible) { - EnsureIsAdded(context); return size; } if (Control != null) { - EnsureIsAdded(context); + UpdateVisibility(context); if (WrapDefaultWidth is { } wrapDefaultWidth) { size = GetWrappedSize(context, parentBounds, wrapDefaultWidth); @@ -106,19 +111,29 @@ public override SizeF GetPreferredSize(LayoutContext context, RectangleF parentB } } size = UpdateFixedDimensions(context, size); - return new SizeF(size.Width + Padding.Horizontal, size.Height + Padding.Vertical); + size = new SizeF( + size.Width + Padding.Horizontal * context.Scale, + size.Height + Padding.Vertical * context.Scale); + if (DEBUG_SIZE) + { + var text = Control is TextControl txt ? $"\"{txt.Text}\" " : ""; + Debug.WriteLine( + $"{new string(' ', context.Depth)}{text}{Control?.GetType().Name ?? "ZeroSpace"} size {size}"); + } + return size; } private SizeF GetWrappedSize(LayoutContext context, RectangleF parentBounds, int wrapDefaultWidth) { if (Control == null) throw new InvalidOperationException(); + int scaledWrapDefaultWidth = (int) Math.Round(wrapDefaultWidth * context.Scale); // Label wrapping is fairly complicated. if (!context.IsLayout) { // If we're not in a layout operation (i.e. we're getting a minimum or default form size), the // measured size should be based on the default width for wrapping. // This produces the label width (if small) or the default width (if long enough to wrap). - return EtoPlatform.Current.GetWrappedSize(Control, wrapDefaultWidth); + return EtoPlatform.Current.GetWrappedSize(Control, scaledWrapDefaultWidth); } if (context.IsCellLengthQuery) { @@ -129,7 +144,8 @@ private SizeF GetWrappedSize(LayoutContext context, RectangleF parentBounds, int // usually what we want. return new SizeF( EtoPlatform.Current.GetWrappedSize(Control, (int) parentBounds.Width).Width, - EtoPlatform.Current.GetWrappedSize(Control, wrapDefaultWidth).Height); + EtoPlatform.Current.GetWrappedSize(Control, Math.Min((int) parentBounds.Width, scaledWrapDefaultWidth)) + .Height); } // Now that we've handled the special cases, this measures the real dimensions of the label given // the parent bounds. In a layout cell, this ensures we align correctly (e.g. centered vertically). @@ -140,57 +156,81 @@ private SizeF UpdateFixedDimensions(LayoutContext context, SizeF size) { if (MaxWidth != null) { - size.Width = Math.Min(size.Width, MaxWidth.Value); + size.Width = Math.Min(size.Width, MaxWidth.Value * context.Scale); } if (MinWidth != null) { - size.Width = Math.Max(size.Width, MinWidth.Value); + size.Width = Math.Max(size.Width, MinWidth.Value * context.Scale); } if (Width != null) { - size.Width = Width.Value; + size.Width = Width.Value * context.Scale; } - if (context.IsNaturalSizeQuery && NaturalWidth != null) + if (!context.IsLayout && NaturalWidth != null) { - size.Width = Math.Max(size.Width, NaturalWidth.Value); + size.Width = NaturalWidth.Value * context.Scale; } if (Height != null) { - size.Height = Height.Value; + size.Height = Height.Value * context.Scale; } if (MaxHeight != null) { - size.Height = Math.Min(size.Height, MaxHeight.Value); + size.Height = Math.Min(size.Height, MaxHeight.Value * context.Scale); } if (MinHeight != null) { - size.Height = Math.Max(size.Height, MinHeight.Value); + size.Height = Math.Max(size.Height, MinHeight.Value * context.Scale); } - if (context.IsNaturalSizeQuery && NaturalHeight != null) + if (!context.IsLayout && NaturalHeight != null) { - size.Height = Math.Max(size.Height, NaturalHeight.Value); + size.Height = NaturalHeight.Value * context.Scale; } return size; } - private void EnsureIsAdded(LayoutContext context) + public override void Materialize(LayoutContext context) { if (Control == null) return; - Control.Visible = IsVisible; - if (context.IsFirstLayout && !_isAdded) + if (!_isAdded) { - EtoPlatform.Current.AddToContainer(context.Layout, Control, context.InOverlay); + EtoPlatform.Current.AddToContainer(context.Container, Control, context.InOverlay); _isAdded = true; + void OnVisibleChanged(object? sender, EventArgs args) + { + context.Invalidate(); + // TODO: This is suuuper hacky. But I don't have the time right now to figure out a better fix. + // See https://github.com/cyanfish/naps2/issues/652 + if (EtoPlatform.Current.IsGtk) + { + Task.Delay(100).ContinueWith(_ => context.Invalidate()); + } + } if (Visibility != null) { - Visibility.IsVisibleChanged += (_, _) => context.Invalidate(); + Visibility.IsVisibleChanged += OnVisibleChanged; + } + if (context.ParentVisibility != null) + { + context.ParentVisibility.IsVisibleChanged += OnVisibleChanged; } } - if (context.IsFirstLayout && !_isWindowSet && context.Window != null) + if (!_isWindowSet && context.Window != null) { Control.Properties.Set(VisualParentField.GetValue(null), context.Window); + TriggerPreLoadMethod.Invoke(Control, new object[] { EventArgs.Empty }); TriggerLoadMethod.Invoke(Control, new object[] { EventArgs.Empty }); + TriggerLoadCompleteMethod.Invoke(Control, new object[] { EventArgs.Empty }); _isWindowSet = true; } } + + private void UpdateVisibility(LayoutContext context) + { + if (Control == null) return; + if (Visibility != null || context.ParentVisibility != null) + { + Control.Visible = IsVisible && (context.ParentVisibility?.IsVisible ?? true); + } + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutController.cs b/NAPS2.Lib/EtoForms/Layout/LayoutController.cs index 62d05443b0..979525d210 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutController.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutController.cs @@ -11,9 +11,9 @@ public class LayoutController private readonly Control _layout = EtoPlatform.Current.CreateContainer(); private LayoutElement? _content; private Window? _window; - private bool _firstLayout = true; private bool _isShown; private bool _layoutQueued; + private HashSet _controlSet = new(); public LayoutElement? Content { @@ -30,6 +30,8 @@ public LayoutElement? Content } } + public Control Container => _layout; + public int RootPadding { get; set; } = 10; public int DefaultLabelSpacing { get; set; } = 2; public int DefaultSpacing { get; set; } = 6; @@ -39,19 +41,33 @@ public void Bind(Window window) if (_window != null) throw new InvalidOperationException(); _window = window; window.Content = _layout; - window.Shown += (_, _) => + window.LoadComplete += (_, _) => { - _isShown = true; + if (window is not Form) + { + // For dialogs loading is equivalent to being shown + _isShown = true; + } DoLayout(); }; + window.Shown += (_, _) => + { + // For forms, it's possible they might be maximized between LoadComplete and Shown so we need to relayout + if (window is Form) + { + _isShown = true; + DoLayout(); + } + }; window.SizeChanged += (_, _) => DoLayout(); } - public Size GetLayoutSize(bool natural) + public Size GetLayoutSize() { if (_content == null) throw new InvalidOperationException(); var bounds = new RectangleF(0, 0, MAX_SIZE, MAX_SIZE); - var context = GetLayoutContext() with { IsNaturalSizeQuery = natural }; + var context = GetLayoutContext(); + _content.Materialize(context); var contentSize = _content.GetPreferredSize(context, bounds); var padding = new SizeF(RootPadding * 2, RootPadding * 2); var naturalSize = Size.Ceiling(contentSize + padding); @@ -60,17 +76,25 @@ public Size GetLayoutSize(bool natural) public void Invalidate() { + if (_layoutQueued) return; + if (_window == null || _content == null || !_isShown) + { + Invoker.Current.InvokeDispatch(() => { Invalidated?.Invoke(this, EventArgs.Empty); }); + return; + } _layoutQueued = true; - Invalidated?.Invoke(this, EventArgs.Empty); - _layoutQueued = false; - DoLayout(); + Invoker.Current.InvokeDispatch(() => + { + Invalidated?.Invoke(this, EventArgs.Empty); + _layoutQueued = false; + DoLayout(); + }); } private void DoLayout() { if (_window == null || _content == null || !_isShown || _layoutQueued) return; - // TODO: Handle added/removed things - var size = EtoPlatform.Current.GetClientSize(_window); + var size = EtoPlatform.Current.GetClientSize(_window, true); int p = RootPadding; var bounds = new Rectangle(p, p, size.Width - 2 * p, size.Height - 2 * p); if (bounds.Width < 0 || bounds.Height < 0) @@ -78,27 +102,65 @@ private void DoLayout() return; } var context = GetLayoutContext() with { Window = _window, IsLayout = true }; - _firstLayout = false; if (LayoutElement.DEBUG_LAYOUT) { Debug.WriteLine("\n(((Starting layout)))"); } _window.SuspendLayout(); + _content.Materialize(context); _content.DoLayout(context, bounds); + RemoveControls(); _window.ResumeLayout(); EtoPlatform.Current.SetContainerSize(_window, _layout, size, p); } - private LayoutContext GetLayoutContext() + private void RemoveControls() { - return new LayoutContext(_layout) + var newControlSet = new HashSet(); + PopulateControlSet(newControlSet, _content!); + foreach (var removedControl in _controlSet.Except(newControlSet)) { + EtoPlatform.Current.RemoveFromContainer(_layout, removedControl); + } + _controlSet = newControlSet; + } + + private void PopulateControlSet(HashSet controlSet, LayoutElement element) + { + if (element is LayoutContainer container) + { + foreach (var child in container.Children) + { + PopulateControlSet(controlSet, child); + } + } + if (element is LayoutControl { Control: { } } control) + { + controlSet.Add(control.Control); + } + } + + internal LayoutContext GetLayoutContext() + { + return new LayoutContext + { + Container = _layout, DefaultSpacing = DefaultSpacing, DefaultLabelSpacing = DefaultLabelSpacing, - IsFirstLayout = _firstLayout, - Invalidate = Invalidate + Invalidate = Invalidate, + Scale = EtoPlatform.Current.GetLayoutScaleFactor(_window!) }; } public event EventHandler? Invalidated; + + public Size GetSizeFor(LayoutElement element) + { + var context = GetLayoutContext(); + element.Materialize(context); + return Size.Truncate( + element.GetPreferredSize( + context, + new RectangleF(0, 0, MAX_SIZE, MAX_SIZE))); + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutElement.cs b/NAPS2.Lib/EtoForms/Layout/LayoutElement.cs index 72908cbf28..e12e2745af 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutElement.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutElement.cs @@ -6,11 +6,12 @@ namespace NAPS2.EtoForms.Layout; public abstract class LayoutElement { internal const bool DEBUG_LAYOUT = false; + internal const bool DEBUG_SIZE = false; - protected static LayoutElement[] ExpandChildren(LayoutElement[] children) + protected static List ExpandChildren(IEnumerable children) { return children.SelectMany(x => x is ExpandLayoutElement expand ? expand.Children : new[] { x }) - .Where(c => c is not SkipLayoutElement).ToArray(); + .Where(c => c is not SkipLayoutElement).ToList(); } protected internal bool Scale { get; set; } @@ -18,11 +19,46 @@ protected static LayoutElement[] ExpandChildren(LayoutElement[] children) protected internal LayoutVisibility? Visibility { get; set; } protected internal bool IsVisible => Visibility?.IsVisible ?? true; protected internal int? SpacingAfter { get; set; } + protected internal int? Width { get; set; } + protected internal int? Height { get; set; } public static implicit operator LayoutElement(Control control) => - new ControlWithLayoutAttributes(control); + new LayoutControl(control); + /// + /// Adds the contents of the layout element to the layout's container and window. This should be called before any + /// call to GetPreferredSize or DoLayout. + /// + /// The layout context. + public abstract void Materialize(LayoutContext context); + + /// + /// Performs the actual layout operation, updating the size and position of the layout element and its chidlren. + /// + /// The layout context. + /// The size and location of the layout element. public abstract void DoLayout(LayoutContext context, RectangleF bounds); - public abstract SizeF GetPreferredSize(LayoutContext context, RectangleF parentBounds); + /// + /// Gets the preferred size of the layout element. This is generally the minimum size required to fit the element's + /// contents. + /// + /// The layout context. + /// The bounds constraining the size of the layout element. + /// + public SizeF GetPreferredSize(LayoutContext context, RectangleF parentBounds) + { + if (context.UseCache && context.PreferredSizeCache.TryGetValue(this, out var size)) + { + return size; + } + size = GetPreferredSizeCore(context, parentBounds); + if (context.UseCache) + { + context.PreferredSizeCache[this] = size; + } + return size; + } + + protected abstract SizeF GetPreferredSizeCore(LayoutContext context, RectangleF parentBounds); } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutLeftPanel.cs b/NAPS2.Lib/EtoForms/Layout/LayoutLeftPanel.cs new file mode 100644 index 0000000000..e0adce59d4 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Layout/LayoutLeftPanel.cs @@ -0,0 +1,104 @@ +using Eto.Drawing; +using Eto.Forms; + +namespace NAPS2.EtoForms.Layout; + +public class LayoutLeftPanel : LayoutContainer +{ + private readonly LayoutElement _left; + private readonly LayoutElement _right; + private readonly LayoutOverlay _overlay; + + private Func _widthGetter = () => 0; + private Action _widthSetter = _ => { }; + private int? _minWidth; + private bool _isInitialized; + private bool _inLayout; + private float _lastScale; + + public LayoutLeftPanel(LayoutElement left, LayoutElement right) : base([left, right]) + { + _left = left; + _right = right; + Splitter = new Splitter + { + Orientation = Orientation.Horizontal, + Panel1 = new Panel(), + Panel2 = new Panel(), + FixedPanel = SplitterFixedPanel.Panel1 + }; + _overlay = L.Overlay(Splitter, L.Row(left, right).Spacing(EtoPlatform.Current.IsWinForms ? 3 : 2)); + } + + public Splitter Splitter { get; } + + public override void Materialize(LayoutContext context) => _overlay.Materialize(context); + + public override void DoLayout(LayoutContext context, RectangleF bounds) + { + var w = _minWidth.HasValue ? (int) (_minWidth * context.Scale) : MeasureWidth(context, bounds, _left); + if (Splitter.Position < w) + { + EtoPlatform.Current.SetSplitterPosition(Splitter, w); + _left.Width = w; + } + Splitter.Panel1MinimumSize = w; + Splitter.Panel2MinimumSize = (int) (100 * context.Scale); + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (!_isInitialized || context.Scale != _lastScale) + { + _lastScale = context.Scale; + int initialWidth = Math.Max((int) (_widthGetter() * context.Scale), w); + _inLayout = true; + EtoPlatform.Current.SetSplitterPosition(Splitter, initialWidth); + _inLayout = false; + _left.Width = initialWidth; + if (!_isInitialized) + { + Splitter.PositionChanged += (_, _) => + { + if (!_inLayout && _left.Width != Splitter.Position) + { + _left.Width = Splitter.Position; + _widthSetter((int) (Splitter.Position / context.Scale)); + context.Invalidate(); + } + }; + // TODO: We could hide the splitter based on the side panel's visibility, but currently on WinForms it's + // responsible for drawing content borders so we don't want to do that. The splitter is hidden behind the + // listview in any case. + _isInitialized = true; + } + } + + _overlay.DoLayout(context, bounds); + } + + private int MeasureWidth(LayoutContext context, RectangleF bounds, LayoutElement element) + { + var w = element.Width; + element.Width = null; + var measureContext = context with + { + IsLayout = false, + UseCache = false + }; + int result = (int) element.GetPreferredSize(measureContext, bounds).Width; + element.Width = w; + return result; + } + + protected override SizeF GetPreferredSizeCore(LayoutContext context, RectangleF parentBounds) + { + return _overlay.GetPreferredSize(context, parentBounds); + } + + public LayoutLeftPanel SizeConfig(Func getter, Action setter, int? minWidth = null) + { + _widthGetter = getter; + _widthSetter = setter; + _minWidth = minWidth; + return this; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutLine.cs b/NAPS2.Lib/EtoForms/Layout/LayoutLine.cs index 529fec1b0b..63b87d02e5 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutLine.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutLine.cs @@ -7,11 +7,9 @@ namespace NAPS2.EtoForms.Layout; /// /// Abstract base class for LayoutColumn and LayoutRow. We use this class to generalize column and row layout logic. /// -/// The orthogonal type (e.g. LayoutRow if this is LayoutColumn). -public abstract class LayoutLine : LayoutContainer - where TOrthogonal : LayoutContainer +public abstract class LayoutLine : LayoutContainer { - protected LayoutLine(LayoutElement[] children) : base(children) + protected LayoutLine(IEnumerable children) : base(children) { } @@ -19,12 +17,31 @@ protected LayoutLine(LayoutElement[] children) : base(children) protected int? Spacing { get; init; } + protected bool Aligned { get; set; } + + protected abstract bool IsOrthogonalTo(LayoutLine other); + protected abstract PointF UpdatePosition(PointF position, float delta); protected abstract PointF UpdateOrthogonalPosition(PointF position, float delta); protected abstract SizeF UpdateTotalSize(SizeF size, SizeF childSize, int spacing); + protected abstract float GetBreadth(SizeF size); + + protected abstract float GetLength(SizeF size); + + protected abstract SizeF GetSize(float length, float breadth); + + public override void Materialize(LayoutContext context) + { + var childContext = GetChildContext(context, RectangleF.Empty, false); + foreach (var child in Children) + { + child.Materialize(childContext); + } + } + public override void DoLayout(LayoutContext context, RectangleF bounds) { if (DEBUG_LAYOUT) @@ -34,10 +51,10 @@ public override void DoLayout(LayoutContext context, RectangleF bounds) if (Padding is { } padding) { bounds = new RectangleF( - bounds.X + padding.Left, bounds.Y + padding.Top, - bounds.Width - padding.Horizontal, bounds.Height - padding.Vertical); + bounds.X + padding.Left * context.Scale, bounds.Y + padding.Top * context.Scale, + bounds.Width - padding.Horizontal * context.Scale, bounds.Height - padding.Vertical * context.Scale); } - var childContext = GetChildContext(context, bounds); + var childContext = GetChildContext(context, bounds, true); GetInitialCellLengthsAndScaling(context, childContext, bounds, out var cellLengths, out var cellScaling); UpdateCellLengthsForAvailableSpace(cellLengths, cellScaling, bounds, context); @@ -46,7 +63,7 @@ public override void DoLayout(LayoutContext context, RectangleF bounds) // the actual space the control fills. The child always fills the cell length-wise, but breadth-wise it depends // on the control alignment. var cellOrigin = bounds.Location; - for (int i = 0; i < Children.Length; i++) + for (int i = 0; i < Children.Count; i++) { var child = Children[i]; var cellSize = GetSize(cellLengths[i], GetBreadth(bounds.Size)); @@ -69,7 +86,8 @@ private int GetSpacing(int i, LayoutContext context) protected virtual int GetSpacingCore(int i, LayoutContext context) { - return Children[i].SpacingAfter ?? Spacing ?? context.DefaultSpacing; + int spacing = Children[i].SpacingAfter ?? Spacing ?? context.DefaultSpacing; + return (int) (context.Scale * spacing); } private void GetChildSizeAndOrigin(LayoutElement child, LayoutContext childContext, @@ -91,30 +109,50 @@ private void GetChildSizeAndOrigin(LayoutElement child, LayoutContext childConte childOrigin = UpdateOrthogonalPosition(cellOrigin, alignmentOffset); } - public override SizeF GetPreferredSize(LayoutContext context, RectangleF parentBounds) + protected override SizeF GetPreferredSizeCore(LayoutContext context, RectangleF parentBounds) { - var childContext = GetChildContext(context, parentBounds); + var childContext = GetChildContext(context, parentBounds, true); + if (childContext.ParentVisibility?.IsVisible == false) + { + return SizeF.Empty; + } var size = SizeF.Empty; GetInitialCellLengthsAndScaling(context, childContext, parentBounds, out var cellLengths, out var cellScaling); UpdateCellLengthsWithPreferredLength(cellLengths, cellScaling); - for (int i = 0; i < Children.Length; i++) + for (int i = 0; i < Children.Count; i++) { var childSize = Children[i].GetPreferredSize(childContext, parentBounds); var childLayoutSize = GetSize(cellLengths[i], GetBreadth(childSize)); size = UpdateTotalSize(size, childLayoutSize, GetSpacing(i, context)); } - size += new SizeF(Padding?.Horizontal ?? 0, Padding?.Vertical ?? 0); + size += new SizeF(Padding?.Horizontal ?? 0, Padding?.Vertical ?? 0) * context.Scale; + if (Width != null) + { + size.Width = Width.Value; + } + if (Height != null) + { + size.Height = Height.Value; + } return size; } - private LayoutContext GetChildContext(LayoutContext context, RectangleF bounds) + private LayoutContext GetChildContext(LayoutContext context, RectangleF bounds, bool includeCellInfo) { - return context with + var childContext = context with { - CellLengths = GetChildCellLengths(context, bounds), - CellScaling = GetChildCellScaling(), - Depth = context.Depth + 1 + Depth = context.Depth + 1, + ParentVisibility = LayoutVisibility.Combine(context.ParentVisibility, Visibility) }; + if (includeCellInfo) + { + childContext = childContext with + { + CellLengths = GetChildCellLengths(context, bounds), + CellScaling = GetChildCellScaling() + }; + } + return childContext; } private void GetInitialCellLengthsAndScaling(LayoutContext context, LayoutContext childContext, RectangleF bounds, @@ -130,8 +168,8 @@ private void GetInitialCellLengthsAndScaling(LayoutContext context, LayoutContex } // If we aren't aligned or we don't have a parent to do that pre-calculation, then we just determine our cell // sizes and scaling directly without any special alignment constraints. - cellLengths = new List(); - cellScaling = new List(); + cellLengths = []; + cellScaling = []; var lengthChildContext = childContext with { IsCellLengthQuery = true }; foreach (var child in Children) { @@ -177,7 +215,7 @@ private void UpdateCellLengthsForAvailableSpace(List cellLengths, List cellLengths, List GetChildCellLengths(LayoutContext context, RectangleF bounds var cellLengths = new List(); foreach (var child in Children) { - if (child is TOrthogonal { Aligned: true } opposite) + if (child is LayoutLine { Aligned: true } opposite && IsOrthogonalTo(opposite)) { - for (int i = 0; i < opposite.Children.Length; i++) + for (int i = 0; i < opposite.Children.Count; i++) { if (cellLengths.Count <= i) cellLengths.Add(0); // TODO: We should probably shrink the bounds if needed @@ -224,9 +262,9 @@ private List GetChildCellScaling() var cellScaling = new List(); foreach (var child in Children) { - if (child is TOrthogonal { Aligned: true } opposite) + if (child is LayoutLine { Aligned: true } opposite && IsOrthogonalTo(opposite)) { - for (int i = 0; i < opposite.Children.Length; i++) + for (int i = 0; i < opposite.Children.Count; i++) { if (cellScaling.Count <= i) cellScaling.Add(false); cellScaling[i] = cellScaling[i] || opposite.Children[i].Scale; diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutOverlay.cs b/NAPS2.Lib/EtoForms/Layout/LayoutOverlay.cs index 4de597eaab..c399d23ec0 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutOverlay.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutOverlay.cs @@ -1,43 +1,89 @@ using Eto.Drawing; +using Eto.Forms; namespace NAPS2.EtoForms.Layout; -public class LayoutOverlay : LayoutElement +public class LayoutOverlay : LayoutContainer { - public LayoutOverlay(LayoutElement[] children) + private bool _createdContainer; + private Control? _container; + + public LayoutOverlay(IEnumerable children) + : base(children) { - Children = ExpandChildren(children); } public LayoutOverlay(LayoutOverlay original, bool? scale = null, int? spacingAfter = null) + : base(original.Children) { - Children = original.Children; Scale = scale ?? original.Scale; SpacingAfter = spacingAfter ?? original.SpacingAfter; + Width = original.Width; + Height = original.Height; } - public LayoutElement[] Children { get; set; } + public override void Materialize(LayoutContext context) + { + if (!_createdContainer) + { + _container = EtoPlatform.Current.MaybeCreateOverlayContainer(); + if (_container != null) + { + EtoPlatform.Current.AddToContainer(context.Container, _container, context.InOverlay); + } + _createdContainer = true; + } + bool inOverlay = false; + foreach (var child in Children) + { + child.Materialize(GetChildContext(context, inOverlay)); + inOverlay = true; + } + } public override void DoLayout(LayoutContext context, RectangleF bounds) { bool inOverlay = false; + if (_container != null) + { + EtoPlatform.Current.SetFrame(context.Container, _container, Point.Round(bounds.Location), + Size.Round(bounds.Size), false); + bounds = new RectangleF(0, 0, bounds.Width, bounds.Height); + } foreach (var child in Children) { - child.DoLayout(context with { InOverlay = inOverlay }, bounds); + child.DoLayout(GetChildContext(context, inOverlay), bounds); inOverlay = true; } } - public override SizeF GetPreferredSize(LayoutContext context, RectangleF parentBounds) + protected override SizeF GetPreferredSizeCore(LayoutContext context, RectangleF parentBounds) { bool inOverlay = false; SizeF size = SizeF.Empty; foreach (var child in Children) { - var childSize = child.GetPreferredSize(context with { InOverlay = inOverlay }, parentBounds); + var childSize = child.GetPreferredSize(GetChildContext(context, inOverlay), parentBounds); size = SizeF.Max(size, childSize); inOverlay = true; } + if (Width != null) + { + size.Width = Width.Value; + } + if (Height != null) + { + size.Height = Height.Value; + } return size; } + + private LayoutContext GetChildContext(LayoutContext context, bool inOverlay) + { + return context with + { + InOverlay = context.InOverlay || inOverlay, + Container = EtoPlatform.Current.GetOverlayContainer(_container, inOverlay) ?? context.Container + }; + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutRow.cs b/NAPS2.Lib/EtoForms/Layout/LayoutRow.cs index a52062708f..b33243917d 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutRow.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutRow.cs @@ -1,16 +1,16 @@ using Eto.Drawing; -using Eto.Forms; namespace NAPS2.EtoForms.Layout; -public class LayoutRow : LayoutLine +public class LayoutRow : LayoutLine { public LayoutRow(LayoutElement[] children) : base(children) { } public LayoutRow(LayoutRow original, Padding? padding = null, int? spacing = null, int? spacingAfter = null, - bool? scale = null, bool? aligned = null, LayoutVisibility? visibility = null) + bool? scale = null, bool? aligned = null, LayoutAlignment? alignment = null, + LayoutVisibility? visibility = null) : base(original.Children) { Padding = padding ?? original.Padding; @@ -18,9 +18,14 @@ public LayoutRow(LayoutRow original, Padding? padding = null, int? spacing = nul SpacingAfter = spacingAfter ?? original.SpacingAfter; Scale = scale ?? original.Scale; Aligned = aligned ?? original.Aligned; + Alignment = alignment ?? original.Alignment; Visibility = visibility ?? original.Visibility; + Width = original.Width; + Height = original.Height; } + protected override bool IsOrthogonalTo(LayoutLine other) => other is LayoutColumn; + protected override PointF UpdatePosition(PointF position, float delta) { position.X += delta; diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutVisibility.cs b/NAPS2.Lib/EtoForms/Layout/LayoutVisibility.cs index 2ad0e47606..cbd4c1654a 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutVisibility.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutVisibility.cs @@ -2,6 +2,18 @@ namespace NAPS2.EtoForms.Layout; public class LayoutVisibility { + public static LayoutVisibility? Combine(LayoutVisibility? first, LayoutVisibility? second) + { + if (first == null && second == null) return null; + if (first == null) return second; + if (second == null) return first; + var vis = new LayoutVisibility(first.IsVisible && second.IsVisible); + void VisHandler(object? sender, EventArgs e) => vis.IsVisible = first.IsVisible && second.IsVisible; + first.IsVisibleChanged += VisHandler; + second.IsVisibleChanged += VisHandler; + return vis; + } + private bool _isVisible; public LayoutVisibility(bool isVisible) diff --git a/NAPS2.Lib/EtoForms/Layout/SkipLayoutElement.cs b/NAPS2.Lib/EtoForms/Layout/SkipLayoutElement.cs index 85abe36e96..6edf41cd0f 100644 --- a/NAPS2.Lib/EtoForms/Layout/SkipLayoutElement.cs +++ b/NAPS2.Lib/EtoForms/Layout/SkipLayoutElement.cs @@ -4,6 +4,7 @@ namespace NAPS2.EtoForms.Layout; public class SkipLayoutElement : LayoutElement { + public override void Materialize(LayoutContext context) => throw new NotSupportedException(); public override void DoLayout(LayoutContext context, RectangleF bounds) => throw new NotSupportedException(); - public override SizeF GetPreferredSize(LayoutContext context, RectangleF parentBounds) => throw new NotSupportedException(); + protected override SizeF GetPreferredSizeCore(LayoutContext context, RectangleF parentBounds) => SizeF.Empty; } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/MenuProvider.cs b/NAPS2.Lib/EtoForms/MenuProvider.cs index 5ff167b76a..7fcc5df756 100644 --- a/NAPS2.Lib/EtoForms/MenuProvider.cs +++ b/NAPS2.Lib/EtoForms/MenuProvider.cs @@ -4,7 +4,7 @@ namespace NAPS2.EtoForms; public class MenuProvider { - private readonly List _items = new(); + private readonly List _items = []; public MenuProvider Dynamic(ListProvider commandListProvider) { @@ -16,11 +16,15 @@ public MenuProvider Dynamic(ListProvider commandListProvider) return this; } - public MenuProvider Append(Command command) + public MenuProvider Append(Command? command) { + if (command == null) + { + return this; + } _items.Add(new CommandItem { - Command = command ?? throw new ArgumentNullException(nameof(command)) + Command = command }); return this; } diff --git a/NAPS2.Lib/EtoForms/MessageBoxErrorOutput.cs b/NAPS2.Lib/EtoForms/MessageBoxErrorOutput.cs index c25ca89375..cc296f106a 100644 --- a/NAPS2.Lib/EtoForms/MessageBoxErrorOutput.cs +++ b/NAPS2.Lib/EtoForms/MessageBoxErrorOutput.cs @@ -1,5 +1,6 @@ using Eto.Forms; using NAPS2.EtoForms.Ui; +using NAPS2.Scan.Exceptions; namespace NAPS2.EtoForms; @@ -14,17 +15,25 @@ public MessageBoxErrorOutput(IFormFactory formFactory) public override void DisplayError(string errorMessage) { - Invoker.Current.SafeInvoke(() => MessageBox.Show(errorMessage, MiscResources.Error, MessageBoxButtons.OK, MessageBoxType.Error)); + Invoker.Current.Invoke(() => + MessageBox.Show(errorMessage, MiscResources.Error, MessageBoxButtons.OK, MessageBoxType.Error)); } public override void DisplayError(string errorMessage, string details) { - Invoker.Current.SafeInvoke(() => ShowErrorWithDetails(errorMessage, details)); + Invoker.Current.Invoke(() => ShowErrorWithDetails(errorMessage, details)); } public override void DisplayError(string errorMessage, Exception exception) { - Invoker.Current.SafeInvoke(() => ShowErrorWithDetails(errorMessage, exception.ToString())); + // If the error is wrapped in ScanDriverUnknownException, we only need to display the inner exception + var displayException = + exception is ScanDriverUnknownException { InnerException: { } inner } + ? inner + : exception; + // Note we don't want to use the ToStringDemystified() helper + // https://github.com/benaadams/Ben.Demystifier/issues/85 + Invoker.Current.Invoke(() => ShowErrorWithDetails(errorMessage, displayException.Demystify().ToString())); } private void ShowErrorWithDetails(string errorMessage, string details) diff --git a/NAPS2.Lib/EtoForms/Notifications/CloseButton.cs b/NAPS2.Lib/EtoForms/Notifications/CloseButton.cs new file mode 100644 index 0000000000..1d87060b7a --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/CloseButton.cs @@ -0,0 +1,71 @@ +using Eto.Drawing; +using Eto.Forms; + +namespace NAPS2.EtoForms.Notifications; + +public class CloseButton : Drawable +{ + private const int CLOSE_BUTTON_PADDING = 5; + + private readonly ColorScheme _colorScheme; + + private bool _hover; + private bool _active; + + public CloseButton(ColorScheme colorScheme) + { + _colorScheme = colorScheme; + Cursor = Cursors.Pointer; + Paint += OnPaint; + MouseEnter += (_, _) => + { + _hover = true; + Invalidate(); + }; + MouseLeave += (_, _) => + { + _hover = false; + Invalidate(); + }; + MouseDown += (_, _) => + { + _active = true; + Invalidate(); + }; + MouseUp += (_, _) => + { + var actualHover = new Rectangle(0, 0, Width, Height).Contains(Point.Round(PointFromScreen(Mouse.Position))); + if (_active && _hover && actualHover) + { + Click?.Invoke(this, EventArgs.Empty); + } + _active = false; + Invalidate(); + }; + } + + private Color PenColor => _colorScheme.NotificationBorderColor; + private Color DefaultBackground => _colorScheme.NotificationBackgroundColor; + private Color HoverBackground => Color.Blend( + _colorScheme.NotificationBackgroundColor, + _colorScheme.NotificationBorderColor, + 0.3f); + private Color ActiveBackground => Color.Blend( + _colorScheme.NotificationBackgroundColor, + _colorScheme.NotificationBorderColor, + 0.6f); + + private void OnPaint(object? sender, PaintEventArgs e) + { + var bgColor = _active && _hover ? ActiveBackground : _hover ? HoverBackground : DefaultBackground; + var w = Width; + var h = Height; + e.Graphics.FillRectangle(bgColor, 0, 0, w, h); + var p = CLOSE_BUTTON_PADDING; + var pen = new Pen(PenColor, 3); + e.Graphics.DrawLine(pen, p - 1, p - 1, w - p, h - p); + e.Graphics.DrawLine(pen, w - p, p - 1, p - 1, h - p); + } + + public event EventHandler? Click; +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/DonateNotification.cs b/NAPS2.Lib/EtoForms/Notifications/DonateNotification.cs new file mode 100644 index 0000000000..95e7bbcdb1 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/DonateNotification.cs @@ -0,0 +1,9 @@ +namespace NAPS2.EtoForms.Notifications; + +public class DonateNotification : NotificationModel +{ + public override NotificationView CreateView() + { + return new DonateNotificationView(this); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/DonateNotificationView.cs b/NAPS2.Lib/EtoForms/Notifications/DonateNotificationView.cs new file mode 100644 index 0000000000..2b56eaff32 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/DonateNotificationView.cs @@ -0,0 +1,18 @@ +namespace NAPS2.EtoForms.Notifications; + +public class DonateNotificationView : LinkNotificationView +{ + private const string DONATE_URL = "https://www.naps2.com/donate?src=notif"; + + public DonateNotificationView(DonateNotification model) + : base(model, MiscResources.DonatePrompt, MiscResources.Donate, DONATE_URL, null) + { + HideTimeout = HIDE_LONG; + } + + protected override void LinkClick() + { + base.LinkClick(); + Manager!.Hide(Model); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/INotify.cs b/NAPS2.Lib/EtoForms/Notifications/INotify.cs new file mode 100644 index 0000000000..58c9ed7e03 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/INotify.cs @@ -0,0 +1,11 @@ +using NAPS2.Update; + +namespace NAPS2.EtoForms.Notifications; + +public interface INotify : ISaveNotify +{ + void DonatePrompt(); + void ReviewPrompt(); + void OperationProgress(OperationProgress progress, IOperation op); + void UpdateAvailable(IUpdateChecker updateChecker, UpdateInfo update); +} \ No newline at end of file diff --git a/NAPS2.Lib/Util/ISaveNotify.cs b/NAPS2.Lib/EtoForms/Notifications/ISaveNotify.cs similarity index 88% rename from NAPS2.Lib/Util/ISaveNotify.cs rename to NAPS2.Lib/EtoForms/Notifications/ISaveNotify.cs index 67342dfe34..2cfb8fb1ba 100644 --- a/NAPS2.Lib/Util/ISaveNotify.cs +++ b/NAPS2.Lib/EtoForms/Notifications/ISaveNotify.cs @@ -1,9 +1,7 @@ -namespace NAPS2.Util; +namespace NAPS2.EtoForms.Notifications; /// /// A base interface for objects that can display information about saved files to the user. -/// -/// Implementors: NotificationManager /// public interface ISaveNotify { diff --git a/NAPS2.Lib/EtoForms/Notifications/LinkNotificationView.cs b/NAPS2.Lib/EtoForms/Notifications/LinkNotificationView.cs new file mode 100644 index 0000000000..cde57acac2 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/LinkNotificationView.cs @@ -0,0 +1,67 @@ +using Eto.Drawing; +using Eto.Forms; +using NAPS2.EtoForms.Layout; + +namespace NAPS2.EtoForms.Notifications; + +public class LinkNotificationView : NotificationView +{ + private readonly string? _linkTarget; + private readonly string? _folderTarget; + + private readonly Label _label = new(); + private readonly ContextMenu _contextMenu = new(); + private readonly LinkButton _link; + + protected LinkNotificationView( + NotificationModel model, string title, string linkLabel, string? linkTarget, string? folderTarget) + : base(model) + { + _label.Text = title; + _label.Font = new Font(_label.Font.Family, _label.Font.Size, FontStyle.Bold); + _link = C.Link(linkLabel); + _linkTarget = linkTarget; + _folderTarget = folderTarget; + + if (_folderTarget != null) + { + _contextMenu.Items.Add(new ActionCommand(OpenFolder) { Text = UiStrings.OpenFolder }); + } + + _link.Click += (_, _) => LinkClick(); + _link.MouseUp += (_, args) => + { + if (args.Buttons == MouseButtons.Alternate) + { + LinkRightClick(); + } + }; + } + + protected override void BeforeCreateContent() + { + _label.BackgroundColor = _link.BackgroundColor = BackgroundColor; + } + + protected override LayoutElement PrimaryContent => _label.DynamicWrap(180).MaxWidth(180).Scale(); + + protected override LayoutElement SecondaryContent => _link; + + protected virtual void LinkClick() + { + ProcessHelper.OpenFile(_linkTarget!); + } + + protected virtual void LinkRightClick() + { + if (_folderTarget != null) + { + _contextMenu.Show(_link); + } + } + + protected virtual void OpenFolder() + { + ProcessHelper.OpenFolder(_folderTarget!); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/NotificationArea.cs b/NAPS2.Lib/EtoForms/Notifications/NotificationArea.cs new file mode 100644 index 0000000000..0e6d244dbf --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/NotificationArea.cs @@ -0,0 +1,49 @@ +using NAPS2.EtoForms.Layout; + +namespace NAPS2.EtoForms.Notifications; + +public class NotificationArea : IDisposable +{ + private readonly NotificationManager _manager; + private readonly LayoutController _layoutController; + private readonly Dictionary _items = new(); + + public NotificationArea(NotificationManager manager, LayoutController layoutController) + { + _manager = manager; + _layoutController = layoutController; + _manager.Updated += NotificationsUpdated; + UpdateViews(); + } + + public LayoutColumn Content { get; } = L.Column(C.Filler()).Spacing(20).Padding(15); + + private void NotificationsUpdated(object? sender, EventArgs e) + { + UpdateViews(); + _layoutController.Invalidate(); + } + + private void UpdateViews() + { + foreach (var added in _manager.Notifications.Except(_items.Keys).ToList()) + { + var view = added.CreateView(); + view.Manager = _manager; + var content = view.CreateContent(); + Content.Children.Add(content); + _items.Add(added, (view, content)); + } + foreach (var removed in _items.Keys.Except(_manager.Notifications).ToList()) + { + var (view, content) = _items[removed]; + Content.Children.Remove(content); + view.Dispose(); + } + } + + public void Dispose() + { + _manager.Updated -= NotificationsUpdated; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/NotificationManager.cs b/NAPS2.Lib/EtoForms/Notifications/NotificationManager.cs new file mode 100644 index 0000000000..cfeed309e6 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/NotificationManager.cs @@ -0,0 +1,50 @@ +namespace NAPS2.EtoForms.Notifications; + +public class NotificationManager +{ + public NotificationManager(ColorScheme colorScheme, Naps2Config config) + { + ColorScheme = colorScheme; + Config = config; + } + + public List Notifications { get; } = []; + + public ColorScheme ColorScheme { get; } + + public Naps2Config Config { get; } + + public event EventHandler? Updated; + + public event EventHandler? TimersStarting; + + public void Show(NotificationModel notification) + { + Invoker.Current.Invoke(() => + { + Notifications.Add(notification); + Updated?.Invoke(this, EventArgs.Empty); + }); + } + + public void Hide(NotificationModel notification) + { + Invoker.Current.Invoke(() => + { + if (Notifications.Remove(notification)) + { + Updated?.Invoke(this, EventArgs.Empty); + } + }); + } + + public void StartTimers() + { + Invoker.Current.Invoke(() => TimersStarting?.Invoke(this, EventArgs.Empty)); + } + + public void InvokeUpdated() + { + Invoker.Current.Invoke(() => Updated?.Invoke(this, EventArgs.Empty)); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/NotificationModel.cs b/NAPS2.Lib/EtoForms/Notifications/NotificationModel.cs new file mode 100644 index 0000000000..624257d65c --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/NotificationModel.cs @@ -0,0 +1,6 @@ +namespace NAPS2.EtoForms.Notifications; + +public abstract class NotificationModel +{ + public abstract NotificationView CreateView(); +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/NotificationView.cs b/NAPS2.Lib/EtoForms/Notifications/NotificationView.cs new file mode 100644 index 0000000000..1f9ffe959a --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/NotificationView.cs @@ -0,0 +1,147 @@ +using System.Threading; +using Eto.Drawing; +using Eto.Forms; +using NAPS2.EtoForms.Layout; + +namespace NAPS2.EtoForms.Notifications; + +public abstract class NotificationView : IDisposable +{ + private const int BORDER_RADIUS = 7; + private const int CLOSE_BUTTON_SIZE = 18; + + protected const int HIDE_LONG = 60 * 1000; + protected const int HIDE_SHORT = 5 * 1000; + + protected NotificationView(NotificationModel model) + { + Model = model; + } + + public NotificationModel Model { get; } + + protected int HideTimeout { get; set; } + + protected bool ShowClose { get; set; } = true; + + public NotificationManager? Manager { get; set; } + + protected abstract LayoutElement PrimaryContent { get; } + + protected abstract LayoutElement SecondaryContent { get; } + + protected Color BackgroundColor => Manager!.ColorScheme.NotificationBackgroundColor; + + protected Color BorderColor => Manager!.ColorScheme.NotificationBorderColor; + + public LayoutElement CreateContent() + { + BeforeCreateContent(); + var drawable = new Drawable(); + drawable.Paint += DrawableOnPaint; + var closeButton = new CloseButton(Manager!.ColorScheme); + closeButton.Click += (_, _) => Manager!.Hide(Model); + drawable.MouseUp += (_, _) => NotificationClicked(); + drawable.Load += (_, _) => SetUpHideTimeout(drawable); + return L.Row( + C.Filler(), + L.Overlay( + drawable.MinWidth(120), + L.Column( + L.Row( + PrimaryContent, + ShowClose ? C.Spacer().Width(CLOSE_BUTTON_SIZE) : C.None()), + SecondaryContent + ).Padding(10, 8, 10, 8), + ShowClose + ? L.Column( + L.Row( + C.Filler(), + closeButton.Size(CLOSE_BUTTON_SIZE, CLOSE_BUTTON_SIZE) + ), + C.Filler() + ).Padding(5, 5, 5, 5) + : C.None() + ) + ); + } + + private void SetUpHideTimeout(Control control) + { + Timer CreateTimer() + { + return new Timer(_ => Manager!.Hide(Model), null, HideTimeout, -1); + } + + if (HideTimeout > 0) + { + // Don't start the timer until the user is interacting with the window + void StartTimer(object? sender, EventArgs e) + { + Manager!.TimersStarting -= StartTimer; + control.ParentWindow.MouseMove -= StartTimer; + var timer = CreateTimer(); + control.MouseEnter += (_, _) => timer.Dispose(); + control.MouseLeave += (_, _) => timer = CreateTimer(); + } + + Manager!.TimersStarting += StartTimer; + } + } + + protected virtual void BeforeCreateContent() + { + } + + protected virtual void NotificationClicked() + { + } + + private void DrawableOnPaint(object? sender, PaintEventArgs e) + { + var drawable = (Drawable) sender!; + var w = drawable.Width; + var h = drawable.Height; + e.Graphics.FillRectangle(BackgroundColor, 0, 0, w, h); + e.Graphics.DrawRectangle(BorderColor, 0, 0, w - 1, h - 1); + } + + private void DrawWithRoundedCorners(Drawable drawable, PaintEventArgs e) + { + // TODO: We're not using this as the few pixels on the edges aren't transparent, which is a problem if there's + // an image underneath. Not sure if there's a way to make that work but I don't care enough about rounded + // corners at the moment. + var w = drawable.Width; + var h = drawable.Height; + var r = BORDER_RADIUS; + var d = r * 2; + var q = r / 2; + e.Graphics.FillRectangle(Manager!.ColorScheme.BackgroundColor, 0, 0, w, h); + // Corners + e.Graphics.FillEllipse(BackgroundColor, -1, -1, d, d); + e.Graphics.FillEllipse(BackgroundColor, w - d, -1, d, d); + e.Graphics.FillEllipse(BackgroundColor, -1, h - d, d, d); + e.Graphics.FillEllipse(BackgroundColor, w - d, h - d, d, d); + // Corner borders + e.Graphics.DrawEllipse(BorderColor, -1, -1, d, d); + e.Graphics.DrawEllipse(BorderColor, w - d, -1, d, d); + e.Graphics.DrawEllipse(BorderColor, -1, h - d, d, d); + e.Graphics.DrawEllipse(BorderColor, w - d, h - d, d, d); + // Middle + e.Graphics.FillRectangle(BackgroundColor, r, r, w - d, h - d); + // Sides + e.Graphics.FillRectangle(BackgroundColor, 0, r, r, h - d); + e.Graphics.FillRectangle(BackgroundColor, r, 0, w - d, r); + e.Graphics.FillRectangle(BackgroundColor, w - r, r, r, h - d); + e.Graphics.FillRectangle(BackgroundColor, r, h - r, w - d, r); + // Side borders + e.Graphics.DrawLine(BorderColor, 0, q, 0, h - q); + e.Graphics.DrawLine(BorderColor, q, 0, w - q, 0); + e.Graphics.DrawLine(BorderColor, w - 1, q, w - 1, h - q); + e.Graphics.DrawLine(BorderColor, q, h - 1, w - q, h - 1); + } + + public virtual void Dispose() + { + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/Notify.cs b/NAPS2.Lib/EtoForms/Notifications/Notify.cs new file mode 100644 index 0000000000..f9e67db265 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/Notify.cs @@ -0,0 +1,46 @@ +using NAPS2.Update; + +namespace NAPS2.EtoForms.Notifications; + +public class Notify : INotify +{ + private readonly NotificationManager _notificationManager; + + public Notify(NotificationManager notificationManager) + { + _notificationManager = notificationManager; + } + + public void PdfSaved(string path) + { + _notificationManager.Show(new SaveNotification(MiscResources.PdfSaved, path)); + } + + public void ImagesSaved(int imageCount, string path) + { + var title = imageCount == 1 + ? MiscResources.ImageSaved + : string.Format(MiscResources.ImagesSaved, imageCount); + _notificationManager.Show(new SaveNotification(title, path)); + } + + public void DonatePrompt() + { + _notificationManager.Show(new DonateNotification()); + } + + public void ReviewPrompt() + { + _notificationManager.Show(new ReviewNotification()); + } + + public void OperationProgress(OperationProgress progress, IOperation op) + { + _notificationManager.Show(new ProgressNotification(progress, op)); + } + + public void UpdateAvailable(IUpdateChecker updateChecker, UpdateInfo update) + { + _notificationManager.Show(new UpdateNotification(updateChecker, update)); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/ProgressNotification.cs b/NAPS2.Lib/EtoForms/Notifications/ProgressNotification.cs new file mode 100644 index 0000000000..6c90fcfaf1 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/ProgressNotification.cs @@ -0,0 +1,18 @@ +namespace NAPS2.EtoForms.Notifications; + +public class ProgressNotification : NotificationModel +{ + public ProgressNotification(OperationProgress progress, IOperation op) + { + Progress = progress; + Op = op; + } + + public OperationProgress Progress { get; } + public IOperation Op { get; } + + public override NotificationView CreateView() + { + return new ProgressNotificationView(this); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/ProgressNotificationView.cs b/NAPS2.Lib/EtoForms/Notifications/ProgressNotificationView.cs new file mode 100644 index 0000000000..0d8030f446 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/ProgressNotificationView.cs @@ -0,0 +1,84 @@ +using Eto.Forms; +using NAPS2.EtoForms.Layout; + +namespace NAPS2.EtoForms.Notifications; + +public class ProgressNotificationView : NotificationView +{ + private readonly OperationProgress _operationProgress; + private readonly IOperation _op; + + private readonly Label _textLabel = new(); + private readonly Label _numberLabel = new(); + private readonly ProgressBar _progressBar = new(); + private readonly LayoutVisibility _numberVis = new(true); + + public ProgressNotificationView(ProgressNotification model) + : base(model) + { + _operationProgress = model.Progress; + _op = model.Op; + + ShowClose = false; + _op.StatusChanged += OnStatusChanged; + _op.Finished += OnFinished; + if (_op.IsFinished) + { + Manager?.Hide(Model); + } + _textLabel.MouseUp += (_, _) => NotificationClicked(); + _numberLabel.MouseUp += (_, _) => NotificationClicked(); + _progressBar.MouseUp += (_, _) => NotificationClicked(); + UpdateStatus(); + } + + private void OnStatusChanged(object? sender, EventArgs eventArgs) + { + Invoker.Current.Invoke(UpdateStatus); + } + + private void OnFinished(object? sender, EventArgs e) + { + Manager?.Hide(Model); + } + + private void UpdateStatus() + { + var text1 = (_textLabel.Text, _numberLabel.Text); + EtoOperationProgress.RenderStatus(_op, _textLabel, _numberLabel, _progressBar); + var text2 = (_textLabel.Text, _numberLabel.Text); + if (text1 != text2) + { + // The text width may have changed, so the notification size could change + Manager?.InvokeUpdated(); + } + // Don't display the number if the progress bar is precise + // Otherwise, the widget will be too cluttered + // The number is only shown for OcrOperation at the moment + _numberVis.IsVisible = _op.Status?.IndeterminateProgress == true; + } + + protected override void NotificationClicked() + { + var bgOps = Manager!.Config.Get(c => c.BackgroundOperations); + bgOps = bgOps.Remove(_op.GetType().Name); + Manager.Config.User.Set(c => c.BackgroundOperations, bgOps); + + Manager.Hide(Model); + _operationProgress.ShowModalProgress(_op); + } + + public override void Dispose() + { + base.Dispose(); + _op.StatusChanged -= OnStatusChanged; + _op.Finished -= OnFinished; + } + + protected override LayoutElement PrimaryContent => L.Row( + _textLabel.DynamicWrap(180).MaxWidth(180), + C.Filler().Visible(_numberVis), + _numberLabel.Visible(_numberVis)); + + protected override LayoutElement SecondaryContent => _progressBar.MaxHeight(10); +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/ReviewNotification.cs b/NAPS2.Lib/EtoForms/Notifications/ReviewNotification.cs new file mode 100644 index 0000000000..0d86618ac5 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/ReviewNotification.cs @@ -0,0 +1,9 @@ +namespace NAPS2.EtoForms.Notifications; + +public class ReviewNotification : NotificationModel +{ + public override NotificationView CreateView() + { + return new ReviewNotificationView(this); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/ReviewNotificationView.cs b/NAPS2.Lib/EtoForms/Notifications/ReviewNotificationView.cs new file mode 100644 index 0000000000..2caa2c1835 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/ReviewNotificationView.cs @@ -0,0 +1,18 @@ +namespace NAPS2.EtoForms.Notifications; + +public class ReviewNotificationView : LinkNotificationView +{ + private const string REVIEW_URL = "ms-windows-store://review/?ProductId=9N3QQ9W0B23Q"; + + public ReviewNotificationView(ReviewNotification model) + : base(model, MiscResources.ReviewPrompt, MiscResources.LeaveAReview, REVIEW_URL, null) + { + HideTimeout = HIDE_LONG; + } + + protected override void LinkClick() + { + base.LinkClick(); + Manager!.Hide(Model); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/SaveNotification.cs b/NAPS2.Lib/EtoForms/Notifications/SaveNotification.cs new file mode 100644 index 0000000000..8d78a78f47 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/SaveNotification.cs @@ -0,0 +1,18 @@ +namespace NAPS2.EtoForms.Notifications; + +public class SaveNotification : NotificationModel +{ + public SaveNotification(string title, string path) + { + Title = title; + Path = path; + } + + public string Title { get; } + public string Path { get; } + + public override NotificationView CreateView() + { + return new SaveNotificationView(this); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/SaveNotificationView.cs b/NAPS2.Lib/EtoForms/Notifications/SaveNotificationView.cs new file mode 100644 index 0000000000..fbe6772a10 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/SaveNotificationView.cs @@ -0,0 +1,10 @@ +namespace NAPS2.EtoForms.Notifications; + +public class SaveNotificationView : LinkNotificationView +{ + public SaveNotificationView(SaveNotification model) + : base(model, model.Title, Path.GetFileName(model.Path), model.Path, Path.GetDirectoryName(model.Path)) + { + HideTimeout = HIDE_SHORT; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Util/SaveNotifyStub.cs b/NAPS2.Lib/EtoForms/Notifications/SaveNotifyStub.cs similarity index 80% rename from NAPS2.Lib/Util/SaveNotifyStub.cs rename to NAPS2.Lib/EtoForms/Notifications/SaveNotifyStub.cs index f14054a1ed..8a12f0af78 100644 --- a/NAPS2.Lib/Util/SaveNotifyStub.cs +++ b/NAPS2.Lib/EtoForms/Notifications/SaveNotifyStub.cs @@ -1,4 +1,4 @@ -namespace NAPS2.Util; +namespace NAPS2.EtoForms.Notifications; public class SaveNotifyStub : ISaveNotify { diff --git a/NAPS2.Lib/EtoForms/Notifications/StubNotify.cs b/NAPS2.Lib/EtoForms/Notifications/StubNotify.cs new file mode 100644 index 0000000000..9b49ae697c --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/StubNotify.cs @@ -0,0 +1,30 @@ +using NAPS2.Update; + +namespace NAPS2.EtoForms.Notifications; + +public class StubNotify : INotify +{ + public void PdfSaved(string path) + { + } + + public void ImagesSaved(int imageCount, string path) + { + } + + public void DonatePrompt() + { + } + + public void ReviewPrompt() + { + } + + public void OperationProgress(OperationProgress progress, IOperation op) + { + } + + public void UpdateAvailable(IUpdateChecker updateChecker, UpdateInfo update) + { + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/UpdateNotification.cs b/NAPS2.Lib/EtoForms/Notifications/UpdateNotification.cs new file mode 100644 index 0000000000..d7c7097420 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/UpdateNotification.cs @@ -0,0 +1,20 @@ +using NAPS2.Update; + +namespace NAPS2.EtoForms.Notifications; + +public class UpdateNotification : NotificationModel +{ + public UpdateNotification(IUpdateChecker updateChecker, UpdateInfo update) + { + UpdateChecker = updateChecker; + Update = update; + } + + public IUpdateChecker UpdateChecker { get; } + public UpdateInfo Update { get; } + + public override NotificationView CreateView() + { + return new UpdateNotificationView(this); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Notifications/UpdateNotificationView.cs b/NAPS2.Lib/EtoForms/Notifications/UpdateNotificationView.cs new file mode 100644 index 0000000000..4e0e54d803 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Notifications/UpdateNotificationView.cs @@ -0,0 +1,27 @@ +using NAPS2.Update; + +namespace NAPS2.EtoForms.Notifications; + +public class UpdateNotificationView : LinkNotificationView +{ + private readonly IUpdateChecker _updateChecker; + private readonly UpdateInfo _update; + + public UpdateNotificationView(UpdateNotification model) + : base( + model, + MiscResources.UpdateAvailable, + string.Format(MiscResources.Install, model.Update.Name), + null, null) + { + _updateChecker = model.UpdateChecker; + _update = model.Update; + HideTimeout = HIDE_LONG; + } + + protected override void LinkClick() + { + _updateChecker.StartUpdate(_update); + Manager!.Hide(Model); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/SliderWithTextBox.cs b/NAPS2.Lib/EtoForms/SliderWithTextBox.cs deleted file mode 100644 index 0274e3469a..0000000000 --- a/NAPS2.Lib/EtoForms/SliderWithTextBox.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Eto.Drawing; -using Eto.Forms; -using NAPS2.EtoForms.Layout; - -namespace NAPS2.EtoForms; - -public class SliderWithTextBox -{ - private readonly Slider _slider = new() { MinValue = -1000, MaxValue = 1000, TickFrequency = 200 }; - private readonly NumericMaskedTextBox _textBox = new() { Text = 0.ToString("G") }; - - private int _valueCache; - - public SliderWithTextBox() - { - _slider.ValueChanged += (_, _) => { Value = _slider.Value; }; - _textBox.TextChanged += (_, _) => - { - if (int.TryParse(_textBox.Text, out int value)) - { - if (value >= MinValue && value <= MaxValue) - { - Value = value; - } - } - }; - } - - public int MinValue - { - get => _slider.MinValue; - set => _slider.MinValue = value; - } - - public int MaxValue - { - get => _slider.MaxValue; - set => _slider.MaxValue = value; - } - - public int TickFrequency - { - get => _slider.TickFrequency; - set => _slider.TickFrequency = value; - } - - public int Value - { - get => _valueCache; - set - { - if (value == _valueCache) return; - _valueCache = value; - _slider.Value = value; - _textBox.Text = value.ToString("G"); - ValueChanged?.Invoke(); - } - } - - public bool Enabled - { - get => _slider.Enabled; - set - { - _slider.Enabled = value; - _textBox.Enabled = value; - } - } - - public Image? Icon { get; set; } - - public static implicit operator LayoutElement(SliderWithTextBox control) - { - return control.AsControl(); - } - - public event Action? ValueChanged; - - public LayoutRow AsControl() - { - return L.Row( - Icon != null - ? new ImageView { Image = Icon } - .Align(EtoPlatform.Current.IsWinForms ? LayoutAlignment.Leading : LayoutAlignment.Center) - .Padding(top: 2, bottom: 2) - : C.None(), - _slider.Scale(), - _textBox.Width(EtoPlatform.Current.IsGtk ? 50 : 40) - .Align(EtoPlatform.Current.IsWinForms ? LayoutAlignment.Leading : LayoutAlignment.Center) - ); - } -} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/StubDarkModeProvider.cs b/NAPS2.Lib/EtoForms/StubDarkModeProvider.cs new file mode 100644 index 0000000000..28fb758fca --- /dev/null +++ b/NAPS2.Lib/EtoForms/StubDarkModeProvider.cs @@ -0,0 +1,10 @@ +namespace NAPS2.EtoForms; + +public class StubDarkModeProvider : IDarkModeProvider +{ + public bool IsDarkModeEnabled => false; + +#pragma warning disable CS0067 + public event EventHandler? DarkModeChanged; +#pragma warning restore CS0067 +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/AboutForm.cs b/NAPS2.Lib/EtoForms/Ui/AboutForm.cs index 28dd3dc58e..55ca15aa4e 100644 --- a/NAPS2.Lib/EtoForms/Ui/AboutForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/AboutForm.cs @@ -1,6 +1,8 @@ using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; +using NAPS2.Scan; using NAPS2.Update; namespace NAPS2.EtoForms.Ui; @@ -9,38 +11,38 @@ public class AboutForm : EtoDialogBase { private const string NAPS2_HOMEPAGE = "https://www.naps2.com"; private const string ICONS_HOMEPAGE = "https://www.fatcow.com/free-icons"; - private const string DONATE_URL = "https://www.naps2.com/donate"; + private const string DONATE_URL = "https://www.naps2.com/donate?src=about"; + private readonly Button _donateButton; private readonly UpdateChecker _updateChecker; + private readonly CheckBox _enableDebugLogging = C.CheckBox(UiStrings.EnableDebugLogging); - private readonly Control _donateButton; - private readonly CheckBox _checkForUpdates; - private readonly Panel _updatePanel; - - private bool _hasCheckedForUpdates; - private UpdateInfo? _update; - - public AboutForm(Naps2Config config, UpdateChecker updateChecker) + public AboutForm(Naps2Config config, UpdateChecker updateChecker, ScanningContext scanningContext) : base(config) { - _updateChecker = updateChecker; + Title = UiStrings.AboutFormTitle; + IconName = "information_small"; - _donateButton = EtoPlatform.Current.AccessibleImageButton( - Icons.btn_donate_LG.ToEtoImage(), - UiStrings.Donate, - () => Process.Start(new ProcessStartInfo(DONATE_URL) { UseShellExecute = true })); - _checkForUpdates = new CheckBox { Text = UiStrings.CheckForUpdates }; - _checkForUpdates.CheckedChanged += CheckForUpdatesChanged; - _updatePanel = new Panel(); + _donateButton = C.Button(UiStrings.Donate, () => ProcessHelper.OpenUrl(DONATE_URL)); + _donateButton.BackgroundColor = Color.FromRgb(0xfeda96); + _donateButton.TextColor = Color.FromRgb(0x1b464e); + _donateButton.Font = new Font(_donateButton.Font.Family, _donateButton.Font.Size * 11 / 10, + FontStyle.Italic | FontStyle.Bold); + EtoPlatform.Current.ConfigureDonateButton(_donateButton); - UpdateControls(); + _enableDebugLogging.Checked = config.Get(c => c.EnableDebugLogging); + _enableDebugLogging.CheckedChanged += (_, _) => + { + config.User.Set(c => c.EnableDebugLogging, _enableDebugLogging.IsChecked()); + NLogConfig.EnvDebugLogging = _enableDebugLogging.IsChecked(); + scanningContext.WorkerFactory?.RecreateSpareWorkers(); + }; + + _updateChecker = updateChecker; } protected override void BuildLayout() { - Title = UiStrings.AboutFormTitle; - Icon = new Icon(1f, Icons.information_small.ToEtoImage()); - FormStateController.Resizable = false; FormStateController.RestoreFormState = false; @@ -54,16 +56,19 @@ protected override void BuildLayout() C.NoWrap(string.Format(MiscResources.Version, AssemblyHelper.Version)), C.UrlLink(NAPS2_HOMEPAGE) ), - L.Column( - C.Filler(), - _donateButton - ).Padding(left: 10) + Config.Get(c => c.HiddenButtons).HasFlag(ToolbarButtons.Donate) + ? C.None() + : L.Column( + C.Filler(), + _donateButton + ).Padding(left: 10) ), - C.TextSpace(), - _checkForUpdates.Padding(left: 4), - _updatePanel, + GetUpdateWidget(), C.TextSpace(), C.NoWrap(string.Format(UiStrings.CopyrightFormat, AssemblyHelper.COPYRIGHT_YEARS)), + Config.AppLocked.Has(c => c.EnableDebugLogging) + ? C.None() + : new[] { C.Spacer(), _enableDebugLogging.Padding(left: 4) }.Expand(), C.TextSpace(), L.Row( L.Column( @@ -79,65 +84,15 @@ protected override void BuildLayout() ); } - private void DoUpdateCheck() - { - if (_checkForUpdates.IsChecked()) - { - _updateChecker.CheckForUpdates().ContinueWith(task => - { - if (task.IsFaulted) - { - Log.ErrorException("Error checking for updates", task.Exception!); - } - else - { - var transact = Config.User.BeginTransaction(); - transact.Set(c => c.HasCheckedForUpdates, true); - transact.Set(c => c.LastUpdateCheckDate, DateTime.Now); - transact.Commit(); - } - _update = task.Result; - _hasCheckedForUpdates = true; - Invoker.Current.SafeInvoke(UpdateControls); - }); - } - } - - private void UpdateControls() - { - _updatePanel.Content = GetUpdatePanelContent(); - } - - private Control GetUpdatePanelContent() - { - if (!_checkForUpdates.IsChecked()) - { - return C.NoWrap(MiscResources.UpdateCheckDisabled); - } - if (!_hasCheckedForUpdates) - { - return C.NoWrap(MiscResources.CheckingForUpdates); - } - if (_update == null) - { - return C.NoWrap(MiscResources.NoUpdates); - } - return C.Link(string.Format(MiscResources.Install, _update.Name), - InstallLinkClicked); - } - - private void InstallLinkClicked() - { - if (_update != null) - { - _updateChecker.StartUpdate(_update); - } - } - - private void CheckForUpdatesChanged(object? sender, EventArgs e) + private LayoutElement GetUpdateWidget() { - Config.User.Set(c => c.CheckForUpdates, _checkForUpdates.IsChecked()); - UpdateControls(); - DoUpdateCheck(); +#if MSI + return C.None(); +#else +#if NET6_0_OR_GREATER + if (!OperatingSystem.IsWindows()) return C.None(); +#endif + return new UpdateCheckWidget(_updateChecker, Config); +#endif } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/AdvancedProfileForm.cs b/NAPS2.Lib/EtoForms/Ui/AdvancedProfileForm.cs index 24f579ecd4..33f4a3c0b9 100644 --- a/NAPS2.Lib/EtoForms/Ui/AdvancedProfileForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/AdvancedProfileForm.cs @@ -1,6 +1,8 @@ using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; +using NAPS2.Lang; using NAPS2.Scan; namespace NAPS2.EtoForms.Ui; @@ -8,35 +10,50 @@ namespace NAPS2.EtoForms.Ui; public class AdvancedProfileForm : EtoDialogBase { private readonly CheckBox _maximumQuality = new() { Text = UiStrings.MaximumQuality }; - private readonly SliderWithTextBox _quality = new() { MinValue = 0, MaxValue = 100, TickFrequency = 25 }; + private readonly SliderWithTextBox _quality = new(new SliderWithTextBox.IntConstraints(0, 100, 25)); private readonly CheckBox _excludeBlank = new() { Text = UiStrings.ExcludeBlankPages }; - private readonly SliderWithTextBox _whiteThreshold = new() { MinValue = 0, MaxValue = 100, TickFrequency = 10 }; - private readonly SliderWithTextBox _coverageThreshold = new() { MinValue = 0, MaxValue = 100, TickFrequency = 10 }; + private readonly SliderWithTextBox _whiteThreshold = new(new SliderWithTextBox.IntConstraints(0, 100, 10)); + private readonly SliderWithTextBox _coverageThreshold = new(new SliderWithTextBox.IntConstraints(0, 100, 10)); private readonly CheckBox _deskew = new() { Text = UiStrings.DeskewScannedPages }; private readonly CheckBox _brightContAfterScan = new() { Text = UiStrings.BrightnessContrastAfterScan }; private readonly CheckBox _offsetWidth = new() { Text = UiStrings.OffsetWidth }; private readonly CheckBox _stretchToPageSize = new() { Text = UiStrings.StretchToPageSize }; private readonly CheckBox _cropToPageSize = new() { Text = UiStrings.CropToPageSize }; - private readonly CheckBox _flipDuplexed = new() { Text = UiStrings.FlipDuplexedPages }; - private readonly DropDown _wiaVersion = C.EnumDropDown(value => value switch + private readonly CheckBox _flipDuplexed = new() { - WiaApiVersion.Default => SettingsResources.WiaVersion_Default, - WiaApiVersion.Wia10 => SettingsResources.WiaVersion_Wia10, - WiaApiVersion.Wia20 => SettingsResources.WiaVersion_Wia20, - _ => value.ToString() - }); - private readonly DropDown _twainImpl = C.EnumDropDown(); + Text = TranslationMigrator.PickTranslated( + UiStrings.ResourceManager, + "FlipDuplexedPages", + "FlipBackSidesOfDuplexPages") + }; + + private readonly EnumDropDownWidget _wiaVersion = new(); + + private readonly EnumDropDownWidget _twainImpl = new(); + private readonly CheckBox _twainProgress = new() { Text = UiStrings.ShowNativeTwainProgress }; private readonly Button _restoreDefaults = new() { Text = UiStrings.RestoreDefaults }; - public AdvancedProfileForm(Naps2Config config) : base(config) + public AdvancedProfileForm(Naps2Config config, IIconProvider iconProvider) : base(config) { + Title = UiStrings.AdvancedProfileFormTitle; + IconName = "blueprints_small"; + _restoreDefaults.Click += RestoreDefaults_Click; _maximumQuality.CheckedChanged += MaximumQuality_CheckedChanged; _excludeBlank.CheckedChanged += ExcludeBlank_CheckedChanged; + _wiaVersion.Format = value => value switch + { + WiaApiVersion.Default => SettingsResources.WiaVersion_Default, + WiaApiVersion.Wia10 => SettingsResources.WiaVersion_Wia10, + WiaApiVersion.Wia20 => SettingsResources.WiaVersion_Wia20, + _ => value.ToString() + }; if (!Environment.Is64BitProcess) { - _twainImpl.Items.RemoveAt((int) TwainImpl.X64); + _twainImpl.Items = EnumDropDownWidget.DefaultItems.Where(x => x != TwainImpl.X64); } + // Remove obsolete options + _twainImpl.Items = _twainImpl.Items.Except([TwainImpl.Legacy]); } protected override void BuildLayout() @@ -44,9 +61,6 @@ protected override void BuildLayout() UpdateValues(ScanProfile!); UpdateEnabled(); - Title = UiStrings.AdvancedProfileFormTitle; - Icon = new Icon(1f, Icons.blueprints_small.ToEtoImage()); - FormStateController.DefaultExtraLayoutSize = new Size(60, 0); FormStateController.FixedHeightLayout = true; @@ -75,15 +89,18 @@ protected override void BuildLayout() L.GroupBox( UiStrings.Compatibility, L.Column( - _brightContAfterScan, - _offsetWidth, + PlatformCompat.System.IsWiaDriverSupported || PlatformCompat.System.IsTwainDriverSupported + ? _brightContAfterScan + : C.None(), + PlatformCompat.System.IsWiaDriverSupported ? _offsetWidth : C.None(), _stretchToPageSize, _cropToPageSize, _flipDuplexed, - C.Label(UiStrings.WiaVersionLabel), - _wiaVersion, - C.Label(UiStrings.TwainImplLabel), - _twainImpl + PlatformCompat.System.IsWiaDriverSupported ? C.Label(UiStrings.WiaVersionLabel) : C.None(), + PlatformCompat.System.IsWiaDriverSupported ? _wiaVersion : C.None(), + PlatformCompat.System.IsTwainDriverSupported ? C.Label(UiStrings.TwainImplLabel) : C.None(), + PlatformCompat.System.IsTwainDriverSupported ? _twainImpl : C.None(), + PlatformCompat.System.IsTwainDriverSupported ? _twainProgress : C.None() ) ), L.Row( @@ -99,27 +116,25 @@ protected override void BuildLayout() private void UpdateValues(ScanProfile scanProfile) { _maximumQuality.Checked = scanProfile.MaxQuality; - _quality.Value = scanProfile.Quality; + _quality.IntValue = scanProfile.Quality; _brightContAfterScan.Checked = scanProfile.BrightnessContrastAfterScan; _deskew.Checked = scanProfile.AutoDeskew; _offsetWidth.Checked = scanProfile.WiaOffsetWidth; - _wiaVersion.SelectedIndex = (int)scanProfile.WiaVersion; + _wiaVersion.SelectedItem = scanProfile.WiaVersion; _stretchToPageSize.Checked = scanProfile.ForcePageSize; _cropToPageSize.Checked = scanProfile.ForcePageSizeCrop; _flipDuplexed.Checked = scanProfile.FlipDuplexedPages; - _twainImpl.SelectedIndex = (int)scanProfile.TwainImpl; + _twainImpl.SelectedItem = scanProfile.TwainImpl == TwainImpl.Legacy ? TwainImpl.OldDsm : scanProfile.TwainImpl; + _twainProgress.Checked = scanProfile.TwainProgress; _excludeBlank.Checked = scanProfile.ExcludeBlankPages; - _whiteThreshold.Value = scanProfile.BlankPageWhiteThreshold; - _coverageThreshold.Value = scanProfile.BlankPageCoverageThreshold; + _whiteThreshold.IntValue = scanProfile.BlankPageWhiteThreshold; + _coverageThreshold.IntValue = scanProfile.BlankPageCoverageThreshold; } private void UpdateEnabled() { - _twainImpl.Enabled = ScanProfile!.DriverName == DriverNames.TWAIN; - _offsetWidth.Enabled = ScanProfile.DriverName == DriverNames.WIA; - _wiaVersion.Enabled = ScanProfile.DriverName == DriverNames.WIA; _quality.Enabled = !_maximumQuality.IsChecked(); - _whiteThreshold.Enabled = _excludeBlank.IsChecked() && ScanProfile.BitDepth != ScanBitDepth.BlackWhite; + _whiteThreshold.Enabled = _excludeBlank.IsChecked() && ScanProfile!.BitDepth != ScanBitDepth.BlackWhite; _coverageThreshold.Enabled = _excludeBlank.IsChecked(); } @@ -127,25 +142,20 @@ private void UpdateEnabled() private void SaveSettings() { - ScanProfile!.Quality = _quality.Value; + ScanProfile!.Quality = _quality.IntValue; ScanProfile.MaxQuality = _maximumQuality.IsChecked(); ScanProfile.BrightnessContrastAfterScan = _brightContAfterScan.IsChecked(); ScanProfile.AutoDeskew = _deskew.IsChecked(); ScanProfile.WiaOffsetWidth = _offsetWidth.IsChecked(); - if (_wiaVersion.SelectedIndex != -1) - { - ScanProfile.WiaVersion = (WiaApiVersion) _wiaVersion.SelectedIndex; - } + ScanProfile.WiaVersion = _wiaVersion.SelectedItem; ScanProfile.ForcePageSize = _stretchToPageSize.IsChecked(); ScanProfile.ForcePageSizeCrop = _cropToPageSize.IsChecked(); ScanProfile.FlipDuplexedPages = _flipDuplexed.IsChecked(); - if (_twainImpl.SelectedIndex != -1) - { - ScanProfile.TwainImpl = (TwainImpl)_twainImpl.SelectedIndex; - } + ScanProfile.TwainImpl = _twainImpl.SelectedItem; + ScanProfile.TwainProgress = _twainProgress.IsChecked(); ScanProfile.ExcludeBlankPages = _excludeBlank.IsChecked(); - ScanProfile.BlankPageWhiteThreshold = _whiteThreshold.Value; - ScanProfile.BlankPageCoverageThreshold = _coverageThreshold.Value; + ScanProfile.BlankPageWhiteThreshold = _whiteThreshold.IntValue; + ScanProfile.BlankPageCoverageThreshold = _coverageThreshold.IntValue; } private void MaximumQuality_CheckedChanged(object? sender, EventArgs e) diff --git a/NAPS2.Lib/EtoForms/Ui/AuthorizeForm.cs b/NAPS2.Lib/EtoForms/Ui/AuthorizeForm.cs index 359b8dcf48..3eeba69b1c 100644 --- a/NAPS2.Lib/EtoForms/Ui/AuthorizeForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/AuthorizeForm.cs @@ -10,16 +10,16 @@ public class AuthorizeForm : EtoDialogBase private readonly ErrorOutput _errorOutput; private CancellationTokenSource? _cancelTokenSource; - public AuthorizeForm(Naps2Config config, ErrorOutput errorOutput) : base(config) + public AuthorizeForm(Naps2Config config, IIconProvider iconProvider, ErrorOutput errorOutput) : base(config) { + Title = UiStrings.AuthorizeFormTitle; + IconName = "key_small"; + _errorOutput = errorOutput; } protected override void BuildLayout() { - Title = UiStrings.AuthorizeFormTitle; - Icon = new Icon(1f, Icons.key_small.ToEtoImage()); - FormStateController.FixedHeightLayout = true; FormStateController.RestoreFormState = false; FormStateController.Resizable = false; @@ -46,7 +46,7 @@ protected override void OnLoad(EventArgs e) try { OauthProvider.AcquireToken(_cancelTokenSource.Token); - Invoker.Current.SafeInvoke(() => + Invoker.Current.Invoke(() => { Result = true; Close(); @@ -57,9 +57,12 @@ protected override void OnLoad(EventArgs e) } catch (Exception ex) { - _errorOutput.DisplayError(MiscResources.AuthError, ex); - Log.ErrorException("Error acquiring Oauth token", ex); - Invoker.Current.SafeInvoke(Close); + if (!_cancelTokenSource.IsCancellationRequested) + { + _errorOutput.DisplayError(MiscResources.AuthError, ex); + Log.ErrorException("Error acquiring Oauth token", ex); + } + Invoker.Current.Invoke(Close); } }); } diff --git a/NAPS2.Lib/EtoForms/Ui/AutoSaveSettingsForm.cs b/NAPS2.Lib/EtoForms/Ui/AutoSaveSettingsForm.cs index a8744a1f1c..75b316d701 100644 --- a/NAPS2.Lib/EtoForms/Ui/AutoSaveSettingsForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/AutoSaveSettingsForm.cs @@ -1,5 +1,6 @@ using Eto.Forms; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; using NAPS2.ImportExport; using NAPS2.Scan; diff --git a/NAPS2.Lib/EtoForms/Ui/BatchPromptForm.cs b/NAPS2.Lib/EtoForms/Ui/BatchPromptForm.cs index b7fdd02276..b070fb05d9 100644 --- a/NAPS2.Lib/EtoForms/Ui/BatchPromptForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/BatchPromptForm.cs @@ -16,7 +16,7 @@ public BatchPromptForm(Naps2Config config) : base(config) }) { Text = UiStrings.Scan, - Image = Icons.control_play_blue_small.ToEtoImage() + IconName = "control_play_blue_small" }; _scanButton = C.Button(scanNextCommand, ButtonImagePosition.Left); DefaultButton = _scanButton; diff --git a/NAPS2.Lib/EtoForms/Ui/BatchScanForm.cs b/NAPS2.Lib/EtoForms/Ui/BatchScanForm.cs index 45a2acbdd2..18b5aba06c 100644 --- a/NAPS2.Lib/EtoForms/Ui/BatchScanForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/BatchScanForm.cs @@ -1,9 +1,10 @@ -using System.ComponentModel; using System.Globalization; using System.Threading; +using Eto.Drawing; using Eto.Forms; using NAPS2.Config.Model; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; using NAPS2.ImportExport; using NAPS2.Scan; using NAPS2.Scan.Batch; @@ -28,7 +29,7 @@ public class BatchScanForm : EtoDialogBase private readonly Label _status = new() { Text = UiStrings.PressStartWhenReady }; private readonly Button _start = new() { Text = UiStrings.Start }; private readonly Button _cancel = new() { Text = UiStrings.Cancel }; - private readonly DropDown _profile = C.DropDown(); + private readonly DropDownWidget _profile = new(); private readonly RadioButton _singleScan; private readonly RadioButton _multipleScansPrompt; private readonly RadioButton _multipleScansDelay; @@ -52,9 +53,12 @@ public class BatchScanForm : EtoDialogBase private CancellationTokenSource _cts = new(); public BatchScanForm(Naps2Config config, DialogHelper dialogHelper, IProfileManager profileManager, - IBatchScanPerformer batchScanPerformer, ErrorOutput errorOutput) + IBatchScanPerformer batchScanPerformer, ErrorOutput errorOutput, IIconProvider iconProvider) : base(config) { + Title = UiStrings.BatchScanFormTitle; + IconName = "application_cascade_small"; + _profileManager = profileManager; _batchScanPerformer = batchScanPerformer; _errorOutput = errorOutput; @@ -68,6 +72,7 @@ public BatchScanForm(Naps2Config config, DialogHelper dialogHelper, IProfileMana _filePerPage = new RadioButton(_filePerScan) { Text = UiStrings.OneFilePerPage }; _separateByPatchT = new RadioButton(_filePerScan) { Text = UiStrings.SeparateByPatchT }; _filePath = new(this, dialogHelper); + _profile.Format = x => x.DisplayName; _start.Click += Start; _cancel.Click += Cancel; @@ -78,13 +83,20 @@ public BatchScanForm(Naps2Config config, DialogHelper dialogHelper, IProfileMana _userTransact = Config.User.BeginTransaction(); _transactionConfig = Config.WithTransaction(_userTransact); - EditProfileCommand = new ActionCommand(EditProfile) { Image = Icons.pencil_small.ToEtoImage() }; - NewProfileCommand = new ActionCommand(NewProfile) { Image = Icons.add_small.ToEtoImage() }; + EditProfileCommand = new ActionCommand(EditProfile) + { + ToolTip = UiStrings.Edit, + IconName = "pencil_small" + }; + NewProfileCommand = new ActionCommand(NewProfile) + { + ToolTip = UiStrings.New, + IconName = "add_small" + }; } private void UpdateVisibility(object? sender, EventArgs e) { - // TODO: Bundle multiple updates together before invalidating somehow _delayVis.IsVisible = _multipleScansDelay.Checked; _multiVis.IsVisible = _saveToMultipleFiles.Checked; _fileVis.IsVisible = _saveToSingleFile.Checked || _saveToMultipleFiles.Checked; @@ -93,9 +105,9 @@ private void UpdateVisibility(object? sender, EventArgs e) public Action ImageCallback { get; set; } = null!; - private Command NewProfileCommand { get; } + private ActionCommand NewProfileCommand { get; } - private Command EditProfileCommand { get; } + private ActionCommand EditProfileCommand { get; } protected override void BuildLayout() { @@ -103,9 +115,8 @@ protected override void BuildLayout() !(Config.Get(c => c.NoUserProfiles) && _profileManager.Profiles.Any(x => x.IsLocked)); UpdateUIFromSettings(); - Title = UiStrings.BatchScanFormTitle; - FormStateController.FixedHeightLayout = true; + AbortButton = _cancel; LayoutController.Content = L.Column( L.Row( @@ -120,19 +131,19 @@ protected override void BuildLayout() L.Column( C.Label(UiStrings.ProfileLabel), L.Row( - _profile.Scale(), - C.Button(EditProfileCommand, ButtonImagePosition.Overlay), - C.Button(NewProfileCommand, ButtonImagePosition.Overlay) + _profile.AsControl().Scale().NaturalWidth(100), + C.Button(EditProfileCommand, ButtonImagePosition.Overlay).Width(30), + C.Button(NewProfileCommand, ButtonImagePosition.Overlay).Width(30) ), _singleScan, _multipleScansPrompt, _multipleScansDelay, L.Column( - C.Label(UiStrings.NumberOfScansLabel).Visible(_delayVis), - _numberOfScans.Width(50).Visible(_delayVis), - C.Label(UiStrings.TimeBetweenScansLabel).Visible(_delayVis), - _timeBetweenScans.Width(50).Visible(_delayVis) - ).Padding(left: 20) + C.Label(UiStrings.NumberOfScansLabel), + _numberOfScans.Width(50), + C.Label(UiStrings.TimeBetweenScansLabel), + _timeBetweenScans.Width(50) + ).Padding(left: 20).Visible(_delayVis) ) ), L.GroupBox( @@ -141,17 +152,16 @@ protected override void BuildLayout() _load, _saveToSingleFile, _saveToMultipleFiles, - // TODO: Support visibility on rows/columns L.Column( - _filePerScan.Visible(_multiVis), - _filePerPage.Visible(_multiVis), - _separateByPatchT.Visible(_multiVis), - _moreInfo.Visible(_multiVis) - ).Padding(left: 20), + _filePerScan, + _filePerPage, + _separateByPatchT, + _moreInfo + ).Padding(left: 20).Visible(_multiVis), L.Column( - C.Label(UiStrings.FilePathLabel).Visible(_fileVis), - _filePath.Visible(_fileVis) - ) + C.Label(UiStrings.FilePathLabel), + _filePath + ).Visible(_fileVis) ) ) ); @@ -191,11 +201,11 @@ private bool ValidateSettings() { bool ok = true; - _userTransact.Set(c => c.BatchSettings.ProfileDisplayName, _profile.SelectedKey); - if (_profile.SelectedIndex == -1) + _userTransact.Set(c => c.BatchSettings.ProfileDisplayName, _profile.SelectedItem?.DisplayName); + if (_profile.SelectedItem == null) { ok = false; - _profile.Focus(); + _profile.AsControl().Focus(); } _userTransact.Set(c => c.BatchSettings.ScanType, _multipleScansPrompt.Checked @@ -244,32 +254,17 @@ private bool ValidateSettings() private void UpdateProfiles() { - _profile.Items.Clear(); - _profile.Items.AddRange(_profileManager.Profiles.Select(profile => new ListItem - { - Text = profile.DisplayName, - Key = profile.DisplayName, - Tag = profile - })); - if (!string.IsNullOrEmpty(_transactionConfig.Get(c => c.BatchSettings.ProfileDisplayName)) && - _profileManager.Profiles.Any(x => - x.DisplayName == _transactionConfig.Get(c => c.BatchSettings.ProfileDisplayName))) - { - _profile.SelectedKey = _transactionConfig.Get(c => c.BatchSettings.ProfileDisplayName); - } - else if (_profileManager.DefaultProfile != null) - { - _profile.SelectedKey = _profileManager.DefaultProfile.DisplayName; - } - else - { - _profile.SelectedKey = null; - } + _profile.Items = _profileManager.Profiles; + var selectedName = _transactionConfig.Get(c => c.BatchSettings.ProfileDisplayName); + var selectedProfile = selectedName != null + ? _profileManager.Profiles.FirstOrDefault(x => x.DisplayName == selectedName) + : null; + _profile.SelectedItem = selectedProfile ?? _profileManager.DefaultProfile; } private void EditProfile() { - var originalProfile = (ScanProfile) ((ListItem) _profile.SelectedValue).Tag; + var originalProfile = _profile.SelectedItem; if (originalProfile != null) { var fedit = FormFactory.Create(); @@ -290,6 +285,7 @@ private void NewProfile() if (!(Config.Get(c => c.NoUserProfiles) && _profileManager.Profiles.Any(x => x.IsLocked))) { var fedit = FormFactory.Create(); + fedit.NewProfile = true; fedit.ScanProfile = Config.DefaultProfileSettings(); fedit.ShowModal(); if (fedit.Result) @@ -334,7 +330,7 @@ private void EnableDisableSettings(bool enabled) { var controls = new Control[] { - _profile, _singleScan, _multipleScansPrompt, _multipleScansDelay, _numberOfScans, + _profile.AsControl(), _singleScan, _multipleScansPrompt, _multipleScansDelay, _numberOfScans, _timeBetweenScans, _load, _saveToSingleFile, _saveToMultipleFiles, _filePerScan, _filePerPage, _separateByPatchT, _moreInfo }; @@ -351,9 +347,9 @@ private async Task DoBatchScan() { try { - await _batchScanPerformer.PerformBatchScan(Config.Get(c => c.BatchSettings), this, - image => Invoker.Current.SafeInvoke(() => ImageCallback(image)), ProgressCallback, _cts.Token); - Invoker.Current.SafeInvoke(() => + await _batchScanPerformer.PerformBatchScan(_transactionConfig.Get(c => c.BatchSettings), this, + image => Invoker.Current.Invoke(() => ImageCallback(image)), ProgressCallback, _cts.Token); + Invoker.Current.Invoke(() => { _status.Text = _cts.IsCancellationRequested ? MiscResources.BatchStatusCancelled @@ -376,9 +372,9 @@ await _batchScanPerformer.PerformBatchScan(Config.Get(c => c.BatchSettings), thi { Log.ErrorException("Error in batch scan", ex); _errorOutput.DisplayError(MiscResources.BatchError, ex); - Invoker.Current.SafeInvoke(() => { _status.Text = MiscResources.BatchStatusError; }); + Invoker.Current.Invoke(() => { _status.Text = MiscResources.BatchStatusError; }); } - Invoker.Current.SafeInvoke(() => + Invoker.Current.Invoke(() => { _batchRunning = false; _cts = new CancellationTokenSource(); @@ -392,7 +388,7 @@ await _batchScanPerformer.PerformBatchScan(Config.Get(c => c.BatchSettings), thi private void ProgressCallback(string status) { - Invoker.Current.SafeInvoke(() => { _status.Text = status; }); + Invoker.Current.Invoke(() => { _status.Text = status; }); } private void Cancel(object? sender, EventArgs e) @@ -400,7 +396,7 @@ private void Cancel(object? sender, EventArgs e) if (_batchRunning) { if (MessageBox.Show(MiscResources.ConfirmCancelBatch, MiscResources.CancelBatch, MessageBoxButtons.YesNo, - MessageBoxType.Question) == DialogResult.Yes) + MessageBoxType.Question, MessageBoxDefaultButton.Yes) == DialogResult.Yes) { _cts.Cancel(); _cancel.Enabled = false; @@ -412,13 +408,4 @@ private void Cancel(object? sender, EventArgs e) Close(); } } - - protected override void OnClosing(CancelEventArgs e) - { - if (_cts.IsCancellationRequested) - { - // Keep dialog open while cancel is in progress to avoid concurrency issues - e.Cancel = true; - } - } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/BlackWhiteForm.cs b/NAPS2.Lib/EtoForms/Ui/BlackWhiteForm.cs index 316bfc5ee8..116860989f 100644 --- a/NAPS2.Lib/EtoForms/Ui/BlackWhiteForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/BlackWhiteForm.cs @@ -1,24 +1,24 @@ -using Eto.Drawing; +using NAPS2.EtoForms.Widgets; namespace NAPS2.EtoForms.Ui; -public class BlackWhiteForm : ImageFormBase +public class BlackWhiteForm : UnaryImageFormBase { private readonly SliderWithTextBox _thresholdSlider = new(); - public BlackWhiteForm(Naps2Config config, ThumbnailController thumbnailController, IIconProvider iconProvider) : - base(config, thumbnailController) + public BlackWhiteForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController, + IIconProvider iconProvider) : + base(config, imageList, thumbnailController) { - Icon = new Icon(1f, Icons.contrast_high.ToEtoImage()); + IconName = "contrast_high_small"; Title = UiStrings.BlackAndWhite; - _thresholdSlider.Icon = iconProvider.GetIcon("contrast_high"); - Sliders = new[] { _thresholdSlider }; + EtoPlatform.Current.AttachDpiDependency(this, + scale => _thresholdSlider.Icon = iconProvider.GetIcon("contrast_high_small", scale)); + Sliders = [_thresholdSlider]; + // BlackWhiteTransform is not commutative with scaling + CanScaleWorkingImage = false; } - protected override IEnumerable Transforms => - new Transform[] - { - new BlackWhiteTransform(_thresholdSlider.Value) - }; + protected override List Transforms => [new BlackWhiteTransform(_thresholdSlider.IntValue)]; } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/BrightContForm.cs b/NAPS2.Lib/EtoForms/Ui/BrightContForm.cs index fe0e29069f..0e869807dc 100644 --- a/NAPS2.Lib/EtoForms/Ui/BrightContForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/BrightContForm.cs @@ -1,27 +1,30 @@ -using Eto.Drawing; +using NAPS2.EtoForms.Widgets; namespace NAPS2.EtoForms.Ui; -public class BrightContForm : ImageFormBase +public class BrightContForm : UnaryImageFormBase { private readonly SliderWithTextBox _brightnessSlider = new(); private readonly SliderWithTextBox _contrastSlider = new(); - public BrightContForm(Naps2Config config, ThumbnailController thumbnailController, IIconProvider iconProvider) : - base(config, thumbnailController) + public BrightContForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController, + IIconProvider iconProvider) : + base(config, imageList, thumbnailController) { - Icon = new Icon(1f, Icons.contrast_with_sun.ToEtoImage()); + IconName = "contrast_with_sun_small"; Title = UiStrings.BrightnessContrast; - _brightnessSlider.Icon = iconProvider.GetIcon("weather_sun"); - _contrastSlider.Icon = iconProvider.GetIcon("contrast"); - Sliders = new[] { _brightnessSlider, _contrastSlider }; + EtoPlatform.Current.AttachDpiDependency(this, scale => + { + _brightnessSlider.Icon = iconProvider.GetIcon("weather_sun_small", scale); + _contrastSlider.Icon = iconProvider.GetIcon("contrast_small", scale); + }); + Sliders = [_brightnessSlider, _contrastSlider]; } - protected override IEnumerable Transforms => - new Transform[] - { - new BrightnessTransform(_brightnessSlider.Value), - new TrueContrastTransform(_contrastSlider.Value) - }; + protected override List Transforms => + [ + new BrightnessTransform(_brightnessSlider.IntValue), + new TrueContrastTransform(_contrastSlider.IntValue) + ]; } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/ChooseDeviceForm.cs b/NAPS2.Lib/EtoForms/Ui/ChooseDeviceForm.cs new file mode 100644 index 0000000000..61a3f2fe99 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/ChooseDeviceForm.cs @@ -0,0 +1,393 @@ +using System.Threading; +using Eto.Drawing; +using Eto.Forms; +using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; +using NAPS2.Lang; +using NAPS2.Scan; +using NAPS2.Scan.Internal; + +namespace NAPS2.EtoForms.Ui; + +public class ChooseDeviceForm : EtoDialogBase +{ + private const string FLATPAK_MISSING_SCANNER_URL = "https://www.naps2.com/doc/flatpak-missing-scanner"; + + private readonly ScanDevice AlwaysAskMarker = new(Driver.Default, "*always*ask*", UiStrings.AlwaysAsk); + private readonly ScanDevice ManualIpMarker = new(Driver.Default, "*manual*ip*", UiStrings.ManualIp); + + private readonly RadioButton _wiaDriver; + private readonly RadioButton _twainDriver; + private readonly RadioButton _appleDriver; + private readonly RadioButton _saneDriver; + private readonly RadioButton _esclDriver; + private readonly IIconProvider _iconProvider; + private readonly DeviceListViewBehavior _deviceListViewBehavior; + private readonly ScanningContext _scanningContext; + private readonly DeviceCapsCache _deviceCapsCache; + private readonly LayoutVisibility _textListVis = new(false); + private readonly ListBox _deviceTextList = new(); + private readonly IListView _deviceIconList; + private readonly Button _selectDevice; + // TODO: The spinner doesn't seem to animate on WinForms + private readonly Spinner _spinner = new() { Enabled = true }; + private readonly ImageView _statusIcon = new(); + private readonly Label _statusLabel = new() { Text = UiStrings.SearchingForDevices }; + + private readonly LinkButton _flatpakMissingDevice = + C.UrlLink(FLATPAK_MISSING_SCANNER_URL, UiStrings.CantFindScannerFlatpak); + + private readonly LayoutVisibility _saneDriverVis = new(false); + private readonly LayoutVisibility _spinnerVis = new(true); + + private CancellationTokenSource? _getDevicesCts; + private Driver? _activeQuery; + private string? _statusIconName; + + public ChooseDeviceForm(Naps2Config config, IIconProvider iconProvider, + DeviceListViewBehavior deviceListViewBehavior, ScanningContext scanningContext, + DeviceCapsCache deviceCapsCache) : base(config) + { + _iconProvider = iconProvider; + _deviceListViewBehavior = deviceListViewBehavior; + _scanningContext = scanningContext; + _deviceCapsCache = deviceCapsCache; + _selectDevice = C.OkButton(this, SelectDevice, UiStrings.Select); + _deviceIconList = EtoPlatform.Current.CreateListView(deviceListViewBehavior); + _deviceIconList.ImageSize = new Size(48, 32); + deviceListViewBehavior.SetIconName(AlwaysAskMarker, "ask"); + deviceListViewBehavior.SetIconName(ManualIpMarker, "network_ip"); + + EtoPlatform.Current.AttachDpiDependency(this, _ => + { + _deviceIconList.RegenerateImages(); + UpdateStatusIcon(); + }); + + _deviceTextList.Activated += (_, _) => _selectDevice.PerformClick(); + _deviceIconList.ItemClicked += (_, _) => _selectDevice.PerformClick(); + + _wiaDriver = new RadioButton { Text = UiStrings.WiaDriver }; + _twainDriver = new RadioButton(_wiaDriver) { Text = UiStrings.TwainDriver }; + _appleDriver = new RadioButton(_wiaDriver) { Text = UiStrings.AppleDriver }; + _saneDriver = new RadioButton(_wiaDriver) { Text = UiStrings.SaneDriver }; + _esclDriver = new RadioButton(_wiaDriver) { Text = UiStrings.EsclDriver }; + + _textListVis.IsVisible = config.Get(c => c.DeviceListAsTextOnly); + } + + private void UpdateStatusIcon() + { + if (_statusIconName != null) + { + _statusIcon.Image = _iconProvider.GetIcon(_statusIconName, EtoPlatform.Current.GetScaleFactor(this)); + _statusIcon.Size = Size.Round(new SizeF(16, 16) * EtoPlatform.Current.GetLayoutScaleFactor(this)); + } + } + + private void Driver_MouseUp(object? sender, EventArgs e) + { + QueryForDevices(); + } + + private void Driver_CheckedChanged(object? sender, EventArgs e) + { + QueryForDevices(); + } + + private Driver DeviceDriver + { + get => _twainDriver.Checked ? Driver.Twain + : _wiaDriver.Checked ? Driver.Wia + : _appleDriver.Checked ? Driver.Apple + : _saneDriver.Checked ? Driver.Sane + : _esclDriver.Checked ? Driver.Escl + : ScanOptionsValidator.SystemDefaultDriver; + set + { + if (value == Driver.Twain) + { + _twainDriver.Checked = true; + } + else if (value == Driver.Wia) + { + _wiaDriver.Checked = true; + } + else if (value == Driver.Apple) + { + _appleDriver.Checked = true; + } + else if (value == Driver.Sane) + { + _saneDriver.Checked = true; + } + else if (value == Driver.Escl) + { + _esclDriver.Checked = true; + } + } + } + + protected override void BuildLayout() + { + // TODO: Don't show if only one driver is available + var driverElements = new List(); + if (PlatformCompat.System.IsWiaDriverSupported) + { + driverElements.Add(_wiaDriver.Scale()); + } + if (PlatformCompat.System.IsTwainDriverSupported) + { + driverElements.Add(_twainDriver.Scale()); + } + if (PlatformCompat.System.IsAppleDriverSupported) + { + driverElements.Add(_appleDriver.Scale()); + } + if (PlatformCompat.System.IsSaneDriverSupported) + { + driverElements.Add(_saneDriver.Scale()); + } + if (PlatformCompat.System.IsEsclDriverSupported) + { + driverElements.Add(_esclDriver.Scale()); + } + + Title = TranslationMigrator.PickTranslated(UiStrings.ResourceManager, nameof(UiStrings.SelectSource), + nameof(UiStrings.SelectDevice)); + + FormStateController.SaveFormState = FormStateController.RestoreFormState = true; + FormStateController.DefaultExtraLayoutSize = new Size(150, 100); + + LayoutController.Content = L.Column( + L.Row( + [ + ..driverElements, + C.IconButton("large_tiles_small", () => SetListView(false)) + .Visible(_textListVis), + C.IconButton("text_align_justify_small", () => SetListView(true)) + .Visible(!_textListVis) + ] + ), + _deviceIconList.Control.NaturalSize(150, 100).Scale().Visible(!_textListVis), + _deviceTextList.NaturalSize(150, 100).Scale().Visible(_textListVis), +#if NET6_0_OR_GREATER + OperatingSystem.IsLinux() && File.Exists("/.flatpak-info") + ? _flatpakMissingDevice.Visible(_saneDriverVis) + : C.None(), +#endif + L.Row( + _spinner.Visible(_spinnerVis).AlignCenter(), + _statusIcon.Visible(!_spinnerVis).AlignCenter(), + _statusLabel.AlignCenter(), + C.Filler(), + L.OkCancel(_selectDevice, C.CancelButton(this)) + ) + ); + } + + private void SetListView(bool value) + { + _textListVis.IsVisible = value; + Config.User.Set(c => c.DeviceListAsTextOnly, value); + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + + DeviceDriver = ScanOptions!.Driver; + + _wiaDriver.CheckedChanged += Driver_CheckedChanged; + _twainDriver.CheckedChanged += Driver_CheckedChanged; + _appleDriver.CheckedChanged += Driver_CheckedChanged; + _saneDriver.CheckedChanged += Driver_CheckedChanged; + _esclDriver.CheckedChanged += Driver_CheckedChanged; + // TODO: Maybe have a refresh button instead? As part of the status indicator? + _wiaDriver.MouseUp += Driver_MouseUp; + _twainDriver.MouseUp += Driver_MouseUp; + _appleDriver.MouseUp += Driver_MouseUp; + _saneDriver.MouseUp += Driver_MouseUp; + _esclDriver.MouseUp += Driver_MouseUp; + + QueryForDevices(); + } + + private async void QueryForDevices() + { + if (_activeQuery == DeviceDriver) + { + return; + } + + _saneDriverVis.IsVisible = _saneDriver.Checked; + _deviceIconList.ImageSize = DeviceDriver == Driver.Escl ? new Size(48, 48) : new Size(48, 32); + + _getDevicesCts?.Cancel(); + _spinnerVis.IsVisible = true; + _statusLabel.Text = UiStrings.SearchingForDevices; + _activeQuery = DeviceDriver; + + DeviceList = new List(); + DeviceSet = new HashSet(); + ExtraItems = new List(); + if (DeviceDriver == Driver.Escl) + { + ExtraItems.Add(ManualIpMarker); + } + if (AllowAlwaysAsk && DeviceDriver is not (Driver.Wia or Driver.Twain)) + { + ExtraItems.Add(AlwaysAskMarker); + } + + UpdateDevices(true); + + var cts = new CancellationTokenSource(); + _getDevicesCts = cts; + var optionsWithDriver = ScanOptions!.Clone(); + optionsWithDriver.Driver = DeviceDriver; + var controller = new ScanController(_scanningContext); + + try + { + await foreach (var device in controller.GetDevices(optionsWithDriver, cts.Token)) + { + if (!cts.IsCancellationRequested) + { + var cachedIcon = _deviceCapsCache.GetCachedIcon(device.IconUri); + if (cachedIcon != null) + { + _deviceListViewBehavior.SetImage(device, cachedIcon); + } + else + { + var icon = await _deviceCapsCache.LoadIcon(device); + if (icon != null) + { + _deviceListViewBehavior.SetImage(device, icon); + _deviceIconList.RegenerateImages(); + } + } + if (!DeviceSet.Contains(device)) + { + DeviceList.Add(device); + DeviceSet.Add(device); + } + UpdateDevices(false); + } + } + if (AllowAlwaysAsk && DeviceDriver is Driver.Wia or Driver.Twain) + { + if (!cts.IsCancellationRequested) + { + ExtraItems.Add(AlwaysAskMarker); + UpdateDevices(false); + } + } + if (!cts.IsCancellationRequested) + { + _spinnerVis.IsVisible = false; + _statusIconName = DeviceList.Count > 0 ? "accept_small" : "exclamation_small"; + UpdateStatusIcon(); + _statusLabel.Text = DeviceList.Count switch + { + > 1 => string.Format(UiStrings.DevicesFound, DeviceList.Count), + 1 => UiStrings.DeviceFoundSingular, + _ => UiStrings.NoDevicesFound + }; + } + } + catch (Exception ex) + { + if (!cts.IsCancellationRequested) + { + _spinnerVis.IsVisible = false; + _statusIconName = "exclamation_small"; + UpdateStatusIcon(); + _statusLabel.Text = ex.Message; + } + } + finally + { + if (!cts.IsCancellationRequested) + { + _activeQuery = null; + } + } + } + + private void UpdateDevices(bool clear) + { + _deviceIconList.SetItems(DeviceList!.Concat(ExtraItems!)); + if (clear) + { + _deviceTextList.Items.Clear(); + } + foreach (var device in ExtraItems!) + { + if (_deviceTextList.Items.All(x => x.Key != device.ID)) + { + _deviceTextList.Items.Add(new ListItem + { + Key = device.ID, + Text = device.Name + }); + } + } + foreach (var device in DeviceList!.Skip(_deviceTextList.Items.Count - ExtraItems.Count)) + { + _deviceTextList.Items.Insert(_deviceTextList.Items.Count - ExtraItems.Count, new ListItem + { + Key = device.ID, + Text = device.Name + }); + } + } + + public ScanOptions? ScanOptions { get; set; } + + public bool AllowAlwaysAsk { get; set; } + + private List? DeviceList { get; set; } + + private HashSet? DeviceSet { get; set; } + + private List? ExtraItems { get; set; } + + public DeviceChoice Choice { get; private set; } = DeviceChoice.None; + + private bool SelectDevice() + { + if (_textListVis.IsVisible) + { + if (_deviceTextList.SelectedValue == null) + { + _deviceTextList.Focus(); + return false; + } + Choice = DeviceChoice.ForDevice(DeviceList!.Concat(ExtraItems!) + .First(x => x.ID == _deviceTextList.SelectedKey)); + } + else + { + if (_deviceIconList.Selection.Count == 0) + { + _deviceIconList.Control.Focus(); + return false; + } + Choice = DeviceChoice.ForDevice(_deviceIconList.Selection.First()); + } + if (Choice.Device == AlwaysAskMarker) + { + Choice = DeviceChoice.ForAlwaysAsk(DeviceDriver); + } + if (Choice.Device == ManualIpMarker) + { + var ipForm = FormFactory.Create(); + ipForm.ShowModal(); + Choice = ipForm.Device != null ? DeviceChoice.ForDevice(ipForm.Device) : DeviceChoice.None; + return ipForm.Result; + } + return true; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/CombineForm.cs b/NAPS2.Lib/EtoForms/Ui/CombineForm.cs new file mode 100644 index 0000000000..c1caee49e3 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/CombineForm.cs @@ -0,0 +1,169 @@ +using Eto.Drawing; +using NAPS2.EtoForms.Layout; +using NAPS2.Scan; + +namespace NAPS2.EtoForms.Ui; + +public class CombineForm : ImageFormBase +{ + private readonly ScanningContext _scanningContext; + + private readonly LayoutVisibility _horizontalOrientationVis = new(false); + private readonly LayoutVisibility _alignVis = new(true); + private CombineOrientation _orientation; + private double _hOffset = 0.5; + private double _vOffset = 0.5; + + public CombineForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController, + ScanningContext scanningContext) : + base(config, imageList, thumbnailController) + { + Title = UiStrings.Combine; + IconName = "combine_small"; + + _scanningContext = scanningContext; + } + + private UiImage Image1 { get; set; } = null!; + private UiImage Image2 { get; set; } = null!; + + private IMemoryImage WorkingImage1 { get; set; } = null!; + private IMemoryImage WorkingImage2 { get; set; } = null!; + + protected override LayoutElement CreateControls() + { + return L.Row( + C.Filler(), + L.Row( + L.Row( + C.IconButton("shape_align_left_small", () => SetHOffset(0)), + C.IconButton("shape_align_center_small", () => SetHOffset(0.5)), + C.IconButton("shape_align_right_small", () => SetHOffset(1.0)) + ).Visible(_alignVis), + C.Spacer().Width(14), + C.IconButton("combine_hor_small", + () => SetOrientation(CombineOrientation.Horizontal)), + C.IconButton("switch_ver_small", SwapImages) + ).Visible(!_horizontalOrientationVis), + L.Row( + L.Row( + C.IconButton("shape_align_top_small", () => SetVOffset(0)), + C.IconButton("shape_align_middle_small", () => SetVOffset(0.5)), + C.IconButton("shape_align_bottom_small", () => SetVOffset(1.0)) + ).Visible(_alignVis), + C.Spacer().Width(14), + C.IconButton("combine_ver_small", + () => SetOrientation(CombineOrientation.Vertical)), + C.IconButton("switch_hor_small", SwapImages) + ).Visible(_horizontalOrientationVis), + C.Filler() + ); + } + + private void SwapImages() + { + (Image1, Image2) = (Image2, Image1); + (WorkingImage1, WorkingImage2) = (WorkingImage2, WorkingImage1); + UpdatePreviewBox(); + } + + private void SetHOffset(double value) + { + _hOffset = value; + UpdatePreviewBox(); + } + + private void SetVOffset(double value) + { + _vOffset = value; + UpdatePreviewBox(); + } + + private void SetOrientation(CombineOrientation orientation) + { + _orientation = orientation; + _horizontalOrientationVis.IsVisible = _orientation == CombineOrientation.Horizontal; + UpdatePreviewBox(); + } + + protected override void InitDisplayImage() + { + // If there's an image after this one, then this is the first image, and the subsequent image is the second. + // Otherwise, we look for the previous image in the list, which should be considered the first image, and then + // this image is the second. + var nextImage = SelectedImages.ElementAtOrDefault(1) ?? + ImageList.Images.ElementAtOrDefault(ImageList.Images.IndexOf(Image) + 1); + Image1 = nextImage != null + ? Image + : ImageList.Images.ElementAtOrDefault(ImageList.Images.IndexOf(Image) - 1) ?? + throw new InvalidOperationException("No image to combine with"); + Image2 = nextImage ?? Image; + + using var processedImage1 = Image1.GetClonedImage(); + WorkingImage1 = processedImage1.Render(); + using var processedImage2 = Image2.GetClonedImage(); + WorkingImage2 = processedImage2.Render(); + + _orientation = WorkingImage1.Width + WorkingImage2.Width > WorkingImage1.Height + WorkingImage2.Height + ? CombineOrientation.Vertical + : CombineOrientation.Horizontal; + _horizontalOrientationVis.IsVisible = _orientation == CombineOrientation.Horizontal; + // We could make these visibilities different (i.e. hAlignVis + vAlignVis), but + // that means the button alignment can change underneath the mouse which isn't great. + _alignVis.IsVisible = + WorkingImage1.Width != WorkingImage2.Width || WorkingImage1.Height != WorkingImage2.Height; + + var workingArea = GetScreenWorkingArea(); + var widthRatio1 = WorkingImage1.Width / workingArea.Width; + var heightRatio1 = WorkingImage1.Height / workingArea.Height; + var widthRatio2 = WorkingImage1.Width / workingArea.Width; + var heightRatio2 = WorkingImage1.Height / workingArea.Height; + var maxRatio = new[] { widthRatio1, heightRatio1, widthRatio2, heightRatio2 }.Max(); + if (maxRatio > 1) + { + WorkingImage1 = WorkingImage1.PerformTransform(new ScaleTransform(1 / maxRatio)); + WorkingImage2 = WorkingImage2.PerformTransform(new ScaleTransform(1 / maxRatio)); + } + + // TODO: We probably want to scale up any lower-res images to match the higher resolution. Here and in Apply(). + + DisplayImage = RenderPreview(); + } + + protected override IMemoryImage RenderPreview() + { + return CombineImages(WorkingImage1, WorkingImage2); + } + + private IMemoryImage CombineImages(IMemoryImage first, IMemoryImage second) + { + return MoreImageTransforms.Combine(first, second, _orientation, + _orientation == CombineOrientation.Horizontal ? _vOffset : _hOffset); + } + + protected override void Apply() + { + // TODO: Consider the latency of this, especially with "Apply all". Does it make sense to be async? Have a operation? + using var processedImage1 = Image1.GetClonedImage(); + using var renderedImage1 = processedImage1.Render(); + using var processedImage2 = Image2.GetClonedImage(); + using var renderedImage2 = processedImage2.Render(); + using var combinedImage = CombineImages(renderedImage1, renderedImage2); + + // TODO: Use working images for thumbnail? + var thumbnail = combinedImage.Clone().PerformTransform(new ThumbnailTransform(ThumbnailController.RenderSize)); + var ppd = new PostProcessingData { Thumbnail = thumbnail, ThumbnailTransformState = TransformState.Empty }; + var processedImage = _scanningContext.CreateProcessedImage(combinedImage).WithPostProcessingData(ppd, true); + + ImageList.Mutate(new ListMutation.InsertAfter(new UiImage(processedImage), Image)); + // TODO: Maybe have a checkbox to keep the original images? + ImageList.Mutate(new ListMutation.DeleteSelected(), ListSelection.Of(Image1, Image2)); + } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + WorkingImage1.Dispose(); + WorkingImage2.Dispose(); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/CropForm.cs b/NAPS2.Lib/EtoForms/Ui/CropForm.cs index d95364d82f..ae5f4d47f8 100644 --- a/NAPS2.Lib/EtoForms/Ui/CropForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/CropForm.cs @@ -3,10 +3,16 @@ namespace NAPS2.EtoForms.Ui; -public class CropForm : ImageFormBase +public class CropForm : UnaryImageFormBase { + private static CropTransform? _lastTransform; + private const int HANDLE_WIDTH = 3; - private const int HANDLE_LENGTH = 20; + private const double HANDLE_LENGTH_RATIO = 0.07; + private const int HANDLE_MIN_LENGTH = 30; + private const int FREEFORM_MIN_SIZE = 10; + + private readonly ColorScheme _colorScheme; // TODO: Textboxes for direct editing @@ -19,14 +25,20 @@ public class CropForm : ImageFormBase // Crop amounts from each side as pixels of the image to be cropped (updated on mouse up) private float _realL, _realR, _realT, _realB; - // Whether the given crop handle is currently being moved by the user (multiple can be true for corners) - private bool _activeT, _activeL, _activeB, _activeR; + // The crop handle currently being moved by the user + private Handle _activeHandle; + + private bool _freeformAvailable; + private bool _freeformActive; - public CropForm(Naps2Config config, ThumbnailController thumbnailController) : - base(config, thumbnailController) + public CropForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController, + ColorScheme colorScheme, IIconProvider iconProvider) : + base(config, imageList, thumbnailController) { - Icon = new Icon(1f, Icons.transform_crop.ToEtoImage()); Title = UiStrings.Crop; + IconName = "transform_crop_small"; + + _colorScheme = colorScheme; OverlayBorderSize = HANDLE_WIDTH; Overlay.MouseDown += Overlay_MouseDown; @@ -34,33 +46,82 @@ public CropForm(Naps2Config config, ThumbnailController thumbnailController) : Overlay.MouseUp += Overlay_MouseUp; } - protected override void ResetTransform() + // The handle length is proportional to the window size + private int HandleLength => + (int) Math.Max(Math.Round(Math.Min(_overlayH, _overlayW) * HANDLE_LENGTH_RATIO), HANDLE_MIN_LENGTH); + + protected override void OnPreLoad(EventArgs e) + { + base.OnPreLoad(e); + if (_lastTransform != null && _lastTransform.OriginalWidth == RealImageWidth && + _lastTransform.OriginalHeight == RealImageHeight) + { + _realL = _lastTransform.Left; + _realR = _lastTransform.Right; + _realT = _lastTransform.Top; + _realB = _lastTransform.Bottom; + _cropL = _realL / RealImageWidth; + _cropR = _realR / RealImageWidth; + _cropT = _realT / RealImageHeight; + _cropB = _realB / RealImageHeight; + } + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + // The crop form has no other focusable elements, so focus "OK" instead of "Revert" + DefaultButton.Focus(); + } + + protected override void Apply() + { + base.Apply(); + _lastTransform = (CropTransform) Transforms.Single(); + } + + protected override void Revert() { _cropT = _cropL = _cropB = _cropR = _realT = _realL = _realB = _realR = 0; Overlay.Invalidate(); } - protected override IEnumerable Transforms => - new Transform[] - { - new CropTransform( - (int) Math.Round(_realL), - (int) Math.Round(_realR), - (int) Math.Round(_realT), - (int) Math.Round(_realB), - ImageWidth, - ImageHeight) - }; + protected override IMemoryImage RenderPreview() + { + return WorkingImage!.Clone(); + } + + protected override List Transforms => + [ + new CropTransform( + (int) Math.Round(_realL), + (int) Math.Round(_realR), + (int) Math.Round(_realT), + (int) Math.Round(_realB), + RealImageWidth, + RealImageHeight) + ]; private void Overlay_MouseDown(object? sender, MouseEventArgs e) + { + _activeHandle = GetHandleUnderMouse(e); + if (_activeHandle == Handle.None) + { + _freeformAvailable = true; + } + _mouseOrigin = e.Location; + Overlay.Invalidate(); + } + + private Handle GetHandleUnderMouse(MouseEventArgs e) { // We calculate the distance between the mouse and each handle side // The 0.1 offset is to provide a bit of affinity so that if the crop size is 0 (so all distances are the same), // you can still e.g. pick the top-left handle if you put the mouse a bit top-left of it. - var t = _overlayT + _realT * _overlayH / ImageHeight; - var b = _overlayB - _realB * _overlayH / ImageHeight; - var l = _overlayL + _realL * _overlayW / ImageWidth; - var r = _overlayR - _realR * _overlayW / ImageWidth; + var t = _overlayT + _realT * _overlayH / RealImageHeight; + var b = _overlayB - _realB * _overlayH / RealImageHeight; + var l = _overlayL + _realL * _overlayW / RealImageWidth; + var r = _overlayR - _realR * _overlayW / RealImageWidth; var dyT = Math.Abs(e.Location.Y - (t - 0.1f)); var dyB = Math.Abs(e.Location.Y - (b + 0.1f)); var dyM = Math.Abs(e.Location.Y - (t + b) / 2); @@ -69,57 +130,93 @@ private void Overlay_MouseDown(object? sender, MouseEventArgs e) var dxM = Math.Abs(e.Location.X - (l + r) / 2); var dxMin = Math.Min(Math.Min(dxL, dxR), dxM); var dyMin = Math.Min(Math.Min(dyT, dyB), dyM); - if (dxMin < HANDLE_LENGTH && dyMin < HANDLE_LENGTH) + // The user can click/drag the handle even if they miss a bit + if (dxMin < HandleLength * 1.5 && dyMin < HandleLength * 1.5) { // ReSharper disable CompareOfFloatsByEqualityOperator - if (dyT == dyMin && dxL == dxMin) _activeT = _activeL = true; - else if (dyT == dyMin && dxR == dxMin) _activeT = _activeR = true; - else if (dyB == dyMin && dxL == dxMin) _activeB = _activeL = true; - else if (dyB == dyMin && dxR == dxMin) _activeB = _activeR = true; - else if (dyT == dyMin && dxM == dxMin) _activeT = true; - else if (dyM == dyMin && dxL == dxMin) _activeL = true; - else if (dyB == dyMin && dxM == dxMin) _activeB = true; - else if (dyM == dyMin && dxR == dxMin) _activeR = true; - _mouseOrigin = e.Location; + if (dyT == dyMin && dxL == dxMin) return Handle.TopLeft; + if (dyT == dyMin && dxR == dxMin) return Handle.TopRight; + if (dyB == dyMin && dxL == dxMin) return Handle.BottomLeft; + if (dyB == dyMin && dxR == dxMin) return Handle.BottomRight; + if (dyT == dyMin && dxM == dxMin) return Handle.Top; + if (dyM == dyMin && dxL == dxMin) return Handle.Left; + if (dyB == dyMin && dxM == dxMin) return Handle.Bottom; + if (dyM == dyMin && dxR == dxMin) return Handle.Right; } - Overlay.Invalidate(); + return Handle.None; } private void Overlay_MouseUp(object? sender, MouseEventArgs e) { - _realT = _cropT * ImageHeight; - _realB = _cropB * ImageHeight; - _realL = _cropL * ImageWidth; - _realR = _cropR * ImageWidth; - _activeT = _activeL = _activeB = _activeR = false; + _realT = _cropT * RealImageHeight; + _realB = _cropB * RealImageHeight; + _realL = _cropL * RealImageWidth; + _realR = _cropR * RealImageWidth; + _activeHandle = Handle.None; + _freeformAvailable = false; + _freeformActive = false; Overlay.Invalidate(); } private void UpdateCrop(PointF mousePos) { var delta = mousePos - _mouseOrigin; - if (_activeT) - { - _cropT = (_realT / ImageHeight + delta.Y / _overlayH).Clamp(0, 1 - _cropB); - } - if (_activeR) - { - _cropR = (_realR / ImageWidth - delta.X / _overlayW).Clamp(0, 1 - _cropL); - } - if (_activeB) + if (_freeformActive) { - _cropB = (_realB / ImageHeight - delta.Y / _overlayH).Clamp(0, 1 - _cropT); + var origin = _mouseOrigin - new PointF(_overlayL, _overlayT); + var current = mousePos - new PointF(_overlayL, _overlayT); + if (delta.Y > 0) + { + _cropT = (origin.Y / _overlayH).Clamp(0, 1); + _cropB = (1 - current.Y / _overlayH).Clamp(0, 1); + } + else + { + _cropT = (current.Y / _overlayH).Clamp(0, 1); + _cropB = (1 - origin.Y / _overlayH).Clamp(0, 1); + } + if (delta.X > 0) + { + _cropL = (origin.X / _overlayW).Clamp(0, 1); + _cropR = (1 - current.X / _overlayW).Clamp(0, 1); + } + else + { + _cropL = (current.X / _overlayW).Clamp(0, 1); + _cropR = (1 - origin.X / _overlayW).Clamp(0, 1); + } } - if (_activeL) + else { - _cropL = (_realL / ImageWidth + delta.X / _overlayW).Clamp(0, 1 - _cropR); + if (_activeHandle.HasFlag(Handle.Top)) + { + _cropT = (_realT / RealImageHeight + delta.Y / _overlayH).Clamp(0, 1 - _cropB); + } + if (_activeHandle.HasFlag(Handle.Right)) + { + _cropR = (_realR / RealImageWidth - delta.X / _overlayW).Clamp(0, 1 - _cropL); + } + if (_activeHandle.HasFlag(Handle.Bottom)) + { + _cropB = (_realB / RealImageHeight - delta.Y / _overlayH).Clamp(0, 1 - _cropT); + } + if (_activeHandle.HasFlag(Handle.Left)) + { + _cropL = (_realL / RealImageWidth + delta.X / _overlayW).Clamp(0, 1 - _cropR); + } } } private void Overlay_MouseMove(object? sender, MouseEventArgs e) { - // TODO: Update cursor based on proximity to handle & state - Overlay.Cursor = Cursors.Crosshair; + Overlay.Cursor = _freeformAvailable || (_activeHandle == Handle.None && GetHandleUnderMouse(e) == Handle.None) + ? Cursors.Crosshair + : Cursors.Pointer; + if (_freeformAvailable && (Math.Abs(e.Location.Y - _mouseOrigin.Y) > FREEFORM_MIN_SIZE || + Math.Abs(e.Location.X - _mouseOrigin.X) > FREEFORM_MIN_SIZE)) + { + _freeformActive = true; + } UpdateCrop(e.Location); Overlay.Invalidate(); } @@ -128,24 +225,32 @@ protected override void PaintOverlay(object? sender, PaintEventArgs e) { base.PaintOverlay(sender, e); + if (_overlayW == 0 || _overlayH == 0) + { + return; + } + var offsetL = _cropL * _overlayW; var offsetT = _cropT * _overlayH; var offsetR = _cropR * _overlayW; var offsetB = _cropB * _overlayH; var fillColor = new Color(0.3f, 0.3f, 0.3f, 0.5f); - var handlePen = new Pen(new Color(0, 0, 0), HANDLE_WIDTH); - - // Fade out cropped-out portions of the image - using var fade = new Bitmap((int) _overlayW, (int) _overlayH, PixelFormat.Format32bppRgba); - var fadeGraphics = new Graphics(fade); - fadeGraphics.FillRectangle(fillColor, 0, 0, _overlayW, _overlayH); - fadeGraphics.SetClip(new RectangleF( - offsetL, offsetT, - _overlayW - offsetL - offsetR, - _overlayH - offsetT - offsetB)); - fadeGraphics.Clear(); - fadeGraphics.Dispose(); - e.Graphics.DrawImage(fade, _overlayL, _overlayT); + var handlePen = new Pen(_colorScheme.CropColor, HANDLE_WIDTH); + + if (_overlayW >= 1 && _overlayH >= 1) + { + // Fade out cropped-out portions of the image + using var fade = new Bitmap((int) _overlayW, (int) _overlayH, PixelFormat.Format32bppRgba); + var fadeGraphics = new Graphics(fade); + fadeGraphics.FillRectangle(fillColor, 0, 0, _overlayW, _overlayH); + fadeGraphics.SetClip(new RectangleF( + offsetL, offsetT, + _overlayW - offsetL - offsetR, + _overlayH - offsetT - offsetB)); + fadeGraphics.Clear(); + fadeGraphics.Dispose(); + e.Graphics.DrawImage(fade, _overlayL, _overlayT); + } var x1 = _overlayL + offsetL - HANDLE_WIDTH / 2f; var y1 = _overlayT + offsetT - HANDLE_WIDTH / 2f; @@ -154,28 +259,54 @@ protected override void PaintOverlay(object? sender, PaintEventArgs e) var xMid = (x1 + x2) / 2; var yMid = (y1 + y2) / 2; - // Draw corner handles - e.Graphics.DrawLines(handlePen, - new PointF(x1, y1 + HANDLE_LENGTH), - new PointF(x1, y1), - new PointF(x1 + HANDLE_LENGTH, y1)); - e.Graphics.DrawLines(handlePen, - new PointF(x1, y2 - HANDLE_LENGTH), - new PointF(x1, y2), - new PointF(x1 + HANDLE_LENGTH, y2)); - e.Graphics.DrawLines(handlePen, - new PointF(x2, y1 + HANDLE_LENGTH), - new PointF(x2, y1), - new PointF(x2 - HANDLE_LENGTH, y1)); - e.Graphics.DrawLines(handlePen, - new PointF(x2, y2 - HANDLE_LENGTH), - new PointF(x2, y2), - new PointF(x2 - HANDLE_LENGTH, y2)); - - // Draw edge handles - e.Graphics.DrawLine(handlePen, x1, yMid - HANDLE_LENGTH / 2f, x1, yMid + HANDLE_LENGTH / 2f); - e.Graphics.DrawLine(handlePen, x2, yMid - HANDLE_LENGTH / 2f, x2, yMid + HANDLE_LENGTH / 2f); - e.Graphics.DrawLine(handlePen, xMid - HANDLE_LENGTH / 2f, y1, xMid + HANDLE_LENGTH / 2f, y1); - e.Graphics.DrawLine(handlePen, xMid - HANDLE_LENGTH / 2f, y2, xMid + HANDLE_LENGTH / 2f, y2); + // For a small crop selection, we shrink the handles so they don't overlap + var xHandleLen = Math.Min(HandleLength, (x2 - x1) / 5); + var yHandleLen = Math.Min(HandleLength, (y2 - y1) / 5); + + if (_freeformActive) + { + // Draw border + e.Graphics.DrawRectangle(new Pen(_colorScheme.CropColor), x1, y1, x2 - x1 - 1, y2 - y1 - 1); + } + else + { + // Draw corner handles + e.Graphics.DrawLines(handlePen, + new PointF(x1, y1 + yHandleLen), + new PointF(x1, y1), + new PointF(x1 + xHandleLen, y1)); + e.Graphics.DrawLines(handlePen, + new PointF(x1, y2 - yHandleLen), + new PointF(x1, y2), + new PointF(x1 + xHandleLen, y2)); + e.Graphics.DrawLines(handlePen, + new PointF(x2, y1 + yHandleLen), + new PointF(x2, y1), + new PointF(x2 - xHandleLen, y1)); + e.Graphics.DrawLines(handlePen, + new PointF(x2, y2 - yHandleLen), + new PointF(x2, y2), + new PointF(x2 - xHandleLen, y2)); + + // Draw edge handles + e.Graphics.DrawLine(handlePen, x1, yMid - yHandleLen / 2f, x1, yMid + yHandleLen / 2f); + e.Graphics.DrawLine(handlePen, x2, yMid - yHandleLen / 2f, x2, yMid + yHandleLen / 2f); + e.Graphics.DrawLine(handlePen, xMid - xHandleLen / 2f, y1, xMid + xHandleLen / 2f, y1); + e.Graphics.DrawLine(handlePen, xMid - xHandleLen / 2f, y2, xMid + xHandleLen / 2f, y2); + } + } + + [Flags] + private enum Handle + { + None = 0, + Left = 1, + Right = 2, + Top = 4, + Bottom = 8, + TopLeft = Top | Left, + TopRight = Top | Right, + BottomLeft = Bottom | Left, + BottomRight = Bottom | Right } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs b/NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs index f68d840329..ef1185058e 100644 --- a/NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs +++ b/NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs @@ -1,4 +1,3 @@ -using Eto.Forms; using NAPS2.EtoForms.Desktop; namespace NAPS2.EtoForms.Ui; @@ -12,10 +11,13 @@ public class DesktopCommands private readonly ImageListActions _imageListActions; private readonly ThumbnailController _thumbnailController; private readonly IIconProvider _iconProvider; + private readonly Naps2Config _config; + private readonly DesktopFormProvider _desktopFormProvider; public DesktopCommands(DesktopController desktopController, DesktopScanController desktopScanController, IDesktopSubFormController desktopSubFormController, UiImageList imageList, ImageListActions imageListActions, - ThumbnailController thumbnailController, IIconProvider iconProvider) + ThumbnailController thumbnailController, IIconProvider iconProvider, Naps2Config config, + DesktopFormProvider desktopFormProvider) { _desktopController = desktopController; _desktopScanController = desktopScanController; @@ -24,59 +26,60 @@ public DesktopCommands(DesktopController desktopController, DesktopScanControlle _imageListActions = imageListActions; _thumbnailController = thumbnailController; _iconProvider = iconProvider; + _config = config; + _desktopFormProvider = desktopFormProvider; + + var hiddenButtons = config.Get(c => c.HiddenButtons); Scan = new ActionCommand(desktopScanController.ScanDefault) { Text = UiStrings.Scan, - Image = iconProvider.GetIcon("control_play_blue"), - Shortcut = Application.Instance.CommonModifier | Keys.Period + IconName = "control_play_blue" }; NewProfile = new ActionCommand(desktopScanController.ScanWithNewProfile) { Text = UiStrings.NewProfile, - Image = iconProvider.GetIcon("add_small"), - Shortcut = Application.Instance.CommonModifier | Keys.N + IconName = "add_small" }; BatchScan = new ActionCommand(desktopSubFormController.ShowBatchScanForm) { Text = UiStrings.BatchScan, - Image = iconProvider.GetIcon("application_cascade"), - Shortcut = Application.Instance.CommonModifier | Keys.B + IconName = "application_cascade_small" + }; + ScannerSharing = new ActionCommand(desktopSubFormController.ShowScannerSharingForm) + { + Text = UiStrings.ScannerSharing, + IconName = "wireless_small" }; Profiles = new ActionCommand(desktopSubFormController.ShowProfilesForm) { Text = UiStrings.Profiles, - Image = iconProvider.GetIcon("blueprints"), - Shortcut = Application.Instance.CommonModifier | Keys.L + IconName = "blueprints" }; Ocr = new ActionCommand(desktopSubFormController.ShowOcrForm) { Text = UiStrings.Ocr, - Image = iconProvider.GetIcon("text"), - Shortcut = Application.Instance.CommonModifier | Keys.Alt | Keys.O + IconName = "text" }; Import = new ActionCommand(desktopController.Import) { Text = UiStrings.Import, - Image = iconProvider.GetIcon("folder_picture"), - Shortcut = Application.Instance.CommonModifier | Keys.O + IconName = "folder_picture" }; - SaveAll = new ActionCommand(_imageListActions.SaveAllAsPdfOrImages) + SaveAll = new ActionCommand(imageListActions.SaveAllAsPdfOrImages) { Text = UiStrings.SaveAll, - Image = iconProvider.GetIcon("diskette"), - Shortcut = Application.Instance.CommonModifier | Keys.S + IconName = "diskette" }; - SaveSelected = new ActionCommand(_imageListActions.SaveSelectedAsPdfOrImages) + SaveSelected = new ActionCommand(imageListActions.SaveSelectedAsPdfOrImages) { Text = UiStrings.SaveSelected, - Image = iconProvider.GetIcon("diskette"), - Shortcut = Application.Instance.CommonModifier | Keys.Shift | Keys.S + IconName = "diskette" }; SavePdf = new ActionCommand(desktopController.SavePdf) { Text = UiStrings.SavePdf, - Image = iconProvider.GetIcon("file_extension_pdf") + IconName = "file_extension_pdf" }; SaveAllPdf = new ActionCommand(imageListActions.SaveAllAsPdf) { @@ -88,13 +91,12 @@ public DesktopCommands(DesktopController desktopController, DesktopScanControlle }; PdfSettings = new ActionCommand(desktopSubFormController.ShowPdfSettingsForm) { - Text = UiStrings.PdfSettings, - Shortcut = Application.Instance.CommonModifier | Keys.Alt | Keys.P + Text = UiStrings.PdfSettings }; SaveImages = new ActionCommand(desktopController.SaveImages) { Text = UiStrings.SaveImages, - Image = iconProvider.GetIcon("pictures") + IconName = "pictures" }; SaveAllImages = new ActionCommand(imageListActions.SaveAllAsImages) { @@ -106,74 +108,93 @@ public DesktopCommands(DesktopController desktopController, DesktopScanControlle }; ImageSettings = new ActionCommand(desktopSubFormController.ShowImageSettingsForm) { - Text = UiStrings.ImageSettings, - Shortcut = Application.Instance.CommonModifier | Keys.Alt | Keys.I + Text = UiStrings.ImageSettings }; EmailPdf = new ActionCommand(desktopController.EmailPdf) { Text = UiStrings.EmailPdf, - Image = iconProvider.GetIcon("email_attach") + IconName = "email_attach" }; EmailAll = new ActionCommand(imageListActions.EmailAllAsPdf) { - Text = UiStrings.EmailAll, - Shortcut = Application.Instance.CommonModifier | Keys.E + Text = UiStrings.EmailAll }; EmailSelected = new ActionCommand(imageListActions.EmailSelectedAsPdf) { - Text = UiStrings.EmailSelected, - Shortcut = Application.Instance.CommonModifier | Keys.Shift | Keys.E + Text = UiStrings.EmailSelected }; EmailSettings = new ActionCommand(desktopSubFormController.ShowEmailSettingsForm) { - Text = UiStrings.EmailSettings, - Shortcut = Application.Instance.CommonModifier | Keys.Alt | Keys.E + Text = UiStrings.EmailSettings }; Print = new ActionCommand(desktopController.Print) { Text = UiStrings.Print, - Image = iconProvider.GetIcon("printer"), - Shortcut = Application.Instance.CommonModifier | Keys.P + IconName = "printer" }; ImageMenu = new ActionCommand { Text = UiStrings.Image, - Image = iconProvider.GetIcon("picture_edit") + IconName = "picture_edit" }; ViewImage = new ActionCommand(desktopSubFormController.ShowViewerForm) { Text = UiStrings.View, - Image = iconProvider.GetIcon("zoom_small") + IconName = "zoom_small" }; Crop = new ActionCommand(desktopSubFormController.ShowCropForm) { Text = UiStrings.Crop, - Image = iconProvider.GetIcon("transform_crop") + IconName = "transform_crop_small" }; BrightCont = new ActionCommand(desktopSubFormController.ShowBrightnessContrastForm) { Text = UiStrings.BrightnessContrast, - Image = iconProvider.GetIcon("contrast_with_sun") + IconName = "contrast_with_sun_small" }; HueSat = new ActionCommand(desktopSubFormController.ShowHueSaturationForm) { Text = UiStrings.HueSaturation, - Image = iconProvider.GetIcon("color_management") + IconName = "color_management_small" }; BlackWhite = new ActionCommand(desktopSubFormController.ShowBlackWhiteForm) { Text = UiStrings.BlackAndWhite, - Image = iconProvider.GetIcon("contrast_high") + IconName = "contrast_high_small" }; Sharpen = new ActionCommand(desktopSubFormController.ShowSharpenForm) { Text = UiStrings.Sharpen, - Image = iconProvider.GetIcon("sharpen") + IconName = "sharpen_small" }; // TODO: Make this an image form with options - DocumentCorrection = new ActionCommand(desktopController.RunDocumentCorrection) + DocumentCorrection = new ActionCommand(imageListActions.DocumentCorrection) + { + Text = UiStrings.DocumentCorrection, + IconName = "document_small" + }; + Split = new ActionCommand(desktopSubFormController.ShowSplitForm) + { + Text = UiStrings.Split, + IconName = "split_small" + }; + Combine = new ActionCommand(desktopSubFormController.ShowCombineForm) + { + Text = UiStrings.Combine, + IconName = "combine_small" + }; + string? editWithAppName = _config.Get(c => c.EditWithAppName); + EditWithApp = new ActionCommand(imageListActions.EditWithApp) { - Text = UiStrings.DocumentCorrection + Text = string.IsNullOrEmpty(editWithAppName) + ? UiStrings.EditWith + : string.Format(UiStrings.EditWithAppName, editWithAppName), + IconName = "pencil_small" + }; + EditWithPick = new ActionCommand(imageListActions.EditWithPick) + { + Text = UiStrings.EditWith, + IconName = "pencil_small" }; ResetImage = new ActionCommand(desktopController.ResetImage) { @@ -182,22 +203,22 @@ public DesktopCommands(DesktopController desktopController, DesktopScanControlle RotateMenu = new ActionCommand { Text = UiStrings.Rotate, - Image = iconProvider.GetIcon("arrow_rotate_anticlockwise") + IconName = "arrow_rotate_anticlockwise" }; RotateLeft = new ActionCommand(imageListActions.RotateLeft) { Text = UiStrings.RotateLeft, - Image = iconProvider.GetIcon("arrow_rotate_anticlockwise_small") + IconName = "arrow_rotate_anticlockwise_small" }; RotateRight = new ActionCommand(imageListActions.RotateRight) { Text = UiStrings.RotateRight, - Image = iconProvider.GetIcon("arrow_rotate_clockwise_small") + IconName = "arrow_rotate_clockwise_small" }; Flip = new ActionCommand(imageListActions.Flip) { Text = UiStrings.Flip, - Image = iconProvider.GetIcon("arrow_switch_small") + IconName = "arrow_switch_small" }; Deskew = new ActionCommand(imageListActions.Deskew) { @@ -210,17 +231,17 @@ public DesktopCommands(DesktopController desktopController, DesktopScanControlle MoveUp = new ActionCommand(imageListActions.MoveUp) { Text = UiStrings.MoveUp, - Image = iconProvider.GetIcon("arrow_up_small") + IconName = "arrow_up_small" }; MoveDown = new ActionCommand(imageListActions.MoveDown) { Text = UiStrings.MoveDown, - Image = iconProvider.GetIcon("arrow_down_small") + IconName = "arrow_down_small" }; ReorderMenu = new ActionCommand { Text = UiStrings.Reorder, - Image = iconProvider.GetIcon("arrow_refresh") + IconName = "arrow_refresh" }; Interleave = new ActionCommand(imageListActions.Interleave) { @@ -242,39 +263,51 @@ public DesktopCommands(DesktopController desktopController, DesktopScanControlle { Text = UiStrings.Reverse }; - ReverseAll = new ActionCommand(imageListActions.ReverseAll); - ReverseSelected = new ActionCommand(imageListActions.ReverseSelected); + ReverseAll = new ActionCommand(imageListActions.ReverseAll) + { + Text = UiStrings.ReverseAll + }; + ReverseSelected = new ActionCommand(imageListActions.ReverseSelected) + { + Text = UiStrings.ReverseSelected + }; Delete = new ActionCommand(desktopController.Delete) { Text = UiStrings.Delete, - Image = iconProvider.GetIcon("cross") + IconName = "cross" }; ClearAll = new ActionCommand(desktopController.Clear) { ToolBarText = UiStrings.Clear, MenuText = UiStrings.ClearAll, - Image = iconProvider.GetIcon("cancel"), - Shortcut = Application.Instance.CommonModifier | Keys.Shift | Keys.Delete + IconName = "broom" }; LanguageMenu = new ActionCommand { Text = UiStrings.Language, - Image = iconProvider.GetIcon("world") + IconName = "world" + }; + Settings = new ActionCommand(desktopSubFormController.ShowSettingsForm) + { + Text = UiStrings.Settings, + IconName = hiddenButtons.HasFlag(ToolbarButtons.About) ? "cog" : "cog_small" }; About = new ActionCommand(desktopSubFormController.ShowAboutForm) { Text = UiStrings.About, - Image = iconProvider.GetIcon("information") + IconName = hiddenButtons.HasFlag(ToolbarButtons.Settings) + ? "information" + : "information_small" }; ZoomIn = new ActionCommand(() => thumbnailController.StepSize(1)) { Text = UiStrings.ZoomIn, - Image = iconProvider.GetIcon("zoom_in") + IconName = "zoom_in_small" }; ZoomOut = new ActionCommand(() => thumbnailController.StepSize(-1)) { Text = UiStrings.ZoomOut, - Image = iconProvider.GetIcon("zoom_out") + IconName = "zoom_out_small" }; SelectAll = new ActionCommand(imageListActions.SelectAll) { @@ -282,29 +315,51 @@ public DesktopCommands(DesktopController desktopController, DesktopScanControlle }; Copy = new ActionCommand(desktopController.Copy) { - Text = UiStrings.Copy + Text = UiStrings.Copy, + IconName = "copy_small" }; Paste = new ActionCommand(desktopController.Paste) { - Text = UiStrings.Paste + Text = UiStrings.Paste, + IconName = "paste_small" + }; + Undo = new ActionCommand(imageListActions.Undo) + { + Text = UiStrings.Undo, + IconName = "undo_small" + }; + Redo = new ActionCommand(imageListActions.Redo) + { + Text = UiStrings.Redo, + IconName = "redo_small" + }; + ToggleSidebar = new ActionCommand(() => _desktopFormProvider.DesktopForm.ToggleSidebar()) + { + Text = UiStrings.ToggleSidebar, + IconName = "application_side_list_small" }; } - public DesktopCommands WithSelection(ListSelection selection) + public DesktopCommands WithSelection(Func> selectionFunc) { return new DesktopCommands( _desktopController, _desktopScanController, - _desktopSubFormController, + _desktopSubFormController.WithSelection(selectionFunc), _imageList, - _imageListActions.WithSelection(selection), + _imageListActions.WithSelection(selectionFunc), _thumbnailController, - _iconProvider); + _iconProvider, + _config, + _desktopFormProvider); } + public ImageListActions ImageListActions => _imageListActions; + public ActionCommand Scan { get; set; } public ActionCommand NewProfile { get; set; } public ActionCommand BatchScan { get; set; } + public ActionCommand ScannerSharing { get; set; } public ActionCommand Profiles { get; set; } public ActionCommand Ocr { get; set; } public ActionCommand Import { get; set; } @@ -331,6 +386,10 @@ public DesktopCommands WithSelection(ListSelection selection) public ActionCommand BlackWhite { get; set; } public ActionCommand Sharpen { get; set; } public ActionCommand DocumentCorrection { get; set; } + public ActionCommand Split { get; set; } + public ActionCommand Combine { get; set; } + public ActionCommand EditWithApp { get; set; } + public ActionCommand EditWithPick { get; set; } public ActionCommand ResetImage { get; set; } public ActionCommand RotateMenu { get; set; } public ActionCommand RotateLeft { get; set; } @@ -351,10 +410,14 @@ public DesktopCommands WithSelection(ListSelection selection) public ActionCommand Delete { get; set; } public ActionCommand ClearAll { get; set; } public ActionCommand LanguageMenu { get; set; } + public ActionCommand Settings { get; set; } public ActionCommand About { get; set; } public ActionCommand ZoomIn { get; set; } public ActionCommand ZoomOut { get; set; } public ActionCommand SelectAll { get; set; } public ActionCommand Copy { get; set; } public ActionCommand Paste { get; set; } + public ActionCommand Undo { get; set; } + public ActionCommand Redo { get; set; } + public ActionCommand ToggleSidebar { get; set; } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs b/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs index 54144ccb94..1ad1a5a578 100644 --- a/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs @@ -5,6 +5,8 @@ using Eto.Forms; using NAPS2.EtoForms.Desktop; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Notifications; +using NAPS2.EtoForms.Widgets; using NAPS2.ImportExport.Images; using NAPS2.Scan; @@ -13,48 +15,59 @@ namespace NAPS2.EtoForms.Ui; public abstract class DesktopForm : EtoFormBase { private readonly DesktopKeyboardShortcuts _keyboardShortcuts; - private readonly INotificationManager _notify; + private readonly NotificationManager _notificationManager; private readonly CultureHelper _cultureHelper; - private readonly IProfileManager _profileManager; - private readonly ImageTransfer _imageTransfer; + protected readonly ColorScheme _colorScheme; + protected readonly IProfileManager _profileManager; protected readonly ThumbnailController _thumbnailController; private readonly UiThumbnailProvider _thumbnailProvider; - private readonly DesktopController _desktopController; - private readonly IDesktopScanController _desktopScanController; + protected readonly DesktopController _desktopController; + protected readonly IDesktopScanController _desktopScanController; private readonly ImageListActions _imageListActions; private readonly DesktopFormProvider _desktopFormProvider; private readonly IDesktopSubFormController _desktopSubFormController; + private readonly ImageTransfer _imageTransfer = new(); + private readonly Lazy _commands; + private readonly Sidebar _sidebar; + protected readonly IIconProvider _iconProvider; - private readonly ListProvider _scanMenuCommands = new(); + protected readonly ListProvider _scanMenuCommands = new(); private readonly ListProvider _languageMenuCommands = new(); + protected readonly ListProvider _editWithCommands = new(); private readonly ContextMenu _contextMenu = new(); + private readonly NotificationArea _notificationArea; protected IListView _listView; private ImageListSyncer? _imageListSyncer; public DesktopForm( Naps2Config config, DesktopKeyboardShortcuts keyboardShortcuts, - INotificationManager notify, + NotificationManager notificationManager, CultureHelper cultureHelper, + ColorScheme colorScheme, IProfileManager profileManager, UiImageList imageList, - ImageTransfer imageTransfer, ThumbnailController thumbnailController, UiThumbnailProvider thumbnailProvider, DesktopController desktopController, IDesktopScanController desktopScanController, ImageListActions imageListActions, + ImageListViewBehavior imageListViewBehavior, DesktopFormProvider desktopFormProvider, IDesktopSubFormController desktopSubFormController, - DesktopCommands commands) : base(config) + Lazy commands, + Sidebar sidebar, + IIconProvider iconProvider) : base(config) { + Icon = EtoPlatform.Current.IsGtk ? new Icon(1f, Icons.scanner_128.ToEtoImage()) : Icons.favicon.ToEtoIcon(); + _keyboardShortcuts = keyboardShortcuts; - _notify = notify; + _notificationManager = notificationManager; _cultureHelper = cultureHelper; + _colorScheme = colorScheme; _profileManager = profileManager; ImageList = imageList; - _imageTransfer = imageTransfer; _thumbnailController = thumbnailController; _thumbnailProvider = thumbnailProvider; _desktopController = desktopController; @@ -62,121 +75,165 @@ public DesktopForm( _imageListActions = imageListActions; _desktopFormProvider = desktopFormProvider; _desktopSubFormController = desktopSubFormController; - Commands = commands; + _sidebar = sidebar; + _iconProvider = iconProvider; + _commands = commands; - // PostInitializeComponent(); - // + _desktopFormProvider.DesktopForm = this; + _keyboardShortcuts.Assign(Commands); CreateToolbarsAndMenus(); UpdateScanButton(); + EditWithAppChanged(); + UpdateProfilesToolbar(); InitLanguageDropdown(); - _listView = EtoPlatform.Current.CreateListView(new ImageListViewBehavior(_thumbnailProvider, _imageTransfer)); + _listView = EtoPlatform.Current.CreateListView(imageListViewBehavior); _listView.Selection = ImageList.Selection; _listView.ItemClicked += ListViewItemClicked; _listView.Drop += ListViewDrop; _listView.SelectionChanged += ListViewSelectionChanged; - _listView.ImageSize = _thumbnailController.VisibleSize; + _listView.ImageSize = new Size(_thumbnailController.VisibleSize, _thumbnailController.VisibleSize); _listView.ContextMenu = _contextMenu; // TODO: Fix Eto so that we don't need to set an item here (otherwise the first time we right click nothing happens) _contextMenu.Items.Add(Commands.SelectAll); _contextMenu.Opening += OpeningContextMenu; - _keyboardShortcuts.Assign(Commands); - KeyDown += OnKeyDown; - _listView.Control.KeyDown += OnKeyDown; - _listView.Control.MouseWheel += ListViewMouseWheel; + EtoPlatform.Current.AttachMouseWheelEvent(_listView.Control, ListViewMouseWheel); + EtoPlatform.Current.AttachMouseMoveEvent(_listView.Control, ListViewMouseMove); + EtoPlatform.Current.HandleKeyDown(this, _keyboardShortcuts.Perform); + EtoPlatform.Current.HandleKeyDown(_listView.Control, _keyboardShortcuts.Perform); // // Shown += FDesktop_Shown; // Closing += FDesktop_Closing; // Closed += FDesktop_Closed; - _desktopFormProvider.DesktopForm = this; _thumbnailController.ListView = _listView; _thumbnailController.ThumbnailSizeChanged += ThumbnailController_ThumbnailSizeChanged; - SetThumbnailSpacing(_thumbnailController.VisibleSize); + EtoPlatform.Current.AttachDpiDependency(this, scale => _thumbnailController.Oversample = scale); ImageList.SelectionChanged += ImageList_SelectionChanged; ImageList.ImagesUpdated += ImageList_ImagesUpdated; + ImageList.ImagesThumbnailInvalidated += ImageList_ImagesThumbnailInvalidated; _profileManager.ProfilesUpdated += ProfileManager_ProfilesUpdated; + _notificationArea = new NotificationArea(_notificationManager, LayoutController); } protected override void BuildLayout() { - Icon = Icons.favicon.ToEtoIcon(); - FormStateController.AutoLayoutSize = false; FormStateController.DefaultClientSize = new Size(1210, 600); + EtoPlatform.Current.AttachDpiDependency(this, _ => + MinimumSize = Size.Round(new SizeF(600, 300) * EtoPlatform.Current.GetLayoutScaleFactor(this))); LayoutController.RootPadding = 0; - LayoutController.Content = L.Overlay( - GetMainContent(), - L.Column( - C.Filler(), - L.Row(GetZoomButtons(), C.Filler()) - ).Padding(10) - ); + LayoutController.Content = L.LeftPanel( + Config.Get(c => c.HiddenButtons).HasFlag(ToolbarButtons.Sidebar) + ? C.None() + : _sidebar.CreateView(this), + L.Overlay( + // For WinForms, we add 1px of top padding to give us room to draw a border above the listview + _listView.Control.Padding(top: EtoPlatform.Current.IsWinForms ? 1 : 0), + L.Column( + C.Filler(), + L.Row( + GetControlButtons(), + C.Filler(), + _notificationArea.Content) + ).Padding(8) + ).Scale() + ).SizeConfig( + () => Config.Get(c => c.SidebarWidth), + width => Config.User.Set(c => c.SidebarWidth, width), + 200); } private void OpeningContextMenu(object? sender, EventArgs e) { _contextMenu.Items.Clear(); - Commands.Paste.Enabled = _imageTransfer.IsInClipboard(); + if (!EtoPlatform.Current.IsMac) + { + // TODO: Can't do this on Mac yet as it disables the menu item indefinitely + Commands.Paste.Enabled = _imageTransfer.IsInClipboard() || Clipboard.Instance.ContainsImage; + } if (ImageList.Selection.Any()) { - // TODO: Remove icon from delete command somehow // TODO: Is this memory leaking (because of event handlers) when commands are converted to menuitems? - _contextMenu.Items.AddRange(new List - { - Commands.ViewImage, + _contextMenu.Items.AddRange( + [ + C.ButtonMenuItem(this, Commands.ViewImage), new SeparatorMenuItem(), - Commands.SelectAll, - Commands.Copy, - Commands.Paste, + C.ButtonMenuItem(this, Commands.SelectAll), + C.ButtonMenuItem(this, Commands.Copy), + C.ButtonMenuItem(this, Commands.Paste), new SeparatorMenuItem(), - Commands.Delete - }); + C.ButtonMenuItem(this, Commands.Undo), + C.ButtonMenuItem(this, Commands.Redo), + new SeparatorMenuItem(), + C.ButtonMenuItem(this, Commands.Delete) + ]); } else { - _contextMenu.Items.AddRange(new List - { - Commands.SelectAll, - Commands.Paste - }); + _contextMenu.Items.AddRange( + [ + C.ButtonMenuItem(this, Commands.SelectAll), + C.ButtonMenuItem(this, Commands.Paste), + new SeparatorMenuItem(), + C.ButtonMenuItem(this, Commands.Undo), + C.ButtonMenuItem(this, Commands.Redo) + ]); } } private void ImageList_SelectionChanged(object? sender, EventArgs e) { - Invoker.Current.SafeInvoke(() => + Invoker.Current.InvokeDispatch(() => { UpdateToolbar(); - _listView!.Selection = ImageList.Selection; + _listView.Selection = ImageList.Selection; }); } private void ImageList_ImagesUpdated(object? sender, ImageListEventArgs e) { - Invoker.Current.SafeInvoke(UpdateToolbar); + Invoker.Current.InvokeDispatch(UpdateToolbar); + } + + private void ImageList_ImagesThumbnailInvalidated(object? sender, ImageListEventArgs e) + { + Invoker.Current.InvokeDispatch(UpdateToolbar); } private void ProfileManager_ProfilesUpdated(object? sender, EventArgs e) { UpdateScanButton(); + UpdateProfilesToolbar(); } private void ThumbnailController_ThumbnailSizeChanged(object? sender, EventArgs e) { - SetThumbnailSpacing(_thumbnailController.VisibleSize); + SetThumbnailSpacing(_thumbnailController.VisibleSize, EtoPlatform.Current.GetScaleFactor(this)); UpdateToolbar(); } protected UiImageList ImageList { get; } - protected DesktopCommands Commands { get; } + protected DesktopCommands Commands => _commands.Value; + + public void ReassignKeyboardShortcuts() + { + _keyboardShortcuts.Assign(Commands); + UpdateScanButton(); + RecreateToolbarsAndMenus(); + } + + protected virtual void RecreateToolbarsAndMenus() => CreateToolbarsAndMenus(); protected override void OnLoad(EventArgs e) { base.OnLoad(e); _imageListSyncer = new ImageListSyncer(ImageList, _listView.ApplyDiffs, SynchronizationContext.Current!); + EtoPlatform.Current.AttachDpiDependency(_listView.Control, + scale => SetThumbnailSpacing(_thumbnailController.VisibleSize, scale)); + _desktopController.PreInitialize(); } protected override async void OnShown(EventArgs e) @@ -186,13 +243,13 @@ protected override async void OnShown(EventArgs e) await _desktopController.Initialize(); } - // protected override void OnClosing(CancelEventArgs e) - // { - // if (!_desktopController.PrepareForClosing(e.CloseReason == CloseReason.UserClosing)) - // { - // e.Cancel = true; - // } - // } + protected override void OnClosing(CancelEventArgs e) + { + if (!_desktopController.PrepareForClosing(true)) + { + e.Cancel = true; + } + } protected override void OnClosed(EventArgs e) { @@ -203,23 +260,33 @@ protected override void OnClosed(EventArgs e) _thumbnailController.ThumbnailSizeChanged -= ThumbnailController_ThumbnailSizeChanged; ImageList.SelectionChanged -= ImageList_SelectionChanged; ImageList.ImagesUpdated -= ImageList_ImagesUpdated; + ImageList.ImagesThumbnailInvalidated -= ImageList_ImagesThumbnailInvalidated; _profileManager.ProfilesUpdated -= ProfileManager_ProfilesUpdated; + _notificationArea.Dispose(); _imageListSyncer?.Dispose(); } protected virtual void CreateToolbarsAndMenus() { - ToolBar = new ToolBar(); - ConfigureToolbar(); + if (ToolBar == null) + { + ToolBar = new ToolBar(); + ConfigureToolbars(); + } + else + { + ToolBar.Items.Clear(); + } var hiddenButtons = Config.Get(c => c.HiddenButtons); if (!hiddenButtons.HasFlag(ToolbarButtons.Scan)) - CreateToolbarButtonWithMenu(Commands.Scan, new MenuProvider() + CreateToolbarButtonWithMenu(Commands.Scan, DesktopToolbarMenuType.Scan, new MenuProvider() .Dynamic(_scanMenuCommands) .Separator() .Append(Commands.NewProfile) - .Append(Commands.BatchScan)); + .Append(Commands.BatchScan) + .Append(Config.Get(c => c.DisableScannerSharing) ? null : Commands.ScannerSharing)); if (!hiddenButtons.HasFlag(ToolbarButtons.Profiles)) CreateToolbarButton(Commands.Profiles); if (!hiddenButtons.HasFlag(ToolbarButtons.Ocr)) @@ -228,19 +295,19 @@ protected virtual void CreateToolbarsAndMenus() CreateToolbarButton(Commands.Import); CreateToolbarSeparator(); if (!hiddenButtons.HasFlag(ToolbarButtons.SavePdf)) - CreateToolbarButtonWithMenu(Commands.SavePdf, new MenuProvider() + CreateToolbarButtonWithMenu(Commands.SavePdf, DesktopToolbarMenuType.SavePdf, new MenuProvider() .Append(Commands.SaveAllPdf) .Append(Commands.SaveSelectedPdf) .Separator() .Append(Commands.PdfSettings)); if (!hiddenButtons.HasFlag(ToolbarButtons.SaveImages)) - CreateToolbarButtonWithMenu(Commands.SaveImages, new MenuProvider() + CreateToolbarButtonWithMenu(Commands.SaveImages, DesktopToolbarMenuType.SaveImages, new MenuProvider() .Append(Commands.SaveAllImages) .Append(Commands.SaveSelectedImages) .Separator() .Append(Commands.ImageSettings)); if (!hiddenButtons.HasFlag(ToolbarButtons.EmailPdf) && PlatformCompat.System.CanEmail) - CreateToolbarButtonWithMenu(Commands.EmailPdf, new MenuProvider() + CreateToolbarButtonWithMenu(Commands.EmailPdf, DesktopToolbarMenuType.EmailPdf, new MenuProvider() .Append(Commands.EmailAll) .Append(Commands.EmailSelected) .Separator() @@ -260,6 +327,12 @@ protected virtual void CreateToolbarsAndMenus() .Append(Commands.Sharpen) .Append(Commands.DocumentCorrection) .Separator() + .Append(Commands.Split) + .Append(Commands.Combine) + .Separator() + .Dynamic(_editWithCommands) + .Append(Commands.EditWithPick) + .Separator() .Append(Commands.ResetImage)); if (!hiddenButtons.HasFlag(ToolbarButtons.Rotate)) CreateToolbarMenu(Commands.RotateMenu, GetRotateMenuProvider()); @@ -284,8 +357,29 @@ protected virtual void CreateToolbarsAndMenus() CreateToolbarSeparator(); if (!hiddenButtons.HasFlag(ToolbarButtons.Language)) CreateToolbarMenu(Commands.LanguageMenu, GetLanguageMenuProvider()); - if (!hiddenButtons.HasFlag(ToolbarButtons.About)) - CreateToolbarButton(Commands.About); + MaybeCreateToolbarStackedButtons( + Commands.Settings, !hiddenButtons.HasFlag(ToolbarButtons.Settings), + Commands.About, !hiddenButtons.HasFlag(ToolbarButtons.About)); + } + + private void MaybeCreateToolbarStackedButtons(Command command1, bool show1, Command command2, bool show2) + { + if (show1 && show2) + { + CreateToolbarStackedButtons(command1, command2); + } + else if (show1) + { + CreateToolbarButton(command1); + } + else if (show2) + { + CreateToolbarButton(command2); + } + } + + public virtual void ShowToolbarMenu(DesktopToolbarMenuType menuType) + { } protected MenuProvider GetRotateMenuProvider() => @@ -301,13 +395,22 @@ protected MenuProvider GetLanguageMenuProvider() return new MenuProvider().Dynamic(_languageMenuCommands); } - protected virtual void ConfigureToolbar() + protected virtual void ConfigureToolbars() + { + } + + protected virtual void UpdateProfilesToolbar() + { + } + + public virtual void PlaceProfilesToolbar() { } protected virtual void CreateToolbarButton(Command command) => throw new InvalidOperationException(); - protected virtual void CreateToolbarButtonWithMenu(Command command, MenuProvider menu) => + protected virtual void CreateToolbarButtonWithMenu(Command command, DesktopToolbarMenuType menuType, + MenuProvider menu) => throw new InvalidOperationException(); protected virtual void CreateToolbarMenu(Command command, MenuProvider menu) => @@ -323,9 +426,10 @@ protected SubMenuItem CreateSubMenu(Command menuCommand, MenuProvider menuProvid { var menuItem = new SubMenuItem { - Text = menuCommand.MenuText, - Image = menuCommand.Image + Text = menuCommand.MenuText }; + EtoPlatform.Current.AttachDpiDependency(this, + scale => menuItem.Image = ((ActionCommand) menuCommand).GetIconImage(scale)); menuProvider.Handle(subItems => { menuItem.Items.Clear(); @@ -334,51 +438,45 @@ protected SubMenuItem CreateSubMenu(Command menuCommand, MenuProvider menuProvid switch (subItem) { case MenuProvider.CommandItem { Command: var command }: - menuItem.Items.Add(new ButtonMenuItem(command)); + menuItem.Items.Add(C.ButtonMenuItem(this, (ActionCommand) command)); break; case MenuProvider.SeparatorItem: menuItem.Items.Add(new SeparatorMenuItem()); break; - case MenuProvider.SubMenuItem: - throw new NotImplementedException(); + case MenuProvider.SubMenuItem { Command: var command, MenuProvider: var subMenuProvider }: + menuItem.Items.Add(CreateSubMenu(command, subMenuProvider)); + break; } } }); return menuItem; } - protected virtual LayoutElement GetMainContent() => _listView.Control; + protected virtual LayoutElement GetControlButtons() + { + return L.Row(GetSidebarButton(), GetZoomButtons()); + } - protected virtual LayoutElement GetZoomButtons() + protected LayoutElement GetSidebarButton() + { + if (Config.Get(c => c.HiddenButtons).HasFlag(ToolbarButtons.Sidebar)) + { + return C.None(); + } + var toggleSidebar = C.ImageButton(Commands.ToggleSidebar); + EtoPlatform.Current.ConfigureZoomButton(toggleSidebar, "application_side_list_small"); + return toggleSidebar.AlignTrailing(); + } + + protected LayoutElement GetZoomButtons() { var zoomIn = C.ImageButton(Commands.ZoomIn); - EtoPlatform.Current.ConfigureZoomButton(zoomIn); + EtoPlatform.Current.ConfigureZoomButton(zoomIn, "zoom_in_small"); var zoomOut = C.ImageButton(Commands.ZoomOut); - EtoPlatform.Current.ConfigureZoomButton(zoomOut); - return L.Row(zoomOut, zoomIn).Spacing(-1); - } - - // /// - // /// Runs when the form is first loaded and every time the language is changed. - // /// - // private void PostInitializeComponent() - // { - // - // int thumbnailSize = Config.ThumbnailSize(); - // _listView.ImageSize = thumbnailSize; - // SetThumbnailSpacing(thumbnailSize); - // - // LoadToolStripLocation(); - // InitLanguageDropdown(); - // AssignKeyboardShortcuts(); - // UpdateScanButton(); - // - // _listView.NativeControl.SizeChanged += (_, _) => _layoutManager.UpdateLayout(); - // - // _imageListSyncer = new ImageListSyncer(_imageList, _listView.ApplyDiffs, SynchronizationContext.Current!); - // _listView.NativeControl.Focus(); - // } - // + EtoPlatform.Current.ConfigureZoomButton(zoomOut, "zoom_out_small"); + return L.Row(zoomOut.AlignTrailing(), zoomIn.AlignTrailing()).Spacing(-1); + } + private void InitLanguageDropdown() { _languageMenuCommands.Value = _cultureHelper.GetAvailableCultures().Select(x => @@ -388,7 +486,7 @@ private void InitLanguageDropdown() }).ToImmutableList(); } - private void SetCulture(string cultureId) + protected virtual void SetCulture(string cultureId) { _desktopController.Suspend(); try @@ -398,7 +496,7 @@ private void SetCulture(string cultureId) FormStateController.DoSaveFormState(); var newDesktop = FormFactory.Create(); newDesktop.Show(); - SetMainForm(newDesktop); + Application.Instance.MainForm = newDesktop; Close(); } finally @@ -408,11 +506,6 @@ private void SetCulture(string cultureId) // TODO: If we make any other forms non-modal, we will need to refresh them too } - protected virtual void SetMainForm(Form newMainForm) - { - Application.Instance.MainForm = newMainForm; - } - protected virtual void UpdateToolbar() { // Top-level toolbar items @@ -423,25 +516,28 @@ protected virtual void UpdateToolbar() Commands.ReorderMenu.Enabled = Commands.EmailPdf.Enabled = Commands.Print.Enabled = ImageList.Images.Any(); - // TODO: Changing the text on the command doesn't actually propagate to the widget // "All" dropdown items - Commands.SaveAllPdf.MenuText = Commands.SaveAllImages.MenuText = Commands.EmailAll.MenuText = - Commands.ReverseAll.MenuText = string.Format(MiscResources.AllCount, ImageList.Images.Count); - Commands.SaveAllPdf.Enabled = Commands.SaveAllImages.Enabled = Commands.EmailAll.Enabled = - Commands.ReverseAll.Enabled = ImageList.Images.Any(); + Commands.SaveAllPdf.Text = Commands.SaveAllImages.Text = Commands.EmailAll.Text = + Commands.ReverseAll.Text = string.Format(MiscResources.AllCount, ImageList.Images.Count); + Commands.SaveAllPdf.Enabled = Commands.SaveAllImages.Enabled = Commands.SaveAll.Enabled = + Commands.EmailAll.Enabled = Commands.ReverseAll.Enabled = ImageList.Images.Any(); // "Selected" dropdown items - Commands.SaveSelectedPdf.MenuText = Commands.SaveSelectedImages.MenuText = Commands.EmailSelected.MenuText = - Commands.ReverseSelected.MenuText = string.Format(MiscResources.SelectedCount, ImageList.Selection.Count); - Commands.SaveSelectedPdf.Enabled = Commands.SaveSelectedImages.Enabled = Commands.EmailSelected.Enabled = - Commands.ReverseSelected.Enabled = ImageList.Selection.Any(); + Commands.SaveSelectedPdf.Text = Commands.SaveSelectedImages.Text = Commands.EmailSelected.Text = + Commands.ReverseSelected.Text = string.Format(MiscResources.SelectedCount, ImageList.Selection.Count); + Commands.SaveSelectedPdf.Enabled = Commands.SaveSelectedImages.Enabled = Commands.SaveSelected.Enabled = + Commands.EmailSelected.Enabled = Commands.ReverseSelected.Enabled = ImageList.Selection.Any(); // Other Commands.SelectAll.Enabled = ImageList.Images.Any(); + Commands.Undo.Enabled = ImageList.CanUndo; + Commands.Redo.Enabled = ImageList.CanRedo; + // TODO: Set undo/redo text here (e.g. "Undo Brightness/Contrast" instead of just "Undo") Commands.ZoomIn.Enabled = ImageList.Images.Any() && _thumbnailController.VisibleSize < ThumbnailSizes.MAX_SIZE; Commands.ZoomOut.Enabled = ImageList.Images.Any() && _thumbnailController.VisibleSize > ThumbnailSizes.MIN_SIZE; Commands.NewProfile.Enabled = !(Config.Get(c => c.NoUserProfiles) && _profileManager.Profiles.Any(x => x.IsLocked)); + Commands.Combine.Enabled = ImageList.Images.Count > 1; } private void UpdateScanButton() @@ -451,9 +547,8 @@ private void UpdateScanButton() var commandList = _profileManager.Profiles.Select(profile => new ActionCommand(() => _desktopScanController.ScanWithProfile(profile)) { - // TODO: Does this need to change on non-WinForms? MenuText = profile.DisplayName.Replace("&", "&&"), - Image = profile == defaultProfile ? Icons.accept_small.ToEtoImage() : null + IconName = profile == defaultProfile ? "accept_small" : null }) .ToImmutableList(); for (int i = 0; i < commandList.Count; i++) @@ -463,10 +558,18 @@ private void UpdateScanButton() _scanMenuCommands.Value = commandList; } - private void OnKeyDown(object? sender, KeyEventArgs e) + public void EditWithAppChanged() { - // TODO: The custom listview control isn't propagating events back to the parent window - e.Handled = _keyboardShortcuts.Perform(e.KeyData); + var appName = Config.Get(c => c.EditWithAppName); + if (!string.IsNullOrEmpty(appName)) + { + Commands.EditWithApp.Text = string.Format(UiStrings.EditWithAppName, appName); + _editWithCommands.Value = ImmutableList.Create((Command) Commands.EditWithApp); + } + else + { + _editWithCommands.Value = ImmutableList.Empty; + } } protected virtual void UpdateTitle(ScanProfile? defaultProfile) @@ -478,11 +581,17 @@ private void ListViewMouseWheel(object? sender, MouseEventArgs e) { if (e.Modifiers.HasFlag(Keys.Control)) { - _thumbnailController.StepSize(e.Delta.Height); // / (double) SystemInformation.MouseWheelScrollDelta + _thumbnailController.StepSize(e.Delta.Height); + e.Handled = true; } } - protected virtual void SetThumbnailSpacing(int thumbnailSize) + private void ListViewMouseMove(object? sender, MouseEventArgs e) + { + _notificationManager.StartTimers(); + } + + protected virtual void SetThumbnailSpacing(int thumbnailSize, float scale) { } @@ -525,4 +634,9 @@ private void DragMoveImages(int position) _imageListActions.MoveTo(position); } } + + public void ToggleSidebar() + { + _sidebar.ToggleVisibility(); + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/DownloadProgressForm.cs b/NAPS2.Lib/EtoForms/Ui/DownloadProgressForm.cs index b5363e7b7b..a090465015 100644 --- a/NAPS2.Lib/EtoForms/Ui/DownloadProgressForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/DownloadProgressForm.cs @@ -1,11 +1,9 @@ using System.ComponentModel; -using System.Net; -using System.Security.Cryptography; using Eto.Drawing; using Eto.Forms; using NAPS2.Dependencies; using NAPS2.EtoForms.Layout; -using Label = Eto.Forms.Label; +using NAPS2.Scan; namespace NAPS2.EtoForms.Ui; @@ -16,33 +14,28 @@ public class DownloadProgressForm : EtoDialogBase private readonly ProgressBar _totalProgressBar = new(); private readonly ProgressBar _fileProgressBar = new(); - private readonly List _filesToDownload = new(); - private int _filesDownloaded; - private int _urlIndex; - private double _currentFileSize; - private double _currentFileProgress; - private bool _hasError; - private bool _cancel; + public DownloadProgressForm(ScanningContext scanningContext, Naps2Config config, IIconProvider iconProvider) : + base(config) + { + Title = UiStrings.DownloadProgressFormTitle; + IconName = "text_small"; - // TODO: Migrate to HttpClient -#pragma warning disable SYSLIB0014 - private readonly WebClient _client = new(); -#pragma warning restore SYSLIB0014 + Controller = new DownloadController(scanningContext); + Controller.DownloadError += OnDownloadError; + Controller.DownloadComplete += (_, _) => Close(); + Controller.DownloadProgress += OnDownloadProgress; + } - public DownloadProgressForm(Naps2Config config) : base(config) + private void OnDownloadError(object? sender, EventArgs e) { - // TODO: Is this needed for net462? - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; - - _client.DownloadFileCompleted += client_DownloadFileCompleted; - _client.DownloadProgressChanged += client_DownloadProgressChanged; + MessageBox.Show(MiscResources.FilesCouldNotBeDownloaded, MiscResources.DownloadError, MessageBoxButtons.OK, + MessageBoxType.Error); } + public DownloadController Controller { get; } + protected override void BuildLayout() { - Title = UiStrings.DownloadProgressFormTitle; - Icon = new Icon(1f, Icons.text_small.ToEtoImage()); - FormStateController.RestoreFormState = false; LayoutController.Content = L.Column( @@ -60,139 +53,30 @@ protected override void BuildLayout() protected override void OnLoad(EventArgs e) { base.OnLoad(e); - DisplayProgress(); - StartNextDownload(); - } - - void client_DownloadProgressChanged(object? sender, DownloadProgressChangedEventArgs e) - { - _currentFileProgress = e.BytesReceived; - _currentFileSize = e.TotalBytesToReceive; - DisplayProgress(); - } - - void client_DownloadFileCompleted(object? sender, AsyncCompletedEventArgs e) - { - var file = _filesToDownload[_filesDownloaded]; - if (e.Error != null) - { - _hasError = true; - if (!_cancel) - { - Log.ErrorException("Error downloading file: " + file.DownloadInfo.FileName, e.Error); - } - } - else if (file.DownloadInfo.Sha1 != CalculateSha1(Path.Combine(file.TempFolder!, file.DownloadInfo.FileName))) - { - _hasError = true; - Log.Error("Error downloading file (invalid checksum): " + file.DownloadInfo.FileName); - } - else - { - _filesDownloaded++; - } - _currentFileProgress = 0; - _currentFileSize = 0; - DisplayProgress(); - StartNextDownload(); - } - - private string CalculateSha1(string filePath) - { - using var sha = SHA1.Create(); - using FileStream stream = File.OpenRead(filePath); - byte[] checksum = sha.ComputeHash(stream); - string str = BitConverter.ToString(checksum).Replace("-", String.Empty).ToLowerInvariant(); - return str; - } - - private void StartNextDownload() - { - if (_hasError) - { - var prev = _filesToDownload[_filesDownloaded]; - Directory.Delete(prev.TempFolder!, true); - if (_cancel) - { - return; - } - // Retry if possible - _urlIndex++; - _hasError = false; - } - else - { - _urlIndex = 0; - } - if (_filesDownloaded > 0 && _urlIndex == 0) - { - var prev = _filesToDownload[_filesDownloaded - 1]; - var filePath = Path.Combine(prev.TempFolder!, prev.DownloadInfo.FileName); - try - { - var preparedFilePath = prev.DownloadInfo.Format.Prepare(filePath); - prev.FileCallback(preparedFilePath); - } - catch (Exception ex) - { - Log.ErrorException("Error preparing downloaded file", ex); - MessageBox.Show(MiscResources.FilesCouldNotBeDownloaded, MiscResources.DownloadError, MessageBoxButtons.OK, MessageBoxType.Error); - } - Directory.Delete(prev.TempFolder!, true); - } - if (_filesDownloaded >= _filesToDownload.Count) - { - Close(); - return; - } - if (_urlIndex >= _filesToDownload[_filesDownloaded].DownloadInfo.Urls.Count) - { - Close(); - MessageBox.Show(MiscResources.FilesCouldNotBeDownloaded, MiscResources.DownloadError, MessageBoxButtons.OK, MessageBoxType.Error); - return; - } - var next = _filesToDownload[_filesDownloaded]; - next.TempFolder = Path.Combine(Paths.Temp, Path.GetRandomFileName()); - Directory.CreateDirectory(next.TempFolder); - _client.DownloadFileAsync(new Uri(next.DownloadInfo.Urls[_urlIndex]), Path.Combine(next.TempFolder, next.DownloadInfo.FileName)); + Controller.StartDownloadsAsync().AssertNoAwait(); } - public void QueueFile(DownloadInfo downloadInfo, Action fileCallback) + private void OnDownloadProgress(object? sender, EventArgs e) { - _filesToDownload.Add(new QueueItem { DownloadInfo = downloadInfo, FileCallback = fileCallback }); - } - - public void QueueFile(IExternalComponent component) - { - _filesToDownload.Add(new QueueItem { DownloadInfo = component.DownloadInfo, FileCallback = component.Install }); - } - - private void DisplayProgress() - { - _totalStatus.Text = string.Format(MiscResources.FilesProgressFormat, _filesDownloaded, _filesToDownload.Count); - _totalProgressBar.MaxValue = _filesToDownload.Count * 1000; - _totalProgressBar.Value = _filesDownloaded * 1000 + (_currentFileSize == 0 ? 0 : (int)(_currentFileProgress / _currentFileSize * 1000)); - _fileStatus.Text = string.Format(MiscResources.SizeProgress, (_currentFileProgress / 1e6).ToString("f1"), (_currentFileSize / 1e6).ToString("f1")); - if (_currentFileSize > 0) + var f = Controller.FilesDownloaded; + var fTot = Controller.TotalFiles; + var c = Controller.CurrentFileProgress; + var cTot = Controller.CurrentFileSize; + _totalStatus.Text = string.Format(MiscResources.FilesProgressFormat, f, fTot); + _totalProgressBar.MaxValue = fTot * 1000; + _totalProgressBar.Value = f * 1000 + (cTot == 0 ? 0 : (int) (c * 1e3 / cTot)); + _fileStatus.Text = + string.Format(MiscResources.SizeProgress, (c / 1e6).ToString("f1"), (cTot / 1e6).ToString("f1")); + if (c > 0) { - _fileProgressBar.MaxValue = (int) (_currentFileSize); - _fileProgressBar.Value = (int) (_currentFileProgress); + _fileProgressBar.MaxValue = (int) cTot; + _fileProgressBar.Value = (int) c; } } protected override void OnClosing(CancelEventArgs e) { base.OnClosing(e); - _cancel = true; - _client.CancelAsync(); - } - - private class QueueItem - { - public required DownloadInfo DownloadInfo { get; set; } - - public string? TempFolder { get; set; } - - public required Action FileCallback { get; set; } + Controller.Stop(); } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/EditProfileForm.cs b/NAPS2.Lib/EtoForms/Ui/EditProfileForm.cs index 3f1282fb83..624fbab263 100644 --- a/NAPS2.Lib/EtoForms/Ui/EditProfileForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/EditProfileForm.cs @@ -1,126 +1,109 @@ +using System.Globalization; +using System.Threading; using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; using NAPS2.Scan; -using NAPS2.Scan.Exceptions; using NAPS2.Scan.Internal; namespace NAPS2.EtoForms.Ui; public class EditProfileForm : EtoDialogBase { - private readonly IScanPerformer _scanPerformer; private readonly ErrorOutput _errorOutput; private readonly ProfileNameTracker _profileNameTracker; + private readonly DeviceCapsCache _deviceCapsCache; private readonly TextBox _displayName = new(); - private readonly RadioButton _wiaDriver; - private readonly RadioButton _twainDriver; - private readonly RadioButton _appleDriver; - private readonly RadioButton _saneDriver; - private readonly TextBox _deviceName = new() { Enabled = false }; - private readonly Button _chooseDevice = new() { Text = UiStrings.ChooseDevice }; + private readonly DeviceSelectorWidget _deviceSelectorWidget; private readonly RadioButton _predefinedSettings; private readonly RadioButton _nativeUi; - private readonly DropDown _paperSource = C.EnumDropDown(); - private readonly DropDown _pageSize = C.EnumDropDown(); - private readonly DropDown _resolution = C.EnumDropDown(); - private readonly DropDown _bitDepth = C.EnumDropDown(); - private readonly DropDown _horAlign = C.EnumDropDown(); - private readonly DropDown _scale = C.EnumDropDown(); + private readonly LayoutVisibility _nativeUiVis = new(true); + private readonly EnumDropDownWidget _paperSource = new(); + private readonly PageSizeDropDownWidget _pageSize; + private readonly ResolutionDropDownWidget _resolution; + private readonly EnumDropDownWidget _bitDepth = new(); + private readonly EnumDropDownWidget _horAlign = new(); + private readonly EnumDropDownWidget _scale = new(); private readonly CheckBox _enableAutoSave = new() { Text = UiStrings.EnableAutoSave }; - private readonly LinkButton _autoSaveSettings = new() { Text = UiStrings.AutoSaveSettings }; + private readonly LinkButton _autoSaveSettings = C.Link(UiStrings.AutoSaveSettings); private readonly Button _advanced = new() { Text = UiStrings.Advanced }; - private readonly Button _ok = new() { Text = UiStrings.OK }; - private readonly Button _cancel = new() { Text = UiStrings.Cancel }; private readonly SliderWithTextBox _brightnessSlider = new(); private readonly SliderWithTextBox _contrastSlider = new(); private ScanProfile _scanProfile = null!; - private ScanDevice? _currentDevice; private bool _isDefault; private bool _result; private bool _suppressChangeEvent; + private CancellationTokenSource? _updateCapsCts; public EditProfileForm(Naps2Config config, IScanPerformer scanPerformer, ErrorOutput errorOutput, - ProfileNameTracker profileNameTracker) : base(config) + ProfileNameTracker profileNameTracker, DeviceCapsCache deviceCapsCache, + IIconProvider iconProvider) : base(config) { - _scanPerformer = scanPerformer; + Title = UiStrings.EditProfileFormTitle; + IconName = "blueprints_small"; + _errorOutput = errorOutput; _profileNameTracker = profileNameTracker; + _deviceCapsCache = deviceCapsCache; + _deviceSelectorWidget = new(scanPerformer, deviceCapsCache, iconProvider, this) + { + ProfileFunc = GetUpdatedScanProfile, + AllowAlwaysAsk = true + }; + _pageSize = new(this); + _resolution = new(this); + _deviceSelectorWidget.DeviceChanged += DeviceChanged; - _wiaDriver = new RadioButton { Text = UiStrings.WiaDriver }; - _twainDriver = new RadioButton(_wiaDriver) { Text = UiStrings.TwainDriver }; - _appleDriver = new RadioButton(_wiaDriver) { Text = UiStrings.AppleDriver }; - _saneDriver = new RadioButton(_wiaDriver) { Text = UiStrings.SaneDriver }; _predefinedSettings = new RadioButton { Text = UiStrings.UsePredefinedSettings }; _nativeUi = new RadioButton(_predefinedSettings) { Text = UiStrings.UseNativeUi }; - _pageSize.SelectedIndexChanged += PageSize_SelectedIndexChanged; - _wiaDriver.CheckedChanged += Driver_CheckedChanged; - _twainDriver.CheckedChanged += Driver_CheckedChanged; - _appleDriver.CheckedChanged += Driver_CheckedChanged; - _saneDriver.CheckedChanged += Driver_CheckedChanged; + _paperSource.SelectedItemChanged += PaperSource_SelectedItemChanged; _predefinedSettings.CheckedChanged += PredefinedSettings_CheckedChanged; _nativeUi.CheckedChanged += NativeUi_CheckedChanged; - _ok.Click += Ok_Click; - _cancel.Click += Cancel_Click; - _chooseDevice.Click += ChooseDevice; _enableAutoSave.CheckedChanged += EnableAutoSave_CheckedChanged; _autoSaveSettings.Click += AutoSaveSettings_LinkClicked; _advanced.Click += Advanced_Click; - _deviceName.KeyDown += DeviceName_KeyDown; } - protected override void BuildLayout() + public void SetDevice(ScanDevice device) { - // TODO: Don't show if only one driver is available - var driverElements = new List(); - if (PlatformCompat.System.IsWiaDriverSupported) - { - driverElements.Add(_wiaDriver.Scale()); - } - if (PlatformCompat.System.IsTwainDriverSupported) - { - driverElements.Add(_twainDriver.Scale()); - } - if (PlatformCompat.System.IsAppleDriverSupported) - { - driverElements.Add(_appleDriver.Scale()); - } - if (PlatformCompat.System.IsSaneDriverSupported) + _deviceSelectorWidget.Choice = DeviceChoice.ForDevice(device); + } + + private void DeviceChanged(object? sender, DeviceChangedEventArgs e) + { + if (e.NewChoice.Device != null && (string.IsNullOrEmpty(_displayName.Text) || + e.PreviousChoice.Device?.Name == _displayName.Text)) { - driverElements.Add(_saneDriver.Scale()); + _displayName.Text = e.NewChoice.Device.Name; } + DeviceDriver = e.NewChoice.Driver; + IconUri = e.NewChoice.Device?.IconUri; - Title = UiStrings.EditProfileFormTitle; - Icon = new Icon(1f, Icons.blueprints_small.ToEtoImage()); + UpdateCaps(); + UpdateEnabledControls(); + } + protected override void BuildLayout() + { FormStateController.DefaultExtraLayoutSize = new Size(60, 0); FormStateController.FixedHeightLayout = true; LayoutController.Content = L.Column( - L.Row( - L.Column( - C.Label(UiStrings.DisplayNameLabel), - _displayName, - L.Row( - driverElements.ToArray() - ), - C.Spacer(), - C.Label(UiStrings.DeviceLabel), - L.Row( - _deviceName.Scale(), - _chooseDevice - ) - ).Scale(), - new ImageView { Image = Icons.scanner_48.ToEtoImage() } - ), + C.Label(UiStrings.DisplayNameLabel), + _displayName, C.Spacer(), - L.Row( - _predefinedSettings, - _nativeUi - ), + _deviceSelectorWidget, + C.Spacer(), + PlatformCompat.System.IsWiaDriverSupported || PlatformCompat.System.IsTwainDriverSupported + ? L.Row( + _predefinedSettings, + _nativeUi + ).Visible(_nativeUiVis) + : C.None(), C.Spacer(), L.Row( L.Column( @@ -153,8 +136,8 @@ protected override void BuildLayout() _advanced, C.Filler(), L.OkCancel( - _ok, - _cancel) + C.OkButton(this, SaveSettings), + C.CancelButton(this)) ) ); } @@ -164,209 +147,220 @@ protected override void BuildLayout() public ScanProfile ScanProfile { get => _scanProfile; - set => _scanProfile = value.Clone(); - } - - public ScanDevice? CurrentDevice - { - get => _currentDevice; set { - _currentDevice = value; - _deviceName.Text = value?.Name ?? ""; + _scanProfile = value.Clone(); + UpdateUiForScanProfile(); } } - private Driver DeviceDriver - { - get => _twainDriver.Checked ? Driver.Twain - : _wiaDriver.Checked ? Driver.Wia - : _appleDriver.Checked ? Driver.Apple - : _saneDriver.Checked ? Driver.Sane - : ScanOptionsValidator.SystemDefaultDriver; - set - { - if (value == Driver.Twain) - { - _twainDriver.Checked = true; - } - else if (value == Driver.Wia) - { - _wiaDriver.Checked = true; - } - else if (value == Driver.Apple) - { - _appleDriver.Checked = true; - } - else if (value == Driver.Sane) - { - _saneDriver.Checked = true; - } - } - } + public bool NewProfile { get; set; } - protected override void OnLoad(EventArgs e) + private void UpdateUiForCaps() { - base.OnLoad(e); - - // Don't trigger any onChange events _suppressChangeEvent = true; - _displayName.Text = ScanProfile.DisplayName; - CurrentDevice ??= ScanProfile.Device; - _isDefault = ScanProfile.IsDefault; + _paperSource.Items = ScanProfile.Caps?.PaperSources?.Values is [_, ..] paperSources + ? paperSources + : EnumDropDownWidget.DefaultItems; - _paperSource.SelectedIndex = (int) ScanProfile.PaperSource; - _bitDepth.SelectedIndex = (int) ScanProfile.BitDepth; - _resolution.SelectedIndex = (int) ScanProfile.Resolution; - _contrastSlider.Value = ScanProfile.Contrast; - _brightnessSlider.Value = ScanProfile.Brightness; - UpdatePageSizeList(); - SelectPageSize(); - _scale.SelectedIndex = (int) ScanProfile.AfterScanScale; - _horAlign.SelectedIndex = (int) ScanProfile.PageAlign; + var selectedSource = _paperSource.SelectedItem; + var perSource = selectedSource switch + { + ScanSource.Glass => ScanProfile.Caps?.Glass, + ScanSource.Feeder => ScanProfile.Caps?.Feeder, + ScanSource.Duplex => ScanProfile.Caps?.Duplex, + _ => null + }; - _enableAutoSave.Checked = ScanProfile.EnableAutoSave; + var validResolutions = perSource?.Resolutions; + _resolution.VisiblePresets = validResolutions is [_, ..] + ? validResolutions + : EnumDropDownWidget.DefaultItems.Select(x => x.ToIntDpi()); - DeviceDriver = new ScanOptionsValidator().ValidateDriver( - Enum.TryParse(ScanProfile.DriverName, true, out var driver) - ? driver - : Driver.Default); + var scanArea = perSource?.ScanArea; + var sizeCaps = new PageSizeCaps { ScanArea = scanArea }; - _nativeUi.Checked = ScanProfile.UseNativeUI; - _predefinedSettings.Checked = !ScanProfile.UseNativeUI; + var allPresets = EnumDropDownWidget.DefaultItems.SkipLast(2).ToList(); + var conditionalPresets = new[] { ScanPageSize.A3, ScanPageSize.B4 }; + _pageSize.VisiblePresets = allPresets.Where(preset => + !conditionalPresets.Contains(preset) || sizeCaps.Fits(preset.PageDimensions()!.ToPageSize())); - // Start triggering onChange events again _suppressChangeEvent = false; - - UpdateEnabledControls(); } - private async void ChooseDevice(object? sender, EventArgs args) + private void UpdateCaps() { - ScanProfile.DriverName = DeviceDriver.ToString().ToLowerInvariant(); - try + var cts = new CancellationTokenSource(); + _updateCapsCts?.Cancel(); + _updateCapsCts = cts; + var updatedProfile = GetUpdatedScanProfile(); + var cachedCaps = _deviceCapsCache.GetCachedCaps(updatedProfile); + if (cachedCaps != null) { - var device = await _scanPerformer.PromptForDevice(ScanProfile, NativeHandle); - if (device != null) - { - if (string.IsNullOrEmpty(_displayName.Text) || - CurrentDevice != null && CurrentDevice.Name == _displayName.Text) - { - _displayName.Text = device.Name; - } - CurrentDevice = device; - } + ScanProfile.Caps = MapCaps(cachedCaps); } - catch (ScanDriverException ex) + else { - if (ex is ScanDriverUnknownException) - { - Log.ErrorException(ex.Message, ex.InnerException!); - _errorOutput.DisplayError(ex.Message, ex); - } - else + ScanProfile.Caps = null; + if (updatedProfile.Device != null) { - _errorOutput.DisplayError(ex.Message); + Task.Run(async () => + { + var caps = await _deviceCapsCache.QueryCaps(updatedProfile); + if (caps != null) + { + Invoker.Current.Invoke(() => + { + if (!cts.IsCancellationRequested) + { + ScanProfile.Caps = MapCaps(caps); + UpdateUiForCaps(); + } + }); + } + }); } } + UpdateUiForCaps(); } - private void UpdatePageSizeList() + private ScanProfileCaps MapCaps(ScanCaps? caps) { - _pageSize.Items.Clear(); - - // Defaults - foreach (ScanPageSize item in Enum.GetValues(typeof(ScanPageSize))) + List? paperSources = null; + if (caps?.PaperSourceCaps is { } paperSourceCaps) { - _pageSize.Items.Add(new PageSizeListItem - { - Type = item, - Text = item.Description() - }); + paperSources = new List(); + if (paperSourceCaps.SupportsFlatbed) paperSources.Add(ScanSource.Glass); + if (paperSourceCaps.SupportsFeeder) paperSources.Add(ScanSource.Feeder); + if (paperSourceCaps.SupportsDuplex) paperSources.Add(ScanSource.Duplex); } - // Custom Presets - foreach (var preset in Config.Get(c => c.CustomPageSizePresets).OrderBy(x => x.Name)) + return new ScanProfileCaps { - _pageSize.Items.Insert(_pageSize.Items.Count - 1, new PageSizeListItem + PaperSources = new PaperSourceProfileCaps { Values = paperSources }, + FeederCheck = caps?.PaperSourceCaps?.CanCheckIfFeederHasPaper, + Glass = new PerSourceProfileCaps { - Type = ScanPageSize.Custom, - Text = string.Format(MiscResources.NamedPageSizeFormat, preset.Name, preset.Dimens.Width, - preset.Dimens.Height, preset.Dimens.Unit.Description()), - CustomName = preset.Name, - CustomDimens = preset.Dimens - }); - } + ScanArea = caps?.FlatbedCaps?.PageSizeCaps?.ScanArea, + Resolutions = caps?.FlatbedCaps?.DpiCaps?.CommonValues?.ToList() + }, + Feeder = new PerSourceProfileCaps + { + ScanArea = caps?.FeederCaps?.PageSizeCaps?.ScanArea, + Resolutions = caps?.FeederCaps?.DpiCaps?.CommonValues?.ToList() + }, + Duplex = new PerSourceProfileCaps + { + ScanArea = caps?.DuplexCaps?.PageSizeCaps?.ScanArea, + Resolutions = caps?.DuplexCaps?.DpiCaps?.CommonValues?.ToList() + } + }; + } + + private Driver DeviceDriver { get; set; } + + private string? IconUri { get; set; } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + UpdateUiForCaps(); + UpdateEnabledControls(); } - private void SelectPageSize() + private void UpdateUiForScanProfile() { - if (ScanProfile.PageSize == ScanPageSize.Custom) + // Don't trigger any onChange events + _suppressChangeEvent = true; + + DeviceDriver = new ScanOptionsValidator().ValidateDriver( + Enum.TryParse(ScanProfile.DriverName, true, out var driver) + ? driver + : Driver.Default); + IconUri = ScanProfile.Device?.IconUri; + + _displayName.Text = ScanProfile.DisplayName; + if (_deviceSelectorWidget.Choice == DeviceChoice.None) { - if (ScanProfile.CustomPageSizeName != null && ScanProfile.CustomPageSize != null) + var device = ScanProfile.Device?.ToScanDevice(DeviceDriver); + if (device != null) { - SelectCustomPageSize(ScanProfile.CustomPageSizeName, ScanProfile.CustomPageSize); + _deviceSelectorWidget.Choice = DeviceChoice.ForDevice(device); } - else + else if (!NewProfile) { - _pageSize.SelectedIndex = 0; + _deviceSelectorWidget.Choice = DeviceChoice.ForAlwaysAsk(DeviceDriver); } } + _isDefault = ScanProfile.IsDefault; + + if (ScanProfile.PageSize == ScanPageSize.Custom && ScanProfile.CustomPageSize != null) + { + _pageSize.SetCustom(ScanProfile.CustomPageSizeName, ScanProfile.CustomPageSize); + } else { - _pageSize.SelectedIndex = (int) ScanProfile.PageSize; + _pageSize.SetPreset(ScanProfile.PageSize); } + + _paperSource.SelectedItem = ScanProfile.PaperSource; + _bitDepth.SelectedItem = ScanProfile.BitDepth; + _resolution.SetDpi(ScanProfile.Resolution.Dpi); + _contrastSlider.IntValue = ScanProfile.Contrast; + _brightnessSlider.IntValue = ScanProfile.Brightness; + _scale.SelectedItem = ScanProfile.AfterScanScale; + _horAlign.SelectedItem = ScanProfile.PageAlign; + + _enableAutoSave.Checked = ScanProfile.EnableAutoSave; + + _nativeUi.Checked = ScanProfile.UseNativeUI; + _predefinedSettings.Checked = !ScanProfile.UseNativeUI; + + // Start triggering onChange events again + _suppressChangeEvent = false; } - private void SelectCustomPageSize(string name, PageDimensions dimens) + private bool SaveSettings() { - for (int i = 0; i < _pageSize.Items.Count; i++) + if (_displayName.Text == "") { - var item = (PageSizeListItem) _pageSize.Items[i]; - if (item.Type == ScanPageSize.Custom && item.CustomName == name && item.CustomDimens == dimens) - { - _pageSize.SelectedIndex = i; - return; - } + _errorOutput.DisplayError(MiscResources.NameMissing); + return false; } - - // Not found, so insert a new item - _pageSize.Items.Insert(_pageSize.Items.Count - 1, new PageSizeListItem + if (_deviceSelectorWidget.Choice == DeviceChoice.None) { - Type = ScanPageSize.Custom, - Text = string.IsNullOrEmpty(name) - ? string.Format(MiscResources.CustomPageSizeFormat, dimens.Width, dimens.Height, - dimens.Unit.Description()) - : string.Format(MiscResources.NamedPageSizeFormat, name, dimens.Width, dimens.Height, - dimens.Unit.Description()), - CustomName = name, - CustomDimens = dimens - }); - _pageSize.SelectedIndex = _pageSize.Items.Count - 2; - } - + _errorOutput.DisplayError(MiscResources.NoDeviceSelected); + return false; + } + _result = true; - private void SaveSettings() - { if (ScanProfile.IsLocked) { if (!ScanProfile.IsDeviceLocked) { - ScanProfile.Device = CurrentDevice; + ScanProfile.Device = ScanProfileDevice.FromScanDevice(_deviceSelectorWidget.Choice.Device); } - return; + return true; } - var pageSize = (PageSizeListItem) _pageSize.SelectedValue; if (ScanProfile.DisplayName != null) { _profileNameTracker.RenamingProfile(ScanProfile.DisplayName, _displayName.Text); } - _scanProfile = new ScanProfile + _scanProfile = GetUpdatedScanProfile(); + return true; + } + + private ScanProfile GetUpdatedScanProfile() + { + var pageSize = _pageSize.SelectedItem!; + return new ScanProfile { Version = ScanProfile.CURRENT_VERSION, - Device = CurrentDevice, + Device = ScanProfileDevice.FromScanDevice(_deviceSelectorWidget.Choice.Device), + Caps = ScanProfile.Caps, IsDefault = _isDefault, DriverName = DeviceDriver.ToString().ToLowerInvariant(), DisplayName = _displayName.Text, @@ -374,16 +368,16 @@ private void SaveSettings() MaxQuality = ScanProfile.MaxQuality, UseNativeUI = _nativeUi.Checked, - AfterScanScale = (ScanScale) _scale.SelectedIndex, - BitDepth = (ScanBitDepth) _bitDepth.SelectedIndex, - Brightness = _brightnessSlider.Value, - Contrast = _contrastSlider.Value, - PageAlign = (ScanHorizontalAlign) _horAlign.SelectedIndex, + AfterScanScale = _scale.SelectedItem, + BitDepth = _bitDepth.SelectedItem, + Brightness = _brightnessSlider.IntValue, + Contrast = _contrastSlider.IntValue, + PageAlign = _horAlign.SelectedItem, PageSize = pageSize.Type, CustomPageSizeName = pageSize.CustomName, CustomPageSize = pageSize.CustomDimens, - Resolution = (ScanDpi) _resolution.SelectedIndex, - PaperSource = (ScanSource) _paperSource.SelectedIndex, + Resolution = new ScanResolution { Dpi = _resolution.SelectedItem?.Dpi ?? 0 }, + PaperSource = _paperSource.SelectedItem, EnableAutoSave = _enableAutoSave.IsChecked(), AutoSaveSettings = ScanProfile.AutoSaveSettings, @@ -399,6 +393,7 @@ private void SaveSettings() ForcePageSizeCrop = ScanProfile.ForcePageSizeCrop, FlipDuplexedPages = ScanProfile.FlipDuplexedPages, TwainImpl = ScanProfile.TwainImpl, + TwainProgress = ScanProfile.TwainProgress, ExcludeBlankPages = ScanProfile.ExcludeBlankPages, BlankPageWhiteThreshold = ScanProfile.BlankPageWhiteThreshold, @@ -406,25 +401,6 @@ private void SaveSettings() }; } - private void Ok_Click(object? sender, EventArgs e) - { - // Note: If CurrentDevice is null, that's fine. A prompt will be shown when scanning. - - if (_displayName.Text == "") - { - _errorOutput.DisplayError(MiscResources.NameMissing); - return; - } - _result = true; - SaveSettings(); - Close(); - } - - private void Cancel_Click(object? sender, EventArgs e) - { - Close(); - } - private void PredefinedSettings_CheckedChanged(object? sender, EventArgs e) { UpdateEnabledControls(); @@ -447,9 +423,9 @@ private void UpdateEnabledControls() bool settingsEnabled = !locked && (_predefinedSettings.Checked || !canUseNativeUi); _displayName.Enabled = !locked; - _wiaDriver.Enabled = _twainDriver.Enabled = _appleDriver.Enabled = _saneDriver.Enabled = !locked; - _chooseDevice.Enabled = !deviceLocked; + _deviceSelectorWidget.Enabled = !deviceLocked; _predefinedSettings.Enabled = _nativeUi.Enabled = !locked; + _nativeUiVis.IsVisible = _deviceSelectorWidget.Choice.Device == null || canUseNativeUi; _paperSource.Enabled = settingsEnabled; _resolution.Enabled = settingsEnabled; @@ -466,52 +442,15 @@ private void UpdateEnabledControls() _advanced.Enabled = !locked; - // TODO: Adjust form height? - _suppressChangeEvent = false; } } - private void Driver_CheckedChanged(object? sender, EventArgs e) - { - if (((RadioButton) sender!).Checked && !_suppressChangeEvent) - { - ScanProfile.Device = null; - CurrentDevice = null; - UpdateEnabledControls(); - } - } - - private int _lastPageSizeIndex = -1; - private PageSizeListItem? _lastPageSizeItem; - private void PageSize_SelectedIndexChanged(object? sender, EventArgs e) + private void PaperSource_SelectedItemChanged(object? sender, EventArgs e) { - if (_pageSize.SelectedIndex == _pageSize.Items.Count - 1) - { - if (_lastPageSizeItem == null) - { - Log.Error("Expected last page size to be set"); - return; - } - // "Custom..." selected - var form = FormFactory.Create(); - form.PageSizeDimens = _lastPageSizeItem.Type == ScanPageSize.Custom - ? _lastPageSizeItem.CustomDimens - : _lastPageSizeItem.Type.PageDimensions(); - form.ShowModal(); - if (form.Result) - { - UpdatePageSizeList(); - SelectCustomPageSize(form.PageSizeName!, form.PageSizeDimens!); - } - else - { - _pageSize.SelectedIndex = _lastPageSizeIndex; - } - } - _lastPageSizeIndex = _pageSize.SelectedIndex; - _lastPageSizeItem = (PageSizeListItem) _pageSize.SelectedValue; + if (_suppressChangeEvent) return; + UpdateUiForCaps(); } private void AutoSaveSettings_LinkClicked(object? sender, EventArgs eventArgs) @@ -530,7 +469,7 @@ private void Advanced_Click(object? sender, EventArgs e) { var form = FormFactory.Create(); ScanProfile.DriverName = DeviceDriver.ToString().ToLowerInvariant(); - ScanProfile.BitDepth = (ScanBitDepth)_bitDepth.SelectedIndex; + ScanProfile.BitDepth = _bitDepth.SelectedItem; form.ScanProfile = ScanProfile; form.ShowModal(); } @@ -553,25 +492,4 @@ private void EnableAutoSave_CheckedChanged(object? sender, EventArgs e) } _autoSaveSettings.Enabled = _enableAutoSave.IsChecked(); } - - private void DeviceName_KeyDown(object? sender, KeyEventArgs e) - { - if (e.Key == Keys.Delete) - { - CurrentDevice = null; - } - } - - private class PageSizeListItem : IListItem - { - public string Text { get; set; } = null!; - - public string Key => Text; - - public ScanPageSize Type { get; set; } - - public string? CustomName { get; set; } - - public PageDimensions? CustomDimens { get; set; } - } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/EditWithForm.cs b/NAPS2.Lib/EtoForms/Ui/EditWithForm.cs new file mode 100644 index 0000000000..1c5253ee38 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/EditWithForm.cs @@ -0,0 +1,43 @@ +using Eto.Forms; +using NAPS2.EtoForms.Layout; + +namespace NAPS2.EtoForms.Ui; + +internal class EditWithForm : EtoDialogBase +{ + private readonly IOpenWith _openWith; + + public EditWithForm(Naps2Config config, IOpenWith openWith) : + base(config) + { + Title = UiStrings.EditWithFormTitle; + IconName = "pencil_small"; + + _openWith = openWith; + } + + protected override void BuildLayout() + { + FormStateController.FixedHeightLayout = true; + + LayoutController.DefaultSpacing = 0; + LayoutController.Content = L.Column( + _openWith.GetEntries(".jpg") + .Where(entry => !entry.Name.StartsWith("NAPS2")) + .Select(entry => C.Button(new ActionCommand(() => + { + Result = entry; + Close(); + }) + { + Text = entry.Name, + Image = _openWith.LoadIcon(entry)?.ToEtoImage(), + IconName = "pencil" + }, ButtonImagePosition.Left, ButtonFlags.LargeText | ButtonFlags.LargeIcon).NaturalWidth(500) + .Height(50)) + .Expand() + ); + } + + public OpenWithEntry? Result { get; private set; } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/EmailProviderForm.cs b/NAPS2.Lib/EtoForms/Ui/EmailProviderForm.cs index 4b7c112c95..31aa5022a9 100644 --- a/NAPS2.Lib/EtoForms/Ui/EmailProviderForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/EmailProviderForm.cs @@ -1,170 +1,46 @@ -using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; using NAPS2.ImportExport.Email; -using NAPS2.ImportExport.Email.Mapi; -using NAPS2.ImportExport.Email.Oauth; -using NAPS2.Scan; namespace NAPS2.EtoForms.Ui; -public class EmailProviderForm : EtoDialogBase +internal class EmailProviderForm : EtoDialogBase { - private readonly SystemEmailClients _systemEmailClients; - private readonly GmailOauthProvider _gmailOauthProvider; - private readonly OutlookWebOauthProvider _outlookWebOauthProvider; + private readonly EmailProviderController _controller; - private readonly List _providerWidgets; - private readonly string[] _systemClientNames; - private readonly string? _defaultSystemClientName; - - public EmailProviderForm(Naps2Config config, SystemEmailClients systemEmailClients, - GmailOauthProvider gmailOauthProvider, OutlookWebOauthProvider outlookWebOauthProvider) : base(config) + public EmailProviderForm(Naps2Config config, EmailProviderController controller, IIconProvider iconProvider) : + base(config) { - _systemEmailClients = systemEmailClients; - _gmailOauthProvider = gmailOauthProvider; - _outlookWebOauthProvider = outlookWebOauthProvider; - - _providerWidgets = new List(); -#if NET6_0_OR_GREATER - if (!OperatingSystem.IsWindowsVersionAtLeast(7)) - { - _systemClientNames = Array.Empty(); - _defaultSystemClientName = null; - } - else - { -#endif - _systemClientNames = _systemEmailClients.GetNames(); - _defaultSystemClientName = _systemEmailClients.GetDefaultName(); - - foreach (var clientName in _systemClientNames.OrderBy(x => x == _defaultSystemClientName ? 0 : 1)) - { - var exePath = _systemEmailClients.GetExePath(clientName); - var icon = exePath == null ? null : EtoPlatform.Current.ExtractAssociatedIcon(exePath); - _providerWidgets.Add(new EmailProviderWidget - { - ProviderType = EmailProviderType.System, - ProviderIcon = icon ?? Icons.mail_yellow.ToEtoImage(), - ProviderName = clientName, - ClickAction = () => ChooseSystem(clientName) - }); - } -#if NET6_0_OR_GREATER - } -#endif - - if (_gmailOauthProvider.HasClientCreds) - { - _providerWidgets.Add(new EmailProviderWidget - { - ProviderType = EmailProviderType.Gmail, - ProviderIcon = Icons.gmail.ToEtoImage(), - ProviderName = EmailProviderType.Gmail.Description(), - ClickAction = () => ChooseOauth(_gmailOauthProvider) - }); - } - - if (_outlookWebOauthProvider.HasClientCreds) - { - _providerWidgets.Add(new EmailProviderWidget - { - ProviderType = EmailProviderType.OutlookWeb, - ProviderIcon = Icons.outlookweb.ToEtoImage(), - ProviderName = EmailProviderType.OutlookWeb.Description(), - ClickAction = () => ChooseOauth(_outlookWebOauthProvider) - }); - } - - //providerWidgets.Add(new EmailProviderWidget - //{ - // ProviderType = EmailProviderType.CustomSmtp, - // ProviderIcon = Icons.email_setting, - // ProviderName = EmailProviderType.CustomSmtp.Description(), - // ClickAction = ChooseCustomSmtp - //}); + Title = UiStrings.EmailProviderFormTitle; + IconName = "email_small"; - // Put the configured provider at the top - var defaultWidget = GetDefaultWidget(); - if (defaultWidget != null) - { - _providerWidgets.Remove(defaultWidget); - _providerWidgets.Insert(0, defaultWidget); - } + _controller = controller; } protected override void BuildLayout() { - Title = UiStrings.EmailProviderFormTitle; - Icon = new Icon(1f, Icons.email_small.ToEtoImage()); - FormStateController.FixedHeightLayout = true; LayoutController.DefaultSpacing = 0; LayoutController.Content = L.Column( - _providerWidgets.Select(x => C.Button(new ActionCommand(x.ClickAction) - { - Text = x.ProviderName, - Image = x.ProviderIcon - }, ButtonImagePosition.Left, big: true).NaturalWidth(500).Height(50)).Expand() - ); - } - - public bool Result { get; private set; } - - private void ChooseSystem(string clientName) - { - var emailSetup = Config.Get(c => c.EmailSetup); - emailSetup.SystemProviderName = clientName; - emailSetup.ProviderType = EmailProviderType.System; - Config.User.Set(c => c.EmailSetup, emailSetup); - Result = true; - Close(); - } - - private void ChooseOauth(OauthProvider provider) - { - var authForm = FormFactory.Create(); - authForm.OauthProvider = provider; - authForm.ShowModal(); - if (authForm.Result) - { - Result = true; - Close(); - } - } - - private EmailProviderWidget? GetDefaultWidget() - { - var emailSetup = Config.Get(c => c.EmailSetup); - foreach (var widget in _providerWidgets) - { - if (widget.ProviderType == emailSetup.ProviderType) - { - if (widget.ProviderType == EmailProviderType.System) + _controller.GetWidgets().Select(x => C.Button(new ActionCommand(() => { - // System providers need additional logic since there may be more than one - if (widget.ProviderName == emailSetup.SystemProviderName - || string.IsNullOrEmpty(emailSetup.SystemProviderName) && - widget.ProviderName == _defaultSystemClientName) + if (x.Choose()) { - return widget; + Result = true; + Close(); } - } - else + }) { - return widget; - } - } - } - return null; + Text = x.ProviderName, + Image = x.ProviderIcon, + IconName = x.ProviderIconName, + Enabled = x.Enabled + }, ButtonImagePosition.Left, ButtonFlags.LargeText | ButtonFlags.LargeIcon).NaturalWidth(500) + .Height(50)) + .Expand() + ); } - public class EmailProviderWidget - { - public required EmailProviderType ProviderType { get; init; } - public required Bitmap ProviderIcon { get; init; } - public required string ProviderName { get; init; } - public required Action ClickAction { get; init; } - } + public bool Result { get; private set; } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/EmailSettingsForm.cs b/NAPS2.Lib/EtoForms/Ui/EmailSettingsForm.cs index c27b672166..6e0eef31c7 100644 --- a/NAPS2.Lib/EtoForms/Ui/EmailSettingsForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/EmailSettingsForm.cs @@ -1,24 +1,24 @@ using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; using NAPS2.ImportExport.Email; -using NAPS2.ImportExport.Email.Mapi; -using NAPS2.ImportExport.Images; namespace NAPS2.EtoForms.Ui; -public class EmailSettingsForm : EtoDialogBase +internal class EmailSettingsForm : EtoDialogBase { - private readonly SystemEmailClients _systemEmailClients; - private readonly Label _provider = new() { Text = " \n " }; private readonly FilePathWithPlaceholders _attachmentName; private readonly CheckBox _rememberSettings = new() { Text = UiStrings.RememberTheseSettings }; private readonly Button _restoreDefaults = new() { Text = UiStrings.RestoreDefaults }; - public EmailSettingsForm(Naps2Config config, SystemEmailClients systemEmailClients) : base(config) + public EmailSettingsForm(Naps2Config config) : + base(config) { - _systemEmailClients = systemEmailClients; + Title = UiStrings.EmailSettingsFormTitle; + IconName = "email_small"; + _attachmentName = new(this); UpdateValues(Config); @@ -29,9 +29,6 @@ public EmailSettingsForm(Naps2Config config, SystemEmailClients systemEmailClien protected override void BuildLayout() { - Title = UiStrings.EmailSettingsFormTitle; - Icon = new Icon(1f, Icons.email_small.ToEtoImage()); - FormStateController.DefaultExtraLayoutSize = new Size(60, 0); FormStateController.FixedHeightLayout = true; @@ -39,7 +36,7 @@ protected override void BuildLayout() L.GroupBox( UiStrings.Provider, L.Row( - _provider.AlignCenter(), + _provider.DynamicWrap(EtoPlatform.Current.IsGtk ? 150 : 0).AlignCenter(), C.Filler(), C.Button(UiStrings.Change, ChangeProvider).AlignCenter().Padding(top: 4, bottom: 4) ) @@ -66,32 +63,35 @@ private void UpdateValues(Naps2Config config) private void UpdateProvider(Naps2Config config) { + if (!config.User.Has(c => c.EmailSetup.ProviderType)) + { + _provider.Text = SettingsResources.EmailProvider_NotSelected; + return; + } switch (config.Get(c => c.EmailSetup.ProviderType)) { case EmailProviderType.Gmail: _provider.Text = SettingsResources.EmailProviderType_Gmail + '\n' + config.Get(c => c.EmailSetup.GmailUser); break; + case EmailProviderType.OutlookNew: + _provider.Text = SettingsResources.EmailProviderType_OutlookNew; + break; case EmailProviderType.OutlookWeb: _provider.Text = SettingsResources.EmailProviderType_OutlookWeb + '\n' + - config.Get(c => c.EmailSetup.OutlookWebToken); + config.Get(c => c.EmailSetup.OutlookWebUser); + break; + case EmailProviderType.Thunderbird: + _provider.Text = SettingsResources.EmailProviderType_Thunderbird; + break; + case EmailProviderType.AppleMail: + _provider.Text = SettingsResources.EmailProviderType_AppleMail; break; case EmailProviderType.CustomSmtp: _provider.Text = config.Get(c => c.EmailSetup.SmtpHost) + '\n' + config.Get(c => c.EmailSetup.SmtpUser); break; case EmailProviderType.System: -#if NET6_0_OR_GREATER - if (!OperatingSystem.IsWindowsVersionAtLeast(7)) - { - _provider.Text = SettingsResources.EmailProvider_NotSelected; - break; - } -#endif - _provider.Text = config.Get(c => c.EmailSetup.SystemProviderName) ?? - _systemEmailClients.GetDefaultName(); - break; - default: - _provider.Text = SettingsResources.EmailProvider_NotSelected; + _provider.Text = config.Get(c => c.EmailSetup.SystemProviderName); break; } } @@ -122,25 +122,11 @@ private void RestoreDefaults_Click(object? sender, EventArgs e) UpdateValues(Config.DefaultsOnly); } - private void Placeholders_Click(object? sender, EventArgs eventArgs) - { - var form = FormFactory.Create(); - form.FileName = _attachmentName.Text; - form.ShowModal(); - if (form.Updated) - { - _attachmentName.Text = form.FileName; - } - } - private void ChangeProvider() { var form = FormFactory.Create(); form.ShowModal(); - if (form.Result) - { - UpdateProvider(Config); - LayoutController.Invalidate(); - } + UpdateProvider(Config); + LayoutController.Invalidate(); } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/ErrorForm.cs b/NAPS2.Lib/EtoForms/Ui/ErrorForm.cs index 3bed1badf0..bb71f1083a 100644 --- a/NAPS2.Lib/EtoForms/Ui/ErrorForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/ErrorForm.cs @@ -5,14 +5,15 @@ namespace NAPS2.EtoForms.Ui; public class ErrorForm : EtoDialogBase { - private readonly ImageView _image = new() { Image = Icons.exclamation.ToEtoImage() }; + private readonly ImageView _image = new(); private readonly Label _message = new(); private readonly TextArea _details = new() { ReadOnly = true }; private readonly LayoutVisibility _detailsVisibility = new(false); - public ErrorForm(Naps2Config config) + public ErrorForm(Naps2Config config, IIconProvider iconProvider) : base(config) { + _image.Image = iconProvider.GetIcon("exclamation"); } protected override void BuildLayout() @@ -25,7 +26,7 @@ protected override void BuildLayout() LayoutController.Content = L.Column( L.Row( _image.AlignCenter().Padding(right: 5), - _message.Wrap(350).NaturalWidth(350).AlignCenter().Scale() + _message.DynamicWrap(350).NaturalWidth(350).AlignCenter().Scale() ), L.Row( C.Link(UiStrings.TechnicalDetails, ToggleDetails).AlignCenter(), diff --git a/NAPS2.Lib/EtoForms/Ui/HueSatForm.cs b/NAPS2.Lib/EtoForms/Ui/HueSatForm.cs index a8cf413f44..e5e2bda13c 100644 --- a/NAPS2.Lib/EtoForms/Ui/HueSatForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/HueSatForm.cs @@ -1,27 +1,30 @@ -using Eto.Drawing; +using NAPS2.EtoForms.Widgets; namespace NAPS2.EtoForms.Ui; -public class HueSatForm : ImageFormBase +public class HueSatForm : UnaryImageFormBase { private readonly SliderWithTextBox _hueSlider = new(); private readonly SliderWithTextBox _saturationSlider = new(); - public HueSatForm(Naps2Config config, ThumbnailController thumbnailController, IIconProvider iconProvider) : - base(config, thumbnailController) + public HueSatForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController, + IIconProvider iconProvider) : + base(config, imageList, thumbnailController) { - Icon = new Icon(1f, Icons.color_management.ToEtoImage()); + IconName = "color_management_small"; Title = UiStrings.HueSaturation; - _hueSlider.Icon = iconProvider.GetIcon("color_wheel"); - _saturationSlider.Icon = iconProvider.GetIcon("color_gradient"); - Sliders = new[] { _hueSlider, _saturationSlider }; + EtoPlatform.Current.AttachDpiDependency(this, scale => + { + _hueSlider.Icon = iconProvider.GetIcon("color_wheel_small", scale); + _saturationSlider.Icon = iconProvider.GetIcon("color_gradient_small", scale); + }); + Sliders = [_hueSlider, _saturationSlider]; } - protected override IEnumerable Transforms => - new Transform[] - { - new HueTransform(_hueSlider.Value), - new SaturationTransform(_saturationSlider.Value) - }; + protected override List Transforms => + [ + new HueTransform(_hueSlider.IntValue), + new SaturationTransform(_saturationSlider.IntValue) + ]; } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/ImageFormBase.cs b/NAPS2.Lib/EtoForms/Ui/ImageFormBase.cs index 4573b4c095..29848983c9 100644 --- a/NAPS2.Lib/EtoForms/Ui/ImageFormBase.cs +++ b/NAPS2.Lib/EtoForms/Ui/ImageFormBase.cs @@ -6,39 +6,44 @@ namespace NAPS2.EtoForms.Ui; public abstract class ImageFormBase : EtoDialogBase { - private readonly ThumbnailController _thumbnailController; - private readonly ImageView _imageView = new(); - private readonly CheckBox _applyToSelected = new(); - private readonly Button _revert = C.Button(UiStrings.Revert); private readonly RefreshThrottle _renderThrottle; // Image bounds in the coordinate space of the overlay control protected float _overlayT, _overlayL, _overlayR, _overlayB, _overlayW, _overlayH; - public ImageFormBase(Naps2Config config, ThumbnailController thumbnailController) : base(config) + protected ImageFormBase(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController) : + base(config) { - _thumbnailController = thumbnailController; - _revert.Click += Revert; + ImageList = imageList; + ThumbnailController = thumbnailController; _renderThrottle = new RefreshThrottle(RenderImage); Overlay.Paint += PaintOverlay; + Overlay.SizeChanged += (_, _) => UpdateImageCoords(); FormStateController.DefaultExtraLayoutSize = new Size(400, 400); } + public UiImage Image { get; set; } = null!; + public List SelectedImages { get; set; } = null!; + + protected UiImageList ImageList { get; } + protected ThumbnailController ThumbnailController { get; } + + protected int DisplayImageHeight { get; set; } + protected int DisplayImageWidth { get; set; } + + protected IMemoryImage? DisplayImage { get; set; } + protected Drawable Overlay { get; } = new(); + protected int OverlayBorderSize { get; set; } + protected override void BuildLayout() { - foreach (var slider in Sliders) - { - slider.ValueChanged += UpdatePreviewBox; - } - LayoutController.Content = L.Column( Overlay.Scale(), CreateControls(), - SelectedImages is { Count: > 1 } ? _applyToSelected : C.None(), L.Row( - _revert, + CreateExtraButtons(), C.Filler(), L.OkCancel( C.OkButton(this, beforeClose: Apply), @@ -47,15 +52,26 @@ protected override void BuildLayout() ); } - protected int ImageHeight { get; set; } - protected int ImageWidth { get; set; } + protected override void OnPreLoad(EventArgs e) + { + base.OnPreLoad(e); + InitDisplayImage(); + DisplayImageWidth = DisplayImage!.Width; + DisplayImageHeight = DisplayImage.Height; + } - protected IMemoryImage? WorkingImage { get; private set; } - protected IMemoryImage? DisplayImage { get; private set; } - protected Drawable Overlay { get; } = new(); - protected int OverlayBorderSize { get; set; } + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + UpdatePreviewBox(); + } - protected SliderWithTextBox[] Sliders { get; set; } = Array.Empty(); + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + DisplayImage?.Dispose(); + _imageView.Image?.Dispose(); + } protected override void OnSizeChanged(EventArgs e) { @@ -72,8 +88,8 @@ protected override void OnShown(EventArgs e) private void UpdateImageCoords() { if (!Overlay.Loaded) return; - var widthRatio = ImageWidth / (float) (Overlay.Width - OverlayBorderSize * 2); - var heightRatio = ImageHeight / (float) (Overlay.Height - OverlayBorderSize * 2); + var widthRatio = DisplayImageWidth / (float) (Overlay.Width - OverlayBorderSize * 2); + var heightRatio = DisplayImageHeight / (float) (Overlay.Height - OverlayBorderSize * 2); var ratio = widthRatio / heightRatio; if (ratio > 1) { @@ -98,76 +114,36 @@ private void UpdateImageCoords() protected virtual void PaintOverlay(object? sender, PaintEventArgs e) { - e.Graphics.DrawImage(DisplayImage!.ToEtoImage(), _overlayL, _overlayT, _overlayW, _overlayH); + using var etoImage = DisplayImage!.ToEtoImage(); + e.Graphics.DrawImage(etoImage, _overlayL, _overlayT, _overlayW, _overlayH); } private void RenderImage() { var bitmap = RenderPreview(); - Invoker.Current.SafeInvoke(() => + Invoker.Current.Invoke(() => { DisplayImage?.Dispose(); DisplayImage = bitmap; - if (DisplayImage.Width != ImageWidth || DisplayImage.Height != ImageHeight) + if (DisplayImage.Width != DisplayImageWidth || DisplayImage.Height != DisplayImageHeight) { - ImageWidth = DisplayImage.Width; - ImageHeight = DisplayImage.Height; + DisplayImageWidth = DisplayImage.Width; + DisplayImageHeight = DisplayImage.Height; UpdateImageCoords(); } Overlay.Invalidate(); }); } - protected virtual LayoutElement CreateControls() - { - return L.Column(Sliders.Select(x => (LayoutElement) x).ToArray()); - } - - public UiImage Image { get; set; } = null!; - - public List? SelectedImages { get; set; } + protected abstract LayoutElement CreateControls(); - protected virtual IEnumerable Transforms => throw new NotImplementedException(); + protected virtual LayoutElement CreateExtraButtons() => C.None(); - private bool TransformMultiple => SelectedImages != null && _applyToSelected.IsChecked(); + protected abstract IMemoryImage RenderPreview(); - private IEnumerable ImagesToTransform => TransformMultiple ? SelectedImages! : Enumerable.Repeat(Image, 1); + protected abstract void InitDisplayImage(); - protected virtual IMemoryImage RenderPreview() - { - var result = WorkingImage!.Clone(); - return result.PerformAllTransforms(Transforms); - } - - protected virtual void InitTransform() - { - } - - protected virtual void ResetTransform() - { - foreach (var slider in Sliders) - { - slider.Value = 0; - } - } - - protected virtual void TransformSaved() - { - } - - protected override void OnLoad(EventArgs e) - { - base.OnLoad(e); - _applyToSelected.Text = string.Format(UiStrings.ApplyToSelected, SelectedImages?.Count); - - using var imageToRender = Image.GetClonedImage(); - WorkingImage = imageToRender.Render(); - DisplayImage = WorkingImage.Clone(); - ImageWidth = DisplayImage.Width; - ImageHeight = DisplayImage.Height; - InitTransform(); - UpdatePreviewBox(); - } + protected abstract void Apply(); protected void UpdatePreviewBox() { @@ -175,37 +151,22 @@ protected void UpdatePreviewBox() _renderThrottle.RunAction(); } - private void Apply() + protected RectangleF GetScreenWorkingArea() { - if (Transforms.Any(x => !x.IsNull)) + try { - foreach (var img in ImagesToTransform) + var screen = Screen ?? Screen.PrimaryScreen; + if (screen != null) { - IMemoryImage? updatedThumb = null; - if (img == Image && WorkingImage != null) - { - // Optimize thumbnail rendering for the first (or only) image since we already have it loaded into memory - var transformed = WorkingImage.Clone().PerformAllTransforms(Transforms); - updatedThumb = - transformed.PerformTransform(new ThumbnailTransform(_thumbnailController.RenderSize)); - } - img.AddTransforms(Transforms, updatedThumb); + return screen.WorkingArea; } } - TransformSaved(); - } - - private void Revert(object? sender, EventArgs e) - { - ResetTransform(); - UpdatePreviewBox(); - } + catch (Exception) + { + // On Linux sometimes we can't get the working area + } - protected override void OnClosed(EventArgs e) - { - base.OnClosed(e); - WorkingImage?.Dispose(); - DisplayImage?.Dispose(); - _imageView.Image?.Dispose(); + // Assume 1080p screen by default + return new RectangleF(0, 0, 1920, 1080); } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/ImageSettingsForm.cs b/NAPS2.Lib/EtoForms/Ui/ImageSettingsForm.cs index d0b7b67d38..2cd80bd885 100644 --- a/NAPS2.Lib/EtoForms/Ui/ImageSettingsForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/ImageSettingsForm.cs @@ -1,6 +1,7 @@ using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; using NAPS2.ImportExport.Images; namespace NAPS2.EtoForms.Ui; @@ -9,15 +10,18 @@ public class ImageSettingsForm : EtoDialogBase { private readonly FilePathWithPlaceholders _defaultFilePath; private readonly CheckBox _skipSavePrompt = new() { Text = UiStrings.SkipSavePrompt }; - private readonly SliderWithTextBox _jpegQuality = new() { MinValue = 0, MaxValue = 100, TickFrequency = 25 }; + private readonly SliderWithTextBox _jpegQuality = new(new SliderWithTextBox.IntConstraints(0, 100, 25)); private readonly CheckBox _singlePageTiff = new() { Text = UiStrings.SinglePageFiles }; - private readonly DropDown _compression = C.EnumDropDown(); + private readonly EnumDropDownWidget _compression = new(); private readonly CheckBox _rememberSettings = new() { Text = UiStrings.RememberTheseSettings }; private readonly Button _restoreDefaults = new() { Text = UiStrings.RestoreDefaults }; - public ImageSettingsForm(Naps2Config config, DialogHelper dialogHelper) : base(config) + public ImageSettingsForm(Naps2Config config, DialogHelper dialogHelper, IIconProvider iconProvider) : base(config) { - _defaultFilePath = new(this, dialogHelper); + Title = UiStrings.ImageSettingsFormTitle; + IconName = "picture_small"; + + _defaultFilePath = new(this, dialogHelper) { ImagesOnly = true }; UpdateValues(Config); UpdateEnabled(); @@ -28,9 +32,6 @@ public ImageSettingsForm(Naps2Config config, DialogHelper dialogHelper) : base(c protected override void BuildLayout() { - Title = UiStrings.ImageSettingsFormTitle; - Icon = new Icon(1f, Icons.picture_small.ToEtoImage()); - FormStateController.DefaultExtraLayoutSize = new Size(60, 0); FormStateController.FixedHeightLayout = true; @@ -42,7 +43,7 @@ protected override void BuildLayout() UiStrings.JpegQuality, L.Column( _jpegQuality.AsControl().SpacingAfter(0), - C.Label(UiStrings.JpegQualityHelp).Wrap(300) + C.Label(UiStrings.JpegQualityHelp).DynamicWrap(300) ) ), L.GroupBox( @@ -69,9 +70,9 @@ private void UpdateValues(Naps2Config config) { _defaultFilePath.Text = config.Get(c => c.ImageSettings.DefaultFileName); _skipSavePrompt.Checked = config.Get(c => c.ImageSettings.SkipSavePrompt); - _jpegQuality.Value = config.Get(c => c.ImageSettings.JpegQuality); + _jpegQuality.IntValue = config.Get(c => c.ImageSettings.JpegQuality); _singlePageTiff.Checked = config.Get(c => c.ImageSettings.SinglePageTiff); - _compression.SelectedIndex = (int) config.Get(c => c.ImageSettings.TiffCompression); + _compression.SelectedItem = config.Get(c => c.ImageSettings.TiffCompression); _rememberSettings.Checked = config.Get(c => c.RememberImageSettings); } @@ -86,8 +87,8 @@ private void Save() { DefaultFileName = _defaultFilePath.Text, SkipSavePrompt = _skipSavePrompt.IsChecked(), - JpegQuality = _jpegQuality.Value, - TiffCompression = (TiffCompression) _compression.SelectedIndex, + JpegQuality = _jpegQuality.IntValue, + TiffCompression = _compression.SelectedItem, SinglePageTiff = _singlePageTiff.IsChecked() }; diff --git a/NAPS2.Lib/EtoForms/Ui/KeyboardShortcutsForm.cs b/NAPS2.Lib/EtoForms/Ui/KeyboardShortcutsForm.cs new file mode 100644 index 0000000000..949cbca968 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/KeyboardShortcutsForm.cs @@ -0,0 +1,258 @@ +using System.Linq.Expressions; +using Eto.Forms; +using NAPS2.Config.Model; +using NAPS2.EtoForms.Desktop; +using NAPS2.EtoForms.Layout; + +namespace NAPS2.EtoForms.Ui; + +public class KeyboardShortcutsForm : EtoDialogBase +{ + private readonly KeyboardShortcutManager _ksm; + private readonly List Shortcuts = + [ + new(UiStrings.ScanWithDefaultProfile, c => c.KeyboardShortcuts.ScanDefault), + new(string.Format(UiStrings.ScanWithProfile, 1), c => c.KeyboardShortcuts.ScanProfile1), + new(string.Format(UiStrings.ScanWithProfile, 2), c => c.KeyboardShortcuts.ScanProfile2), + new(string.Format(UiStrings.ScanWithProfile, 3), c => c.KeyboardShortcuts.ScanProfile3), + new(string.Format(UiStrings.ScanWithProfile, 4), c => c.KeyboardShortcuts.ScanProfile4), + new(string.Format(UiStrings.ScanWithProfile, 5), c => c.KeyboardShortcuts.ScanProfile5), + new(string.Format(UiStrings.ScanWithProfile, 6), c => c.KeyboardShortcuts.ScanProfile6), + new(string.Format(UiStrings.ScanWithProfile, 7), c => c.KeyboardShortcuts.ScanProfile7), + new(string.Format(UiStrings.ScanWithProfile, 8), c => c.KeyboardShortcuts.ScanProfile8), + new(string.Format(UiStrings.ScanWithProfile, 9), c => c.KeyboardShortcuts.ScanProfile9), + new(string.Format(UiStrings.ScanWithProfile, 10), c => c.KeyboardShortcuts.ScanProfile10), + new(string.Format(UiStrings.ScanWithProfile, 11), c => c.KeyboardShortcuts.ScanProfile11), + new(string.Format(UiStrings.ScanWithProfile, 12), c => c.KeyboardShortcuts.ScanProfile12), + new(UiStrings.ScanWithNewProfile, c => c.KeyboardShortcuts.NewProfile), + new(UiStrings.BatchScan, c => c.KeyboardShortcuts.BatchScan), + Shortcut.Separator, + new(UiStrings.Profiles, c => c.KeyboardShortcuts.Profiles), + new(UiStrings.ScannerSharing, c => c.KeyboardShortcuts.ScannerSharing), + new(UiStrings.Ocr, c => c.KeyboardShortcuts.Ocr), + new(UiStrings.Import, c => c.KeyboardShortcuts.Import), + Shortcut.Separator, + new(UiStrings.SaveAllAsPdf, c => c.KeyboardShortcuts.SavePDFAll), + new(UiStrings.SaveSelectedAsPdf, c => c.KeyboardShortcuts.SavePDFSelected), + new(UiStrings.PdfSettings, c => c.KeyboardShortcuts.PDFSettings), + Shortcut.Separator, + new(UiStrings.SaveAllAsImages, c => c.KeyboardShortcuts.SaveImagesAll), + new(UiStrings.SaveSelectedAsImages, c => c.KeyboardShortcuts.SaveImagesSelected), + new(UiStrings.ImageSettings, c => c.KeyboardShortcuts.ImageSettings), + Shortcut.Separator, + new(UiStrings.EmailAllAsPdf, c => c.KeyboardShortcuts.EmailPDFAll), + new(UiStrings.EmailSelectedAsPdf, c => c.KeyboardShortcuts.EmailPDFSelected), + new(UiStrings.EmailSettings, c => c.KeyboardShortcuts.EmailSettings), + Shortcut.Separator, + new(UiStrings.Print, c => c.KeyboardShortcuts.Print), + Shortcut.Separator, + new(UiStrings.View, c => c.KeyboardShortcuts.ImageView), + new(UiStrings.BlackAndWhite, c => c.KeyboardShortcuts.ImageView), + new(UiStrings.BrightnessContrast, c => c.KeyboardShortcuts.ImageBrightness), + new(UiStrings.Crop, c => c.KeyboardShortcuts.ImageCrop), + new(UiStrings.HueSaturation, c => c.KeyboardShortcuts.ImageHue), + new(UiStrings.Sharpen, c => c.KeyboardShortcuts.ImageSharpen), + new(UiStrings.DocumentCorrection, c => c.KeyboardShortcuts.ImageDocumentCorrection), + new(UiStrings.Split, c => c.KeyboardShortcuts.ImageSplit), + new(UiStrings.Combine, c => c.KeyboardShortcuts.ImageCombine), + new(UiStrings.EditWith, c => c.KeyboardShortcuts.ImageEditWith), + new(UiStrings.Reset, c => c.KeyboardShortcuts.ImageReset), + Shortcut.Separator, + new(UiStrings.RotateLeft, c => c.KeyboardShortcuts.RotateLeft), + new(UiStrings.RotateRight, c => c.KeyboardShortcuts.RotateRight), + new(UiStrings.Flip, c => c.KeyboardShortcuts.RotateFlip), + new(UiStrings.Deskew, c => c.KeyboardShortcuts.RotateDeskew), + new(UiStrings.CustomRotation, c => c.KeyboardShortcuts.RotateCustom), + Shortcut.Separator, + new(UiStrings.MoveUp, c => c.KeyboardShortcuts.MoveUp), + new(UiStrings.MoveDown, c => c.KeyboardShortcuts.MoveDown), + Shortcut.Separator, + new(UiStrings.Interleave, c => c.KeyboardShortcuts.ReorderInterleave), + new(UiStrings.Deinterleave, c => c.KeyboardShortcuts.ReorderDeinterleave), + new(UiStrings.AltInterleave, c => c.KeyboardShortcuts.ReorderAltInterleave), + new(UiStrings.AltDeinterleave, c => c.KeyboardShortcuts.ReorderAltDeinterleave), + new(UiStrings.ReverseAll, c => c.KeyboardShortcuts.ReorderReverseAll), + new(UiStrings.ReverseSelected, c => c.KeyboardShortcuts.ReorderReverseSelected), + Shortcut.Separator, + new(UiStrings.Delete, c => c.KeyboardShortcuts.Delete), + new(UiStrings.Clear, c => c.KeyboardShortcuts.Clear), + new(UiStrings.Settings, c => c.KeyboardShortcuts.Settings), + new(UiStrings.About, c => c.KeyboardShortcuts.About), + Shortcut.Separator, + new(UiStrings.ZoomIn, c => c.KeyboardShortcuts.ZoomIn), + new(UiStrings.ZoomOut, c => c.KeyboardShortcuts.ZoomOut), + ]; + + private readonly DesktopFormProvider _desktopFormProvider; + + private readonly GridView _gridView; + private readonly TextBox _shortcutText = new() { ReadOnly = true }; + private readonly Button _assign = C.Button(UiStrings.Assign); + private readonly Button _unassign = C.Button(UiStrings.Unassign); + private readonly Button _restoreDefaults = C.Button(UiStrings.RestoreDefaults); + + private readonly TransactionConfigScope _transact; + private readonly Naps2Config _transactionConfig; + + public KeyboardShortcutsForm(Naps2Config config, KeyboardShortcutManager ksm, + DesktopFormProvider desktopFormProvider) : base(config) + { + _ksm = ksm; + _desktopFormProvider = desktopFormProvider; + _transact = Config.User.BeginTransaction(); + _transactionConfig = Config.WithTransaction(_transact); + _gridView = new() + { + Columns = + { + new() + { + HeaderText = UiStrings.Action, + DataCell = new TextBoxCell { Binding = new PropertyBinding("Label") }, + Width = 280 + }, + new() + { + HeaderText = UiStrings.Shortcut, + DataCell = new TextBoxCell { Binding = new DelegateBinding(GetShortcutLabel) }, + Width = 150 + } + } + }; + _gridView.SelectionChanged += GridView_SelectionChanged; + _gridView.CellDoubleClick += Assign_Click; + _shortcutText.KeyDown += ShortcutText_KeyDown; + _assign.Click += Assign_Click; + _unassign.Click += Unassign_Click; + _restoreDefaults.Click += RestoreDefaults_Click; + UpdateUi(); + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + _gridView.DataStore = Shortcuts; + } + + protected override void BuildLayout() + { + Title = UiStrings.KeyboardShortcutsFormTitle; + IconName = "keyboard_small"; + + LayoutController.Content = L.Column( + L.Row( + _gridView.NaturalSize(450, 500).Scale(), + L.Column( + C.Filler(), + _shortcutText.Width(150), + _assign, + _unassign, + C.Filler() + ) + ).Scale(), + L.Row( + _restoreDefaults.MinWidth(140), + C.Filler(), + L.OkCancel(C.OkButton(this, Save), C.CancelButton(this)) + ) + ); + } + + private void Assign_Click(object? sender, EventArgs e) + { + var selected = (Shortcut?) _gridView.SelectedItem; + if (selected == null) return; + _shortcutText.ReadOnly = false; + _shortcutText.Focus(); + } + + private void Unassign_Click(object? sender, EventArgs e) + { + var selected = (Shortcut?) _gridView.SelectedItem; + if (selected?.Accessor == null) return; + _transact.Set(selected.Accessor, ""); + UpdateUi(); + } + + private void RestoreDefaults_Click(object? sender, EventArgs e) + { + foreach (var shortcut in Shortcuts) + { + if (shortcut.Accessor != null) + { + _transact.Remove(shortcut.Accessor); + } + } + UpdateUi(); + } + + private void ShortcutText_KeyDown(object? sender, KeyEventArgs e) + { + if (_shortcutText.ReadOnly) return; + + e.Handled = true; + var selected = (Shortcut?) _gridView.SelectedItem; + if (selected?.Accessor == null) return; + if (e.Key is Keys.LeftControl or Keys.LeftAlt or Keys.LeftShift or Keys.LeftApplication + or Keys.RightControl or Keys.RightAlt or Keys.RightShift or Keys.RightApplication) + { + return; + } + var text = _ksm.Stringify(e.KeyData); + _transact.Set(selected.Accessor, text); + UpdateUi(); + } + + private void GridView_SelectionChanged(object? sender, EventArgs e) + { + UpdateUi(); + } + + private void UpdateUi() + { + var selected = (Shortcut?) _gridView.SelectedItem; + if (selected?.Accessor == null) + { + _shortcutText.Text = ""; + _shortcutText.ReadOnly = true; + _assign.Enabled = false; + _unassign.Enabled = false; + } + else + { + bool locked = _transactionConfig.AppLocked.Has(selected.Accessor); + _shortcutText.Text = GetKeyString(selected); + _shortcutText.ReadOnly = true; + _assign.Enabled = !locked; + _unassign.Enabled = !locked && _shortcutText.Text != ""; + } + _gridView.Invalidate(); + } + + private string GetKeyString(Shortcut shortcut) + { + if (shortcut.Accessor == null) return ""; + + var keys = _ksm.Parse(_transactionConfig.Get(shortcut.Accessor)); + return _ksm.Stringify(keys) ?? ""; + } + + private string GetShortcutLabel(Shortcut shortcut) + { + if (shortcut.Accessor == null) return ""; + + var keys = _ksm.Parse(_transactionConfig.Get(shortcut.Accessor)); + return _ksm.Stringify(keys) ?? ""; + } + + private void Save() + { + _transact.Commit(); + _desktopFormProvider.DesktopForm.ReassignKeyboardShortcuts(); + } + + private record Shortcut(string Label, Expression>? Accessor) + { + public static Shortcut Separator { get; } = new("-------", null); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/ManualIpForm.cs b/NAPS2.Lib/EtoForms/Ui/ManualIpForm.cs new file mode 100644 index 0000000000..903aa26c2f --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/ManualIpForm.cs @@ -0,0 +1,97 @@ +using Eto.Drawing; +using Eto.Forms; +using NAPS2.Escl.Client; +using NAPS2.EtoForms.Layout; +using NAPS2.Scan; + +namespace NAPS2.EtoForms.Ui; + +public class ManualIpForm : EtoDialogBase +{ + private readonly TextBox _ipHost = new(); + private readonly TextBox _port = new(); + private readonly CheckBox _https = new() { Text = "HTTPS", Checked = true }; + + private readonly ErrorOutput _errorOutput; + + public ManualIpForm(Naps2Config config, ErrorOutput errorOutput, IIconProvider iconProvider) + : base(config) + { + Title = UiStrings.ManualIpFormTitle; + IconName = "network_ip_small"; + + _errorOutput = errorOutput; + } + + public ScanDevice? Device { get; set; } + + public bool Result { get; private set; } + + protected override void BuildLayout() + { + FormStateController.RestoreFormState = false; + FormStateController.FixedHeightLayout = true; + + LayoutController.Content = L.Column( + C.Label(UiStrings.IpHost), + _ipHost.NaturalWidth(150), + C.Label(UiStrings.Port), + L.Row(_port.Width(50), _https), + C.Filler(), + L.Row( + C.Filler(), + L.OkCancel( + C.OkButton(this, Connect, UiStrings.Connect), + C.CancelButton(this)) + ) + ); + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + _ipHost.Focus(); + } + + private bool Connect() + { + string ipHost = _ipHost.Text; + if (string.IsNullOrWhiteSpace(ipHost)) + { + _ipHost.Focus(); + return false; + } + + int port = int.TryParse(_port.Text, out int p) ? p : -1; + bool https = _https.IsChecked(); + + Task.Run(async () => + { + var uri = new UriBuilder + { + Scheme = https ? "https" : "http", + Host = ipHost, + Port = port, + Path = "eSCL" + }.Uri; + + try + { + var client = new EsclClient(uri); + var caps = await client.GetCapabilities(); + if (caps.Uuid != null && caps.MakeAndModel != null) + { + Device = new ScanDevice(Driver.Escl, uri.ToString(), $"{caps.MakeAndModel} ({ipHost})"); + Result = true; + Invoker.Current.Invoke(Close); + } + } + catch (Exception ex) + { + Log.DebugException($"Error connecting to manual IP/host {uri.ToString()}", ex); + _errorOutput.DisplayError(UiStrings.ConnectionError, ex); + } + }); + return false; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/OcrDownloadForm.cs b/NAPS2.Lib/EtoForms/Ui/OcrDownloadForm.cs index 201ff3550b..01321f907c 100644 --- a/NAPS2.Lib/EtoForms/Ui/OcrDownloadForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/OcrDownloadForm.cs @@ -1,6 +1,7 @@ using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; using NAPS2.Ocr; namespace NAPS2.EtoForms.Ui; @@ -9,14 +10,18 @@ public class OcrDownloadForm : EtoDialogBase { private readonly TesseractLanguageManager _tesseractLanguageManager; - private readonly IListView _languageList = - EtoPlatform.Current.CreateListView(new OcrLanguagesListViewBehavior()); + private readonly IListView _languageList; private readonly Label _downloadSize = new(); private readonly Button _downloadButton; - public OcrDownloadForm(Naps2Config config, TesseractLanguageManager tesseractLanguageManager) : base(config) + public OcrDownloadForm(Naps2Config config, TesseractLanguageManager tesseractLanguageManager, + OcrLanguagesListViewBehavior ocrLanguagesListViewBehavior, IIconProvider iconProvider) : base(config) { + Title = UiStrings.OcrDownloadFormTitle; + IconName = "text_small"; + _tesseractLanguageManager = tesseractLanguageManager; + _languageList = EtoPlatform.Current.CreateListView(ocrLanguagesListViewBehavior); var initialSelection = new HashSet(); // TODO: We used to select old installed languages here, maybe we could do it again if we get new lang data @@ -40,9 +45,6 @@ public OcrDownloadForm(Naps2Config config, TesseractLanguageManager tesseractLan protected override void BuildLayout() { - Title = UiStrings.OcrDownloadFormTitle; - Icon = new Icon(1f, Icons.text_small.ToEtoImage()); - FormStateController.RestoreFormState = false; FormStateController.DefaultExtraLayoutSize = new Size(300, 300); @@ -75,7 +77,7 @@ private void UpdateView() private HashSet SelectedLanguageComponents { - get { return new HashSet(_languageList.Selection.Select(lang => $"ocr-{lang.Code}")); } + get { return [.._languageList.Selection.Select(lang => $"ocr-{lang.Code}")]; } } private void Download() @@ -85,7 +87,7 @@ private void Download() var selected = SelectedLanguageComponents; foreach (var langComponent in _tesseractLanguageManager.LanguageComponents.Where(x => selected.Contains(x.Id))) { - progressForm.QueueFile(langComponent); + progressForm.Controller.QueueFile(langComponent); } Close(); diff --git a/NAPS2.Lib/EtoForms/Ui/OcrMultiLangForm.cs b/NAPS2.Lib/EtoForms/Ui/OcrMultiLangForm.cs new file mode 100644 index 0000000000..23d6f8c7f5 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/OcrMultiLangForm.cs @@ -0,0 +1,48 @@ +using Eto.Drawing; +using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; +using NAPS2.Ocr; + +namespace NAPS2.EtoForms.Ui; + +public class OcrMultiLangForm : EtoDialogBase +{ + private readonly IListView _languageList; + + public OcrMultiLangForm(Naps2Config config, TesseractLanguageManager tesseractLanguageManager, + OcrLanguagesListViewBehavior ocrLanguagesListViewBehavior, IIconProvider iconProvider) : base(config) + { + Title = UiStrings.OcrMultiLangFormTitle; + IconName = "text_small"; + + _languageList = EtoPlatform.Current.CreateListView(ocrLanguagesListViewBehavior); + _languageList.SetItems(tesseractLanguageManager.InstalledLanguages.OrderBy(x => x.Name)); + } + + public string? Code { get; private set; } + + protected override void BuildLayout() + { + FormStateController.RestoreFormState = false; + FormStateController.DefaultExtraLayoutSize = new Size(150, 20); + + LayoutController.Content = L.Column( + _languageList.Control.Scale(), + C.Spacer(), + L.Row( + C.Filler(), + L.OkCancel( + C.OkButton(this, Save), + C.CancelButton(this)) + ) + ); + } + + private void Save() + { + if (_languageList.Selection.Count > 0) + { + Code = string.Join("+", _languageList.Selection.OrderBy(x => x.Name).Select(lang => lang.Code)); + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/OcrSetupForm.cs b/NAPS2.Lib/EtoForms/Ui/OcrSetupForm.cs index 676dfbd608..a40bedda47 100644 --- a/NAPS2.Lib/EtoForms/Ui/OcrSetupForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/OcrSetupForm.cs @@ -1,6 +1,9 @@ +using System.Globalization; using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; +using NAPS2.Lang; using NAPS2.Ocr; namespace NAPS2.EtoForms.Ui; @@ -11,34 +14,53 @@ public class OcrSetupForm : EtoDialogBase private readonly CheckBox _enableOcr = C.CheckBox(UiStrings.MakePdfsSearchable); private readonly DropDown _ocrLang = C.DropDown(); - private readonly DropDown _ocrMode = C.EnumDropDown(LocalizedOcrMode.Fast, LocalizedOcrMode.Best); - private readonly CheckBox _ocrAfterScanning = C.CheckBox(UiStrings.RunOcrAfterScanning); + private readonly EnumDropDownWidget _ocrMode = new() + { Items = [LocalizedOcrMode.Fast, LocalizedOcrMode.Best] }; + private readonly CheckBox _ocrPreProcessing = C.CheckBox(UiStrings.OcrPreProcessing); + private readonly CheckBox _ocrAfterScanning = C.CheckBox( + TranslationMigrator.PickTranslated( + UiStrings.ResourceManager, + "RunOcrAfterScanning", + "PreemptivelyOcrAfterScanning")); private readonly LinkButton _moreLanguages = C.Link(UiStrings.GetMoreLanguages); - public OcrSetupForm(Naps2Config config, TesseractLanguageManager tesseractLanguageManager) : base(config) + private string? _code; + private string? _multiLangCode; + private bool _suppressLangChangeEvent; + + public OcrSetupForm(Naps2Config config, TesseractLanguageManager tesseractLanguageManager, + IIconProvider iconProvider) : base(config) { + Title = UiStrings.OcrSetupFormTitle; + IconName = "text_small"; + _tesseractLanguageManager = tesseractLanguageManager; _enableOcr.CheckedChanged += EnableOcr_CheckedChanged; _moreLanguages.Click += MoreLanguages_Click; - LoadLanguages(); + var configOcrMode = Config.Get(c => c.OcrMode); + if (configOcrMode == LocalizedOcrMode.Legacy) + { + // Legacy is no longer supported + configOcrMode = LocalizedOcrMode.Fast; + } + + _code = Config.Get(c => c.OcrLanguageCode); + _multiLangCode = Config.Get(c => c.LastOcrMultiLangCode); _enableOcr.Checked = Config.Get(c => c.EnableOcr); - _ocrLang.SelectedKey = Config.Get(c => c.OcrLanguageCode) ?? ""; - if (_ocrLang.SelectedIndex == -1) _ocrLang.SelectedIndex = 0; - _ocrMode.SelectedIndex = (int) Config.Get(c => c.OcrMode); - if (_ocrMode.SelectedIndex == -1) _ocrMode.SelectedIndex = 0; + _ocrLang.SelectedIndexChanged += OcrLang_SelectedIndexChanged; + _ocrMode.SelectedItem = configOcrMode; + _ocrPreProcessing.Checked = Config.Get(c => c.OcrPreProcessing); _ocrAfterScanning.Checked = Config.Get(c => c.OcrAfterScanning); + LoadLanguages(); UpdateView(); } protected override void BuildLayout() { - Title = UiStrings.OcrSetupFormTitle; - Icon = new Icon(1f, Icons.text_small.ToEtoImage()); - FormStateController.Resizable = false; LayoutController.Content = L.Column( @@ -49,8 +71,9 @@ protected override void BuildLayout() ).Aligned(), L.Row( C.Label(UiStrings.OcrModeLabel).AlignCenter().Padding(right: 40), - _ocrMode.Scale() + _ocrMode.AsControl().Scale() ).Aligned(), + _ocrPreProcessing, _ocrAfterScanning, C.Filler(), L.Row( @@ -66,27 +89,54 @@ protected override void BuildLayout() private void LoadLanguages() { + _suppressLangChangeEvent = true; var languages = _tesseractLanguageManager.InstalledLanguages .OrderBy(x => x.Name) .ToList(); - var selectedKey = _ocrLang.SelectedKey; _ocrLang.Items.Clear(); _ocrLang.Items.AddRange(languages.Select(lang => new ListItem { Key = lang.Code, Text = lang.Name })); - _ocrLang.SelectedKey = selectedKey; + if (languages.Count > 1) + { + if (_multiLangCode?.Contains("+") == true) + { + _ocrLang.Items.Add(new ListItem + { + Key = _multiLangCode, + Text = string.Join($"{CultureInfo.CurrentCulture.TextInfo.ListSeparator} ", + _multiLangCode.Split('+') + .Select(code => languages.SingleOrDefault(lang => lang.Code == code)?.Name)) + }); + } + _ocrLang.Items.Add(new ListItem + { + Key = "", + Text = UiStrings.MultipleLanguages + }); + } + if (!string.IsNullOrEmpty(_code)) + { + _ocrLang.SelectedKey = _code; + } + if (_ocrLang.SelectedIndex == -1) + { + _ocrLang.SelectedIndex = 0; + } + _suppressLangChangeEvent = false; } private void UpdateView() { bool isEnabled = _enableOcr.IsChecked(); - _enableOcr.Enabled = !Config.AppLocked.TryGet(c => c.EnableOcr, out _); - _ocrLang.Enabled = isEnabled && !Config.AppLocked.TryGet(c => c.OcrLanguageCode, out _); - _ocrMode.Enabled = isEnabled && !Config.AppLocked.TryGet(c => c.OcrMode, out _); - _ocrAfterScanning.Enabled = isEnabled && !Config.AppLocked.TryGet(c => c.OcrAfterScanning, out _); - _moreLanguages.Enabled = !Config.AppLocked.TryGet(c => c.OcrLanguageCode, out _); + _enableOcr.Enabled = !Config.AppLocked.Has(c => c.EnableOcr); + _ocrLang.Enabled = isEnabled && !Config.AppLocked.Has(c => c.OcrLanguageCode); + _ocrMode.Enabled = isEnabled && !Config.AppLocked.Has(c => c.OcrMode); + _ocrPreProcessing.Enabled = isEnabled && !Config.AppLocked.Has(c => c.OcrPreProcessing); + _ocrAfterScanning.Enabled = isEnabled && !Config.AppLocked.Has(c => c.OcrAfterScanning); + _moreLanguages.Enabled = !Config.AppLocked.Has(c => c.OcrLanguageCode); } private void EnableOcr_CheckedChanged(object? sender, EventArgs e) @@ -100,14 +150,39 @@ private void MoreLanguages_Click(object? sender, EventArgs e) LoadLanguages(); } + private void OcrLang_SelectedIndexChanged(object? sender, EventArgs e) + { + if (_suppressLangChangeEvent) return; + if (_ocrLang.SelectedIndex == _ocrLang.Items.Count - 1) + { + var multiLangForm = FormFactory.Create(); + multiLangForm.ShowModal(); + if (multiLangForm.Code != null) + { + _code = multiLangForm.Code; + } + if (multiLangForm.Code?.Contains("+") == true) + { + _multiLangCode = multiLangForm.Code; + } + LoadLanguages(); + } + _code = _ocrLang.SelectedKey; + } + private void Save() { - if (!Config.AppLocked.TryGet(c => c.EnableOcr, out _)) + if (!Config.AppLocked.Has(c => c.EnableOcr)) { var transact = Config.User.BeginTransaction(); transact.Set(c => c.EnableOcr, _enableOcr.IsChecked()); transact.Set(c => c.OcrLanguageCode, _ocrLang.SelectedKey); - transact.Set(c => c.OcrMode, (LocalizedOcrMode) _ocrMode.SelectedIndex); + if (_multiLangCode?.Contains("+") == true) + { + Config.User.Set(c => c.LastOcrMultiLangCode, _multiLangCode); + } + transact.Set(c => c.OcrMode, _ocrMode.SelectedItem); + transact.Set(c => c.OcrPreProcessing, _ocrPreProcessing.IsChecked()); transact.Set(c => c.OcrAfterScanning, _ocrAfterScanning.IsChecked()); transact.Commit(); } diff --git a/NAPS2.Lib/EtoForms/Ui/PageSizeForm.cs b/NAPS2.Lib/EtoForms/Ui/PageSizeForm.cs index b437d3cce6..df43356568 100644 --- a/NAPS2.Lib/EtoForms/Ui/PageSizeForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/PageSizeForm.cs @@ -2,6 +2,7 @@ using System.Globalization; using Eto.Forms; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; using NAPS2.Scan; namespace NAPS2.EtoForms.Ui; @@ -11,27 +12,27 @@ public class PageSizeForm : EtoDialogBase private readonly ComboBox _name = new(); private readonly NumericMaskedTextBox _width = new(); private readonly NumericMaskedTextBox _height = new(); - private readonly DropDown _unit = C.EnumDropDown(); + private readonly EnumDropDownWidget _unit = new(); private PageDimensions? _initialDimens; - public PageSizeForm(Naps2Config config) + public PageSizeForm(Naps2Config config, IIconProvider iconProvider) : base(config) { DeletePageSizeCommand = new ActionCommand(DeletePageSize) { - Image = Icons.cross_small.ToEtoImage() + Image = iconProvider.GetIcon("cross_small") }; _name.SelectedValueChanged += Name_SelectionChange; _name.TextChanged += Name_TextChanged; } - public Command DeletePageSizeCommand { get; set; } + public ActionCommand DeletePageSizeCommand { get; set; } private void DeletePageSize() { if (MessageBox.Show(string.Format(MiscResources.ConfirmDelete, _name.Text), MiscResources.Delete, - MessageBoxButtons.OKCancel, MessageBoxType.Question) == DialogResult.Ok) + MessageBoxButtons.OKCancel, MessageBoxType.Question, MessageBoxDefaultButton.OK) == DialogResult.Ok) { var presets = Config.Get(c => c.CustomPageSizePresets); presets = presets.RemoveAll(x => x.Name == _name.Text); @@ -68,7 +69,7 @@ protected override void BuildLayout() C.Label(UiStrings.NameOptional), L.Row( _name.Scale().NaturalWidth(250).AlignCenter(), - C.Button(DeletePageSizeCommand, ButtonImagePosition.Overlay).AlignCenter() + C.Button(DeletePageSizeCommand, ButtonImagePosition.Overlay).AlignCenter().Width(30) ), C.Spacer(), C.Label(UiStrings.Dimensions), @@ -76,7 +77,7 @@ protected override void BuildLayout() _width.Scale().AlignCenter(), C.Label("X").AlignCenter(), _height.Scale().AlignCenter(), - _unit.Scale().AlignCenter() + _unit.AsControl().Scale().AlignCenter() ), L.Row( C.Filler(), @@ -100,7 +101,7 @@ private void UpdateDimens(PageDimensions dimens) { _width.Text = dimens.Width.ToString(CultureInfo.CurrentCulture); _height.Text = dimens.Height.ToString(CultureInfo.CurrentCulture); - _unit.SelectedIndex = (int) dimens.Unit; + _unit.SelectedItem = dimens.Unit; } private void Name_SelectionChange(object? sender, EventArgs e) @@ -137,7 +138,7 @@ private void Submit() { Width = width, Height = height, - Unit = (LocalizedPageSizeUnit) _unit.SelectedIndex + Unit = _unit.SelectedItem }; if (!string.IsNullOrWhiteSpace(_name.Text)) { diff --git a/NAPS2.Lib/EtoForms/Ui/PdfPasswordForm.cs b/NAPS2.Lib/EtoForms/Ui/PdfPasswordForm.cs index 41842c0415..4bae955388 100644 --- a/NAPS2.Lib/EtoForms/Ui/PdfPasswordForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/PdfPasswordForm.cs @@ -1,4 +1,5 @@ using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; namespace NAPS2.EtoForms.Ui; diff --git a/NAPS2.Lib/EtoForms/Ui/PdfSettingsForm.cs b/NAPS2.Lib/EtoForms/Ui/PdfSettingsForm.cs index b01fcf487b..6fcf849677 100644 --- a/NAPS2.Lib/EtoForms/Ui/PdfSettingsForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/PdfSettingsForm.cs @@ -1,7 +1,8 @@ using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; -using NAPS2.ImportExport.Pdf; +using NAPS2.EtoForms.Widgets; +using NAPS2.Pdf; namespace NAPS2.EtoForms.Ui; @@ -19,9 +20,10 @@ public class PdfSettingsForm : EtoDialogBase private readonly PasswordBoxWithToggle _userPassword = new() { Title = UiStrings.UserPasswordLabel }; private readonly CheckBox _rememberSettings = new() { Text = UiStrings.RememberTheseSettings }; private readonly Button _restoreDefaults = new() { Text = UiStrings.RestoreDefaults }; + private readonly LayoutVisibility _encryptVis = new(false); - private readonly List _permissions = new() - { + private readonly List _permissions = + [ new CheckBox { Text = UiStrings.AllowPrinting }, new CheckBox { Text = UiStrings.AllowFullQualityPrinting }, new CheckBox { Text = UiStrings.AllowDocumentModification }, @@ -30,21 +32,25 @@ public class PdfSettingsForm : EtoDialogBase new CheckBox { Text = UiStrings.AllowContentCopyingForAccessibility }, new CheckBox { Text = UiStrings.AllowAnnotations }, new CheckBox { Text = UiStrings.AllowFormFilling } - }; + ]; - private readonly DropDown _compat = C.EnumDropDown(compat => compat switch - { - PdfCompat.Default => UiStrings.Default, - PdfCompat.PdfA1B => "PDF/A-1b", - PdfCompat.PdfA2B => "PDF/A-2b", - PdfCompat.PdfA3B => "PDF/A-3b", - PdfCompat.PdfA3U => "PDF/A-3u", - _ => throw new ArgumentException() - }); - - public PdfSettingsForm(Naps2Config config, DialogHelper dialogHelper) : base(config) + private readonly EnumDropDownWidget _compat = new(); + + public PdfSettingsForm(Naps2Config config, DialogHelper dialogHelper, IIconProvider iconProvider) : base(config) { - _defaultFilePath = new(this, dialogHelper); + Title = UiStrings.PdfSettingsFormTitle; + IconName = "file_extension_pdf_small"; + + _defaultFilePath = new(this, dialogHelper) { PdfOnly = true }; + _compat.Format = compat => compat switch + { + PdfCompat.Default => UiStrings.Default, + PdfCompat.PdfA1B => "PDF/A-1b", + PdfCompat.PdfA2B => "PDF/A-2b", + PdfCompat.PdfA3B => "PDF/A-3b", + PdfCompat.PdfA3U => "PDF/A-3u", + _ => throw new ArgumentException() + }; UpdateValues(Config); UpdateEnabled(); @@ -56,9 +62,6 @@ public PdfSettingsForm(Naps2Config config, DialogHelper dialogHelper) : base(con protected override void BuildLayout() { - Title = UiStrings.PdfSettingsFormTitle; - Icon = new Icon(1f, Icons.file_extension_pdf_small.ToEtoImage()); - FormStateController.DefaultExtraLayoutSize = new Size(60, 0); FormStateController.FixedHeightLayout = true; @@ -84,9 +87,11 @@ protected override void BuildLayout() UiStrings.Encryption, L.Column( _encryptPdf, - _ownerPassword, - _userPassword, - L.Column(_permissions.Expand()).Spacing(0) + L.Column( + _ownerPassword, + _userPassword, + L.Column(_permissions.Expand()).Spacing(0) + ).Visible(_encryptVis) ) ), L.GroupBox( @@ -126,22 +131,15 @@ private void UpdateValues(Naps2Config config) config.Get(c => c.PdfSettings.Encryption.AllowContentCopyingForAccessibility); _permissions[6].Checked = config.Get(c => c.PdfSettings.Encryption.AllowAnnotations); _permissions[7].Checked = config.Get(c => c.PdfSettings.Encryption.AllowFormFilling); - _compat.SelectedIndex = (int) config.Get(c => c.PdfSettings.Compat); + _compat.SelectedItem = config.Get(c => c.PdfSettings.Compat); _rememberSettings.Checked = config.Get(c => c.RememberPdfSettings); } private void UpdateEnabled() { _skipSavePrompt.Enabled = Path.IsPathRooted(_defaultFilePath.Text); - - bool encrypt = _encryptPdf.IsChecked(); - _userPassword.Enabled = _ownerPassword.Enabled = encrypt; - foreach (var perm in _permissions) - { - perm.Enabled = encrypt; - } - - _compat.Enabled = !Config.AppLocked.TryGet(c => c.PdfSettings.Compat, out _); + _encryptVis.IsVisible = _encryptPdf.IsChecked(); + _compat.Enabled = !Config.AppLocked.Has(c => c.PdfSettings.Compat); } private void Save() @@ -172,7 +170,7 @@ private void Save() AllowAnnotations = _permissions[6].IsChecked(), AllowFormFilling = _permissions[7].IsChecked() }, - Compat = (PdfCompat) _compat.SelectedIndex + Compat = _compat.SelectedItem }; var runTransact = Config.Run.BeginTransaction(); diff --git a/NAPS2.Lib/EtoForms/Ui/PlaceholdersForm.cs b/NAPS2.Lib/EtoForms/Ui/PlaceholdersForm.cs index 25f61424be..90bcf021db 100644 --- a/NAPS2.Lib/EtoForms/Ui/PlaceholdersForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/PlaceholdersForm.cs @@ -27,7 +27,6 @@ private static readonly (string val, string text)[] PlaceholderButtons = { public PlaceholdersForm(Naps2Config config) : base(config) { // TODO: Ellipsis aren't working, presumably because Eto uses custom label rendering on WinForms - EtoPlatform.Current.ConfigureEllipsis(_preview); _fileName.TextChanged += FileName_TextChanged; } @@ -44,7 +43,7 @@ protected override void BuildLayout() C.Label(UiStrings.FileNameLabel), _fileName, C.Label(UiStrings.PreviewLabel), - _preview, + _preview.Ellipsize(), L.Row( C.Filler(), L.OkCancel( diff --git a/NAPS2.Lib/EtoForms/Ui/PreviewForm.cs b/NAPS2.Lib/EtoForms/Ui/PreviewForm.cs index 2eb35c944a..19d399891f 100644 --- a/NAPS2.Lib/EtoForms/Ui/PreviewForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/PreviewForm.cs @@ -1,48 +1,148 @@ using Eto.Drawing; using Eto.Forms; +using NAPS2.EtoForms.Desktop; +using NAPS2.EtoForms.Widgets; namespace NAPS2.EtoForms.Ui; public class PreviewForm : EtoDialogBase { private readonly DesktopCommands _desktopCommands; + private readonly IIconProvider _iconProvider; + private readonly KeyboardShortcutManager _previewKsm; - private readonly ImageView _imageView = new(); + private readonly ButtonToolItem _pageNumberButton = new(); + private readonly ButtonToolItem _zoomPercentButton = new(); private UiImage? _currentImage; public PreviewForm(Naps2Config config, DesktopCommands desktopCommands, UiImageList imageList, - IIconProvider iconProvider) : base(config) + IIconProvider iconProvider, ColorScheme colorScheme) : base(config) { + Title = UiStrings.PreviewFormTitle; + Icon = EtoPlatform.Current.IsGtk ? new Icon(1f, Icons.scanner_128.ToEtoImage()) : Icons.favicon.ToEtoIcon(); + _desktopCommands = desktopCommands; ImageList = imageList; + _iconProvider = iconProvider; + + ImageViewer.ColorScheme = colorScheme; + ImageViewer.ZoomChanged += ImageViewerZoomChanged; + ImageList.ImagesUpdated += ImageList_ImagesUpdated; GoToPrevCommand = new ActionCommand(() => GoTo(ImageIndex - 1)) { Text = UiStrings.Previous, - Image = iconProvider.GetIcon("arrow_left") + IconName = "arrow_left_small" }; GoToNextCommand = new ActionCommand(() => GoTo(ImageIndex + 1)) { Text = UiStrings.Next, - Image = iconProvider.GetIcon("arrow_right") + IconName = "arrow_right_small" + }; + ZoomInCommand = new ActionCommand(() => ImageViewer.ChangeZoom(1)) + { + Text = UiStrings.ZoomIn, + IconName = "zoom_in_small" + }; + ZoomOutCommand = new ActionCommand(() => ImageViewer.ChangeZoom(-1)) + { + Text = UiStrings.ZoomOut, + IconName = "zoom_out_small" + }; + ZoomWindowCommand = new ActionCommand(ImageViewer.ZoomToContainer) + { + // TODO: Update this string as it's now a button and not a toggle + Text = UiStrings.ScaleWithWindow, + IconName = "arrow_out_small" + }; + ZoomActualCommand = new ActionCommand(ImageViewer.ZoomToActual) + { + Text = UiStrings.ZoomActual, + IconName = "zoom_actual_small" + }; + DeleteCurrentImageCommand = new ActionCommand(DeleteCurrentImage) + { + Text = UiStrings.Delete, + IconName = "cross_small" }; + + _previewKsm = new KeyboardShortcutManager(); + EtoPlatform.Current.HandleKeyDown(this, _previewKsm.Perform); } - protected override void BuildLayout() + private void ImageList_ImagesUpdated(object? sender, ImageListEventArgs e) { - Title = UiStrings.PreviewFormTitle; - Icon = new Icon(1f, Icons.picture.ToEtoImage()); + Invoker.Current.InvokeDispatch(async () => + { + bool shouldClose = false; + lock (ImageList) + { + if (ImageList.Images.Contains(CurrentImage)) + { + UpdateImageIndex(); + UpdatePage(); + return; + } + if (ImageList.Images.Any()) + { + // Update the GUI for the newly displayed image + var nextIndex = ImageIndex >= ImageList.Images.Count ? ImageList.Images.Count - 1 : ImageIndex; + CurrentImage = ImageList.Images[nextIndex]; + ImageList.UpdateSelection(ListSelection.Of(CurrentImage)); + } + else + { + shouldClose = true; + ImageList.UpdateSelection(ListSelection.Empty()); + } + } + if (shouldClose) + { + // No images left to display, so no point keeping the form open + Close(); + } + else + { + UpdatePage(); + await UpdateImage(); + } + }); + } + + private void UpdateImageIndex() + { + var index = ImageList.Images.IndexOf(CurrentImage); + if (index == -1) + { + index = 0; + } + ImageIndex = index; + } + + protected ScrollZoomImageViewer ImageViewer { get; } = new(); + + private void ImageViewerZoomChanged(object? sender, ZoomChangedEventArgs e) + { + _zoomPercentButton.Text = e.Zoom.ToString("P0"); + } + protected override void BuildLayout() + { FormStateController.AutoLayoutSize = false; FormStateController.DefaultClientSize = new Size(800, 600); LayoutController.RootPadding = 0; - LayoutController.Content = _imageView; + LayoutController.Content = ImageViewer; } protected DesktopCommands Commands { get; set; } = null!; + protected ActionCommand DeleteCurrentImageCommand { get; } protected ActionCommand GoToPrevCommand { get; } protected ActionCommand GoToNextCommand { get; } + protected ActionCommand ZoomInCommand { get; } + protected ActionCommand ZoomOutCommand { get; } + protected ActionCommand ZoomWindowCommand { get; } + protected ActionCommand ZoomActualCommand { get; } protected UiImageList ImageList { get; } @@ -56,45 +156,24 @@ public UiImage CurrentImage _currentImage.ThumbnailInvalidated -= ImageThumbnailInvalidated; } _currentImage = value; - Commands = _desktopCommands.WithSelection(ListSelection.Of(_currentImage)); + UpdateImageIndex(); + Commands = _desktopCommands.WithSelection(() => ListSelection.Of(_currentImage)); _currentImage.ThumbnailInvalidated += ImageThumbnailInvalidated; } } private void ImageThumbnailInvalidated(object? sender, EventArgs e) { - Invoker.Current.SafeInvoke(() => UpdateImage().AssertNoAwait()); + Invoker.Current.InvokeDispatch(() => UpdateImage().AssertNoAwait()); } - protected int ImageIndex - { - get - { - var index = ImageList!.Images.IndexOf(CurrentImage); - if (index == -1) - { - index = 0; - } - return index; - } - } + protected int ImageIndex { get; private set; } protected override async void OnLoad(EventArgs eventArgs) { base.OnLoad(eventArgs); - // TODO: Implement - // _tbPageCurrent.Visible = PlatformCompat.Runtime.IsToolbarTextboxSupported; - // if (Config.Get(c => c.HiddenButtons).HasFlag(ToolbarButtons.SavePdf)) - // { - // _toolStrip1.Items.Remove(_tsSavePdf); - // } - // if (Config.Get(c => c.HiddenButtons).HasFlag(ToolbarButtons.SaveImages)) - // { - // _toolStrip1.Items.Remove(_tsSaveImage); - // } - - // TODO: Implement mouse and keyboard controls - // AssignKeyboardShortcuts(); + + AssignKeyboardShortcuts(); // TODO: We should definitely start with separate image forms, but it might be fairly trivial to, when opened // from the preview form, have the temporary rendering be propagated back to the viewer form and have the @@ -102,8 +181,14 @@ protected override async void OnLoad(EventArgs eventArgs) // desktop form should still open the full image editing form (for now at least). CreateToolbar(); - UpdatePage(); await UpdateImage(); + UpdatePage(); + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + ImageViewer.ZoomToContainer(); } protected virtual void CreateToolbar() @@ -112,9 +197,20 @@ protected virtual void CreateToolbar() { Items = { - new DropDownToolItem + // TODO: Either embed an editable textbox, or make this button pop up a window or widget to enter a page number + _pageNumberButton, + MakeToolButton(GoToPrevCommand), + MakeToolButton(GoToNextCommand), + new SeparatorToolItem(), + MakeToolButton(ZoomWindowCommand), + MakeToolButton(ZoomActualCommand), + MakeToolButton(ZoomInCommand), + MakeToolButton(ZoomOutCommand), + _zoomPercentButton, + new SeparatorToolItem(), + WithToolItemIcon(new DropDownToolItem { - Image = Commands.RotateMenu.Image, + ToolTip = UiStrings.Rotate, Items = { Commands.RotateLeft, @@ -123,19 +219,51 @@ protected virtual void CreateToolbar() Commands.Deskew, Commands.CustomRotate } - }, - Commands.Crop, - Commands.BrightCont, - Commands.HueSat, - Commands.BlackWhite, - Commands.Sharpen, + }, "arrow_rotate_anticlockwise_small"), + MakeToolButton(Commands.Crop), + MakeToolButton(Commands.BrightCont), + MakeToolButton(Commands.HueSat), + MakeToolButton(Commands.BlackWhite), + MakeToolButton(Commands.Sharpen), + MakeToolButton(Commands.DocumentCorrection), + new SeparatorToolItem(), + MakeToolButton(Commands.Split), + MakeToolButton(Commands.Combine), + MakeToolButton(Commands.EditWithApp), new SeparatorToolItem(), - Commands.SaveSelectedPdf, - Commands.SaveSelectedImages, + MakeToolButton(Commands.SaveSelectedPdf, "file_extension_pdf"), + MakeToolButton(Commands.SaveSelectedImages, "picture_small"), new SeparatorToolItem(), - Commands.Delete + MakeToolButton(DeleteCurrentImageCommand), } }; + if (Config.Get(c => c.HiddenButtons).HasFlag(ToolbarButtons.SavePdf)) + { + ToolBar.Items.Remove(ToolBar.Items.Single(x => x.Command == Commands.SaveSelectedPdf)); + } + if (Config.Get(c => c.HiddenButtons).HasFlag(ToolbarButtons.SaveImages)) + { + ToolBar.Items.Remove(ToolBar.Items.Single(x => x.Command == Commands.SaveSelectedImages)); + } + } + + private ToolItem MakeToolButton(ActionCommand command, string? iconName = null) => + WithToolItemIcon(new ButtonToolItem + { + Command = command, + Text = null, + ToolTip = command.Text + }, iconName ?? command.IconName!); + + private ToolItem WithToolItemIcon(ToolItem toolItem, string iconName) + { + EtoPlatform.Current.AttachDpiDependency(this, + scale => + { + EtoPlatform.Current.SetImageSize(toolItem, (int) (16 * scale)); + toolItem.Image = EtoPlatform.Current.IconProvider.GetIcon(iconName, scale); + }); + return toolItem; } private async Task GoTo(int index) @@ -149,160 +277,117 @@ private async Task GoTo(int index) CurrentImage = ImageList.Images[index]; ImageList.UpdateSelection(ListSelection.Of(CurrentImage)); } - UpdatePage(); await UpdateImage(); + UpdatePage(); } protected virtual void UpdatePage() { - // TODO: Implement - // _tbPageCurrent.Text = (ImageIndex + 1).ToString(CultureInfo.CurrentCulture); - // _lblPageTotal.Text = string.Format(MiscResources.OfN, _imageList.Images.Count); - // if (!PlatformCompat.Runtime.IsToolbarTextboxSupported) - // { - // _lblPageTotal.Text = _tbPageCurrent.Text + ' ' + _lblPageTotal.Text; - // } + _pageNumberButton.Text = string.Format(UiStrings.XOfY, ImageIndex + 1, ImageList.Images.Count); } private async Task UpdateImage() { - // TODO: Implement - // _tiffViewer1.Image?.Dispose(); - // _tiffViewer1.Image = null; using var imageToRender = CurrentImage.GetClonedImage(); - var rendered = await Task.Run(() => imageToRender.Render()); - _imageView.Image = rendered.ToEtoImage(); - // _tiffViewer1.Image = imageToRender.RenderToBitmap(); + using var rendered = await Task.Run(() => imageToRender.Render()); + ImageViewer.Image?.Dispose(); + ImageViewer.Image = rendered.ToEtoImage(); + ImageViewer.ZoomToContainer(); + } + + protected override void OnSizeChanged(EventArgs e) + { + base.OnSizeChanged(e); + if (EtoPlatform.Current.IsGtk) + { + // Gtk delays adjusting the imageview size after the container size changes, which messes this up unless + // we queue it after the current UI thread options. + // We don't want to do this on Windows/Mac as it results in the render size lagging the window size. + Invoker.Current.InvokeDispatch(ImageViewer.ZoomToContainer); + } + else + { + ImageViewer.ZoomToContainer(); + } + } + + private void DeleteCurrentImage() + { + if (MessageBox.Show(this, + string.Format(MiscResources.ConfirmDeleteItems, 1), + MiscResources.Delete, MessageBoxButtons.OKCancel, + MessageBoxType.Question, MessageBoxDefaultButton.OK) == DialogResult.Ok) + { + // We don't want to run Commands.Delete as that runs on DesktopController and uses that selection. + Commands.ImageListActions.DeleteSelected(); + } + } + + private void AssignKeyboardShortcuts() + { + // Unconfigurable defaults + + _previewKsm.Assign("Esc", Close); + _previewKsm.Assign("Del", DeleteCurrentImageCommand); + _previewKsm.Assign("Mod+0", ZoomActualCommand); + _previewKsm.Assign("Mod+Z", Commands.Undo); + _previewKsm.Assign(EtoPlatform.Current.IsWinForms ? "Mod+Y" : "Mod+Shift+Z", Commands.Redo); + + _previewKsm.Assign("PageDown", () => _ = GoTo(ImageIndex + 1)); + _previewKsm.Assign("Right", () => _ = GoTo(ImageIndex + 1)); + _previewKsm.Assign("Down", () => _ = GoTo(ImageIndex + 1)); + + _previewKsm.Assign("PageUp", () => _ = GoTo(ImageIndex - 1)); + _previewKsm.Assign("Left", () => _ = GoTo(ImageIndex - 1)); + _previewKsm.Assign("Up", () => _ = GoTo(ImageIndex - 1)); + + // Configured defaults + + var ks = Config.Get(c => c.KeyboardShortcuts); + + _previewKsm.Assign(ks.Delete, DeleteCurrentImageCommand); + _previewKsm.Assign(ks.ImageBlackWhite, Commands.BlackWhite); + _previewKsm.Assign(ks.ImageBrightness, Commands.BrightCont); + _previewKsm.Assign(ks.ImageContrast, Commands.BrightCont); + _previewKsm.Assign(ks.ImageCrop, Commands.Crop); + _previewKsm.Assign(ks.ImageHue, Commands.HueSat); + _previewKsm.Assign(ks.ImageSaturation, Commands.HueSat); + _previewKsm.Assign(ks.ImageSharpen, Commands.Sharpen); + _previewKsm.Assign(ks.ImageDocumentCorrection, Commands.DocumentCorrection); + _previewKsm.Assign(ks.ImageSplit, Commands.Split); + _previewKsm.Assign(ks.ImageCombine, Commands.Combine); + _previewKsm.Assign(ks.ImageEditWith, Commands.EditWithApp); + + _previewKsm.Assign(ks.RotateCustom, Commands.CustomRotate); + _previewKsm.Assign(ks.RotateDeskew, Commands.Deskew); + _previewKsm.Assign(ks.RotateFlip, Commands.Flip); + _previewKsm.Assign(ks.RotateLeft, Commands.RotateLeft); + _previewKsm.Assign(ks.RotateRight, Commands.RotateRight); + + if (PlatformCompat.System.CombinedPdfAndImageSaving) + { + _previewKsm.Assign(ks.SavePDFAll, Commands.SaveSelected); + } + else + { + _previewKsm.Assign(ks.SavePDF, Commands.SaveSelectedPdf); + _previewKsm.Assign(ks.SaveImages, Commands.SaveSelectedImages); + _previewKsm.Assign(ks.SavePDFAll, Commands.SaveSelectedPdf); + _previewKsm.Assign(ks.SaveImagesAll, Commands.SaveSelectedImages); + } + + _previewKsm.Assign(ks.ZoomIn, ZoomInCommand); + _previewKsm.Assign(ks.ZoomOut, ZoomOutCommand); } protected override void Dispose(bool disposing) { if (disposing) { - // TODO: Implement - // _components?.Dispose(); - // _tiffViewer1?.Image?.Dispose(); - // _tiffViewer1?.Dispose(); + ImageViewer.Image?.Dispose(); + ImageViewer.Image = null; + ImageList.ImagesUpdated -= ImageList_ImagesUpdated; } base.Dispose(disposing); } - - // private async void tbPageCurrent_TextChanged(object sender, EventArgs e) - // { - // if (int.TryParse(_tbPageCurrent.Text, out int indexOffBy1)) - // { - // await GoTo(indexOffBy1 - 1); - // } - // } - - // private async Task DeleteCurrentImage() - // { - // // TODO: Are the file access issues still a thing? - // // Need to dispose the bitmap first to avoid file access issues - // _tiffViewer1.Image?.Dispose(); - // - // var lastIndex = ImageIndex; - // await _imageList.MutateAsync(new ImageListMutation.DeleteSelected(), - // ListSelection.Of(CurrentImage)); - // - // bool shouldClose = false; - // lock (_imageList) - // { - // if (_imageList.Images.Any()) - // { - // // Update the GUI for the newly displayed image - // var nextIndex = lastIndex >= _imageList.Images.Count ? _imageList.Images.Count - 1 : lastIndex; - // CurrentImage = _imageList.Images[nextIndex]; - // } - // else - // { - // shouldClose = true; - // } - // } - // if (shouldClose) - // { - // // No images left to display, so no point keeping the form open - // Close(); - // } - // else - // { - // UpdatePage(); - // await UpdateImage(); - // } - // } - - // private async void tiffViewer1_KeyDown(object sender, KeyEventArgs e) - // { - // if (!(e.Control || e.Shift || e.Alt)) - // { - // switch (e.KeyCode) - // { - // case Keys.Escape: - // Close(); - // return; - // case Keys.PageDown: - // case Keys.Right: - // case Keys.Down: - // await GoTo(ImageIndex + 1); - // return; - // case Keys.PageUp: - // case Keys.Left: - // case Keys.Up: - // await GoTo(ImageIndex - 1); - // return; - // } - // } - // - // e.Handled = _ksm.Perform(e.KeyData); - // } - // - // private async void tbPageCurrent_KeyDown(object sender, KeyEventArgs e) - // { - // if (!(e.Control || e.Shift || e.Alt)) - // { - // switch (e.KeyCode) - // { - // case Keys.PageDown: - // case Keys.Right: - // case Keys.Down: - // await GoTo(ImageIndex + 1); - // return; - // case Keys.PageUp: - // case Keys.Left: - // case Keys.Up: - // await GoTo(ImageIndex - 1); - // return; - // } - // } - // - // e.Handled = _ksm.Perform(e.KeyData); - // } - // - // private void AssignKeyboardShortcuts() - // { - // // Defaults - // - // _ksm.Assign("Del", _tsDelete); - // - // // Configured - // - // // TODO: Granular - // var ks = Config.Get(c => c.KeyboardShortcuts); - // - // _ksm.Assign(ks.Delete, _tsDelete); - // _ksm.Assign(ks.ImageBlackWhite, _tsBlackWhite); - // _ksm.Assign(ks.ImageBrightness, _tsBrightnessContrast); - // _ksm.Assign(ks.ImageContrast, _tsBrightnessContrast); - // _ksm.Assign(ks.ImageCrop, _tsCrop); - // _ksm.Assign(ks.ImageHue, _tsHueSaturation); - // _ksm.Assign(ks.ImageSaturation, _tsHueSaturation); - // _ksm.Assign(ks.ImageSharpen, _tsSharpen); - // - // _ksm.Assign(ks.RotateCustom, _tsCustomRotation); - // _ksm.Assign(ks.RotateFlip, _tsFlip); - // _ksm.Assign(ks.RotateLeft, _tsRotateLeft); - // _ksm.Assign(ks.RotateRight, _tsRotateRight); - // _ksm.Assign(ks.SaveImages, _tsSaveImage); - // _ksm.Assign(ks.SavePDF, _tsSavePdf); - // } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/ProfilesForm.cs b/NAPS2.Lib/EtoForms/Ui/ProfilesForm.cs index 3c8e7a7a42..4b7b538bf6 100644 --- a/NAPS2.Lib/EtoForms/Ui/ProfilesForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/ProfilesForm.cs @@ -1,6 +1,7 @@ using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; using NAPS2.ImportExport.Profiles; using NAPS2.Scan; using NAPS2.Serialization; @@ -12,29 +13,34 @@ public class ProfilesForm : EtoDialogBase private readonly IScanPerformer _scanPerformer; private readonly ProfileNameTracker _profileNameTracker; private readonly IProfileManager _profileManager; - private readonly ProfileTransfer _profileTransfer; private readonly ThumbnailController _thumbnailController; + private readonly IIconProvider _iconProvider; + private readonly ProfileTransfer _profileTransfer = new(); private readonly IListView _listView; - private readonly Command _scanCommand; - private readonly Command _addCommand; - private readonly Command _editCommand; - private readonly Command _deleteCommand; - private readonly Command _setDefaultCommand; - private readonly Command _copyCommand; - private readonly Command _pasteCommand; + private readonly ActionCommand _scanCommand; + private readonly ActionCommand _addCommand; + private readonly ActionCommand _editCommand; + private readonly ActionCommand _deleteCommand; + private readonly ActionCommand _setDefaultCommand; + private readonly ActionCommand _copyCommand; + private readonly ActionCommand _pasteCommand; + private readonly ActionCommand _scannerSharingCommand; public ProfilesForm(Naps2Config config, IScanPerformer scanPerformer, ProfileNameTracker profileNameTracker, IProfileManager profileManager, ProfileListViewBehavior profileListViewBehavior, - ProfileTransfer profileTransfer, ThumbnailController thumbnailController) + ThumbnailController thumbnailController, IIconProvider iconProvider) : base(config) { + Title = UiStrings.ProfilesFormTitle; + IconName = "blueprints_small"; + _scanPerformer = scanPerformer; _profileNameTracker = profileNameTracker; _profileManager = profileManager; - _profileTransfer = profileTransfer; _thumbnailController = thumbnailController; + _iconProvider = iconProvider; // TODO: Do this only in WinForms (?) // switch (Handler) @@ -49,41 +55,51 @@ public ProfilesForm(Naps2Config config, IScanPerformer scanPerformer, ProfileNam _scanCommand = new ActionCommand(DoScan) { MenuText = UiStrings.Scan, - Image = Icons.control_play_blue_small.ToEtoImage(), + IconName = "control_play_blue_small", }; _addCommand = new ActionCommand(DoAdd) { MenuText = UiStrings.New, - Image = Icons.add_small.ToEtoImage() + IconName = "add_small" }; _editCommand = new ActionCommand(DoEdit) { MenuText = UiStrings.Edit, - Image = Icons.pencil_small.ToEtoImage() + IconName = "pencil_small" }; _deleteCommand = new ActionCommand(DoDelete) { MenuText = UiStrings.Delete, - Image = Icons.cross_small.ToEtoImage(), - Shortcut = Keys.Delete + IconName = "cross_small" }; _setDefaultCommand = new ActionCommand(DoSetDefault) { MenuText = UiStrings.SetDefault, - Image = Icons.accept_small.ToEtoImage() + IconName = "accept_small" }; _copyCommand = new ActionCommand(DoCopy) { - MenuText = UiStrings.Copy, - Shortcut = Keys.Control | Keys.C + MenuText = UiStrings.Copy }; _pasteCommand = new ActionCommand(DoPaste) { - MenuText = UiStrings.Paste, - Shortcut = Keys.Control | Keys.V + MenuText = UiStrings.Paste + }; + _scannerSharingCommand = new ActionCommand(OpenScannerSharingForm) + { + MenuText = UiStrings.ScannerSharing, + IconName = "wireless_small" }; - _listView.ImageSize = 48; + var profilesKsm = new KeyboardShortcutManager(); + profilesKsm.Assign("Esc", Close); + profilesKsm.Assign("Del", _deleteCommand); + profilesKsm.Assign("Mod+C", _copyCommand); + profilesKsm.Assign("Mod+V", _pasteCommand); + EtoPlatform.Current.HandleKeyDown(_listView.Control, profilesKsm.Perform); + + EtoPlatform.Current.AttachDpiDependency(this, _ => _listView.RegenerateImages()); + _listView.ImageSize = new Size(48, 48); _listView.ItemClicked += ItemClicked; _listView.SelectionChanged += SelectionChanged; _listView.Drop += Drop; @@ -92,42 +108,34 @@ public ProfilesForm(Naps2Config config, IScanPerformer scanPerformer, ProfileNam _addCommand.Enabled = !NoUserProfiles; _editCommand.Enabled = false; _deleteCommand.Enabled = false; - ReloadProfiles(); - var defaultProfile = _profileManager.Profiles.FirstOrDefault(x => x.IsDefault); - if (defaultProfile != null) - { - _listView.Selection = ListSelection.Of(defaultProfile); - } - ContextMenu = new ContextMenu(); - ContextMenu.AddItems( - new ButtonMenuItem(_scanCommand), - new ButtonMenuItem(_editCommand), - new ButtonMenuItem(_setDefaultCommand), + var contextMenu = new ContextMenu(); + _listView.ContextMenu = contextMenu; + contextMenu.AddItems( + C.ButtonMenuItem(this, _scanCommand), + C.ButtonMenuItem(this, _editCommand), + C.ButtonMenuItem(this, _setDefaultCommand), new SeparatorMenuItem()); if (!NoUserProfiles) { - ContextMenu.AddItems( - new ButtonMenuItem(_copyCommand), - new ButtonMenuItem(_pasteCommand), + contextMenu.AddItems( + C.ButtonMenuItem(this, _copyCommand), + C.ButtonMenuItem(this, _pasteCommand), new SeparatorMenuItem()); } - ContextMenu.AddItems( - new ButtonMenuItem(_deleteCommand)); - ContextMenu.Opening += ContextMenuOpening; + contextMenu.AddItems( + C.ButtonMenuItem(this, _deleteCommand)); + contextMenu.Opening += ContextMenuOpening; } protected override void BuildLayout() { - Title = UiStrings.ProfilesFormTitle; - Icon = new Icon(1f, Icons.blueprints_small.ToEtoImage()); - FormStateController.DefaultExtraLayoutSize = new Size(200, 0); LayoutController.Content = L.Column( L.Row( - _listView.Control.Scale(), - C.Button(_scanCommand, Icons.control_play_blue.ToEtoImage(), ButtonImagePosition.Above) + _listView.Control.NaturalSize(150, 100).Scale(), + C.Button(_scanCommand, "control_play_blue", ButtonImagePosition.Above, ButtonFlags.LargeIcon) .Height(80) ).Aligned().Scale(), L.Row( @@ -135,7 +143,11 @@ protected override void BuildLayout() L.Row( C.Button(_addCommand, ButtonImagePosition.Left), C.Button(_editCommand, ButtonImagePosition.Left), - C.Button(_deleteCommand, ButtonImagePosition.Left) + C.Button(_deleteCommand, ButtonImagePosition.Left), + C.Filler(), + Config.Get(c => c.DisableScannerSharing) + ? C.None() + : C.Button(_scannerSharingCommand, ButtonImagePosition.Left) ) ), C.CancelButton(this, UiStrings.Done) @@ -153,6 +165,17 @@ private bool SelectionLocked private bool NoUserProfiles => Config.Get(c => c.NoUserProfiles) && _profileManager.Profiles.Any(x => x.IsLocked); + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + ReloadProfiles(); + var defaultProfile = _profileManager.Profiles.FirstOrDefault(x => x.IsDefault); + if (defaultProfile != null) + { + _listView.Selection = ListSelection.Of(defaultProfile); + } + } + private void ProfilesUpdated(object? sender, EventArgs e) { ReloadProfiles(); @@ -208,7 +231,8 @@ private void Drop(object? sender, DropEventArgs e) if (!NoUserProfiles) { _profileManager.Mutate( - new ListMutation.Append(data.ScanProfileXml.FromXml()), _listView); + new ListMutation.AppendAndSelect(data.ScanProfileXml.FromXml()), + _listView); } } } @@ -240,6 +264,7 @@ private async void DoScan() if (_profileManager.Profiles.Count == 0) { var editSettingsForm = FormFactory.Create(); + editSettingsForm.NewProfile = true; editSettingsForm.ScanProfile = new ScanProfile { Version = ScanProfile.CURRENT_VERSION @@ -249,8 +274,8 @@ private async void DoScan() { return; } - _profileManager.Mutate(new ListMutation.Append(editSettingsForm.ScanProfile), - ListSelection.Empty()); + _profileManager.Mutate(new ListMutation.AppendAndSelect(editSettingsForm.ScanProfile), + _listView); _profileManager.DefaultProfile = editSettingsForm.ScanProfile; } if (SelectedProfile == null) @@ -274,11 +299,12 @@ private async void DoScan() private void DoAdd() { var fedit = FormFactory.Create(); + fedit.NewProfile = true; fedit.ScanProfile = Config.DefaultProfileSettings(); fedit.ShowModal(); if (fedit.Result) { - _profileManager.Mutate(new ListMutation.Append(fedit.ScanProfile), _listView); + _profileManager.Mutate(new ListMutation.AppendAndSelect(fedit.ScanProfile), _listView); } } @@ -302,8 +328,8 @@ private void DoDelete() if (SelectedProfile != null && !SelectionLocked) { string message = string.Format(MiscResources.ConfirmDeleteSingleProfile, SelectedProfile.DisplayName); - if (MessageBox.Show(message, MiscResources.Delete, MessageBoxButtons.YesNo, MessageBoxType.Warning) == - DialogResult.Yes) + if (MessageBox.Show(message, MiscResources.Delete, MessageBoxButtons.OKCancel, MessageBoxType.Warning, + MessageBoxDefaultButton.OK) == DialogResult.Ok) { foreach (var profile in _listView.Selection) { @@ -340,7 +366,13 @@ private void DoPaste() { var data = _profileTransfer.GetFromClipboard(); var profile = data.ScanProfileXml.FromXml(); - _profileManager.Mutate(new ListMutation.Append(profile), _listView); + _profileManager.Mutate(new ListMutation.AppendAndSelect(profile), _listView); } } + + private void OpenScannerSharingForm() + { + var form = FormFactory.Create(); + form.ShowModal(); + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/ProgressForm.cs b/NAPS2.Lib/EtoForms/Ui/ProgressForm.cs index f8c37904ae..f2c14c4770 100644 --- a/NAPS2.Lib/EtoForms/Ui/ProgressForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/ProgressForm.cs @@ -28,6 +28,7 @@ public ProgressForm(Naps2Config config) : base(config) protected override void BuildLayout() { FormStateController.RestoreFormState = false; + FormStateController.SaveFormState = false; LayoutController.Content = L.Column( _status, @@ -56,7 +57,7 @@ void operation_StatusChanged(object? sender, EventArgs e) { if (_loaded && !_background) { - Invoker.Current.SafeInvoke(DisplayProgress); + Invoker.Current.InvokeDispatch(DisplayProgress); } } @@ -64,7 +65,7 @@ void operation_Finished(object? sender, EventArgs e) { if (_loaded && !_background) { - Invoker.Current.SafeInvoke(Close); + Invoker.Current.InvokeDispatch(Close); } } @@ -78,7 +79,7 @@ protected override void OnLoad(EventArgs e) DisplayProgress(); if (_operation.IsFinished) { - Close(); + Invoker.Current.InvokeDispatch(Close); } } @@ -113,6 +114,10 @@ private void TryCancelOp() private void RunInBg_Click(object? sender, EventArgs e) { + var bgOps = Config.Get(c => c.BackgroundOperations); + bgOps = bgOps.Add(Operation.GetType().Name); + Config.User.Set(c => c.BackgroundOperations, bgOps); + _background = true; Close(); } diff --git a/NAPS2.Lib/EtoForms/Ui/RecoverForm.cs b/NAPS2.Lib/EtoForms/Ui/RecoverForm.cs index d8d0030ee9..a6c9d3257f 100644 --- a/NAPS2.Lib/EtoForms/Ui/RecoverForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/RecoverForm.cs @@ -28,7 +28,7 @@ protected override void BuildLayout() var notNowButton = C.CancelButton(this, UiStrings.NotNow); LayoutController.Content = L.Column( - _prompt.Wrap(400).MinWidth(300), + _prompt.DynamicWrap(400).MinWidth(300), C.Filler(), L.Row( recoverButton.Scale().Height(32), diff --git a/NAPS2.Lib/EtoForms/Ui/ResolutionForm.cs b/NAPS2.Lib/EtoForms/Ui/ResolutionForm.cs new file mode 100644 index 0000000000..f890a58b15 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/ResolutionForm.cs @@ -0,0 +1,55 @@ +using System.Globalization; +using Eto.Forms; +using NAPS2.EtoForms.Layout; + +namespace NAPS2.EtoForms.Ui; + +public class ResolutionForm : EtoDialogBase +{ + private readonly NumericMaskedTextBox _dpiTextbox = new(); + + public ResolutionForm(Naps2Config config) + : base(config) + { + } + + public int Dpi { get; set; } + + public bool Result { get; private set; } + + protected override void BuildLayout() + { + _dpiTextbox.Text = Dpi == 0 ? "" : Dpi.ToString(CultureInfo.InvariantCulture); + _dpiTextbox.SelectAll(); + _dpiTextbox.Focus(); + + Title = UiStrings.ResolutionFormTitle; + + FormStateController.RestoreFormState = false; + FormStateController.FixedHeightLayout = true; + + LayoutController.Content = L.Column( + C.Label(UiStrings.Dpi), + _dpiTextbox.NaturalWidth(EtoPlatform.Current.IsWinForms ? 0 : 250), + L.Row( + C.Filler(), + L.OkCancel( + C.OkButton(this, Submit), + C.CancelButton(this)) + ) + ); + } + + + private void Submit() + { + const NumberStyles numberStyle = NumberStyles.AllowLeadingWhite; + if (!int.TryParse(_dpiTextbox.Text, numberStyle, CultureInfo.CurrentCulture, out int dpi) || dpi <= 0) + { + _dpiTextbox.Focus(); + return; + } + Dpi = dpi; + Result = true; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/RotateForm.cs b/NAPS2.Lib/EtoForms/Ui/RotateForm.cs index 9be707c01b..0a1648f9b6 100644 --- a/NAPS2.Lib/EtoForms/Ui/RotateForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/RotateForm.cs @@ -1,35 +1,34 @@ using Eto.Drawing; using Eto.Forms; +using NAPS2.EtoForms.Widgets; namespace NAPS2.EtoForms.Ui; -public class RotateForm : ImageFormBase +public class RotateForm : UnaryImageFormBase { private const int MIN_LINE_DISTANCE = 50; - private readonly SliderWithTextBox _angleSlider = new() { MinValue = -1800, MaxValue = 1800, TickFrequency = 450 }; + private readonly SliderWithTextBox _angleSlider = new(new SliderWithTextBox.DecimalConstraints(-180, 180, 45, 1)); private bool _guideExists; private PointF _guideStart, _guideEnd; - public RotateForm(Naps2Config config, ThumbnailController thumbnailController, IIconProvider iconProvider) : - base(config, thumbnailController) + public RotateForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController, + IIconProvider iconProvider) : + base(config, imageList, thumbnailController) { - Icon = new Icon(1f, Icons.arrow_rotate_anticlockwise_small.ToEtoImage()); Title = UiStrings.Rotate; + IconName = "arrow_rotate_anticlockwise_small"; - _angleSlider.Icon = iconProvider.GetIcon("arrow_rotate_anticlockwise_small"); - Sliders = new[] { _angleSlider }; + EtoPlatform.Current.AttachDpiDependency(this, + scale => _angleSlider.Icon = iconProvider.GetIcon("arrow_rotate_anticlockwise_small", scale)); + Sliders = [_angleSlider]; Overlay.MouseDown += Overlay_MouseDown; Overlay.MouseMove += Overlay_MouseMove; Overlay.MouseUp += Overlay_MouseUp; } - protected override IEnumerable Transforms => - new Transform[] - { - new RotationTransform(_angleSlider.Value / 10.0) - }; + protected override List Transforms => [new RotationTransform((double) _angleSlider.DecimalValue)]; private void Overlay_MouseDown(object? sender, MouseEventArgs e) { @@ -55,7 +54,7 @@ private void Overlay_MouseUp(object? sender, MouseEventArgs e) { angle += 90.0; } - var oldAngle = _angleSlider.Value / 10.0; + var oldAngle = (double) _angleSlider.DecimalValue; var newAngle = angle + oldAngle; while (newAngle > 180.0) { @@ -65,7 +64,7 @@ private void Overlay_MouseUp(object? sender, MouseEventArgs e) { newAngle += 360.0; } - _angleSlider.Value = (int)Math.Round(newAngle * 10); + _angleSlider.DecimalValue = (decimal) newAngle; } Overlay.Invalidate(); } @@ -84,7 +83,7 @@ protected override void PaintOverlay(object? sender, PaintEventArgs e) if (_guideExists) { e.Graphics.AntiAlias = true; - e.Graphics.DrawLine(new Pen(new Color(0, 0,0 )), _guideStart, _guideEnd); + e.Graphics.DrawLine(new Pen(new Color(0, 0, 0)), _guideStart, _guideEnd); } } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/ScannerSharingForm.cs b/NAPS2.Lib/EtoForms/Ui/ScannerSharingForm.cs new file mode 100644 index 0000000000..e867cf70aa --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/ScannerSharingForm.cs @@ -0,0 +1,219 @@ +using Eto.Drawing; +using Eto.Forms; +using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; +using NAPS2.Remoting.Server; + +namespace NAPS2.EtoForms.Ui; + +public class ScannerSharingForm : EtoDialogBase +{ + private readonly ISharedDeviceManager _sharedDeviceManager; + private readonly IOsServiceManager _osServiceManager; + private readonly ErrorOutput _errorOutput; + + private readonly CheckBox _shareAsService = C.CheckBox(UiStrings.ShareAsService); + private readonly IListView _listView; + + private readonly ActionCommand _addCommand; + private readonly ActionCommand _editCommand; + private readonly ActionCommand _deleteCommand; + + private bool _suppressChangeEvent; + + public ScannerSharingForm(Naps2Config config, SharedDevicesListViewBehavior listViewBehavior, + ISharedDeviceManager sharedDeviceManager, IOsServiceManager osServiceManager, ErrorOutput errorOutput, + IIconProvider iconProvider) + : base(config) + { + Title = UiStrings.ScannerSharingFormTitle; + IconName = "wireless_small"; + + _sharedDeviceManager = sharedDeviceManager; + _osServiceManager = osServiceManager; + _errorOutput = errorOutput; + + _listView = EtoPlatform.Current.CreateListView(listViewBehavior); + _addCommand = new ActionCommand(DoAdd) + { + MenuText = UiStrings.Share, + IconName = "add_small" + }; + _editCommand = new ActionCommand(DoEdit) + { + MenuText = UiStrings.Edit, + IconName = "pencil_small" + }; + _deleteCommand = new ActionCommand(DoDelete) + { + MenuText = UiStrings.Delete, + IconName = "cross_small" + }; + + var sharingKsm = new KeyboardShortcutManager(); + sharingKsm.Assign("Esc", Close); + sharingKsm.Assign("Del", _deleteCommand); + EtoPlatform.Current.HandleKeyDown(_listView.Control, sharingKsm.Perform); + + if (_osServiceManager.CanRegister) + { + _shareAsService.Checked = _osServiceManager.IsRegistered; + _shareAsService.CheckedChanged += ShareAsServiceCheckedChanged; + _sharedDeviceManager.SharingServerStopped += SharingServerStopped; + } + EtoPlatform.Current.AttachDpiDependency(this, _ => _listView.RegenerateImages()); + _listView.ImageSize = new Size(48, 48); + _listView.SelectionChanged += SelectionChanged; + + _addCommand.Enabled = true; + _editCommand.Enabled = false; + _deleteCommand.Enabled = false; + + var contextMenu = new ContextMenu(); + _listView.ContextMenu = contextMenu; + contextMenu.AddItems( + C.ButtonMenuItem(this, _editCommand), + C.ButtonMenuItem(this, _deleteCommand)); + contextMenu.Opening += ContextMenuOpening; + } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + _sharedDeviceManager.SharingServerStopped -= SharingServerStopped; + } + + protected override void BuildLayout() + { + FormStateController.DefaultExtraLayoutSize = new Size(200, 0); + + LayoutController.Content = L.Column( + C.Label(UiStrings.ScannerSharingIntro).DynamicWrap(400), + _osServiceManager.CanRegister ? _shareAsService : C.None(), + C.Spacer(), + _listView.Control.Scale().NaturalHeight(80), + L.Row( + L.Column( + L.Row( + C.Button(_addCommand, ButtonImagePosition.Left), + C.Button(_editCommand, ButtonImagePosition.Left), + C.Button(_deleteCommand, ButtonImagePosition.Left) + ) + ), + C.Filler(), + C.CancelButton(this, UiStrings.Done) + )); + } + + public Action? ImageCallback { get; set; } + + private SharedDevice? SelectedDevice => _listView.Selection.SingleOrDefault(); + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + ReloadDevices(); + } + + private void ReloadDevices() + { + _listView.SetItems(_sharedDeviceManager.SharedDevices); + } + + private void SelectionChanged(object? sender, EventArgs e) + { + _editCommand.Enabled = _listView.Selection.Count == 1; + _deleteCommand.Enabled = _listView.Selection.Count > 0; + } + + private void ContextMenuOpening(object? sender, EventArgs e) + { + _editCommand.Enabled = SelectedDevice != null; + _deleteCommand.Enabled = SelectedDevice != null; + } + + private void ShareAsServiceCheckedChanged(object? sender, EventArgs e) + { + if (_suppressChangeEvent) return; + _suppressChangeEvent = true; + try + { + if (_shareAsService.IsChecked()) + { + if (_osServiceManager.Register()) + { + _sharedDeviceManager.StopSharing(); + } + + } + else + { + _osServiceManager.Unregister(); + _sharedDeviceManager.StartSharing(); + } + } + catch (Exception ex) + { + // TODO: Maybe we display a generic string here? + Log.ErrorException(ex.Message, ex); + _errorOutput.DisplayError(ex.Message, ex); + _shareAsService.Checked = _osServiceManager.IsRegistered; + } + finally + { + _suppressChangeEvent = false; + } + } + + private void SharingServerStopped(object? sender, EventArgs e) + { + Invoker.Current.InvokeDispatch(() => + { + if (_suppressChangeEvent) return; + _suppressChangeEvent = true; + _shareAsService.Checked = false; + _suppressChangeEvent = false; + }); + } + + private void DoAdd() + { + var fedit = FormFactory.Create(); + fedit.ShowModal(); + if (fedit.Result) + { + _sharedDeviceManager.AddSharedDevice(fedit.SharedDevice!); + ReloadDevices(); + } + } + + private void DoEdit() + { + var originalDevice = SelectedDevice; + if (originalDevice != null) + { + var fedit = FormFactory.Create(); + fedit.SharedDevice = originalDevice; + fedit.ShowModal(); + if (fedit.Result) + { + _sharedDeviceManager.ReplaceSharedDevice(originalDevice, fedit.SharedDevice!); + ReloadDevices(); + } + } + } + + private void DoDelete() + { + if (SelectedDevice != null) + { + string message = string.Format(UiStrings.ConfirmDeleteSharedDevice, SelectedDevice.Name); + if (MessageBox.Show(message, MiscResources.Delete, MessageBoxButtons.OKCancel, MessageBoxType.Warning, + MessageBoxDefaultButton.OK) == DialogResult.Ok) + { + _sharedDeviceManager.RemoveSharedDevice(SelectedDevice); + ReloadDevices(); + } + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/SelectDeviceForm.cs b/NAPS2.Lib/EtoForms/Ui/SelectDeviceForm.cs deleted file mode 100644 index 877675875e..0000000000 --- a/NAPS2.Lib/EtoForms/Ui/SelectDeviceForm.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Eto.Drawing; -using Eto.Forms; -using NAPS2.EtoForms.Layout; -using NAPS2.Scan; - -namespace NAPS2.EtoForms.Ui; - -public class SelectDeviceForm : EtoDialogBase -{ - private readonly ListBox _devices = new(); - - public SelectDeviceForm(Naps2Config config) : base(config) - { - } - - protected override void BuildLayout() - { - foreach (var device in DeviceList) - { - _devices.Items.Add(new ListItem - { - Key = device.ID, - Text = device.Name - }); - } - if (_devices.Items.Count > 0) - { - _devices.SelectedIndex = 0; - } - - Title = UiStrings.SelectSource; - - FormStateController.SaveFormState = false; - FormStateController.RestoreFormState = false; - FormStateController.DefaultExtraLayoutSize = new Size(50, 0); - - LayoutController.Content = L.Row( - _devices.NaturalSize(150, 100).Scale(), - L.Column( - C.OkButton(this, SelectDevice, UiStrings.Select), - C.CancelButton(this) - ) - ); - } - - public List DeviceList { get; set; } = null!; - - public ScanDevice? SelectedDevice { get; private set; } - - private void SelectDevice() - { - if (_devices.SelectedValue == null) - { - _devices.Focus(); - return; - } - SelectedDevice = DeviceList.FirstOrDefault(x => x.ID == _devices.SelectedKey); - } -} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/ServerTrayIndicator.cs b/NAPS2.Lib/EtoForms/Ui/ServerTrayIndicator.cs new file mode 100644 index 0000000000..4429b81209 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/ServerTrayIndicator.cs @@ -0,0 +1,26 @@ +using Eto.Forms; + +namespace NAPS2.EtoForms.Ui; + +public class ServerTrayIndicator : TrayIndicator +{ + public ServerTrayIndicator() + { + // TODO: Maybe use a higher-quality icon with hidpi? + Image = EtoPlatform.Current.IsWinForms + ? Icons.scanner_16.ToEtoImage() // Windows has small tray icons + : EtoPlatform.Current.IsMac + ? Icons.scanner_gray_32.ToEtoImage() // Gray to match macOS tray style + : Icons.scanner_32.ToEtoImage(); + Title = string.Format(UiStrings.Naps2TitleFormat, UiStrings.ScannerSharing); + Menu = new ContextMenu( + new ButtonMenuItem + { + Text = UiStrings.StopScannerSharing, + Command = new ActionCommand(() => StopClicked?.Invoke(this, EventArgs.Empty)) + } + ); + } + + public event EventHandler? StopClicked; +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/SettingsForm.cs b/NAPS2.Lib/EtoForms/Ui/SettingsForm.cs new file mode 100644 index 0000000000..74b530d227 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/SettingsForm.cs @@ -0,0 +1,179 @@ +using System.Linq.Expressions; +using Eto.Drawing; +using Eto.Forms; +using NAPS2.EtoForms.Desktop; +using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; + +namespace NAPS2.EtoForms.Ui; + +internal class SettingsForm : EtoDialogBase +{ + private readonly DesktopFormProvider _desktopFormProvider; + private readonly EnumDropDownWidget _theme = new(scale: false); + private readonly CheckBox _scanChangesDefaultProfile = C.CheckBox(UiStrings.ScanChangesDefaultProfile); + private readonly CheckBox _showProfilesToolbar = C.CheckBox(UiStrings.ShowProfilesToolbar); + private readonly CheckBox _showPageNumbers = C.CheckBox(UiStrings.ShowPageNumbers); + private readonly EnumDropDownWidget _scanButtonDefaultAction = new(scale: false); + private readonly EnumDropDownWidget _saveButtonDefaultAction = new(scale: false); + private readonly CheckBox _clearAfterSaving = C.CheckBox(UiStrings.ClearAfterSaving); + private readonly CheckBox _keepSession = C.CheckBox(UiStrings.KeepSession); + private readonly CheckBox _singleInstance = C.CheckBox(UiStrings.SingleInstanceDesc); + private readonly ActionCommand _pdfSettingsCommand; + private readonly ActionCommand _imageSettingsCommand; + private readonly ActionCommand _emailSettingsCommand; + private readonly ActionCommand _keyboardShortcutsCommand; + private readonly Button _restoreDefaults = new() { Text = UiStrings.RestoreDefaults }; + + public SettingsForm(Naps2Config config, DesktopSubFormController desktopSubFormController, + DesktopFormProvider desktopFormProvider, IIconProvider iconProvider) : base(config) + { + Title = UiStrings.SettingsFormTitle; + IconName = "cog_small"; + + _desktopFormProvider = desktopFormProvider; + UpdateValues(Config); + _restoreDefaults.Click += RestoreDefaults_Click; + + _pdfSettingsCommand = new ActionCommand(desktopSubFormController.ShowPdfSettingsForm) + { + Text = UiStrings.PdfSettings, + Image = iconProvider.GetIcon("file_extension_pdf_small") + }; + _imageSettingsCommand = new ActionCommand(desktopSubFormController.ShowImageSettingsForm) + { + Text = UiStrings.ImageSettings, + Image = iconProvider.GetIcon("picture_small") + }; + _emailSettingsCommand = new ActionCommand(desktopSubFormController.ShowEmailSettingsForm) + { + Text = UiStrings.EmailSettings, + Image = iconProvider.GetIcon("email_small") + }; + _keyboardShortcutsCommand = new ActionCommand(() => FormFactory.Create().ShowModal()) + { + Text = UiStrings.KeyboardShortcuts, + Image = iconProvider.GetIcon("keyboard_small") + }; + } + + protected override void BuildLayout() + { + FormStateController.DefaultExtraLayoutSize = new Size(60, 0); + FormStateController.FixedHeightLayout = true; + + LayoutController.Content = L.Column( + L.GroupBox( + UiStrings.Interface, + L.Column( + PlatformCompat.System.SupportsTheme + ? L.Row( + C.Label(UiStrings.ThemeLabel).AlignCenter().Padding(right: 10), + _theme.AsControl().MinWidth(100) + ) + : C.None(), + PlatformCompat.System.SupportsShowPageNumbers ? _showPageNumbers : C.None(), + PlatformCompat.System.SupportsProfilesToolbar ? _showProfilesToolbar : C.None(), + _scanChangesDefaultProfile, + PlatformCompat.System.SupportsButtonActions + ? L.Row( + C.Label(UiStrings.ScanButtonDefaultAction).AlignCenter().Padding(right: 20), + _scanButtonDefaultAction + ).Aligned() + : C.None(), + PlatformCompat.System.SupportsButtonActions + ? L.Row( + C.Label(UiStrings.SaveButtonDefaultAction).AlignCenter().Padding(right: 20), + _saveButtonDefaultAction + ).Aligned() + : C.None(), + PlatformCompat.System.SupportsKeyboardShortcuts + ? C.Button(_keyboardShortcutsCommand, ButtonImagePosition.Left).AlignLeading() + : C.None() + ) + ), + L.GroupBox( + UiStrings.Application, + L.Column( + _clearAfterSaving, + _keepSession, + PlatformCompat.System.SupportsSingleInstance + ? _singleInstance + : C.None() + ) + ), + // TODO: Probably only show these after we start adding tabs + // L.Row( + // C.Button(_pdfSettingsCommand, ButtonImagePosition.Left), + // C.Button(_imageSettingsCommand, ButtonImagePosition.Left), + // C.Button(_emailSettingsCommand, ButtonImagePosition.Left)), + C.Filler(), + L.Row( + _restoreDefaults.MinWidth(140), + C.Filler(), + L.OkCancel( + C.OkButton(this, Save), + C.CancelButton(this)) + ) + ); + } + + private void UpdateValues(Naps2Config config) + { + void UpdateCheckbox(CheckBox checkBox, Expression> accessor) + { + checkBox.Checked = config.Get(accessor); + checkBox.Enabled = !config.AppLocked.Has(accessor); + } + + _theme.SelectedItem = config.Get(c => c.Theme); + _theme.Enabled = !config.AppLocked.Has(c => c.Theme); + UpdateCheckbox(_scanChangesDefaultProfile, c => c.ScanChangesDefaultProfile); + UpdateCheckbox(_showProfilesToolbar, c => c.ShowProfilesToolbar); + UpdateCheckbox(_showPageNumbers, c => c.ShowPageNumbers); + _scanButtonDefaultAction.SelectedItem = config.Get(c => c.ScanButtonDefaultAction); + _scanButtonDefaultAction.Enabled = !config.AppLocked.Has(c => c.ScanButtonDefaultAction); + _saveButtonDefaultAction.SelectedItem = config.Get(c => c.SaveButtonDefaultAction); + _saveButtonDefaultAction.Enabled = !config.AppLocked.Has(c => c.SaveButtonDefaultAction); + UpdateCheckbox(_clearAfterSaving, c => c.DeleteAfterSaving); + UpdateCheckbox(_keepSession, c => c.KeepSession); + UpdateCheckbox(_singleInstance, c => c.SingleInstance); + } + + private void Save() + { + var transact = Config.User.BeginTransaction(); + bool SetIfChanged(Expression> accessor, T value) + { + var oldValue = Config.Get(accessor); + if (!Equals(value, oldValue)) + { + transact.Set(accessor, value); + return true; + } + return false; + } + bool themeChanged = SetIfChanged(c => c.Theme, _theme.SelectedItem); + SetIfChanged(c => c.ScanChangesDefaultProfile, _scanChangesDefaultProfile.IsChecked()); + SetIfChanged(c => c.ShowProfilesToolbar, _showProfilesToolbar.IsChecked()); + SetIfChanged(c => c.ShowPageNumbers, _showPageNumbers.IsChecked()); + SetIfChanged(c => c.ScanButtonDefaultAction, _scanButtonDefaultAction.SelectedItem); + SetIfChanged(c => c.SaveButtonDefaultAction, _saveButtonDefaultAction.SelectedItem); + SetIfChanged(c => c.DeleteAfterSaving, _clearAfterSaving.IsChecked()); + SetIfChanged(c => c.KeepSession, _keepSession.IsChecked()); + SetIfChanged(c => c.SingleInstance, _singleInstance.IsChecked()); + transact.Commit(); + + _desktopFormProvider.DesktopForm.Invalidate(); + _desktopFormProvider.DesktopForm.PlaceProfilesToolbar(); + if (themeChanged) + { + EtoPlatform.Current.ColorScheme.UserThemeChanged(); + } + } + + private void RestoreDefaults_Click(object? sender, EventArgs e) + { + UpdateValues(Config.DefaultsOnly); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/SharedDeviceForm.cs b/NAPS2.Lib/EtoForms/Ui/SharedDeviceForm.cs new file mode 100644 index 0000000000..372c9aef37 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/SharedDeviceForm.cs @@ -0,0 +1,137 @@ +using Eto.Drawing; +using Eto.Forms; +using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; +using NAPS2.Remoting.Server; +using NAPS2.Scan; +using NAPS2.Scan.Internal; + +namespace NAPS2.EtoForms.Ui; + +public class SharedDeviceForm : EtoDialogBase +{ + private const int BASE_PORT = 9801; + private const int BASE_TLS_PORT = 9901; + + private readonly ErrorOutput _errorOutput; + private readonly ISharedDeviceManager _sharedDeviceManager; + + private readonly TextBox _displayName = new(); + private readonly DeviceSelectorWidget _deviceSelectorWidget; + + public SharedDeviceForm(Naps2Config config, IScanPerformer scanPerformer, ErrorOutput errorOutput, + ISharedDeviceManager sharedDeviceManager, DeviceCapsCache deviceCapsCache, + IIconProvider iconProvider) : base(config) + { + Title = UiStrings.SharedDeviceFormTitle; + IconName = "wireless_small"; + + _errorOutput = errorOutput; + _sharedDeviceManager = sharedDeviceManager; + _deviceSelectorWidget = new(scanPerformer, deviceCapsCache, iconProvider, this) + { + ProfileFunc = () => new ScanProfile { DriverName = DeviceDriver.ToString().ToLowerInvariant() }, + AllowAlwaysAsk = false + }; + _deviceSelectorWidget.DeviceChanged += DeviceChanged; + } + + private void DeviceChanged(object? sender, DeviceChangedEventArgs e) + { + if (e.NewChoice.Device != null && (string.IsNullOrEmpty(_displayName.Text) || + e.PreviousChoice.Device?.Name == _displayName.Text)) + { + _displayName.Text = e.NewChoice.Device.Name; + } + DeviceDriver = e.NewChoice.Driver; + } + + protected override void BuildLayout() + { + FormStateController.DefaultExtraLayoutSize = new Size(60, 0); + FormStateController.FixedHeightLayout = true; + + LayoutController.Content = L.Column( + C.Label(UiStrings.DisplayNameLabel), + _displayName, + C.Spacer(), + _deviceSelectorWidget, + C.Filler(), + L.Row( + C.Filler(), + L.OkCancel( + C.OkButton(this, SaveSettings), + C.CancelButton(this)) + ) + ); + } + + public bool Result { get; private set; } + + public SharedDevice? SharedDevice { get; set; } + + private int Port { get; set; } + + private int TlsPort { get; set; } + + private Driver DeviceDriver { get; set; } = ScanOptionsValidator.SystemDefaultDriver; + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + if (SharedDevice != null) + { + _displayName.Text = SharedDevice.Name; + _deviceSelectorWidget.Choice = DeviceChoice.ForDevice(SharedDevice.Device); + Port = SharedDevice.Port; + TlsPort = SharedDevice.TlsPort; + DeviceDriver = SharedDevice.Device.Driver; + } + } + + private bool SaveSettings() + { + if (_displayName.Text == "") + { + _errorOutput.DisplayError(MiscResources.NameMissing); + return false; + } + if (_deviceSelectorWidget.Choice.Device == null) + { + _errorOutput.DisplayError(MiscResources.NoDeviceSelected); + return false; + } + SharedDevice = new SharedDevice + { + Name = _displayName.Text, + Device = _deviceSelectorWidget.Choice.Device, + Port = Port == 0 ? NextPort() : Port, + TlsPort = TlsPort == 0 ? NextTlsPort() : TlsPort + }; + Result = true; + return true; + } + + private int NextPort() + { + var devices = _sharedDeviceManager.SharedDevices; + int port = BASE_PORT; + while (devices.Any(x => x.Port == port)) + { + port++; + } + return port; + } + + private int NextTlsPort() + { + var devices = _sharedDeviceManager.SharedDevices; + int tlsPort = BASE_TLS_PORT; + while (devices.Any(x => x.TlsPort == tlsPort)) + { + tlsPort++; + } + return tlsPort; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/SharpenForm.cs b/NAPS2.Lib/EtoForms/Ui/SharpenForm.cs index 31948193ff..8c5720f7f2 100644 --- a/NAPS2.Lib/EtoForms/Ui/SharpenForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/SharpenForm.cs @@ -1,24 +1,23 @@ using Eto.Drawing; +using NAPS2.EtoForms.Widgets; namespace NAPS2.EtoForms.Ui; -public class SharpenForm : ImageFormBase +public class SharpenForm : UnaryImageFormBase { private readonly SliderWithTextBox _sharpenSlider = new(); - public SharpenForm(Naps2Config config, ThumbnailController thumbnailController, IIconProvider iconProvider) : - base(config, thumbnailController) + public SharpenForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController, + IIconProvider iconProvider) : + base(config, imageList, thumbnailController) { - Icon = new Icon(1f, Icons.sharpen.ToEtoImage()); + IconName = "sharpen_small"; Title = UiStrings.Sharpen; - _sharpenSlider.Icon = iconProvider.GetIcon("sharpen"); - Sliders = new[] { _sharpenSlider }; + EtoPlatform.Current.AttachDpiDependency(this, + scale => _sharpenSlider.Icon = iconProvider.GetIcon("sharpen_small", scale)); + Sliders = [_sharpenSlider]; } - protected override IEnumerable Transforms => - new Transform[] - { - new SharpenTransform(_sharpenSlider.Value) - }; + protected override List Transforms => [new SharpenTransform(_sharpenSlider.IntValue)]; } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/SplitForm.cs b/NAPS2.Lib/EtoForms/Ui/SplitForm.cs new file mode 100644 index 0000000000..fc0f984929 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/SplitForm.cs @@ -0,0 +1,266 @@ +using Eto.Drawing; +using Eto.Forms; +using NAPS2.EtoForms.Layout; + +namespace NAPS2.EtoForms.Ui; + +public class SplitForm : UnaryImageFormBase +{ + private const int HANDLE_WIDTH = 1; + private const double HANDLE_RADIUS_RATIO = 0.2; + private const int HANDLE_MIN_RADIUS = 50; + + private static CropTransform? _lastTransform; + + private readonly ColorScheme _colorScheme; + private readonly LayoutControl _vSplit; + private readonly LayoutControl _hSplit; + + // Mouse down location + private PointF _mouseOrigin; + + // Crop amounts from each side as a fraction of the total image size (updated as the user drags) + private float _cropX, _cropY; + + // Crop amounts from each side as pixels of the image to be cropped (updated on mouse up) + private float _realX, _realY; + + private bool _dragging; + private SplitOrientation _orientation; + + public SplitForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController, + ColorScheme colorScheme) : + base(config, imageList, thumbnailController) + { + Title = UiStrings.Split; + IconName = "split_small"; + + _colorScheme = colorScheme; + + _vSplit = C.IconButton("split_ver_small", () => SetOrientation(SplitOrientation.Vertical)); + _hSplit = C.IconButton("split_hor_small", () => SetOrientation(SplitOrientation.Horizontal)); + Overlay.MouseDown += Overlay_MouseDown; + Overlay.MouseMove += Overlay_MouseMove; + Overlay.MouseUp += Overlay_MouseUp; + } + + protected override List Transforms => throw new NotSupportedException(); + + private int HandleClickRadius => + (int) Math.Max( + Math.Round((_orientation == SplitOrientation.Horizontal ? _overlayH : _overlayW) * HANDLE_RADIUS_RATIO), + HANDLE_MIN_RADIUS); + + protected override void OnPreLoad(EventArgs e) + { + base.OnPreLoad(e); + if (_lastTransform != null && _lastTransform.OriginalWidth == RealImageWidth && + _lastTransform.OriginalHeight == RealImageHeight) + { + _realX = _lastTransform.Left == 0 ? RealImageWidth / 2f : _lastTransform.Left; + _realY = _lastTransform.Top == 0 ? RealImageHeight / 2f : _lastTransform.Top; + _cropX = _realX / RealImageWidth; + _cropY = _realY / RealImageHeight; + } + else + { + _cropX = 0.5f; + _cropY = 0.5f; + _realX = RealImageWidth / 2f; + _realY = RealImageHeight / 2f; + } + } + + protected override LayoutElement CreateControls() + { + return L.Column( + L.Row( + C.Filler(), + L.Row(_vSplit, _hSplit), + C.Filler() + ), + ApplyToSelectedControl + ); + } + + protected override void InitDisplayImage() + { + base.InitDisplayImage(); + _orientation = WorkingImage!.Width > WorkingImage!.Height + ? SplitOrientation.Vertical + : SplitOrientation.Horizontal; + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + if (!EtoPlatform.Current.IsMac) + { + (_orientation == SplitOrientation.Horizontal ? _hSplit : _vSplit).Control!.Focus(); + } + } + + private void SetOrientation(SplitOrientation orientation) + { + _orientation = orientation; + UpdatePreviewBox(); + } + + protected override IMemoryImage RenderPreview() + { + return WorkingImage!.Clone(); + } + + protected override void Revert() + { + _cropX = _cropY = 0.5f; + _realX = RealImageWidth / 2f; + _realY = RealImageHeight / 2f; + Overlay.Invalidate(); + } + + private void Overlay_MouseDown(object? sender, MouseEventArgs e) + { + _dragging = IsHandleUnderMouse(e); + _mouseOrigin = e.Location; + Overlay.Invalidate(); + } + + private void Overlay_MouseUp(object? sender, MouseEventArgs e) + { + _realX = _cropX * RealImageWidth; + _realY = _cropY * RealImageHeight; + _dragging = false; + Overlay.Invalidate(); + } + + private void Overlay_MouseMove(object? sender, MouseEventArgs e) + { + Overlay.Cursor = _dragging || IsHandleUnderMouse(e) + ? _orientation == SplitOrientation.Horizontal + ? Cursors.HorizontalSplit + : Cursors.VerticalSplit + : Cursors.Arrow; + if (_dragging) + { + UpdateCrop(e.Location); + Overlay.Invalidate(); + } + } + + protected override void PaintOverlay(object? sender, PaintEventArgs e) + { + base.PaintOverlay(sender, e); + + if (_overlayW == 0 || _overlayH == 0) + { + return; + } + + var offsetX = _cropX * _overlayW; + var offsetY = _cropY * _overlayH; + var fillColor = new Color(0.3f, 0.3f, 0.3f, 0.3f); + var handlePen = new Pen(_colorScheme.CropColor, HANDLE_WIDTH); + + if (_overlayW >= 1 && _overlayH >= 1) + { + // Fade out cropped-out portions of the image + if (_orientation == SplitOrientation.Horizontal) + { + e.Graphics.FillRectangle(fillColor, _overlayL, _overlayT + offsetY, _overlayW, _overlayH - offsetY); + } + else + { + e.Graphics.FillRectangle(fillColor, _overlayL + offsetX, _overlayT, _overlayW - offsetX, _overlayH); + } + } + + if (_orientation == SplitOrientation.Horizontal) + { + var y = _overlayT + offsetY - HANDLE_WIDTH / 2f; + e.Graphics.DrawLine(handlePen, _overlayL, y, _overlayR - 1, y); + } + else + { + var x = _overlayL + offsetX - HANDLE_WIDTH / 2f; + e.Graphics.DrawLine(handlePen, x, _overlayT, x, _overlayB - 1); + } + } + + private bool IsHandleUnderMouse(MouseEventArgs e) + { + var radius = HandleClickRadius; + if (_orientation == SplitOrientation.Horizontal) + { + var y = _overlayT + _cropY * _overlayH; + return e.Location.Y > y - radius && e.Location.Y < y + radius && e.Location.X > _overlayL && + e.Location.X < _overlayR; + } + else + { + var x = _overlayL + _cropX * _overlayW; + return e.Location.X > x - radius && e.Location.X < x + radius && e.Location.Y > _overlayT && + e.Location.Y < _overlayB; + } + } + + private void UpdateCrop(PointF mousePos) + { + var delta = mousePos - _mouseOrigin; + if (_orientation == SplitOrientation.Vertical) + { + _cropX = (_realX / RealImageWidth + delta.X / _overlayW) + .Clamp(1f / RealImageWidth, (RealImageWidth - 1f) / RealImageWidth); + } + else + { + _cropY = (_realY / RealImageHeight + delta.Y / _overlayH) + .Clamp(1f / RealImageHeight, (RealImageHeight - 1f) / RealImageHeight); + } + } + + protected override void Apply() + { + var transform1 = _orientation == SplitOrientation.Horizontal + ? new CropTransform(0, 0, 0, RealImageHeight - (int) Math.Round(_realY), RealImageWidth, RealImageHeight) + : new CropTransform(0, RealImageWidth - (int) Math.Round(_realX), 0, 0, RealImageWidth, RealImageHeight); + var transform2 = _orientation == SplitOrientation.Horizontal + ? new CropTransform(0, 0, (int) Math.Round(_realY), 0, RealImageWidth, RealImageHeight) + : new CropTransform((int) Math.Round(_realX), 0, 0, 0, RealImageWidth, RealImageHeight); + + DoSplit(Image, WorkingImage, transform1, transform2); + foreach (var image in SelectedImages.Skip(1)) + { + DoSplit(image, null, transform1, transform2); + } + + _lastTransform = transform2; + } + + private void DoSplit(UiImage originalImage, IMemoryImage? workingImage, CropTransform transform1, CropTransform transform2) + { + var thumb1 = workingImage?.Clone() + .PerformAllTransforms([transform1, new ThumbnailTransform(ThumbnailController.RenderSize)]); + var thumb2 = workingImage?.Clone() + .PerformAllTransforms([transform2, new ThumbnailTransform(ThumbnailController.RenderSize)]); + + // We keep the second image as the original UiImage reference so that any InsertAfter points come after the + // pair of images. For example, if I'm in the middle of scanning and I split the most-recently scanned image, + // the next scanned image should appear at the end of the list, not in between the split images. + var oldTransforms = originalImage.TransformState; + var image1 = new UiImage(originalImage.GetClonedImage(), useThumbnail: false); + var image2 = originalImage; + image1.AddTransform(transform1, thumb1); + image2.AddTransform(transform2, thumb2); + ImageList.Mutate(new ListMutation.InsertBefore(image1, image2)); + ImageList.AddToSelection(image1); + ImageList.PushUndoElement( + new SplitUndoElement(ImageList, image1, image2, oldTransforms, transform1, transform2)); + } + + private enum SplitOrientation + { + Horizontal, + Vertical + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/UnaryImageFormBase.cs b/NAPS2.Lib/EtoForms/Ui/UnaryImageFormBase.cs new file mode 100644 index 0000000000..3dcf8fd08c --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/UnaryImageFormBase.cs @@ -0,0 +1,117 @@ +using Eto.Forms; +using NAPS2.EtoForms.Layout; +using NAPS2.EtoForms.Widgets; + +namespace NAPS2.EtoForms.Ui; + +public abstract class UnaryImageFormBase( + Naps2Config config, + UiImageList imageList, + ThumbnailController thumbnailController) + : ImageFormBase(config, imageList, thumbnailController) +{ + private readonly CheckBox _applyToSelected = new(); + private readonly Button _revert = C.Button(UiStrings.Revert); + + protected IMemoryImage? WorkingImage { get; set; } + + protected int RealImageWidth { get; private set; } + + protected int RealImageHeight { get; private set; } + + protected SliderWithTextBox[] Sliders { get; set; } = []; + + protected bool CanScaleWorkingImage { get; set; } = true; + + protected abstract List Transforms { get; } + + protected override void OnPreLoad(EventArgs e) + { + _applyToSelected.Text = string.Format(UiStrings.ApplyToSelected, SelectedImages.Count); + _applyToSelected.Checked = Config.Get(c => c.ApplyToAllSelected); + _applyToSelected.CheckedChanged += + (_, _) => Config.User.Set(c => c.ApplyToAllSelected, _applyToSelected.IsChecked()); + _revert.Click += (_, _) => + { + Revert(); + UpdatePreviewBox(); + }; + foreach (var slider in Sliders) + { + slider.ValueChanged += UpdatePreviewBox; + } + base.OnPreLoad(e); + } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + WorkingImage?.Dispose(); + } + + protected override LayoutElement CreateControls() + { + return L.Column([ + ..Sliders.Select(x => (LayoutElement) x), + ApplyToSelectedControl + ]); + } + + protected LayoutElement ApplyToSelectedControl => SelectedImages.Count > 1 ? _applyToSelected : C.None(); + + protected override LayoutElement CreateExtraButtons() => _revert; + + private List ImagesToTransform => _applyToSelected.IsChecked() ? SelectedImages : [Image]; + + protected override IMemoryImage RenderPreview() + { + var result = WorkingImage!.Clone(); + return result.PerformAllTransforms(Transforms); + } + + protected override void InitDisplayImage() + { + using var imageToRender = Image.GetClonedImage(); + WorkingImage = imageToRender.Render(); + RealImageWidth = WorkingImage.Width; + RealImageHeight = WorkingImage.Height; + + if (CanScaleWorkingImage) + { + // Scale down the image to the screen size for better efficiency without losing much fidelity + var workingArea = GetScreenWorkingArea(); + var widthRatio = WorkingImage.Width / workingArea.Width; + var heightRatio = WorkingImage.Height / workingArea.Height; + if (widthRatio > 1 || heightRatio > 1) + { + WorkingImage = WorkingImage.PerformTransform(new ScaleTransform(1 / Math.Max(widthRatio, heightRatio))); + } + } + + DisplayImage = WorkingImage.Clone(); + } + + protected override void Apply() + { + IMemoryImage? firstImageThumb = null; + if (WorkingImage != null) + { + // Optimize thumbnail rendering for the first (or only) image since we already have it loaded into memory + var transformed = WorkingImage.Clone().PerformAllTransforms(Transforms); + firstImageThumb = + transformed.PerformTransform(new ThumbnailTransform(ThumbnailController.RenderSize)); + } + var mutation = new ImageListMutation.AddTransforms( + Transforms.ToList(), + new Dictionary { [Image] = firstImageThumb }); + ImageList.Mutate(mutation, ListSelection.From(ImagesToTransform)); + } + + protected virtual void Revert() + { + foreach (var slider in Sliders) + { + slider.IntValue = 0; + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Widgets/DeviceChangedEventArgs.cs b/NAPS2.Lib/EtoForms/Widgets/DeviceChangedEventArgs.cs new file mode 100644 index 0000000000..3ba18961d7 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Widgets/DeviceChangedEventArgs.cs @@ -0,0 +1,10 @@ +using NAPS2.Scan; + +namespace NAPS2.EtoForms.Widgets; + +public class DeviceChangedEventArgs : EventArgs +{ + public required DeviceChoice PreviousChoice { get; init; } + + public required DeviceChoice NewChoice { get; init; } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Widgets/DeviceListViewBehavior.cs b/NAPS2.Lib/EtoForms/Widgets/DeviceListViewBehavior.cs new file mode 100644 index 0000000000..c6daa757c3 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Widgets/DeviceListViewBehavior.cs @@ -0,0 +1,35 @@ +using Eto.Drawing; +using NAPS2.Scan; + +namespace NAPS2.EtoForms.Widgets; + +public class DeviceListViewBehavior : ListViewBehavior +{ + private readonly Dictionary _imageMap = new(); + private readonly Dictionary _iconNameMap = new(); + + public DeviceListViewBehavior(ColorScheme colorScheme) : base(colorScheme) + { + MultiSelect = false; + ShowLabels = true; + ScrollOnDrag = false; + } + + public void SetImage(ScanDevice item, Image image) => _imageMap[item] = image; + + public void SetIconName(ScanDevice item, string iconName) => _iconNameMap[item] = iconName; + + public override string GetLabel(ScanDevice item) => item.Name; + + public override Image GetImage(IListView listView, ScanDevice item) + { + float scale = EtoPlatform.Current.GetScaleFactor(listView.Control.ParentWindow); + if (_imageMap.Get(item) is { } image) + { + int scaledSize = (int) Math.Round(48 * scale); + return image.Clone().ResizeTo(scaledSize).PadTo(Size.Round(listView.ImageSize * scale)); + } + string iconName = _iconNameMap.Get(item) ?? "device"; + return EtoPlatform.Current.IconProvider.GetIcon(iconName, scale)!.PadTo(Size.Round(listView.ImageSize * scale)); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Widgets/DeviceSelectorWidget.cs b/NAPS2.Lib/EtoForms/Widgets/DeviceSelectorWidget.cs new file mode 100644 index 0000000000..c05e7bc8cf --- /dev/null +++ b/NAPS2.Lib/EtoForms/Widgets/DeviceSelectorWidget.cs @@ -0,0 +1,163 @@ +using System.Threading; +using Eto.Drawing; +using Eto.Forms; +using NAPS2.EtoForms.Layout; +using NAPS2.Scan; + +namespace NAPS2.EtoForms.Widgets; + +public class DeviceSelectorWidget +{ + private readonly IScanPerformer _scanPerformer; + private readonly DeviceCapsCache _deviceCapsCache; + private readonly IIconProvider _iconProvider; + private readonly IFormBase _parentWindow; + + private readonly ImageView _deviceIcon = new() { Size = new Size(32, 32) }; + private readonly Label _deviceName = new(); + private readonly Label _deviceDriver = new(); + private readonly LayoutVisibility _deviceVis = new(false); + private readonly Button _chooseDevice = new() { Text = UiStrings.ChooseDevice }; + + private DeviceChoice _choice = DeviceChoice.None; + private Image? _deviceIconImage; + private string _deviceIconName = "device"; + private CancellationTokenSource? _loadIconCts; + + public DeviceSelectorWidget(IScanPerformer scanPerformer, DeviceCapsCache deviceCapsCache, + IIconProvider iconProvider, IFormBase parentWindow) + { + _scanPerformer = scanPerformer; + _deviceCapsCache = deviceCapsCache; + _iconProvider = iconProvider; + _parentWindow = parentWindow; + _chooseDevice.Click += ChooseDevice; + EtoPlatform.Current.AttachDpiDependency(_deviceIcon, _ => UpdateDeviceIconImage()); + } + + public required Func ProfileFunc { get; init; } + + public bool AllowAlwaysAsk { get; init; } + + public DeviceChoice Choice + { + get => _choice; + set + { + _choice = value; + if (value == DeviceChoice.None) + { + _deviceName.Text = ""; + _deviceVis.IsVisible = false; + } + else + { + _deviceName.Text = value.Device?.Name ?? UiStrings.AlwaysAsk; + _deviceDriver.Text = value.Driver switch + { + Driver.Wia => UiStrings.WiaDriver, + Driver.Twain => UiStrings.TwainDriver, + Driver.Sane => UiStrings.SaneDriver, + Driver.Escl => UiStrings.EsclDriver, + Driver.Apple => UiStrings.AppleDriver, + _ => "" + }; + _deviceVis.IsVisible = true; + SetDeviceIcon(Choice.Device?.IconUri); + } + } + } + + public bool Enabled + { + get => _chooseDevice.Enabled; + set => _chooseDevice.Enabled = value; + } + + public bool ShowChooseDevice { get; set; } = true; + + public event EventHandler? DeviceChanged; + + private async void ChooseDevice(object? sender, EventArgs args) + { + var choice = await _scanPerformer.PromptForDevice(ProfileFunc(), AllowAlwaysAsk, _parentWindow.NativeHandle); + if (choice.Device != null || choice.AlwaysAsk) + { + var previousChoice = Choice; + Choice = choice; + SetDeviceIcon(Choice.Device?.IconUri); + DeviceChanged?.Invoke(this, + new DeviceChangedEventArgs { PreviousChoice = previousChoice, NewChoice = choice }); + } + } + + public void SetDeviceIcon(string? iconUri) + { + _deviceIconImage = _deviceCapsCache.GetCachedIcon(iconUri); + _deviceIconName = _choice.AlwaysAsk ? "ask" : "device"; + UpdateDeviceIconImage(); + + if (((Window) _parentWindow).Loaded) + { + _parentWindow.LayoutController.Invalidate(); + } + if (_deviceIconImage == null && iconUri != null) + { + ReloadDeviceIcon(iconUri); + } + } + + private void ReloadDeviceIcon(string iconUri) + { + var cts = new CancellationTokenSource(); + _loadIconCts?.Cancel(); + _loadIconCts = cts; + Task.Run(async () => + { + var icon = await _deviceCapsCache.LoadIcon(iconUri); + if (icon != null) + { + Invoker.Current.Invoke(() => + { + if (!cts.IsCancellationRequested) + { + _deviceIconImage = icon; + UpdateDeviceIconImage(); + _parentWindow.LayoutController.Invalidate(); + } + }); + } + }); + } + + private void UpdateDeviceIconImage() + { + float scale = EtoPlatform.Current.GetScaleFactor((Window) _parentWindow); + _deviceIcon.Image = _deviceIconImage ?? _iconProvider.GetIcon(_deviceIconName, scale); + var size = _deviceIconImage != null ? new SizeF(48, 48) : new SizeF(32, 32); + _deviceIcon.Size = Size.Round(size * EtoPlatform.Current.GetLayoutScaleFactor((Window) _parentWindow)); + } + + public static implicit operator LayoutElement(DeviceSelectorWidget control) + { + return control.AsControl(); + } + + public LayoutElement AsControl() + { + return L.GroupBox(UiStrings.DeviceLabel, + L.Row( + _deviceIcon.Visible(_deviceVis).AlignCenter().NaturalWidth(48), + L.Column( + C.Filler(), + ShowChooseDevice ? _deviceName.DynamicWrap(300) : _deviceName.Ellipsize().MaxWidth(150), + ShowChooseDevice ? _deviceDriver.DynamicWrap(300) : _deviceDriver.Ellipsize().MaxWidth(150), + C.Filler() + ).Spacing(5).Visible(_deviceVis).Scale(), + // TODO: We can consider a compact choose-device button for the sidebar, but maybe simpler to force + // creation of separate profiles + ShowChooseDevice ? _chooseDevice.AlignCenter() : C.None() + ) + ); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Widgets/DropDownWidget.cs b/NAPS2.Lib/EtoForms/Widgets/DropDownWidget.cs new file mode 100644 index 0000000000..53e8636699 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Widgets/DropDownWidget.cs @@ -0,0 +1,143 @@ +using Eto.Forms; +using NAPS2.EtoForms.Layout; + +namespace NAPS2.EtoForms.Widgets; + +public class DropDownWidget where T : notnull +{ + private readonly DropDown _dropDown = new(); + private T[] _items = []; + private Func _format = x => x?.ToString() ?? ""; + + private bool _hasUserPreferredItem; + private T? _userPreferredItem; + private bool _changingItems; + + public DropDownWidget(bool scale = true) + { + EtoPlatform.Current.ConfigureDropDown(_dropDown, scale); + _dropDown.SelectedIndexChanged += DropDown_SelectedIndexChanged; + if (typeof(IComparable).IsAssignableFrom(typeof(T))) + { + GetClosestItem = GetClosestItemByComparing; + } + } + + public Func Format + { + get => _format; + set + { + _format = value; + // Force the dropdown items to be regenerated + Items = Items; + } + } + + public Func? GetClosestItem { get; set; } + + public bool Enabled + { + get => _dropDown.Enabled; + set => _dropDown.Enabled = value; + } + + public event EventHandler? SelectedItemChanged; + + public IEnumerable Items + { + get => _items; + set + { + _items = value.ToArray(); + _changingItems = true; + var previousSelection = InternalSelectedItem; + _dropDown.Items.Clear(); + foreach (var item in _items) + { + _dropDown.Items.Add(new ListItem + { + Text = Format(item), + Tag = item + }); + } + InternalSelectedItem = GetPreferredSelection(previousSelection); + _changingItems = false; + if (!Equals(InternalSelectedItem, previousSelection)) + { + SelectedItemChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + private T? GetPreferredSelection(T? previousSelection) + { + if (_hasUserPreferredItem && _items.Contains(_userPreferredItem)) + { + return _userPreferredItem; + } + if (_items.Contains(previousSelection)) + { + return previousSelection; + } + var compareTo = _hasUserPreferredItem ? _userPreferredItem : previousSelection; + if (_items.Length > 0 && compareTo != null && GetClosestItem != null) + { + return GetClosestItem(_items, compareTo); + } + if (_items.Length > 0) + { + return _items[0]; + } + return default; + } + + public T? SelectedItem + { + get => InternalSelectedItem; + set + { + _hasUserPreferredItem = true; + _userPreferredItem = value; + _changingItems = true; + InternalSelectedItem = value; + _changingItems = false; + } + } + + private T? InternalSelectedItem + { + get + { + if (_dropDown.SelectedIndex == -1) return default; + return (T) ((ListItem) _dropDown.Items[_dropDown.SelectedIndex]).Tag; + } + set => _dropDown.SelectedIndex = Array.IndexOf(_items, value); + } + + private void DropDown_SelectedIndexChanged(object? sender, EventArgs eventArgs) + { + if (!_changingItems) + { + _hasUserPreferredItem = true; + _userPreferredItem = InternalSelectedItem; + SelectedItemChanged?.Invoke(this, EventArgs.Empty); + } + } + + private T GetClosestItemByComparing(T[] items, T x) + { + foreach (var y in items) + { + if (((IComparable) x).CompareTo(y) <= 0) + { + return y; + } + } + return items[^1]; + } + + public static implicit operator LayoutElement(DropDownWidget control) => control.AsControl(); + + public DropDown AsControl() => _dropDown; +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/DropEventArgs.cs b/NAPS2.Lib/EtoForms/Widgets/DropEventArgs.cs similarity index 93% rename from NAPS2.Lib/EtoForms/DropEventArgs.cs rename to NAPS2.Lib/EtoForms/Widgets/DropEventArgs.cs index eda0328997..c41cfd7b4c 100644 --- a/NAPS2.Lib/EtoForms/DropEventArgs.cs +++ b/NAPS2.Lib/EtoForms/Widgets/DropEventArgs.cs @@ -1,6 +1,6 @@ using System.Collections.Immutable; -namespace NAPS2.EtoForms; +namespace NAPS2.EtoForms.Widgets; public class DropEventArgs : EventArgs { diff --git a/NAPS2.Lib/EtoForms/Widgets/EnumDropDownWidget.cs b/NAPS2.Lib/EtoForms/Widgets/EnumDropDownWidget.cs new file mode 100644 index 0000000000..d1dcf29700 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Widgets/EnumDropDownWidget.cs @@ -0,0 +1,14 @@ +using NAPS2.Scan; + +namespace NAPS2.EtoForms.Widgets; + +public class EnumDropDownWidget : DropDownWidget where T : struct, Enum +{ + public static IEnumerable DefaultItems => (T[]) Enum.GetValues(typeof(T)); + + public EnumDropDownWidget(bool scale = true) : base(scale) + { + Format = x => x.Description(); + Items = DefaultItems; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/FilePathWithPlaceholders.cs b/NAPS2.Lib/EtoForms/Widgets/FilePathWithPlaceholders.cs similarity index 73% rename from NAPS2.Lib/EtoForms/FilePathWithPlaceholders.cs rename to NAPS2.Lib/EtoForms/Widgets/FilePathWithPlaceholders.cs index 1ebd90fcfe..231ed2455b 100644 --- a/NAPS2.Lib/EtoForms/FilePathWithPlaceholders.cs +++ b/NAPS2.Lib/EtoForms/Widgets/FilePathWithPlaceholders.cs @@ -2,7 +2,7 @@ using NAPS2.EtoForms.Layout; using NAPS2.EtoForms.Ui; -namespace NAPS2.EtoForms; +namespace NAPS2.EtoForms.Widgets; public class FilePathWithPlaceholders { @@ -40,6 +40,10 @@ public bool Enabled } } + public bool PdfOnly { get; set; } + + public bool ImagesOnly { get; set; } + public event EventHandler? TextChanged; public static implicit operator LayoutElement(FilePathWithPlaceholders control) @@ -53,7 +57,7 @@ public LayoutColumn AsControl() L.Row( _path.Scale().AlignCenter().Visible(_visibility), _dialogHelper != null - ? _choose.Width(40).MaxHeight(22).Visible(_visibility) + ? _choose.Width(EtoPlatform.Current.IsGtk ? null : 40).MaxHeight(22).Visible(_visibility) : C.None() ).SpacingAfter(2), _placeholders.Visible(_visibility) @@ -62,9 +66,26 @@ public LayoutColumn AsControl() private void OpenPathDialog(object? sender, EventArgs e) { - if (_dialogHelper!.PromptToSavePdf(_path.Text, out string? savePath)) + if (PdfOnly) + { + if (_dialogHelper!.PromptToSavePdf(_path.Text, out string? savePath)) + { + _path.Text = savePath!; + } + } + else if (ImagesOnly) + { + if (_dialogHelper!.PromptToSaveImage(_path.Text, out string? savePath)) + { + _path.Text = savePath!; + } + } + else { - _path.Text = savePath!; + if (_dialogHelper!.PromptToSavePdfOrImage(_path.Text, out string? savePath)) + { + _path.Text = savePath!; + } } } diff --git a/NAPS2.Lib/EtoForms/IListView.cs b/NAPS2.Lib/EtoForms/Widgets/IListView.cs similarity index 79% rename from NAPS2.Lib/EtoForms/IListView.cs rename to NAPS2.Lib/EtoForms/Widgets/IListView.cs index 4dd36ebed5..437a5cf895 100644 --- a/NAPS2.Lib/EtoForms/IListView.cs +++ b/NAPS2.Lib/EtoForms/Widgets/IListView.cs @@ -1,6 +1,6 @@ using Eto.Forms; -namespace NAPS2.EtoForms; +namespace NAPS2.EtoForms.Widgets; public interface IListView : Util.ISelectable where T : notnull { @@ -8,8 +8,7 @@ public interface IListView : Util.ISelectable where T : notnull ContextMenu? ContextMenu { get; set; } - // TODO: Maybe convert this back to a Size - int ImageSize { get; set; } + Eto.Drawing.Size ImageSize { get; set; } event EventHandler SelectionChanged; diff --git a/NAPS2.Lib/EtoForms/ImageListViewBehavior.cs b/NAPS2.Lib/EtoForms/Widgets/ImageListViewBehavior.cs similarity index 77% rename from NAPS2.Lib/EtoForms/ImageListViewBehavior.cs rename to NAPS2.Lib/EtoForms/Widgets/ImageListViewBehavior.cs index 5ff6eb165f..4c67939aca 100644 --- a/NAPS2.Lib/EtoForms/ImageListViewBehavior.cs +++ b/NAPS2.Lib/EtoForms/Widgets/ImageListViewBehavior.cs @@ -3,26 +3,31 @@ using Google.Protobuf; using NAPS2.ImportExport.Images; -namespace NAPS2.EtoForms; +namespace NAPS2.EtoForms.Widgets; public class ImageListViewBehavior : ListViewBehavior { private readonly UiThumbnailProvider _thumbnailProvider; - private readonly ImageTransfer _imageTransfer; + private readonly Naps2Config _config; + private readonly ImageTransfer _imageTransfer = new(); - public ImageListViewBehavior(UiThumbnailProvider thumbnailProvider, ImageTransfer imageTransfer) + public ImageListViewBehavior(UiThumbnailProvider thumbnailProvider, + ColorScheme colorScheme, Naps2Config config) : base(colorScheme) { _thumbnailProvider = thumbnailProvider; - _imageTransfer = imageTransfer; + _config = config; MultiSelect = true; ShowLabels = false; ScrollOnDrag = true; UseHandCursor = true; } - public override Image GetImage(UiImage item, int imageSize) + public override bool ShowPageNumbers => _config.Get(c => c.ShowPageNumbers); + + public override Image GetImage(IListView listView, UiImage item) { - return _thumbnailProvider.GetThumbnail(item, imageSize).ToEtoImage(); + using var thumbnail = _thumbnailProvider.GetThumbnail(item, listView.ImageSize.Width); + return thumbnail.ToEtoImage(); } public override bool AllowDragDrop => true; diff --git a/NAPS2.Lib/EtoForms/ListViewBehavior.cs b/NAPS2.Lib/EtoForms/Widgets/ListViewBehavior.cs similarity index 73% rename from NAPS2.Lib/EtoForms/ListViewBehavior.cs rename to NAPS2.Lib/EtoForms/Widgets/ListViewBehavior.cs index 1e425906cd..8221d1136a 100644 --- a/NAPS2.Lib/EtoForms/ListViewBehavior.cs +++ b/NAPS2.Lib/EtoForms/Widgets/ListViewBehavior.cs @@ -1,15 +1,23 @@ using Eto.Drawing; using Eto.Forms; -using IDataObject = Eto.Forms.IDataObject; -namespace NAPS2.EtoForms; +namespace NAPS2.EtoForms.Widgets; public abstract class ListViewBehavior where T : notnull { + protected ListViewBehavior(ColorScheme colorScheme) + { + ColorScheme = colorScheme; + } + + public ColorScheme ColorScheme { get; } + public bool MultiSelect { get; protected set; } public bool ShowLabels { get; protected set; } + public virtual bool ShowPageNumbers => false; + public bool ScrollOnDrag { get; protected set; } public bool UseHandCursor { get; protected set; } @@ -18,7 +26,7 @@ public abstract class ListViewBehavior where T : notnull public virtual string GetLabel(T item) => throw new NotSupportedException(); - public virtual Image GetImage(T item, int imageSize) => throw new NotSupportedException(); + public virtual Image GetImage(IListView listView, T item) => throw new NotSupportedException(); public virtual bool AllowDragDrop => false; diff --git a/NAPS2.Lib/EtoForms/OcrLanguagesListViewBehavior.cs b/NAPS2.Lib/EtoForms/Widgets/OcrLanguagesListViewBehavior.cs similarity index 67% rename from NAPS2.Lib/EtoForms/OcrLanguagesListViewBehavior.cs rename to NAPS2.Lib/EtoForms/Widgets/OcrLanguagesListViewBehavior.cs index c5c0f9ab12..84eeaa2f09 100644 --- a/NAPS2.Lib/EtoForms/OcrLanguagesListViewBehavior.cs +++ b/NAPS2.Lib/EtoForms/Widgets/OcrLanguagesListViewBehavior.cs @@ -1,10 +1,10 @@ using NAPS2.Ocr; -namespace NAPS2.EtoForms; +namespace NAPS2.EtoForms.Widgets; public class OcrLanguagesListViewBehavior : ListViewBehavior { - public OcrLanguagesListViewBehavior() + public OcrLanguagesListViewBehavior(ColorScheme colorScheme) : base(colorScheme) { ShowLabels = true; Checkboxes = true; diff --git a/NAPS2.Lib/EtoForms/Widgets/PageSizeDropDownWidget.cs b/NAPS2.Lib/EtoForms/Widgets/PageSizeDropDownWidget.cs new file mode 100644 index 0000000000..a00093ebbb --- /dev/null +++ b/NAPS2.Lib/EtoForms/Widgets/PageSizeDropDownWidget.cs @@ -0,0 +1,154 @@ +using Eto.Forms; +using NAPS2.EtoForms.Ui; +using NAPS2.Scan; + +namespace NAPS2.EtoForms.Widgets; + +public class PageSizeDropDownWidget : DropDownWidget +{ + private readonly IFormBase _window; + private PageSizeListItem? _customPageSize; + private PageSizeListItem? _lastPageSizeItem; + private List _visiblePresets = new(); + + public PageSizeDropDownWidget(IFormBase window) + { + _window = window; + + SelectedItemChanged += OnSelectedItemChanged; + Format = x => x.Text; + } + + public IEnumerable VisiblePresets + { + get => _visiblePresets; + set + { + _visiblePresets = value.ToList(); + RegenerateItems(); + } + } + + private void RegenerateItems() + { + var presetSizes = _visiblePresets.Select(size => new PageSizeListItem(size)).ToList(); + var customSizes = _window.Config.Get(c => c.CustomPageSizePresets) + .OrderBy(x => x.Name) + .Select(preset => new PageSizeListItem + { + Type = ScanPageSize.Custom, + Text = string.Format(MiscResources.NamedPageSizeFormat, preset.Name, preset.Dimens.Width, + preset.Dimens.Height, preset.Dimens.Unit.Description()), + CustomName = preset.Name, + CustomDimens = preset.Dimens + }).ToList(); + + if (_customPageSize != null && !customSizes.Contains(_customPageSize)) + { + customSizes.Add(_customPageSize); + } + Items = presetSizes.Concat(customSizes).Append(new PageSizeListItem(ScanPageSize.Custom)); + } + + private void OnSelectedItemChanged(object? sender, EventArgs e) + { + if (Equals(SelectedItem, new PageSizeListItem(ScanPageSize.Custom))) + { + if (_lastPageSizeItem == null) + { + Log.Error("Expected last page size to be set"); + return; + } + // "Custom..." selected + var form = _window.FormFactory.Create(); + form.PageSizeDimens = _lastPageSizeItem.Type == ScanPageSize.Custom + ? _lastPageSizeItem.CustomDimens + : _lastPageSizeItem.Type.PageDimensions(); + form.ShowModal(); + if (form.Result) + { + _customPageSize = GetCustomPageSize(form.PageSizeName!, form.PageSizeDimens!); + SelectedItem = _customPageSize; + RegenerateItems(); + } + else + { + SelectedItem = _lastPageSizeItem; + } + } + _lastPageSizeItem = SelectedItem; + } + + private PageSizeListItem GetCustomPageSize(string? name, PageDimensions dimens) + { + return new PageSizeListItem + { + Type = ScanPageSize.Custom, + Text = string.IsNullOrEmpty(name) + ? string.Format(MiscResources.CustomPageSizeFormat, dimens.Width, dimens.Height, + dimens.Unit.Description()) + : string.Format(MiscResources.NamedPageSizeFormat, name, dimens.Width, dimens.Height, + dimens.Unit.Description()), + CustomName = name, + CustomDimens = dimens + }; + } + + public class PageSizeListItem : IListItem + { + public PageSizeListItem() + { + } + + public PageSizeListItem(ScanPageSize size) + { + Type = size; + Text = size.Description(); + } + + public string Text { get; set; } = null!; + + public string Key => Text; + + public ScanPageSize Type { get; set; } + + public string? CustomName { get; set; } + + public PageDimensions? CustomDimens { get; set; } + + protected bool Equals(PageSizeListItem other) + { + return Type == other.Type && CustomName == other.CustomName && Equals(CustomDimens, other.CustomDimens); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((PageSizeListItem) obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (int) Type; + hashCode = (hashCode * 397) ^ (CustomName != null ? CustomName.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (CustomDimens != null ? CustomDimens.GetHashCode() : 0); + return hashCode; + } + } + } + + public void SetCustom(string? customPageSizeName, PageDimensions customPageSize) + { + _customPageSize = GetCustomPageSize(customPageSizeName, customPageSize); + SelectedItem = _customPageSize; + } + + public void SetPreset(ScanPageSize pageSize) + { + SelectedItem = new PageSizeListItem(pageSize); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/PasswordBoxWithToggle.cs b/NAPS2.Lib/EtoForms/Widgets/PasswordBoxWithToggle.cs similarity index 92% rename from NAPS2.Lib/EtoForms/PasswordBoxWithToggle.cs rename to NAPS2.Lib/EtoForms/Widgets/PasswordBoxWithToggle.cs index bc73247e4f..eb13a17828 100644 --- a/NAPS2.Lib/EtoForms/PasswordBoxWithToggle.cs +++ b/NAPS2.Lib/EtoForms/Widgets/PasswordBoxWithToggle.cs @@ -1,7 +1,7 @@ using Eto.Forms; using NAPS2.EtoForms.Layout; -namespace NAPS2.EtoForms; +namespace NAPS2.EtoForms.Widgets; public class PasswordBoxWithToggle { @@ -80,7 +80,7 @@ public LayoutElement AsInlineControl() GetTitleElement(), _show.Padding(left: 10) ).Aligned().SpacingAfter(2), - _panel.Height(20) + _panel.Height(EtoPlatform.Current.IsGtk ? null : 20) }.Expand(); } @@ -91,7 +91,7 @@ private LayoutElement GetTitleElement() return C.Filler(); } var label = C.Label(Title!); - return TitleWrapWidth > 0 ? label.Wrap(TitleWrapWidth).Scale() : label.Scale(); + return TitleWrapWidth > 0 ? label.DynamicWrap(TitleWrapWidth).Scale() : label.Scale(); } public event Action? TextChanged; diff --git a/NAPS2.Lib/EtoForms/ProfileListViewBehavior.cs b/NAPS2.Lib/EtoForms/Widgets/ProfileListViewBehavior.cs similarity index 61% rename from NAPS2.Lib/EtoForms/ProfileListViewBehavior.cs rename to NAPS2.Lib/EtoForms/Widgets/ProfileListViewBehavior.cs index 750ea0b2dd..86c6549b4f 100644 --- a/NAPS2.Lib/EtoForms/ProfileListViewBehavior.cs +++ b/NAPS2.Lib/EtoForms/Widgets/ProfileListViewBehavior.cs @@ -3,15 +3,14 @@ using NAPS2.ImportExport.Profiles; using NAPS2.Scan; -namespace NAPS2.EtoForms; +namespace NAPS2.EtoForms.Widgets; public class ProfileListViewBehavior : ListViewBehavior { - private readonly ProfileTransfer _profileTransfer; + private readonly ProfileTransfer _profileTransfer = new(); - public ProfileListViewBehavior(ProfileTransfer profileTransfer) + public ProfileListViewBehavior(ColorScheme colorScheme) : base(colorScheme) { - _profileTransfer = profileTransfer; MultiSelect = false; ShowLabels = true; ScrollOnDrag = false; @@ -21,21 +20,17 @@ public ProfileListViewBehavior(ProfileTransfer profileTransfer) public override string GetLabel(ScanProfile item) => item.DisplayName ?? ""; - public override Image GetImage(ScanProfile item, int imageSize) + public override Image GetImage(IListView listView, ScanProfile item) { - if (item.IsDefault && item.IsLocked) + var iconName = (item.IsDefault, item.IsLocked) switch { - return Icons.scanner_lock_default.ToEtoImage(); - } - if (item.IsDefault) - { - return Icons.scanner_default.ToEtoImage(); - } - if (item.IsLocked) - { - return Icons.scanner_lock.ToEtoImage(); - } - return Icons.scanner_48_old.ToEtoImage(); + (true, true) => "scanner_lock_default_48", + (true, false) => "scanner_default_48", + (false, true) => "scanner_lock_48", + (false, false) => "scanner_48" + }; + var scale = EtoPlatform.Current.GetScaleFactor(listView.Control.ParentWindow); + return EtoPlatform.Current.IconProvider.GetIcon(iconName, scale)!; } public override bool AllowDragDrop => true; diff --git a/NAPS2.Lib/EtoForms/Widgets/ResolutionDropDownWidget.cs b/NAPS2.Lib/EtoForms/Widgets/ResolutionDropDownWidget.cs new file mode 100644 index 0000000000..befbde98d0 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Widgets/ResolutionDropDownWidget.cs @@ -0,0 +1,142 @@ +using System.Globalization; +using Eto.Forms; +using NAPS2.EtoForms.Ui; + +namespace NAPS2.EtoForms.Widgets; + +public class ResolutionDropDownWidget : DropDownWidget +{ + private readonly IFormBase _window; + private ResolutionListItem? _customResolution; + private ResolutionListItem? _lastResolutionItem; + private List _visiblePresets = new(); + + public ResolutionDropDownWidget(IFormBase window) + { + _window = window; + + SelectedItemChanged += OnSelectedItemChanged; + Format = x => x.Text; + } + + public IEnumerable VisiblePresets + { + get => _visiblePresets; + set + { + _visiblePresets = value.ToList(); + if (_lastResolutionItem != null && + (_lastResolutionItem.Custom && _visiblePresets.Contains(_lastResolutionItem.Dpi) || + !_lastResolutionItem.Custom && !_visiblePresets.Contains(_lastResolutionItem.Dpi))) + { + SelectedItem = new ResolutionListItem(_lastResolutionItem.Dpi, !_lastResolutionItem.Custom); + } + RegenerateItems(); + } + } + + private void RegenerateItems() + { + var resolutions = _visiblePresets.Select(size => new ResolutionListItem(size, false)).ToList(); + if (_customResolution != null && !resolutions.Contains(_customResolution)) + { + int i = 0; + while (i < resolutions.Count && resolutions[i].Dpi <= _customResolution.Dpi) + { + i++; + } + if (i == 0 || resolutions[i - 1].Dpi != _customResolution.Dpi) + { + resolutions.Insert(i, _customResolution); + } + } + Items = resolutions.Append(new ResolutionListItem { Text = SettingsResources.Resolution_Custom }); + } + + private void OnSelectedItemChanged(object? sender, EventArgs e) + { + if (Equals(SelectedItem, new ResolutionListItem { Text = SettingsResources.Resolution_Custom })) + { + if (_lastResolutionItem == null) + { + Log.Error("Expected last resolution to be set"); + return; + } + // "Custom..." selected + var form = _window.FormFactory.Create(); + form.Dpi = _window.Config.Get(c => c.LastCustomResolution); + form.ShowModal(); + if (form.Result) + { + _window.Config.User.Set(c => c.LastCustomResolution, form.Dpi); + _customResolution = new ResolutionListItem(form.Dpi, true); + SelectedItem = _customResolution; + RegenerateItems(); + } + else + { + SelectedItem = _lastResolutionItem; + } + } + _lastResolutionItem = SelectedItem; + } + + public class ResolutionListItem : IListItem + { + public ResolutionListItem() + { + } + + public ResolutionListItem(int dpi, bool custom) + { + Text = string.Format(SettingsResources.DpiFormat, dpi.ToString(CultureInfo.InvariantCulture)); + Dpi = dpi; + Custom = custom; + } + + public string Text { get; set; } = null!; + + public string Key => Text; + + public int Dpi { get; } + + public bool Custom { get; } + + protected bool Equals(ResolutionListItem other) + { + return Dpi == other.Dpi && Custom == other.Custom; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ResolutionListItem) obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Dpi; + hashCode = (hashCode * 397) ^ Custom.GetHashCode(); + return hashCode; + } + } + } + + public void SetDpi(int dpi) + { + if (VisiblePresets.Contains(dpi)) + { + _customResolution = null; + SelectedItem = _lastResolutionItem = new ResolutionListItem(dpi, false); + } + else + { + _customResolution = new ResolutionListItem(dpi, true); + SelectedItem = _lastResolutionItem = _customResolution; + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Widgets/ScrollZoomImageViewer.cs b/NAPS2.Lib/EtoForms/Widgets/ScrollZoomImageViewer.cs new file mode 100644 index 0000000000..81bc0b6835 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Widgets/ScrollZoomImageViewer.cs @@ -0,0 +1,227 @@ +using Eto.Drawing; +using Eto.Forms; +using NAPS2.EtoForms.Layout; + +namespace NAPS2.EtoForms.Widgets; + +public class ScrollZoomImageViewer +{ + private readonly Scrollable _scrollable = new() { Border = BorderType.None }; + private readonly Drawable _imageView = new() { BackgroundColor = Colors.White }; + private PointF _scrollableLocation; + private Size _renderSize; + private float _renderFactor; + private PointF? _mousePos; + private PointF? _initialMousePos; + private Point? _initialScrollPos; + private bool _mouseIsDown; + + public ScrollZoomImageViewer() + { + _scrollable.Content = _imageView; + _scrollable.BackgroundColor = Colors.White; + _imageView.Paint += ImagePaint; + _scrollable.Shown += OnShown; + _scrollable.MouseEnter += OnMouseEnter; + _scrollable.MouseLeave += OnMouseLeave; + _scrollable.MouseMove += OnMouseMove; + _scrollable.MouseDown += OnMouseDown; + _scrollable.MouseUp += OnMouseUp; + EtoPlatform.Current.AttachMouseWheelEvent(_scrollable, OnMouseWheel); + _scrollable.Cursor = Cursors.Pointer; + } + + // For some reason Gtk can't render a few pixels width around the edge of the image, so we need to correct for that. + private int BorderOffset { get; } = EtoPlatform.Current.IsGtk ? 6 : 0; + + public Bitmap? Image { get; set; } + + public ColorScheme? ColorScheme { get; set; } + + public event EventHandler? ZoomChanged; + + private Size RenderSize + { + get => _renderSize; + set + { + _renderSize = value; + _imageView.Size = Size.Round(value) + new Size(2, 2); + _imageView.Invalidate(); + } + } + + private int AvailableWidth => _scrollable.Width - 2 - BorderOffset; + private int AvailableHeight => _scrollable.Height - 2 - BorderOffset; + + private bool IsWidthBound => + Image!.Width / (float) Image.Height > AvailableWidth / (float) AvailableHeight; + + private bool FitsInBounds => RenderSize.Width <= AvailableWidth && RenderSize.Height <= AvailableHeight; + + private float XOffset => Math.Max((_imageView.Width - RenderSize.Width) / 2, 0); + private float YOffset => Math.Max((_imageView.Height - RenderSize.Height) / 2, 0); + + private void OnShown(object? sender, EventArgs e) + { + _scrollableLocation = _scrollable.Location; + } + + private void OnMouseEnter(object? sender, MouseEventArgs e) + { + // TODO: Mouse enter/leave events aren't firing on WinForms, why? + _mousePos = e.Location; + } + + private void OnMouseUp(object? sender, MouseEventArgs e) + { + _mouseIsDown = false; + } + + private void OnMouseDown(object? sender, MouseEventArgs e) + { + _mouseIsDown = true; + _initialMousePos = e.Location; + _initialScrollPos = _scrollable.ScrollPosition; + } + + private void OnMouseMove(object? sender, MouseEventArgs e) + { + if (_mouseIsDown && _initialMousePos.HasValue && _initialScrollPos.HasValue) + { + _scrollable.ScrollPosition = _initialScrollPos.Value + Point.Round(_initialMousePos.Value - e.Location); + } + _mousePos = e.Location; + } + + private void OnMouseLeave(object? sender, MouseEventArgs e) + { + _mousePos = null; + } + + private void OnMouseWheel(object? sender, MouseEventArgs e) + { + if (e.Modifiers == Keys.Control) + { + ChangeZoom(e.Delta.Height, true); + e.Handled = true; + } + } + + private void ImagePaint(object? sender, PaintEventArgs e) + { + var bgColor = ColorScheme?.BackgroundColor ?? Colors.White; + e.Graphics.FillRectangle(bgColor, 0, 0, _imageView.Width, _imageView.Height); + if (Image != null) + { + e.Graphics.DrawRectangle( + ColorScheme?.BorderColor ?? Colors.Black, + XOffset - 1, YOffset - 1, RenderSize.Width + 1, RenderSize.Height + 1); + e.Graphics.DrawImage(Image, XOffset, YOffset, RenderSize.Width, RenderSize.Height); + } + } + + public float ZoomFactor => _renderFactor; + + // GDI doesn't like when the render size (4 bytes per pixel) exceeds int.MaxValue + public float MaxZoom => (float) Math.Sqrt(int.MaxValue * 0.99 / (Image!.Width * Image.Height * 4)); + + public void ChangeZoom(float step, bool anchorToMouse = false) + { + SetZoom(_renderFactor * (float) Math.Pow(1.2, step), anchorToMouse); + } + + public void SetZoom(float value, bool anchorToMouse = false) + { + _renderFactor = value.Clamp(0.01f, MaxZoom); + _scrollable.SuspendLayout(); + var anchor = GetMouseAnchor(anchorToMouse); + RenderSize = + Size.Round(new SizeF(Image!.Width * _renderFactor, Image.Height * _renderFactor)); + SetMouseAnchor(anchor); + _scrollable.ResumeLayout(); + ZoomChanged?.Invoke(this, new ZoomChangedEventArgs(_renderFactor)); + } + + public void ZoomToActual() + { + RenderSize = new Size(Image!.Width, Image.Height); + _renderFactor = 1f; + ZoomChanged?.Invoke(this, new ZoomChangedEventArgs(_renderFactor)); + } + + public void ZoomToContainer() + { + if (!_scrollable.Loaded || Image == null || _scrollable.Width <= 0 || _scrollable.Height <= 0) + { + return; + } + if (IsWidthBound) + { + RenderSize = new Size(AvailableWidth, + (int) Math.Round(Image!.Height * AvailableWidth / (float) Image.Width)); + _renderFactor = AvailableWidth / (float) Image.Width; + } + else + { + RenderSize = + new Size((int) Math.Round(Image!.Width * AvailableHeight / (float) Image.Height), + AvailableHeight); + _renderFactor = AvailableHeight / (float) Image.Height; + } + ZoomChanged?.Invoke(this, new ZoomChangedEventArgs(_renderFactor)); + } + + // When we zoom in/out (e.g. with Ctrl+mousewheel), we want the point in the image underneath the mouse cursor to + // stay stationary relative to the mouse (as much as permitted by the available scroll region). If the mouse is not + // overtop the image, the middle of the image (as currently visible) should stay stationary. + // + // This function calculates the image anchor as a fraction (i.e. a point in the range [<0,0>, <1,1>]). It also + // returns the mouse position relative to the top left of the Scrollable (or the middle of the scrollable if the + // mouse is not overtop the image). + private (PointF imageAnchor, PointF mouseRelativePos) GetMouseAnchor(bool anchorToMouse) + { + var anchorMiddle = new PointF(0.5f, 0.5f); + var scrollableMiddle = new PointF( + _scrollableLocation.X + _scrollable.Width / 2f, + _scrollableLocation.Y + _scrollable.Height / 2f); + if (!anchorToMouse || + _mousePos is not { } mousePos || + mousePos.X < _scrollableLocation.X || + mousePos.Y < _scrollableLocation.Y || + mousePos.X > _scrollableLocation.X + _scrollable.Width || + mousePos.Y > _scrollableLocation.Y + _scrollable.Height) + { + // Mouse is outside the scrollable + return (anchorMiddle, scrollableMiddle); + } + var mouseRelativePos = mousePos - _scrollableLocation - new Point(1, 1); + var x = (mouseRelativePos.X + _scrollable.ScrollPosition.X - XOffset) / RenderSize.Width; + var y = (mouseRelativePos.Y + _scrollable.ScrollPosition.Y - YOffset) / RenderSize.Height; + if (x < 0 || y < 0 || x > 1 || y > 1) + { + // Mouse is inside the scrollable but outside the area covered by the image + return (anchorMiddle, scrollableMiddle); + } + return (new PointF(x, y), mouseRelativePos); + } + + // This function inverts the calculation done in GetMouseAnchor to get the correct scroll position to move the point + // in the image that was underneath the mouse back there (after the image size has been changed). + private void SetMouseAnchor((PointF imageAnchor, PointF mouseRelativePos) anchor) + { + if (FitsInBounds) + { + return; + } + // TODO: This is off a bit for the "middle" anchor, probably because the scrollbars themselves appear + var xScroll = anchor.imageAnchor.X * RenderSize.Width + XOffset - anchor.mouseRelativePos.X; + var yScroll = anchor.imageAnchor.Y * RenderSize.Height + YOffset - anchor.mouseRelativePos.Y; + _scrollable.ScrollPosition = Point.Round(new PointF(xScroll, yScroll)); + } + + public static implicit operator LayoutElement(ScrollZoomImageViewer control) + { + return control._scrollable; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Widgets/SharedDevicesListViewBehavior.cs b/NAPS2.Lib/EtoForms/Widgets/SharedDevicesListViewBehavior.cs new file mode 100644 index 0000000000..e8ea85b520 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Widgets/SharedDevicesListViewBehavior.cs @@ -0,0 +1,22 @@ +using Eto.Drawing; +using NAPS2.Remoting.Server; + +namespace NAPS2.EtoForms.Widgets; + +public class SharedDevicesListViewBehavior : ListViewBehavior +{ + public SharedDevicesListViewBehavior(ColorScheme colorScheme) : base(colorScheme) + { + MultiSelect = false; + ShowLabels = true; + ScrollOnDrag = false; + } + + public override string GetLabel(SharedDevice item) => item.Name; + + public override Image GetImage(IListView listView, SharedDevice item) + { + var scale = EtoPlatform.Current.GetScaleFactor(listView.Control.ParentWindow); + return EtoPlatform.Current.IconProvider.GetIcon("scanner_wireless_48", scale)!; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Widgets/SliderWithTextBox.cs b/NAPS2.Lib/EtoForms/Widgets/SliderWithTextBox.cs new file mode 100644 index 0000000000..aab40204b2 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Widgets/SliderWithTextBox.cs @@ -0,0 +1,144 @@ +using Eto.Drawing; +using Eto.Forms; +using NAPS2.EtoForms.Layout; + +namespace NAPS2.EtoForms.Widgets; + +public class SliderWithTextBox +{ + public static readonly Constraints DefaultConstraints = new IntConstraints(-1000, 1000, 200); + + private readonly Constraints _constraints; + private readonly Slider _slider = new(); + private readonly ImageView _imageView = new(); + private readonly LayoutVisibility _imageVis = new(false); + private readonly TextBox _textBox; + + private int _valueCache; + + public SliderWithTextBox() : this(DefaultConstraints) + { + } + + public SliderWithTextBox(Constraints constraints) + { + _constraints = constraints; + _textBox = constraints.IsInteger + ? new NumericMaskedTextBox() + : new NumericMaskedTextBox(); + _slider.MinValue = constraints.SliderMinValue; + _slider.MaxValue = constraints.SliderMaxValue; + _slider.TickFrequency = constraints.SliderTickFrequency; + _textBox.Text = 0.ToString("G"); + + _slider.ValueChanged += (_, _) => { IntValue = _slider.Value; }; + _textBox.TextChanged += (_, _) => + { + if (double.TryParse(_textBox.Text, out double value)) + { + if (!constraints.IsInteger) + { + value *= constraints.Multiplier; + } + value = Math.Round(value); + if (value >= constraints.SliderMinValue && value <= constraints.SliderMaxValue) + { + IntValue = (int) value; + } + } + }; + } + + public int IntValue + { + get => _valueCache; + set + { + if (value == _valueCache) return; + _valueCache = value; + _slider.Value = value; + _textBox.Text = _constraints.IsInteger + ? value.ToString("G") + : (value / (decimal) _constraints.Multiplier).ToString("G"); + ValueChanged?.Invoke(); + } + } + + public decimal DecimalValue + { + get => IntValue / (decimal) _constraints.Multiplier; + set => IntValue = (int) Math.Round(value * _constraints.Multiplier); + } + + public bool Enabled + { + get => _slider.Enabled; + set + { + _slider.Enabled = value; + _textBox.Enabled = value; + } + } + + public Image? Icon + { + get => _imageView.Image; + set + { + _imageView.Image = value; + _imageVis.IsVisible = value != null; + } + } + + public static implicit operator LayoutElement(SliderWithTextBox control) + { + return control.AsControl(); + } + + public event Action? ValueChanged; + + public LayoutRow AsControl() + { + return L.Row( + _imageView + .Align(EtoPlatform.Current.IsWinForms ? LayoutAlignment.Leading : LayoutAlignment.Center) + .Padding(top: 2, bottom: 2).Visible(_imageVis), + _slider.Scale(), + _textBox.Width(EtoPlatform.Current.IsGtk ? 50 : 40) + .Align(EtoPlatform.Current.IsWinForms ? LayoutAlignment.Leading : LayoutAlignment.Center) + ); + } + + public abstract class Constraints + { + public bool IsInteger { get; protected init; } + public int Multiplier { get; protected init; } + public int SliderMinValue { get; protected init; } + public int SliderMaxValue { get; protected init; } + public int SliderTickFrequency { get; protected init; } + } + + public class IntConstraints : Constraints + { + public IntConstraints(int minValue, int maxValue, int tickFrequency) + { + IsInteger = true; + Multiplier = 1; + SliderMinValue = minValue; + SliderMaxValue = maxValue; + SliderTickFrequency = tickFrequency; + } + } + + public class DecimalConstraints : Constraints + { + public DecimalConstraints(decimal minValue, decimal maxValue, decimal tickFrequency, int decimalPlaces) + { + IsInteger = false; + Multiplier = (int) Math.Pow(10, decimalPlaces); + SliderMinValue = (int) (minValue * Multiplier); + SliderMaxValue = (int) (maxValue * Multiplier); + SliderTickFrequency = (int) (tickFrequency * Multiplier); + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Widgets/UpdateCheckWidget.cs b/NAPS2.Lib/EtoForms/Widgets/UpdateCheckWidget.cs new file mode 100644 index 0000000000..e7a66def71 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Widgets/UpdateCheckWidget.cs @@ -0,0 +1,103 @@ +using Eto.Forms; +using NAPS2.EtoForms.Layout; +using NAPS2.Update; + +namespace NAPS2.EtoForms.Widgets; + +public class UpdateCheckWidget +{ + private readonly UpdateChecker _updateChecker; + private readonly Naps2Config _config; + private readonly CheckBox _checkForUpdates; + private readonly Panel _updatePanel; + private bool _hasCheckedForUpdates; + private UpdateInfo? _update; + + public UpdateCheckWidget(UpdateChecker updateChecker, Naps2Config config) + { + _updateChecker = updateChecker; + _config = config; + _checkForUpdates = new CheckBox { Text = UiStrings.CheckForUpdates }; + _checkForUpdates.Checked = config.Get(c => c.CheckForUpdates); + _checkForUpdates.CheckedChanged += CheckForUpdatesChanged; + _updatePanel = new Panel(); + UpdateControls(); + DoUpdateCheck(); + } + + public LayoutColumn AsControl() + { + return L.Column( + C.TextSpace(), + _checkForUpdates.Padding(left: 4), + _updatePanel + ); + } + + public static implicit operator LayoutElement(UpdateCheckWidget control) + { + return control.AsControl(); + } + + private void DoUpdateCheck() + { + if (_checkForUpdates.IsChecked()) + { + _updateChecker.CheckForUpdates().ContinueWith(task => + { + if (task.IsFaulted) + { + Log.ErrorException("Error checking for updates", task.Exception!); + } + else + { + var transact = _config.User.BeginTransaction(); + transact.Set(c => c.HasCheckedForUpdates, true); + transact.Set(c => c.LastUpdateCheckDate, DateTime.Now); + transact.Commit(); + } + _update = task.Result; + _hasCheckedForUpdates = true; + Invoker.Current.Invoke(UpdateControls); + }); + } + } + + private void UpdateControls() + { + _updatePanel.Content = GetUpdatePanelContent(); + } + + private Control GetUpdatePanelContent() + { + if (!_checkForUpdates.IsChecked()) + { + return C.NoWrap(MiscResources.UpdateCheckDisabled); + } + if (!_hasCheckedForUpdates) + { + return C.NoWrap(MiscResources.CheckingForUpdates); + } + if (_update == null) + { + return C.NoWrap(MiscResources.NoUpdates); + } + return C.Link(string.Format(MiscResources.Install, _update.Name), + InstallLinkClicked); + } + + private void InstallLinkClicked() + { + if (_update != null) + { + _updateChecker.StartUpdate(_update); + } + } + + private void CheckForUpdatesChanged(object? sender, EventArgs e) + { + _config.User.Set(c => c.CheckForUpdates, _checkForUpdates.IsChecked()); + UpdateControls(); + DoUpdateCheck(); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Widgets/ZoomChangedEventArgs.cs b/NAPS2.Lib/EtoForms/Widgets/ZoomChangedEventArgs.cs new file mode 100644 index 0000000000..85738f5e1a --- /dev/null +++ b/NAPS2.Lib/EtoForms/Widgets/ZoomChangedEventArgs.cs @@ -0,0 +1,3 @@ +namespace NAPS2.EtoForms.Widgets; + +public record ZoomChangedEventArgs(float Zoom); \ No newline at end of file diff --git a/NAPS2.Lib/Icons.Designer.cs b/NAPS2.Lib/Icons.Designer.cs index cf878ca5ba..cf547cfbf4 100644 --- a/NAPS2.Lib/Icons.Designer.cs +++ b/NAPS2.Lib/Icons.Designer.cs @@ -99,6 +99,26 @@ internal static byte[] add_small { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] apple_mail { + get { + object obj = ResourceManager.GetObject("apple_mail", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] apple_mail_hires { + get { + object obj = ResourceManager.GetObject("apple_mail_hires", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// @@ -109,6 +129,36 @@ internal static byte[] application_cascade { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] application_cascade_small { + get { + object obj = ResourceManager.GetObject("application_cascade_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] application_side_list { + get { + object obj = ResourceManager.GetObject("application_side_list", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] application_side_list_small { + get { + object obj = ResourceManager.GetObject("application_side_list_small", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// @@ -139,6 +189,16 @@ internal static byte[] arrow_left { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] arrow_left_small { + get { + object obj = ResourceManager.GetObject("arrow_left_small", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// @@ -149,6 +209,16 @@ internal static byte[] arrow_out { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] arrow_out_small { + get { + object obj = ResourceManager.GetObject("arrow_out_small", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// @@ -159,6 +229,16 @@ internal static byte[] arrow_refresh { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] arrow_refresh_hires { + get { + object obj = ResourceManager.GetObject("arrow_refresh_hires", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// @@ -169,6 +249,16 @@ internal static byte[] arrow_right { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] arrow_right_small { + get { + object obj = ResourceManager.GetObject("arrow_right_small", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// @@ -179,6 +269,16 @@ internal static byte[] arrow_rotate_anticlockwise { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] arrow_rotate_anticlockwise_hires { + get { + object obj = ResourceManager.GetObject("arrow_rotate_anticlockwise_hires", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// @@ -252,9 +352,9 @@ internal static byte[] arrow_up_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] blueprints { + internal static byte[] ask { get { - object obj = ResourceManager.GetObject("blueprints", resourceCulture); + object obj = ResourceManager.GetObject("ask", resourceCulture); return ((byte[])(obj)); } } @@ -262,9 +362,9 @@ internal static byte[] blueprints { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] blueprints_small { + internal static byte[] ask_hires { get { - object obj = ResourceManager.GetObject("blueprints_small", resourceCulture); + object obj = ResourceManager.GetObject("ask_hires", resourceCulture); return ((byte[])(obj)); } } @@ -272,9 +372,9 @@ internal static byte[] blueprints_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] btn_donate_LG { + internal static byte[] blueprints { get { - object obj = ResourceManager.GetObject("btn_donate_LG", resourceCulture); + object obj = ResourceManager.GetObject("blueprints", resourceCulture); return ((byte[])(obj)); } } @@ -282,9 +382,9 @@ internal static byte[] btn_donate_LG { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] cancel { + internal static byte[] blueprints_hires { get { - object obj = ResourceManager.GetObject("cancel", resourceCulture); + object obj = ResourceManager.GetObject("blueprints_hires", resourceCulture); return ((byte[])(obj)); } } @@ -292,9 +392,9 @@ internal static byte[] cancel { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] cancel_small { + internal static byte[] blueprints_small { get { - object obj = ResourceManager.GetObject("cancel_small", resourceCulture); + object obj = ResourceManager.GetObject("blueprints_small", resourceCulture); return ((byte[])(obj)); } } @@ -302,9 +402,9 @@ internal static byte[] cancel_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] close { + internal static byte[] broom { get { - object obj = ResourceManager.GetObject("close", resourceCulture); + object obj = ResourceManager.GetObject("broom", resourceCulture); return ((byte[])(obj)); } } @@ -312,9 +412,9 @@ internal static byte[] close { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] cog { + internal static byte[] broom_hires { get { - object obj = ResourceManager.GetObject("cog", resourceCulture); + object obj = ResourceManager.GetObject("broom_hires", resourceCulture); return ((byte[])(obj)); } } @@ -322,9 +422,9 @@ internal static byte[] cog { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] cog_small { + internal static byte[] cancel { get { - object obj = ResourceManager.GetObject("cog_small", resourceCulture); + object obj = ResourceManager.GetObject("cancel", resourceCulture); return ((byte[])(obj)); } } @@ -332,9 +432,9 @@ internal static byte[] cog_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] color_gradient { + internal static byte[] cancel_hires { get { - object obj = ResourceManager.GetObject("color_gradient", resourceCulture); + object obj = ResourceManager.GetObject("cancel_hires", resourceCulture); return ((byte[])(obj)); } } @@ -342,9 +442,9 @@ internal static byte[] color_gradient { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] color_management { + internal static byte[] cancel_small { get { - object obj = ResourceManager.GetObject("color_management", resourceCulture); + object obj = ResourceManager.GetObject("cancel_small", resourceCulture); return ((byte[])(obj)); } } @@ -352,9 +452,9 @@ internal static byte[] color_management { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] color_wheel { + internal static byte[] close { get { - object obj = ResourceManager.GetObject("color_wheel", resourceCulture); + object obj = ResourceManager.GetObject("close", resourceCulture); return ((byte[])(obj)); } } @@ -362,9 +462,9 @@ internal static byte[] color_wheel { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] contrast { + internal static byte[] cog { get { - object obj = ResourceManager.GetObject("contrast", resourceCulture); + object obj = ResourceManager.GetObject("cog", resourceCulture); return ((byte[])(obj)); } } @@ -372,9 +472,9 @@ internal static byte[] contrast { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] contrast_high { + internal static byte[] cog_small { get { - object obj = ResourceManager.GetObject("contrast_high", resourceCulture); + object obj = ResourceManager.GetObject("cog_small", resourceCulture); return ((byte[])(obj)); } } @@ -382,9 +482,9 @@ internal static byte[] contrast_high { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] contrast_with_sun { + internal static byte[] color_gradient { get { - object obj = ResourceManager.GetObject("contrast_with_sun", resourceCulture); + object obj = ResourceManager.GetObject("color_gradient", resourceCulture); return ((byte[])(obj)); } } @@ -392,9 +492,9 @@ internal static byte[] contrast_with_sun { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] control_play_blue { + internal static byte[] color_gradient_small { get { - object obj = ResourceManager.GetObject("control_play_blue", resourceCulture); + object obj = ResourceManager.GetObject("color_gradient_small", resourceCulture); return ((byte[])(obj)); } } @@ -402,9 +502,9 @@ internal static byte[] control_play_blue { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] control_play_blue_small { + internal static byte[] color_management { get { - object obj = ResourceManager.GetObject("control_play_blue_small", resourceCulture); + object obj = ResourceManager.GetObject("color_management", resourceCulture); return ((byte[])(obj)); } } @@ -412,9 +512,9 @@ internal static byte[] control_play_blue_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] cross { + internal static byte[] color_management_small { get { - object obj = ResourceManager.GetObject("cross", resourceCulture); + object obj = ResourceManager.GetObject("color_management_small", resourceCulture); return ((byte[])(obj)); } } @@ -422,9 +522,9 @@ internal static byte[] cross { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] cross_small { + internal static byte[] color_wheel { get { - object obj = ResourceManager.GetObject("cross_small", resourceCulture); + object obj = ResourceManager.GetObject("color_wheel", resourceCulture); return ((byte[])(obj)); } } @@ -432,9 +532,9 @@ internal static byte[] cross_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] diskette { + internal static byte[] color_wheel_small { get { - object obj = ResourceManager.GetObject("diskette", resourceCulture); + object obj = ResourceManager.GetObject("color_wheel_small", resourceCulture); return ((byte[])(obj)); } } @@ -442,9 +542,9 @@ internal static byte[] diskette { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] email_attach { + internal static byte[] combine { get { - object obj = ResourceManager.GetObject("email_attach", resourceCulture); + object obj = ResourceManager.GetObject("combine", resourceCulture); return ((byte[])(obj)); } } @@ -452,9 +552,9 @@ internal static byte[] email_attach { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] email_setting { + internal static byte[] combine_hor { get { - object obj = ResourceManager.GetObject("email_setting", resourceCulture); + object obj = ResourceManager.GetObject("combine_hor", resourceCulture); return ((byte[])(obj)); } } @@ -462,9 +562,9 @@ internal static byte[] email_setting { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] email_small { + internal static byte[] combine_hor_small { get { - object obj = ResourceManager.GetObject("email_small", resourceCulture); + object obj = ResourceManager.GetObject("combine_hor_small", resourceCulture); return ((byte[])(obj)); } } @@ -472,9 +572,9 @@ internal static byte[] email_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] exclamation { + internal static byte[] combine_small { get { - object obj = ResourceManager.GetObject("exclamation", resourceCulture); + object obj = ResourceManager.GetObject("combine_small", resourceCulture); return ((byte[])(obj)); } } @@ -482,9 +582,9 @@ internal static byte[] exclamation { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] favicon { + internal static byte[] combine_ver { get { - object obj = ResourceManager.GetObject("favicon", resourceCulture); + object obj = ResourceManager.GetObject("combine_ver", resourceCulture); return ((byte[])(obj)); } } @@ -492,9 +592,9 @@ internal static byte[] favicon { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] file_extension_pdf { + internal static byte[] combine_ver_small { get { - object obj = ResourceManager.GetObject("file_extension_pdf", resourceCulture); + object obj = ResourceManager.GetObject("combine_ver_small", resourceCulture); return ((byte[])(obj)); } } @@ -502,9 +602,9 @@ internal static byte[] file_extension_pdf { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] file_extension_pdf_small { + internal static byte[] contrast { get { - object obj = ResourceManager.GetObject("file_extension_pdf_small", resourceCulture); + object obj = ResourceManager.GetObject("contrast", resourceCulture); return ((byte[])(obj)); } } @@ -512,9 +612,9 @@ internal static byte[] file_extension_pdf_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] folder_picture { + internal static byte[] contrast_high { get { - object obj = ResourceManager.GetObject("folder_picture", resourceCulture); + object obj = ResourceManager.GetObject("contrast_high", resourceCulture); return ((byte[])(obj)); } } @@ -522,9 +622,9 @@ internal static byte[] folder_picture { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] gmail { + internal static byte[] contrast_high_small { get { - object obj = ResourceManager.GetObject("gmail", resourceCulture); + object obj = ResourceManager.GetObject("contrast_high_small", resourceCulture); return ((byte[])(obj)); } } @@ -532,9 +632,9 @@ internal static byte[] gmail { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] hourglass_grey { + internal static byte[] contrast_small { get { - object obj = ResourceManager.GetObject("hourglass_grey", resourceCulture); + object obj = ResourceManager.GetObject("contrast_small", resourceCulture); return ((byte[])(obj)); } } @@ -542,9 +642,9 @@ internal static byte[] hourglass_grey { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] image_edit { + internal static byte[] contrast_with_sun { get { - object obj = ResourceManager.GetObject("image_edit", resourceCulture); + object obj = ResourceManager.GetObject("contrast_with_sun", resourceCulture); return ((byte[])(obj)); } } @@ -552,9 +652,9 @@ internal static byte[] image_edit { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] info_rhombus { + internal static byte[] contrast_with_sun_small { get { - object obj = ResourceManager.GetObject("info_rhombus", resourceCulture); + object obj = ResourceManager.GetObject("contrast_with_sun_small", resourceCulture); return ((byte[])(obj)); } } @@ -562,9 +662,9 @@ internal static byte[] info_rhombus { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] information { + internal static byte[] control_play_blue { get { - object obj = ResourceManager.GetObject("information", resourceCulture); + object obj = ResourceManager.GetObject("control_play_blue", resourceCulture); return ((byte[])(obj)); } } @@ -572,9 +672,9 @@ internal static byte[] information { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] information_small { + internal static byte[] control_play_blue_hires { get { - object obj = ResourceManager.GetObject("information_small", resourceCulture); + object obj = ResourceManager.GetObject("control_play_blue_hires", resourceCulture); return ((byte[])(obj)); } } @@ -582,9 +682,9 @@ internal static byte[] information_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] key_small { + internal static byte[] control_play_blue_small { get { - object obj = ResourceManager.GetObject("key_small", resourceCulture); + object obj = ResourceManager.GetObject("control_play_blue_small", resourceCulture); return ((byte[])(obj)); } } @@ -592,9 +692,9 @@ internal static byte[] key_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] mail_yellow { + internal static byte[] copy { get { - object obj = ResourceManager.GetObject("mail_yellow", resourceCulture); + object obj = ResourceManager.GetObject("copy", resourceCulture); return ((byte[])(obj)); } } @@ -602,9 +702,9 @@ internal static byte[] mail_yellow { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] outlookweb { + internal static byte[] copy_small { get { - object obj = ResourceManager.GetObject("outlookweb", resourceCulture); + object obj = ResourceManager.GetObject("copy_small", resourceCulture); return ((byte[])(obj)); } } @@ -612,9 +712,9 @@ internal static byte[] outlookweb { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] pdf_email { + internal static byte[] cross { get { - object obj = ResourceManager.GetObject("pdf_email", resourceCulture); + object obj = ResourceManager.GetObject("cross", resourceCulture); return ((byte[])(obj)); } } @@ -622,9 +722,9 @@ internal static byte[] pdf_email { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] pencil { + internal static byte[] cross_hires { get { - object obj = ResourceManager.GetObject("pencil", resourceCulture); + object obj = ResourceManager.GetObject("cross_hires", resourceCulture); return ((byte[])(obj)); } } @@ -632,9 +732,9 @@ internal static byte[] pencil { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] pencil_small { + internal static byte[] cross_small { get { - object obj = ResourceManager.GetObject("pencil_small", resourceCulture); + object obj = ResourceManager.GetObject("cross_small", resourceCulture); return ((byte[])(obj)); } } @@ -642,9 +742,9 @@ internal static byte[] pencil_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] picture { + internal static byte[] device { get { - object obj = ResourceManager.GetObject("picture", resourceCulture); + object obj = ResourceManager.GetObject("device", resourceCulture); return ((byte[])(obj)); } } @@ -652,9 +752,9 @@ internal static byte[] picture { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] picture_edit { + internal static byte[] device_hires { get { - object obj = ResourceManager.GetObject("picture_edit", resourceCulture); + object obj = ResourceManager.GetObject("device_hires", resourceCulture); return ((byte[])(obj)); } } @@ -662,9 +762,9 @@ internal static byte[] picture_edit { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] picture_save { + internal static byte[] document { get { - object obj = ResourceManager.GetObject("picture_save", resourceCulture); + object obj = ResourceManager.GetObject("document", resourceCulture); return ((byte[])(obj)); } } @@ -672,9 +772,9 @@ internal static byte[] picture_save { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] picture_small { + internal static byte[] document_small { get { - object obj = ResourceManager.GetObject("picture_small", resourceCulture); + object obj = ResourceManager.GetObject("document_small", resourceCulture); return ((byte[])(obj)); } } @@ -682,9 +782,9 @@ internal static byte[] picture_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] pictures { + internal static byte[] draw_ellipse { get { - object obj = ResourceManager.GetObject("pictures", resourceCulture); + object obj = ResourceManager.GetObject("draw_ellipse", resourceCulture); return ((byte[])(obj)); } } @@ -692,9 +792,9 @@ internal static byte[] pictures { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] printer { + internal static byte[] draw_ellipse_small { get { - object obj = ResourceManager.GetObject("printer", resourceCulture); + object obj = ResourceManager.GetObject("draw_ellipse_small", resourceCulture); return ((byte[])(obj)); } } @@ -702,9 +802,9 @@ internal static byte[] printer { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] scanner_128 { + internal static byte[] email { get { - object obj = ResourceManager.GetObject("scanner_128", resourceCulture); + object obj = ResourceManager.GetObject("email", resourceCulture); return ((byte[])(obj)); } } @@ -712,9 +812,9 @@ internal static byte[] scanner_128 { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] scanner_16 { + internal static byte[] email_attach { get { - object obj = ResourceManager.GetObject("scanner_16", resourceCulture); + object obj = ResourceManager.GetObject("email_attach", resourceCulture); return ((byte[])(obj)); } } @@ -722,9 +822,9 @@ internal static byte[] scanner_16 { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] scanner_32 { + internal static byte[] email_attach_hires { get { - object obj = ResourceManager.GetObject("scanner_32", resourceCulture); + object obj = ResourceManager.GetObject("email_attach_hires", resourceCulture); return ((byte[])(obj)); } } @@ -732,9 +832,9 @@ internal static byte[] scanner_32 { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] scanner_48 { + internal static byte[] email_setting { get { - object obj = ResourceManager.GetObject("scanner_48", resourceCulture); + object obj = ResourceManager.GetObject("email_setting", resourceCulture); return ((byte[])(obj)); } } @@ -742,9 +842,9 @@ internal static byte[] scanner_48 { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] scanner_48_old { + internal static byte[] email_small { get { - object obj = ResourceManager.GetObject("scanner_48_old", resourceCulture); + object obj = ResourceManager.GetObject("email_small", resourceCulture); return ((byte[])(obj)); } } @@ -752,9 +852,9 @@ internal static byte[] scanner_48_old { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] scanner_64 { + internal static byte[] exclamation { get { - object obj = ResourceManager.GetObject("scanner_64", resourceCulture); + object obj = ResourceManager.GetObject("exclamation", resourceCulture); return ((byte[])(obj)); } } @@ -762,9 +862,9 @@ internal static byte[] scanner_64 { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] scanner_default { + internal static byte[] exclamation_small { get { - object obj = ResourceManager.GetObject("scanner_default", resourceCulture); + object obj = ResourceManager.GetObject("exclamation_small", resourceCulture); return ((byte[])(obj)); } } @@ -772,9 +872,9 @@ internal static byte[] scanner_default { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] scanner_lock { + internal static byte[] favicon { get { - object obj = ResourceManager.GetObject("scanner_lock", resourceCulture); + object obj = ResourceManager.GetObject("favicon", resourceCulture); return ((byte[])(obj)); } } @@ -782,9 +882,9 @@ internal static byte[] scanner_lock { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] scanner_lock_default { + internal static byte[] file_extension_pdf { get { - object obj = ResourceManager.GetObject("scanner_lock_default", resourceCulture); + object obj = ResourceManager.GetObject("file_extension_pdf", resourceCulture); return ((byte[])(obj)); } } @@ -792,9 +892,9 @@ internal static byte[] scanner_lock_default { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] sharpen { + internal static byte[] file_extension_pdf_hires { get { - object obj = ResourceManager.GetObject("sharpen", resourceCulture); + object obj = ResourceManager.GetObject("file_extension_pdf_hires", resourceCulture); return ((byte[])(obj)); } } @@ -802,9 +902,9 @@ internal static byte[] sharpen { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] text { + internal static byte[] file_extension_pdf_small { get { - object obj = ResourceManager.GetObject("text", resourceCulture); + object obj = ResourceManager.GetObject("file_extension_pdf_small", resourceCulture); return ((byte[])(obj)); } } @@ -812,9 +912,9 @@ internal static byte[] text { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] text_small { + internal static byte[] folder_picture { get { - object obj = ResourceManager.GetObject("text_small", resourceCulture); + object obj = ResourceManager.GetObject("folder_picture", resourceCulture); return ((byte[])(obj)); } } @@ -822,9 +922,9 @@ internal static byte[] text_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] tick { + internal static byte[] folder_picture_hires { get { - object obj = ResourceManager.GetObject("tick", resourceCulture); + object obj = ResourceManager.GetObject("folder_picture_hires", resourceCulture); return ((byte[])(obj)); } } @@ -832,9 +932,9 @@ internal static byte[] tick { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] tick_small { + internal static byte[] gmail { get { - object obj = ResourceManager.GetObject("tick_small", resourceCulture); + object obj = ResourceManager.GetObject("gmail", resourceCulture); return ((byte[])(obj)); } } @@ -842,9 +942,9 @@ internal static byte[] tick_small { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] transform_crop { + internal static byte[] gmail_hires { get { - object obj = ResourceManager.GetObject("transform_crop", resourceCulture); + object obj = ResourceManager.GetObject("gmail_hires", resourceCulture); return ((byte[])(obj)); } } @@ -852,9 +952,9 @@ internal static byte[] transform_crop { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] transform_flip { + internal static byte[] hourglass_grey { get { - object obj = ResourceManager.GetObject("transform_flip", resourceCulture); + object obj = ResourceManager.GetObject("hourglass_grey", resourceCulture); return ((byte[])(obj)); } } @@ -862,9 +962,9 @@ internal static byte[] transform_flip { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] weather_sun { + internal static byte[] information { get { - object obj = ResourceManager.GetObject("weather_sun", resourceCulture); + object obj = ResourceManager.GetObject("information", resourceCulture); return ((byte[])(obj)); } } @@ -872,9 +972,9 @@ internal static byte[] weather_sun { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] wireless16 { + internal static byte[] information_small { get { - object obj = ResourceManager.GetObject("wireless16", resourceCulture); + object obj = ResourceManager.GetObject("information_small", resourceCulture); return ((byte[])(obj)); } } @@ -882,9 +982,9 @@ internal static byte[] wireless16 { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] world { + internal static byte[] key { get { - object obj = ResourceManager.GetObject("world", resourceCulture); + object obj = ResourceManager.GetObject("key", resourceCulture); return ((byte[])(obj)); } } @@ -892,9 +992,9 @@ internal static byte[] world { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] zoom { + internal static byte[] key_small { get { - object obj = ResourceManager.GetObject("zoom", resourceCulture); + object obj = ResourceManager.GetObject("key_small", resourceCulture); return ((byte[])(obj)); } } @@ -902,9 +1002,9 @@ internal static byte[] zoom { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] zoom_actual { + internal static byte[] keyboard { get { - object obj = ResourceManager.GetObject("zoom_actual", resourceCulture); + object obj = ResourceManager.GetObject("keyboard", resourceCulture); return ((byte[])(obj)); } } @@ -912,9 +1012,9 @@ internal static byte[] zoom_actual { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] zoom_in { + internal static byte[] keyboard_small { get { - object obj = ResourceManager.GetObject("zoom_in", resourceCulture); + object obj = ResourceManager.GetObject("keyboard_small", resourceCulture); return ((byte[])(obj)); } } @@ -922,9 +1022,899 @@ internal static byte[] zoom_in { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] zoom_out { + internal static byte[] large_tiles { get { - object obj = ResourceManager.GetObject("zoom_out", resourceCulture); + object obj = ResourceManager.GetObject("large_tiles", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] large_tiles_small { + get { + object obj = ResourceManager.GetObject("large_tiles_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] mail_yellow { + get { + object obj = ResourceManager.GetObject("mail_yellow", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] mail_yellow_hires { + get { + object obj = ResourceManager.GetObject("mail_yellow_hires", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] network_ip { + get { + object obj = ResourceManager.GetObject("network_ip", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] network_ip_hires { + get { + object obj = ResourceManager.GetObject("network_ip_hires", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] network_ip_small { + get { + object obj = ResourceManager.GetObject("network_ip_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] outlooknew { + get { + object obj = ResourceManager.GetObject("outlooknew", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] outlooknew_hires { + get { + object obj = ResourceManager.GetObject("outlooknew_hires", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] outlookweb { + get { + object obj = ResourceManager.GetObject("outlookweb", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] outlookweb_hires { + get { + object obj = ResourceManager.GetObject("outlookweb_hires", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] paste { + get { + object obj = ResourceManager.GetObject("paste", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] paste_small { + get { + object obj = ResourceManager.GetObject("paste_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] pencil { + get { + object obj = ResourceManager.GetObject("pencil", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] pencil_small { + get { + object obj = ResourceManager.GetObject("pencil_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] picture { + get { + object obj = ResourceManager.GetObject("picture", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] picture_edit { + get { + object obj = ResourceManager.GetObject("picture_edit", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] picture_edit_hires { + get { + object obj = ResourceManager.GetObject("picture_edit_hires", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] picture_small { + get { + object obj = ResourceManager.GetObject("picture_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] pictures { + get { + object obj = ResourceManager.GetObject("pictures", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] pictures_hires { + get { + object obj = ResourceManager.GetObject("pictures_hires", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] printer { + get { + object obj = ResourceManager.GetObject("printer", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] printer_hires { + get { + object obj = ResourceManager.GetObject("printer_hires", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] redo { + get { + object obj = ResourceManager.GetObject("redo", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] redo_small { + get { + object obj = ResourceManager.GetObject("redo_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_128 { + get { + object obj = ResourceManager.GetObject("scanner_128", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_16 { + get { + object obj = ResourceManager.GetObject("scanner_16", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_32 { + get { + object obj = ResourceManager.GetObject("scanner_32", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_gray_32 { + get { + object obj = ResourceManager.GetObject("scanner_gray_32", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_48 { + get { + object obj = ResourceManager.GetObject("scanner_48", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_64 { + get { + object obj = ResourceManager.GetObject("scanner_64", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_96 { + get { + object obj = ResourceManager.GetObject("scanner_96", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_default_48 { + get { + object obj = ResourceManager.GetObject("scanner_default_48", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_default_96 { + get { + object obj = ResourceManager.GetObject("scanner_default_96", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_lock_48 { + get { + object obj = ResourceManager.GetObject("scanner_lock_48", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_lock_96 { + get { + object obj = ResourceManager.GetObject("scanner_lock_96", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_lock_default_48 { + get { + object obj = ResourceManager.GetObject("scanner_lock_default_48", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_lock_default_96 { + get { + object obj = ResourceManager.GetObject("scanner_lock_default_96", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_wireless_48 { + get { + object obj = ResourceManager.GetObject("scanner_wireless_48", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] scanner_wireless_96 { + get { + object obj = ResourceManager.GetObject("scanner_wireless_96", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] shape_align_bottom { + get { + object obj = ResourceManager.GetObject("shape_align_bottom", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] shape_align_bottom_small { + get { + object obj = ResourceManager.GetObject("shape_align_bottom_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] shape_align_center { + get { + object obj = ResourceManager.GetObject("shape_align_center", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] shape_align_center_small { + get { + object obj = ResourceManager.GetObject("shape_align_center_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] shape_align_left { + get { + object obj = ResourceManager.GetObject("shape_align_left", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] shape_align_left_small { + get { + object obj = ResourceManager.GetObject("shape_align_left_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] shape_align_middle { + get { + object obj = ResourceManager.GetObject("shape_align_middle", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] shape_align_middle_small { + get { + object obj = ResourceManager.GetObject("shape_align_middle_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] shape_align_right { + get { + object obj = ResourceManager.GetObject("shape_align_right", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] shape_align_right_small { + get { + object obj = ResourceManager.GetObject("shape_align_right_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] shape_align_top { + get { + object obj = ResourceManager.GetObject("shape_align_top", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] shape_align_top_small { + get { + object obj = ResourceManager.GetObject("shape_align_top_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] sharpen { + get { + object obj = ResourceManager.GetObject("sharpen", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] sharpen_small { + get { + object obj = ResourceManager.GetObject("sharpen_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] split { + get { + object obj = ResourceManager.GetObject("split", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] split_hor { + get { + object obj = ResourceManager.GetObject("split_hor", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] split_hor_small { + get { + object obj = ResourceManager.GetObject("split_hor_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] split_small { + get { + object obj = ResourceManager.GetObject("split_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] split_ver { + get { + object obj = ResourceManager.GetObject("split_ver", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] split_ver_small { + get { + object obj = ResourceManager.GetObject("split_ver_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] switch_hor { + get { + object obj = ResourceManager.GetObject("switch_hor", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] switch_hor_small { + get { + object obj = ResourceManager.GetObject("switch_hor_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] switch_ver { + get { + object obj = ResourceManager.GetObject("switch_ver", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] switch_ver_small { + get { + object obj = ResourceManager.GetObject("switch_ver_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] text { + get { + object obj = ResourceManager.GetObject("text", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] text_align_justify { + get { + object obj = ResourceManager.GetObject("text_align_justify", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] text_align_justify_small { + get { + object obj = ResourceManager.GetObject("text_align_justify_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] text_hires { + get { + object obj = ResourceManager.GetObject("text_hires", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] text_small { + get { + object obj = ResourceManager.GetObject("text_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] thunderbird { + get { + object obj = ResourceManager.GetObject("thunderbird", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] thunderbird_hires { + get { + object obj = ResourceManager.GetObject("thunderbird_hires", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] tick { + get { + object obj = ResourceManager.GetObject("tick", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] tick_small { + get { + object obj = ResourceManager.GetObject("tick_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] transform_crop { + get { + object obj = ResourceManager.GetObject("transform_crop", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] transform_crop_small { + get { + object obj = ResourceManager.GetObject("transform_crop_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] undo { + get { + object obj = ResourceManager.GetObject("undo", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] undo_small { + get { + object obj = ResourceManager.GetObject("undo_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] weather_sun { + get { + object obj = ResourceManager.GetObject("weather_sun", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] weather_sun_small { + get { + object obj = ResourceManager.GetObject("weather_sun_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] wireless { + get { + object obj = ResourceManager.GetObject("wireless", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] wireless_small { + get { + object obj = ResourceManager.GetObject("wireless_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] world { + get { + object obj = ResourceManager.GetObject("world", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] world_hires { + get { + object obj = ResourceManager.GetObject("world_hires", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] zoom { + get { + object obj = ResourceManager.GetObject("zoom", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] zoom_actual { + get { + object obj = ResourceManager.GetObject("zoom_actual", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] zoom_actual_small { + get { + object obj = ResourceManager.GetObject("zoom_actual_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] zoom_in { + get { + object obj = ResourceManager.GetObject("zoom_in", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] zoom_in_small { + get { + object obj = ResourceManager.GetObject("zoom_in_small", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] zoom_out { + get { + object obj = ResourceManager.GetObject("zoom_out", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] zoom_out_small { + get { + object obj = ResourceManager.GetObject("zoom_out_small", resourceCulture); return ((byte[])(obj)); } } diff --git a/NAPS2.Lib/Icons.resx b/NAPS2.Lib/Icons.resx index 6b587e6b2e..723151e87e 100644 --- a/NAPS2.Lib/Icons.resx +++ b/NAPS2.Lib/Icons.resx @@ -121,62 +121,119 @@ Icons\arrow_down.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\arrow_down-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\arrow_out.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\arrow_out-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\arrow_rotate_anticlockwise.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\arrow_rotate_anticlockwise-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\arrow_rotate_anticlockwise-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\arrow_rotate_clockwise.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\arrow_rotate_clockwise-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\arrow_switch.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\arrow_switch-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\arrow_up.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\arrow_up-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\arrow_left.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\arrow_left-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\arrow_right.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\arrow_right-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\blueprints.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\blueprints-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\blueprints-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\cross.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\cross-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\cross-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\key.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\key-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\email.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\email-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\email_attach.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\email_attach-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\file_extension_pdf.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\file_extension_pdf-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\file_extension_pdf_small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\information.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\info_rhombus.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Icons\pdf_email.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\information-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\picture.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\picture-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\scanner-16-rev2.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\scanner-32-rev2.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\scanner-48-rev1.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\scanner-32-gray.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\scanner-48-rev2.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -184,30 +241,60 @@ Icons\scanner-64-rev2.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\scanner-96.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\scanner-128.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\scanner-default.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\scanner-default-rev2.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\scanner-default-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\scanner-lock-default-rev2.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\scanner-lock-default.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\scanner-lock-default-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\scanner-lock.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\scanner-lock-rev2.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\transform_flip.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\scanner-lock-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\scanner-wireless-rev2.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\scanner-wireless-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\device.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\device-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\zoom_actual.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\zoom_actual-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\zoom_in.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\zoom_in-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\zoom_out.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\zoom_out-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\accept.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -223,11 +310,17 @@ Icons\cancel.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\cancel-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\cancel-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\cross-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\broom.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\broom-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\pencil.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -241,122 +334,191 @@ Icons\tick-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\blueprints-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Icons\world.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\world-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\folder_picture.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\folder_picture-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\arrow_refresh.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\arrow_rotate_anticlockwise-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Icons\arrow_rotate_clockwise-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Icons\arrow_switch-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\arrow_refresh-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\text.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\text-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\text-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\text_align_justify.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\text_align_justify-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\large_tiles.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\large_tiles-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\control_play_blue.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\control_play_blue-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\control_play_blue-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\color_wheel.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\color_wheel-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\contrast.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\image_edit.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\contrast-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\transform_crop.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\transform_crop-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\weather_sun.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\arrow_down-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Icons\arrow_up-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\weather_sun-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\pictures.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\pictures-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\picture_edit.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\picture_save.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Icons\arrow_left.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Icons\arrow_right.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\picture_edit-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\printer.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\printer-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\application_cascade.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\application_cascade-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\close.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\picture.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Icons\color_gradient.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\color_gradient-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\color_management.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\color_management-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\contrast_high.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\contrast_high-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\contrast_with_sun.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\contrast_with_sun-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\sharpen.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\btn_donate_LG.gif;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\sharpen-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\gmail.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\gmail-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\outlooknew.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\outlooknew-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\outlookweb.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\outlookweb-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\thunderbird.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\thunderbird-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\apple_mail.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\apple_mail-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\email_setting.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\mail_yellow.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\mail_yellow-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\hourglass_grey.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\wireless16.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\wireless.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\wireless-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\network_ip.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\network_ip-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\network_ip-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\cog.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -364,17 +526,26 @@ Icons\cog-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\information-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Icons\favicon.ico;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\ask.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\ask-hires.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Icons\exclamation.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Icons\diskette.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\exclamation-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\draw_ellipse.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\draw_ellipse-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Icons\zoom.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -382,4 +553,130 @@ Icons\zoom-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\copy.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\copy-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\paste.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\paste-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\undo.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\undo-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\redo.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\redo-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\document.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\document-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\split.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\split-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\split.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\split-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\split_hor.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\split_hor-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\combine.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\combine-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\combine.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\combine-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\combine_hor.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\combine_hor-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\switch.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\switch-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\switch_hor.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\switch_hor-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\shape_align_top.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\shape_align_top-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\shape_align_middle.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\shape_align_middle-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\shape_align_bottom.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\shape_align_bottom-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\shape_align_left.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\shape_align_left-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\shape_align_center.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\shape_align_center-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\shape_align_right.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\shape_align_right-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\application_side_list.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\application_side_list-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\keyboard.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Icons\keyboard-small.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/NAPS2.Lib/Icons/apple_mail-hires.png b/NAPS2.Lib/Icons/apple_mail-hires.png new file mode 100644 index 0000000000..967886bfb1 Binary files /dev/null and b/NAPS2.Lib/Icons/apple_mail-hires.png differ diff --git a/NAPS2.Lib/Icons/apple_mail.png b/NAPS2.Lib/Icons/apple_mail.png new file mode 100644 index 0000000000..e36e132f7e Binary files /dev/null and b/NAPS2.Lib/Icons/apple_mail.png differ diff --git a/NAPS2.Lib/Icons/application_cascade-small.png b/NAPS2.Lib/Icons/application_cascade-small.png new file mode 100644 index 0000000000..db4511144e Binary files /dev/null and b/NAPS2.Lib/Icons/application_cascade-small.png differ diff --git a/NAPS2.Lib/Icons/application_cascade.ico b/NAPS2.Lib/Icons/application_cascade.ico deleted file mode 100644 index 42eee7a940..0000000000 Binary files a/NAPS2.Lib/Icons/application_cascade.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/application_cascade.png b/NAPS2.Lib/Icons/application_cascade.png index db4511144e..8cd88bc811 100644 Binary files a/NAPS2.Lib/Icons/application_cascade.png and b/NAPS2.Lib/Icons/application_cascade.png differ diff --git a/NAPS2.Lib/Icons/application_side_list-small.png b/NAPS2.Lib/Icons/application_side_list-small.png new file mode 100644 index 0000000000..045d2a4d80 Binary files /dev/null and b/NAPS2.Lib/Icons/application_side_list-small.png differ diff --git a/NAPS2.Lib/Icons/application_side_list.png b/NAPS2.Lib/Icons/application_side_list.png new file mode 100644 index 0000000000..6cd8e0645f Binary files /dev/null and b/NAPS2.Lib/Icons/application_side_list.png differ diff --git a/NAPS2.Lib/Icons/arrow_left-small.png b/NAPS2.Lib/Icons/arrow_left-small.png new file mode 100644 index 0000000000..d9c50c4d3e Binary files /dev/null and b/NAPS2.Lib/Icons/arrow_left-small.png differ diff --git a/NAPS2.Lib/Icons/arrow_left.png b/NAPS2.Lib/Icons/arrow_left.png index d9c50c4d3e..a7f6e91f5a 100644 Binary files a/NAPS2.Lib/Icons/arrow_left.png and b/NAPS2.Lib/Icons/arrow_left.png differ diff --git a/NAPS2.Lib/Icons/arrow_out-small.png b/NAPS2.Lib/Icons/arrow_out-small.png new file mode 100644 index 0000000000..7475223e30 Binary files /dev/null and b/NAPS2.Lib/Icons/arrow_out-small.png differ diff --git a/NAPS2.Lib/Icons/arrow_out.png b/NAPS2.Lib/Icons/arrow_out.png index 7475223e30..8a96a53be5 100644 Binary files a/NAPS2.Lib/Icons/arrow_out.png and b/NAPS2.Lib/Icons/arrow_out.png differ diff --git a/NAPS2.Lib/Icons/arrow_refresh-hires.png b/NAPS2.Lib/Icons/arrow_refresh-hires.png new file mode 100644 index 0000000000..7fdcdff402 Binary files /dev/null and b/NAPS2.Lib/Icons/arrow_refresh-hires.png differ diff --git a/NAPS2.Lib/Icons/arrow_right-small.png b/NAPS2.Lib/Icons/arrow_right-small.png new file mode 100644 index 0000000000..8d204afeb0 Binary files /dev/null and b/NAPS2.Lib/Icons/arrow_right-small.png differ diff --git a/NAPS2.Lib/Icons/arrow_right.png b/NAPS2.Lib/Icons/arrow_right.png index 8d204afeb0..036b9e2947 100644 Binary files a/NAPS2.Lib/Icons/arrow_right.png and b/NAPS2.Lib/Icons/arrow_right.png differ diff --git a/NAPS2.Lib/Icons/arrow_rotate_anticlockwise-hires.png b/NAPS2.Lib/Icons/arrow_rotate_anticlockwise-hires.png new file mode 100644 index 0000000000..d5fdc0e442 Binary files /dev/null and b/NAPS2.Lib/Icons/arrow_rotate_anticlockwise-hires.png differ diff --git a/NAPS2.Lib/Icons/arrow_rotate_anticlockwise-small.ico b/NAPS2.Lib/Icons/arrow_rotate_anticlockwise-small.ico deleted file mode 100644 index c9b577aed4..0000000000 Binary files a/NAPS2.Lib/Icons/arrow_rotate_anticlockwise-small.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/ask-hires.png b/NAPS2.Lib/Icons/ask-hires.png new file mode 100644 index 0000000000..c7210786da Binary files /dev/null and b/NAPS2.Lib/Icons/ask-hires.png differ diff --git a/NAPS2.Lib/Icons/ask.png b/NAPS2.Lib/Icons/ask.png new file mode 100644 index 0000000000..d66b485260 Binary files /dev/null and b/NAPS2.Lib/Icons/ask.png differ diff --git a/NAPS2.Lib/Icons/blueprints-hires.png b/NAPS2.Lib/Icons/blueprints-hires.png new file mode 100644 index 0000000000..49e414ea02 Binary files /dev/null and b/NAPS2.Lib/Icons/blueprints-hires.png differ diff --git a/NAPS2.Lib/Icons/blueprints-small.ico b/NAPS2.Lib/Icons/blueprints-small.ico deleted file mode 100644 index 0a966fb42f..0000000000 Binary files a/NAPS2.Lib/Icons/blueprints-small.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/broom-hires.png b/NAPS2.Lib/Icons/broom-hires.png new file mode 100644 index 0000000000..30f98ab686 Binary files /dev/null and b/NAPS2.Lib/Icons/broom-hires.png differ diff --git a/NAPS2.Lib/Icons/broom.png b/NAPS2.Lib/Icons/broom.png new file mode 100644 index 0000000000..10e62e6e4a Binary files /dev/null and b/NAPS2.Lib/Icons/broom.png differ diff --git a/NAPS2.Lib/Icons/cancel-hires.png b/NAPS2.Lib/Icons/cancel-hires.png new file mode 100644 index 0000000000..3b71475f8f Binary files /dev/null and b/NAPS2.Lib/Icons/cancel-hires.png differ diff --git a/NAPS2.Lib/Icons/color_gradient-small.png b/NAPS2.Lib/Icons/color_gradient-small.png new file mode 100644 index 0000000000..0f7af7a350 Binary files /dev/null and b/NAPS2.Lib/Icons/color_gradient-small.png differ diff --git a/NAPS2.Lib/Icons/color_gradient.png b/NAPS2.Lib/Icons/color_gradient.png index 0f7af7a350..56d72b12b7 100644 Binary files a/NAPS2.Lib/Icons/color_gradient.png and b/NAPS2.Lib/Icons/color_gradient.png differ diff --git a/NAPS2.Lib/Icons/color_management-small.png b/NAPS2.Lib/Icons/color_management-small.png new file mode 100644 index 0000000000..f876090864 Binary files /dev/null and b/NAPS2.Lib/Icons/color_management-small.png differ diff --git a/NAPS2.Lib/Icons/color_management.ico b/NAPS2.Lib/Icons/color_management.ico deleted file mode 100644 index 049553b4cd..0000000000 Binary files a/NAPS2.Lib/Icons/color_management.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/color_management.png b/NAPS2.Lib/Icons/color_management.png index f876090864..d9d7eb887d 100644 Binary files a/NAPS2.Lib/Icons/color_management.png and b/NAPS2.Lib/Icons/color_management.png differ diff --git a/NAPS2.Lib/Icons/color_wheel-small.png b/NAPS2.Lib/Icons/color_wheel-small.png new file mode 100644 index 0000000000..287a344584 Binary files /dev/null and b/NAPS2.Lib/Icons/color_wheel-small.png differ diff --git a/NAPS2.Lib/Icons/color_wheel.ico b/NAPS2.Lib/Icons/color_wheel.ico deleted file mode 100644 index 5cb0dec18c..0000000000 Binary files a/NAPS2.Lib/Icons/color_wheel.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/color_wheel.png b/NAPS2.Lib/Icons/color_wheel.png index 287a344584..7bc293a01e 100644 Binary files a/NAPS2.Lib/Icons/color_wheel.png and b/NAPS2.Lib/Icons/color_wheel.png differ diff --git a/NAPS2.Lib/Icons/combine-small.png b/NAPS2.Lib/Icons/combine-small.png new file mode 100644 index 0000000000..c35c8f5a0f Binary files /dev/null and b/NAPS2.Lib/Icons/combine-small.png differ diff --git a/NAPS2.Lib/Icons/combine.png b/NAPS2.Lib/Icons/combine.png new file mode 100644 index 0000000000..4f029539d4 Binary files /dev/null and b/NAPS2.Lib/Icons/combine.png differ diff --git a/NAPS2.Lib/Icons/combine_hor-small.png b/NAPS2.Lib/Icons/combine_hor-small.png new file mode 100644 index 0000000000..60d27be95c Binary files /dev/null and b/NAPS2.Lib/Icons/combine_hor-small.png differ diff --git a/NAPS2.Lib/Icons/combine_hor.png b/NAPS2.Lib/Icons/combine_hor.png new file mode 100644 index 0000000000..340f0b3c53 Binary files /dev/null and b/NAPS2.Lib/Icons/combine_hor.png differ diff --git a/NAPS2.Lib/Icons/contrast-small.png b/NAPS2.Lib/Icons/contrast-small.png new file mode 100644 index 0000000000..001e003c8b Binary files /dev/null and b/NAPS2.Lib/Icons/contrast-small.png differ diff --git a/NAPS2.Lib/Icons/contrast.ico b/NAPS2.Lib/Icons/contrast.ico deleted file mode 100644 index 57715e09c5..0000000000 Binary files a/NAPS2.Lib/Icons/contrast.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/contrast.png b/NAPS2.Lib/Icons/contrast.png index 001e003c8b..1bdb483954 100644 Binary files a/NAPS2.Lib/Icons/contrast.png and b/NAPS2.Lib/Icons/contrast.png differ diff --git a/NAPS2.Lib/Icons/contrast_high-small.png b/NAPS2.Lib/Icons/contrast_high-small.png new file mode 100644 index 0000000000..864dbe921b Binary files /dev/null and b/NAPS2.Lib/Icons/contrast_high-small.png differ diff --git a/NAPS2.Lib/Icons/contrast_high.ico b/NAPS2.Lib/Icons/contrast_high.ico deleted file mode 100644 index 7e67b9f200..0000000000 Binary files a/NAPS2.Lib/Icons/contrast_high.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/contrast_high.png b/NAPS2.Lib/Icons/contrast_high.png index 864dbe921b..165fcc639c 100644 Binary files a/NAPS2.Lib/Icons/contrast_high.png and b/NAPS2.Lib/Icons/contrast_high.png differ diff --git a/NAPS2.Lib/Icons/contrast_with_sun-small.png b/NAPS2.Lib/Icons/contrast_with_sun-small.png new file mode 100644 index 0000000000..16ac8c7db4 Binary files /dev/null and b/NAPS2.Lib/Icons/contrast_with_sun-small.png differ diff --git a/NAPS2.Lib/Icons/contrast_with_sun.ico b/NAPS2.Lib/Icons/contrast_with_sun.ico deleted file mode 100644 index b4273ce4e9..0000000000 Binary files a/NAPS2.Lib/Icons/contrast_with_sun.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/contrast_with_sun.png b/NAPS2.Lib/Icons/contrast_with_sun.png index 16ac8c7db4..7bf7b76b31 100644 Binary files a/NAPS2.Lib/Icons/contrast_with_sun.png and b/NAPS2.Lib/Icons/contrast_with_sun.png differ diff --git a/NAPS2.Lib/Icons/control_play_blue-hires.png b/NAPS2.Lib/Icons/control_play_blue-hires.png new file mode 100644 index 0000000000..cd7e9301b8 Binary files /dev/null and b/NAPS2.Lib/Icons/control_play_blue-hires.png differ diff --git a/NAPS2.Lib/Icons/copy-small.png b/NAPS2.Lib/Icons/copy-small.png new file mode 100644 index 0000000000..a61ec13432 Binary files /dev/null and b/NAPS2.Lib/Icons/copy-small.png differ diff --git a/NAPS2.Lib/Icons/copy.png b/NAPS2.Lib/Icons/copy.png new file mode 100644 index 0000000000..7e387d63bf Binary files /dev/null and b/NAPS2.Lib/Icons/copy.png differ diff --git a/NAPS2.Lib/Icons/cross-hires.png b/NAPS2.Lib/Icons/cross-hires.png new file mode 100644 index 0000000000..d30e690d70 Binary files /dev/null and b/NAPS2.Lib/Icons/cross-hires.png differ diff --git a/NAPS2.Lib/Icons/device-hires.png b/NAPS2.Lib/Icons/device-hires.png new file mode 100644 index 0000000000..116f5406db Binary files /dev/null and b/NAPS2.Lib/Icons/device-hires.png differ diff --git a/NAPS2.Lib/Icons/device.png b/NAPS2.Lib/Icons/device.png new file mode 100644 index 0000000000..ace0be6590 Binary files /dev/null and b/NAPS2.Lib/Icons/device.png differ diff --git a/NAPS2.Lib/Icons/diskette.png b/NAPS2.Lib/Icons/diskette.png deleted file mode 100644 index 3a9dcf37ec..0000000000 Binary files a/NAPS2.Lib/Icons/diskette.png and /dev/null differ diff --git a/NAPS2.Lib/Icons/document-small.png b/NAPS2.Lib/Icons/document-small.png new file mode 100644 index 0000000000..d041c57e7b Binary files /dev/null and b/NAPS2.Lib/Icons/document-small.png differ diff --git a/NAPS2.Lib/Icons/document.png b/NAPS2.Lib/Icons/document.png new file mode 100644 index 0000000000..5aa1fa3c37 Binary files /dev/null and b/NAPS2.Lib/Icons/document.png differ diff --git a/NAPS2.Lib/Icons/draw_ellipse-small.png b/NAPS2.Lib/Icons/draw_ellipse-small.png new file mode 100644 index 0000000000..e5f0921fc6 Binary files /dev/null and b/NAPS2.Lib/Icons/draw_ellipse-small.png differ diff --git a/NAPS2.Lib/Icons/draw_ellipse.png b/NAPS2.Lib/Icons/draw_ellipse.png new file mode 100644 index 0000000000..74fdefebf9 Binary files /dev/null and b/NAPS2.Lib/Icons/draw_ellipse.png differ diff --git a/NAPS2.Lib/Icons/email.png b/NAPS2.Lib/Icons/email.png new file mode 100644 index 0000000000..98a99c2883 Binary files /dev/null and b/NAPS2.Lib/Icons/email.png differ diff --git a/NAPS2.Lib/Icons/email_attach-hires.png b/NAPS2.Lib/Icons/email_attach-hires.png new file mode 100644 index 0000000000..86d25f56a1 Binary files /dev/null and b/NAPS2.Lib/Icons/email_attach-hires.png differ diff --git a/NAPS2.Lib/Icons/exclamation-small.png b/NAPS2.Lib/Icons/exclamation-small.png new file mode 100644 index 0000000000..d49653ad8f Binary files /dev/null and b/NAPS2.Lib/Icons/exclamation-small.png differ diff --git a/NAPS2.Lib/Icons/file_extension_pdf-hires.png b/NAPS2.Lib/Icons/file_extension_pdf-hires.png new file mode 100644 index 0000000000..c45bacf0f6 Binary files /dev/null and b/NAPS2.Lib/Icons/file_extension_pdf-hires.png differ diff --git a/NAPS2.Lib/Icons/folder_picture-hires.png b/NAPS2.Lib/Icons/folder_picture-hires.png new file mode 100644 index 0000000000..aced670276 Binary files /dev/null and b/NAPS2.Lib/Icons/folder_picture-hires.png differ diff --git a/NAPS2.Lib/Icons/gmail-hires.png b/NAPS2.Lib/Icons/gmail-hires.png new file mode 100644 index 0000000000..3f8a727c9f Binary files /dev/null and b/NAPS2.Lib/Icons/gmail-hires.png differ diff --git a/NAPS2.Lib/Icons/gmail.png b/NAPS2.Lib/Icons/gmail.png index 58d553360a..05d584d8ef 100644 Binary files a/NAPS2.Lib/Icons/gmail.png and b/NAPS2.Lib/Icons/gmail.png differ diff --git a/NAPS2.Lib/Icons/image_edit.png b/NAPS2.Lib/Icons/image_edit.png deleted file mode 100644 index c398570a99..0000000000 Binary files a/NAPS2.Lib/Icons/image_edit.png and /dev/null differ diff --git a/NAPS2.Lib/Icons/info_rhombus.png b/NAPS2.Lib/Icons/info_rhombus.png deleted file mode 100644 index eeaf16e039..0000000000 Binary files a/NAPS2.Lib/Icons/info_rhombus.png and /dev/null differ diff --git a/NAPS2.Lib/Icons/information-small.ico b/NAPS2.Lib/Icons/information-small.ico deleted file mode 100644 index fa542e5fda..0000000000 Binary files a/NAPS2.Lib/Icons/information-small.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/key.png b/NAPS2.Lib/Icons/key.png new file mode 100644 index 0000000000..3cf460e112 Binary files /dev/null and b/NAPS2.Lib/Icons/key.png differ diff --git a/NAPS2.Lib/Icons/keyboard-small.png b/NAPS2.Lib/Icons/keyboard-small.png new file mode 100644 index 0000000000..7714d47b9a Binary files /dev/null and b/NAPS2.Lib/Icons/keyboard-small.png differ diff --git a/NAPS2.Lib/Icons/keyboard.png b/NAPS2.Lib/Icons/keyboard.png new file mode 100644 index 0000000000..882d48bd34 Binary files /dev/null and b/NAPS2.Lib/Icons/keyboard.png differ diff --git a/NAPS2.Lib/Icons/large_tiles-small.png b/NAPS2.Lib/Icons/large_tiles-small.png new file mode 100644 index 0000000000..974079e932 Binary files /dev/null and b/NAPS2.Lib/Icons/large_tiles-small.png differ diff --git a/NAPS2.Lib/Icons/large_tiles.png b/NAPS2.Lib/Icons/large_tiles.png new file mode 100644 index 0000000000..05dc583c61 Binary files /dev/null and b/NAPS2.Lib/Icons/large_tiles.png differ diff --git a/NAPS2.Lib/Icons/mail_yellow-hires.png b/NAPS2.Lib/Icons/mail_yellow-hires.png new file mode 100644 index 0000000000..6198feede6 Binary files /dev/null and b/NAPS2.Lib/Icons/mail_yellow-hires.png differ diff --git a/NAPS2.Lib/Icons/network_ip-hires.png b/NAPS2.Lib/Icons/network_ip-hires.png new file mode 100644 index 0000000000..10c5ac9a2f Binary files /dev/null and b/NAPS2.Lib/Icons/network_ip-hires.png differ diff --git a/NAPS2.Lib/Icons/network_ip-small.png b/NAPS2.Lib/Icons/network_ip-small.png new file mode 100644 index 0000000000..0d017005da Binary files /dev/null and b/NAPS2.Lib/Icons/network_ip-small.png differ diff --git a/NAPS2.Lib/Icons/network_ip.png b/NAPS2.Lib/Icons/network_ip.png new file mode 100644 index 0000000000..e42846b0a4 Binary files /dev/null and b/NAPS2.Lib/Icons/network_ip.png differ diff --git a/NAPS2.Lib/Icons/outlooknew-hires.png b/NAPS2.Lib/Icons/outlooknew-hires.png new file mode 100644 index 0000000000..0021528894 Binary files /dev/null and b/NAPS2.Lib/Icons/outlooknew-hires.png differ diff --git a/NAPS2.Lib/Icons/outlooknew.png b/NAPS2.Lib/Icons/outlooknew.png new file mode 100644 index 0000000000..694a223b08 Binary files /dev/null and b/NAPS2.Lib/Icons/outlooknew.png differ diff --git a/NAPS2.Lib/Icons/outlookweb-hires.png b/NAPS2.Lib/Icons/outlookweb-hires.png new file mode 100644 index 0000000000..cec206a4ad Binary files /dev/null and b/NAPS2.Lib/Icons/outlookweb-hires.png differ diff --git a/NAPS2.Lib/Icons/outlookweb.png b/NAPS2.Lib/Icons/outlookweb.png index 3537a5ebbf..13823f5a23 100644 Binary files a/NAPS2.Lib/Icons/outlookweb.png and b/NAPS2.Lib/Icons/outlookweb.png differ diff --git a/NAPS2.Lib/Icons/paste-small.png b/NAPS2.Lib/Icons/paste-small.png new file mode 100644 index 0000000000..ee4bd3e94d Binary files /dev/null and b/NAPS2.Lib/Icons/paste-small.png differ diff --git a/NAPS2.Lib/Icons/paste.png b/NAPS2.Lib/Icons/paste.png new file mode 100644 index 0000000000..c963ba62ef Binary files /dev/null and b/NAPS2.Lib/Icons/paste.png differ diff --git a/NAPS2.Lib/Icons/pdf_email.png b/NAPS2.Lib/Icons/pdf_email.png deleted file mode 100644 index d2a0678a60..0000000000 Binary files a/NAPS2.Lib/Icons/pdf_email.png and /dev/null differ diff --git a/NAPS2.Lib/Icons/picture-small.png b/NAPS2.Lib/Icons/picture-small.png new file mode 100644 index 0000000000..3b98f2d0b8 Binary files /dev/null and b/NAPS2.Lib/Icons/picture-small.png differ diff --git a/NAPS2.Lib/Icons/picture.ico b/NAPS2.Lib/Icons/picture.ico deleted file mode 100644 index db3d44f907..0000000000 Binary files a/NAPS2.Lib/Icons/picture.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/picture_edit-hires.png b/NAPS2.Lib/Icons/picture_edit-hires.png new file mode 100644 index 0000000000..55ab22a150 Binary files /dev/null and b/NAPS2.Lib/Icons/picture_edit-hires.png differ diff --git a/NAPS2.Lib/Icons/picture_save.png b/NAPS2.Lib/Icons/picture_save.png deleted file mode 100644 index a12d496f29..0000000000 Binary files a/NAPS2.Lib/Icons/picture_save.png and /dev/null differ diff --git a/NAPS2.Lib/Icons/pictures-hires.png b/NAPS2.Lib/Icons/pictures-hires.png new file mode 100644 index 0000000000..d71a0afc1f Binary files /dev/null and b/NAPS2.Lib/Icons/pictures-hires.png differ diff --git a/NAPS2.Lib/Icons/printer-hires.png b/NAPS2.Lib/Icons/printer-hires.png new file mode 100644 index 0000000000..af77bb2a63 Binary files /dev/null and b/NAPS2.Lib/Icons/printer-hires.png differ diff --git a/NAPS2.Lib/Icons/redo-small.png b/NAPS2.Lib/Icons/redo-small.png new file mode 100644 index 0000000000..bb7673c827 Binary files /dev/null and b/NAPS2.Lib/Icons/redo-small.png differ diff --git a/NAPS2.Lib/Icons/redo.png b/NAPS2.Lib/Icons/redo.png new file mode 100644 index 0000000000..e05a58f623 Binary files /dev/null and b/NAPS2.Lib/Icons/redo.png differ diff --git a/NAPS2.Lib/Icons/scanner-32-gray.png b/NAPS2.Lib/Icons/scanner-32-gray.png new file mode 100644 index 0000000000..31a90fad6b Binary files /dev/null and b/NAPS2.Lib/Icons/scanner-32-gray.png differ diff --git a/NAPS2.Lib/Icons/scanner-96.png b/NAPS2.Lib/Icons/scanner-96.png new file mode 100644 index 0000000000..3da732d0d9 Binary files /dev/null and b/NAPS2.Lib/Icons/scanner-96.png differ diff --git a/NAPS2.Lib/Icons/scanner-default-hires.png b/NAPS2.Lib/Icons/scanner-default-hires.png new file mode 100644 index 0000000000..b78962e03b Binary files /dev/null and b/NAPS2.Lib/Icons/scanner-default-hires.png differ diff --git a/NAPS2.Lib/Icons/scanner-default.png b/NAPS2.Lib/Icons/scanner-default-rev1.png similarity index 100% rename from NAPS2.Lib/Icons/scanner-default.png rename to NAPS2.Lib/Icons/scanner-default-rev1.png diff --git a/NAPS2.Lib/Icons/scanner-default-rev2.png b/NAPS2.Lib/Icons/scanner-default-rev2.png new file mode 100644 index 0000000000..cd6caa25ea Binary files /dev/null and b/NAPS2.Lib/Icons/scanner-default-rev2.png differ diff --git a/NAPS2.Lib/Icons/scanner-lock-default-hires.png b/NAPS2.Lib/Icons/scanner-lock-default-hires.png new file mode 100644 index 0000000000..7a31c38bba Binary files /dev/null and b/NAPS2.Lib/Icons/scanner-lock-default-hires.png differ diff --git a/NAPS2.Lib/Icons/scanner-lock-default.png b/NAPS2.Lib/Icons/scanner-lock-default-rev1.png similarity index 100% rename from NAPS2.Lib/Icons/scanner-lock-default.png rename to NAPS2.Lib/Icons/scanner-lock-default-rev1.png diff --git a/NAPS2.Lib/Icons/scanner-lock-default-rev2.png b/NAPS2.Lib/Icons/scanner-lock-default-rev2.png new file mode 100644 index 0000000000..5031524300 Binary files /dev/null and b/NAPS2.Lib/Icons/scanner-lock-default-rev2.png differ diff --git a/NAPS2.Lib/Icons/scanner-lock-hires.png b/NAPS2.Lib/Icons/scanner-lock-hires.png new file mode 100644 index 0000000000..499bad5dbc Binary files /dev/null and b/NAPS2.Lib/Icons/scanner-lock-hires.png differ diff --git a/NAPS2.Lib/Icons/scanner-lock.png b/NAPS2.Lib/Icons/scanner-lock-rev1.png similarity index 100% rename from NAPS2.Lib/Icons/scanner-lock.png rename to NAPS2.Lib/Icons/scanner-lock-rev1.png diff --git a/NAPS2.Lib/Icons/scanner-lock-rev2.png b/NAPS2.Lib/Icons/scanner-lock-rev2.png new file mode 100644 index 0000000000..e86a6f791b Binary files /dev/null and b/NAPS2.Lib/Icons/scanner-lock-rev2.png differ diff --git a/NAPS2.Lib/Icons/scanner-wireless-hires.png b/NAPS2.Lib/Icons/scanner-wireless-hires.png new file mode 100644 index 0000000000..4babc3c981 Binary files /dev/null and b/NAPS2.Lib/Icons/scanner-wireless-hires.png differ diff --git a/NAPS2.Lib/Icons/scanner-wireless-rev2.png b/NAPS2.Lib/Icons/scanner-wireless-rev2.png new file mode 100644 index 0000000000..e2c1f6bdb6 Binary files /dev/null and b/NAPS2.Lib/Icons/scanner-wireless-rev2.png differ diff --git a/NAPS2.Lib/Icons/shape_align_bottom-small.png b/NAPS2.Lib/Icons/shape_align_bottom-small.png new file mode 100644 index 0000000000..93db1764ff Binary files /dev/null and b/NAPS2.Lib/Icons/shape_align_bottom-small.png differ diff --git a/NAPS2.Lib/Icons/shape_align_bottom.png b/NAPS2.Lib/Icons/shape_align_bottom.png new file mode 100644 index 0000000000..640767ed4b Binary files /dev/null and b/NAPS2.Lib/Icons/shape_align_bottom.png differ diff --git a/NAPS2.Lib/Icons/shape_align_center-small.png b/NAPS2.Lib/Icons/shape_align_center-small.png new file mode 100644 index 0000000000..f318a044f3 Binary files /dev/null and b/NAPS2.Lib/Icons/shape_align_center-small.png differ diff --git a/NAPS2.Lib/Icons/shape_align_center.png b/NAPS2.Lib/Icons/shape_align_center.png new file mode 100644 index 0000000000..f5663d1452 Binary files /dev/null and b/NAPS2.Lib/Icons/shape_align_center.png differ diff --git a/NAPS2.Lib/Icons/shape_align_left-small.png b/NAPS2.Lib/Icons/shape_align_left-small.png new file mode 100644 index 0000000000..82f04de37c Binary files /dev/null and b/NAPS2.Lib/Icons/shape_align_left-small.png differ diff --git a/NAPS2.Lib/Icons/shape_align_left.png b/NAPS2.Lib/Icons/shape_align_left.png new file mode 100644 index 0000000000..fbdc08174d Binary files /dev/null and b/NAPS2.Lib/Icons/shape_align_left.png differ diff --git a/NAPS2.Lib/Icons/shape_align_middle-small.png b/NAPS2.Lib/Icons/shape_align_middle-small.png new file mode 100644 index 0000000000..68e5cd5d88 Binary files /dev/null and b/NAPS2.Lib/Icons/shape_align_middle-small.png differ diff --git a/NAPS2.Lib/Icons/shape_align_middle.png b/NAPS2.Lib/Icons/shape_align_middle.png new file mode 100644 index 0000000000..b1efb14eed Binary files /dev/null and b/NAPS2.Lib/Icons/shape_align_middle.png differ diff --git a/NAPS2.Lib/Icons/shape_align_right-small.png b/NAPS2.Lib/Icons/shape_align_right-small.png new file mode 100644 index 0000000000..266f6d2bfe Binary files /dev/null and b/NAPS2.Lib/Icons/shape_align_right-small.png differ diff --git a/NAPS2.Lib/Icons/shape_align_right.png b/NAPS2.Lib/Icons/shape_align_right.png new file mode 100644 index 0000000000..64bee5b042 Binary files /dev/null and b/NAPS2.Lib/Icons/shape_align_right.png differ diff --git a/NAPS2.Lib/Icons/shape_align_top-small.png b/NAPS2.Lib/Icons/shape_align_top-small.png new file mode 100644 index 0000000000..de50d2383d Binary files /dev/null and b/NAPS2.Lib/Icons/shape_align_top-small.png differ diff --git a/NAPS2.Lib/Icons/shape_align_top.png b/NAPS2.Lib/Icons/shape_align_top.png new file mode 100644 index 0000000000..00545c7237 Binary files /dev/null and b/NAPS2.Lib/Icons/shape_align_top.png differ diff --git a/NAPS2.Lib/Icons/sharpen-small.png b/NAPS2.Lib/Icons/sharpen-small.png new file mode 100644 index 0000000000..a459af6b66 Binary files /dev/null and b/NAPS2.Lib/Icons/sharpen-small.png differ diff --git a/NAPS2.Lib/Icons/sharpen.ico b/NAPS2.Lib/Icons/sharpen.ico deleted file mode 100644 index 7e42001473..0000000000 Binary files a/NAPS2.Lib/Icons/sharpen.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/sharpen.png b/NAPS2.Lib/Icons/sharpen.png index a459af6b66..1c2082be28 100644 Binary files a/NAPS2.Lib/Icons/sharpen.png and b/NAPS2.Lib/Icons/sharpen.png differ diff --git a/NAPS2.Lib/Icons/split-small.png b/NAPS2.Lib/Icons/split-small.png new file mode 100644 index 0000000000..7645ba6731 Binary files /dev/null and b/NAPS2.Lib/Icons/split-small.png differ diff --git a/NAPS2.Lib/Icons/split.png b/NAPS2.Lib/Icons/split.png new file mode 100644 index 0000000000..9cff786d9e Binary files /dev/null and b/NAPS2.Lib/Icons/split.png differ diff --git a/NAPS2.Lib/Icons/split_hor-small.png b/NAPS2.Lib/Icons/split_hor-small.png new file mode 100644 index 0000000000..39f68b034b Binary files /dev/null and b/NAPS2.Lib/Icons/split_hor-small.png differ diff --git a/NAPS2.Lib/Icons/split_hor.png b/NAPS2.Lib/Icons/split_hor.png new file mode 100644 index 0000000000..d8a2e33c04 Binary files /dev/null and b/NAPS2.Lib/Icons/split_hor.png differ diff --git a/NAPS2.Lib/Icons/switch-small.png b/NAPS2.Lib/Icons/switch-small.png new file mode 100644 index 0000000000..e251370e9d Binary files /dev/null and b/NAPS2.Lib/Icons/switch-small.png differ diff --git a/NAPS2.Lib/Icons/switch.png b/NAPS2.Lib/Icons/switch.png new file mode 100644 index 0000000000..13b2c454a8 Binary files /dev/null and b/NAPS2.Lib/Icons/switch.png differ diff --git a/NAPS2.Lib/Icons/switch_hor-small.png b/NAPS2.Lib/Icons/switch_hor-small.png new file mode 100644 index 0000000000..d0acb375fc Binary files /dev/null and b/NAPS2.Lib/Icons/switch_hor-small.png differ diff --git a/NAPS2.Lib/Icons/switch_hor.png b/NAPS2.Lib/Icons/switch_hor.png new file mode 100644 index 0000000000..35628af94e Binary files /dev/null and b/NAPS2.Lib/Icons/switch_hor.png differ diff --git a/NAPS2.Lib/Icons/text-hires.png b/NAPS2.Lib/Icons/text-hires.png new file mode 100644 index 0000000000..a2b024859b Binary files /dev/null and b/NAPS2.Lib/Icons/text-hires.png differ diff --git a/NAPS2.Lib/Icons/text_align_justify-small.png b/NAPS2.Lib/Icons/text_align_justify-small.png new file mode 100644 index 0000000000..cd091db3da Binary files /dev/null and b/NAPS2.Lib/Icons/text_align_justify-small.png differ diff --git a/NAPS2.Lib/Icons/text_align_justify.png b/NAPS2.Lib/Icons/text_align_justify.png new file mode 100644 index 0000000000..7db7b98cb2 Binary files /dev/null and b/NAPS2.Lib/Icons/text_align_justify.png differ diff --git a/NAPS2.Lib/Icons/thunderbird-hires.png b/NAPS2.Lib/Icons/thunderbird-hires.png new file mode 100644 index 0000000000..8c0ffdb82d Binary files /dev/null and b/NAPS2.Lib/Icons/thunderbird-hires.png differ diff --git a/NAPS2.Lib/Icons/thunderbird.png b/NAPS2.Lib/Icons/thunderbird.png new file mode 100755 index 0000000000..fe0a052e90 Binary files /dev/null and b/NAPS2.Lib/Icons/thunderbird.png differ diff --git a/NAPS2.Lib/Icons/transform_crop-small.png b/NAPS2.Lib/Icons/transform_crop-small.png new file mode 100644 index 0000000000..746b1b4542 Binary files /dev/null and b/NAPS2.Lib/Icons/transform_crop-small.png differ diff --git a/NAPS2.Lib/Icons/transform_crop.ico b/NAPS2.Lib/Icons/transform_crop.ico deleted file mode 100644 index f99c145850..0000000000 Binary files a/NAPS2.Lib/Icons/transform_crop.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/transform_crop.png b/NAPS2.Lib/Icons/transform_crop.png index 746b1b4542..2763117c94 100644 Binary files a/NAPS2.Lib/Icons/transform_crop.png and b/NAPS2.Lib/Icons/transform_crop.png differ diff --git a/NAPS2.Lib/Icons/transform_flip.png b/NAPS2.Lib/Icons/transform_flip.png deleted file mode 100644 index a1b2cf55d4..0000000000 Binary files a/NAPS2.Lib/Icons/transform_flip.png and /dev/null differ diff --git a/NAPS2.Lib/Icons/undo-small.png b/NAPS2.Lib/Icons/undo-small.png new file mode 100644 index 0000000000..0c196ec096 Binary files /dev/null and b/NAPS2.Lib/Icons/undo-small.png differ diff --git a/NAPS2.Lib/Icons/undo.png b/NAPS2.Lib/Icons/undo.png new file mode 100644 index 0000000000..8d7e6f775e Binary files /dev/null and b/NAPS2.Lib/Icons/undo.png differ diff --git a/NAPS2.Lib/Icons/weather_sun-small.png b/NAPS2.Lib/Icons/weather_sun-small.png new file mode 100644 index 0000000000..4ca42e888c Binary files /dev/null and b/NAPS2.Lib/Icons/weather_sun-small.png differ diff --git a/NAPS2.Lib/Icons/weather_sun.ico b/NAPS2.Lib/Icons/weather_sun.ico deleted file mode 100644 index 0f983734da..0000000000 Binary files a/NAPS2.Lib/Icons/weather_sun.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/weather_sun.png b/NAPS2.Lib/Icons/weather_sun.png index 4ca42e888c..d9f87a5120 100644 Binary files a/NAPS2.Lib/Icons/weather_sun.png and b/NAPS2.Lib/Icons/weather_sun.png differ diff --git a/NAPS2.Lib/Icons/wireless16.png b/NAPS2.Lib/Icons/wireless-small.png similarity index 100% rename from NAPS2.Lib/Icons/wireless16.png rename to NAPS2.Lib/Icons/wireless-small.png diff --git a/NAPS2.Lib/Icons/wireless.png b/NAPS2.Lib/Icons/wireless.png new file mode 100644 index 0000000000..5b7e202e49 Binary files /dev/null and b/NAPS2.Lib/Icons/wireless.png differ diff --git a/NAPS2.Lib/Icons/wireless16.ico b/NAPS2.Lib/Icons/wireless16.ico deleted file mode 100644 index cbb9719f27..0000000000 Binary files a/NAPS2.Lib/Icons/wireless16.ico and /dev/null differ diff --git a/NAPS2.Lib/Icons/wireless16_filled.png b/NAPS2.Lib/Icons/wireless16_filled.png new file mode 100644 index 0000000000..579441fe71 Binary files /dev/null and b/NAPS2.Lib/Icons/wireless16_filled.png differ diff --git a/NAPS2.Lib/Icons/wireless_filled.png b/NAPS2.Lib/Icons/wireless_filled.png new file mode 100644 index 0000000000..99ae9940a0 Binary files /dev/null and b/NAPS2.Lib/Icons/wireless_filled.png differ diff --git a/NAPS2.Lib/Icons/world-hires.png b/NAPS2.Lib/Icons/world-hires.png new file mode 100644 index 0000000000..74bc7044e4 Binary files /dev/null and b/NAPS2.Lib/Icons/world-hires.png differ diff --git a/NAPS2.Lib/Icons/zoom_actual-small.png b/NAPS2.Lib/Icons/zoom_actual-small.png new file mode 100644 index 0000000000..f1c202c54e Binary files /dev/null and b/NAPS2.Lib/Icons/zoom_actual-small.png differ diff --git a/NAPS2.Lib/Icons/zoom_actual.png b/NAPS2.Lib/Icons/zoom_actual.png index f1c202c54e..bf2817347a 100644 Binary files a/NAPS2.Lib/Icons/zoom_actual.png and b/NAPS2.Lib/Icons/zoom_actual.png differ diff --git a/NAPS2.Lib/Icons/zoom_in-small.png b/NAPS2.Lib/Icons/zoom_in-small.png new file mode 100644 index 0000000000..7170f49b3e Binary files /dev/null and b/NAPS2.Lib/Icons/zoom_in-small.png differ diff --git a/NAPS2.Lib/Icons/zoom_in.png b/NAPS2.Lib/Icons/zoom_in.png index 7170f49b3e..922c8ef9c6 100644 Binary files a/NAPS2.Lib/Icons/zoom_in.png and b/NAPS2.Lib/Icons/zoom_in.png differ diff --git a/NAPS2.Lib/Icons/zoom_out-small.png b/NAPS2.Lib/Icons/zoom_out-small.png new file mode 100644 index 0000000000..d820ca6fc8 Binary files /dev/null and b/NAPS2.Lib/Icons/zoom_out-small.png differ diff --git a/NAPS2.Lib/Icons/zoom_out.png b/NAPS2.Lib/Icons/zoom_out.png index d820ca6fc8..afca302961 100644 Binary files a/NAPS2.Lib/Icons/zoom_out.png and b/NAPS2.Lib/Icons/zoom_out.png differ diff --git a/NAPS2.Lib/Images/DeskewOperation.cs b/NAPS2.Lib/Images/DeskewOperation.cs index 4d4be5131a..d4f7c0c5b0 100644 --- a/NAPS2.Lib/Images/DeskewOperation.cs +++ b/NAPS2.Lib/Images/DeskewOperation.cs @@ -8,7 +8,7 @@ public DeskewOperation() AllowBackground = true; } - public bool Start(ICollection images, DeskewParams deskewParams) + public bool Start(UiImageList imageList, List images, DeskewParams deskewParams) { ProgressTitle = MiscResources.AutoDeskewProgress; Status = new OperationStatus @@ -19,7 +19,9 @@ public bool Start(ICollection images, DeskewParams deskewParams) RunAsync(async () => { - return await Pipeline.For(images, CancelToken).RunParallel(img => + var beforeTransforms = new List(); + var afterTransforms = new List(); + var result = await Pipeline.For(images, CancelToken).StepParallel(img => { using var processedImage = img.GetClonedImage(); var image = processedImage.Render(); @@ -32,18 +34,27 @@ public bool Start(ICollection images, DeskewParams deskewParams) var thumbnail = deskewParams.ThumbnailSize.HasValue ? image.PerformTransform(new ThumbnailTransform(deskewParams.ThumbnailSize.Value)) : null; + var before = img.TransformState; img.AddTransform(transform, thumbnail); + var after = img.TransformState; lock (this) { Status.CurrentProgress += 1; } InvokeStatusChanged(); + return (before, after); } finally { image.Dispose(); } + }).Run(transformState => + { + beforeTransforms.Add(transformState.before); + afterTransforms.Add(transformState.after); }); + imageList.PushUndoElement(new TransformImagesUndoElement(images, beforeTransforms, afterTransforms)); + return result; }); return true; diff --git a/NAPS2.Lib/Images/ExternalEditorSession.cs b/NAPS2.Lib/Images/ExternalEditorSession.cs new file mode 100644 index 0000000000..a38ce64a57 --- /dev/null +++ b/NAPS2.Lib/Images/ExternalEditorSession.cs @@ -0,0 +1,109 @@ +using Microsoft.Extensions.Logging; +using NAPS2.Scan; + +namespace NAPS2.Images; + +public class ExternalEditorSession : IDisposable +{ + private readonly ScanningContext _scanningContext; + private readonly UiImageList _imageList; + private readonly UiImage _uiImage; + private readonly FileSystemWatcher _watcher; + private readonly TimedThrottle _throttle; + + public ExternalEditorSession(ScanningContext scanningContext, UiImageList imageList, UiImage uiImage) + { + _scanningContext = scanningContext; + _imageList = imageList; + _uiImage = uiImage; + TempFilePath = CreateUserEditableFile(); + _watcher = CreateWatcher(); + _throttle = new TimedThrottle(ExternalImageEdited, TimeSpan.FromSeconds(1)); + } + + public string TempFilePath { get; } + + private string CreateUserEditableFile() + { + string dir = Path.Combine(Paths.Temp, Path.GetRandomFileName()); + Directory.CreateDirectory(dir); + string path = Path.Combine(dir, $"page{_imageList.Images.IndexOf(_uiImage) + 1}"); + using var processedImage = _uiImage.GetClonedImage(); + using var renderedImage = processedImage.Render(); + return ImageExportHelper.SaveSmallestFormat(path, renderedImage, false, -1, out _); + } + + private FileSystemWatcher CreateWatcher() + { + var watcher = new FileSystemWatcher(Path.GetDirectoryName(TempFilePath)!, Path.GetFileName(TempFilePath)) + { + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, + EnableRaisingEvents = true, + }; + // Editors might either write the file directly, or they might create a temporary file which gets renamed for + // an atomic write. Either way we need to be able to detect the action. + watcher.Created += (_, _) => _throttle.RunAction(null); + watcher.Changed += (_, _) => _throttle.RunAction(null); + watcher.Renamed += (_, e) => + { + if (Path.GetFileName(e.FullPath) == Path.GetFileName(TempFilePath)) + { + _throttle.RunAction(null); + } + }; + return watcher; + } + + private void ExternalImageEdited() + { + IMemoryImage newMemoryImage; + var fallback = new ExpFallback(50, 1000); + while (true) + { + try + { + newMemoryImage = _scanningContext.ImageContext.Load(TempFilePath); + break; + } + catch (Exception ex) when (ex is not FileNotFoundException) + { + if (fallback.IsAtMax) + { + _scanningContext.Logger.LogError(ex, "Error loading externally-edited image"); + return; + } + fallback.Increase(); + } + } + using (newMemoryImage) + { + var newImage = _scanningContext.CreateProcessedImage(newMemoryImage); + lock (_uiImage) + { + if (!_uiImage.IsDisposed) + { + _uiImage.ReplaceInternalImage(newImage); + _imageList.ClearUndoStack(); + } + else + { + newImage.Dispose(); + } + } + } + } + + public void Dispose() + { + _watcher.Dispose(); + try + { + File.Delete(TempFilePath); + Directory.Delete(Path.GetDirectoryName(TempFilePath)!, true); + } + catch (IOException) + { + // Best effort cleanup; the temp folder will get cleared the next time we start NAPS2 anyway + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Images/IUndoElement.cs b/NAPS2.Lib/Images/IUndoElement.cs new file mode 100644 index 0000000000..8642484a7f --- /dev/null +++ b/NAPS2.Lib/Images/IUndoElement.cs @@ -0,0 +1,7 @@ +namespace NAPS2.Images; + +public interface IUndoElement +{ + void ApplyUndo(); + void ApplyRedo(); +} \ No newline at end of file diff --git a/NAPS2.Lib/Images/ImageListDiffer.cs b/NAPS2.Lib/Images/ImageListDiffer.cs index 6dd5617837..f4dbaa3226 100644 --- a/NAPS2.Lib/Images/ImageListDiffer.cs +++ b/NAPS2.Lib/Images/ImageListDiffer.cs @@ -12,7 +12,7 @@ namespace NAPS2.Images; public class ImageListDiffer { private readonly UiImageList _imageList; - private List _currentState = new(); + private List _currentState = []; public ImageListDiffer(UiImageList imageList) { diff --git a/NAPS2.Sdk/Images/ImageListEventArgs.cs b/NAPS2.Lib/Images/ImageListEventArgs.cs similarity index 100% rename from NAPS2.Sdk/Images/ImageListEventArgs.cs rename to NAPS2.Lib/Images/ImageListEventArgs.cs diff --git a/NAPS2.Lib/Images/ImageListMutation.cs b/NAPS2.Lib/Images/ImageListMutation.cs index c7c1836ea4..f03b408671 100644 --- a/NAPS2.Lib/Images/ImageListMutation.cs +++ b/NAPS2.Lib/Images/ImageListMutation.cs @@ -2,20 +2,13 @@ namespace NAPS2.Images; public abstract class ImageListMutation : ListMutation { - public class RotateFlip : ImageListMutation + public class RotateFlip(double angle) : ImageListMutation { - private readonly double _angle; - - public RotateFlip(double angle) - { - _angle = angle; - } - public override void Apply(List list, ref ListSelection selection) { foreach (UiImage img in selection) { - var transform = new RotationTransform(_angle); + var transform = new RotationTransform(angle); var thumb = img.GetThumbnailClone(); var updatedThumb = thumb?.PerformTransform(transform); img.AddTransform(transform, updatedThumb); @@ -23,6 +16,21 @@ public override void Apply(List list, ref ListSelection select } } + public class AddTransforms(List transforms, Dictionary? updatedThumbnails = null) + : ListMutation + { + public override void Apply(List list, ref ListSelection selection) + { + if (transforms.Any(x => !x.IsNull)) + { + foreach (UiImage img in selection) + { + img.AddTransforms(transforms, updatedThumbnails?.Get(img)); + } + } + } + } + public class ResetTransforms : ListMutation { public override void Apply(List list, ref ListSelection selection) diff --git a/NAPS2.Lib/Images/ImageListSyncer.cs b/NAPS2.Lib/Images/ImageListSyncer.cs index bf4aa9b43f..e44c3c1693 100644 --- a/NAPS2.Lib/Images/ImageListSyncer.cs +++ b/NAPS2.Lib/Images/ImageListSyncer.cs @@ -10,7 +10,7 @@ namespace NAPS2.Images; /// public class ImageListSyncer { - private static readonly TimeSpan SyncThrottleInterval = TimeSpan.FromMilliseconds(100); + private static readonly TimeSpan SyncThrottleInterval = TimeSpan.FromMilliseconds(200); private readonly UiImageList _imageList; private readonly Action> _diffCallback; diff --git a/NAPS2.Lib/Images/ReplaceRangeUndoElement.cs b/NAPS2.Lib/Images/ReplaceRangeUndoElement.cs new file mode 100644 index 0000000000..191c228f8f --- /dev/null +++ b/NAPS2.Lib/Images/ReplaceRangeUndoElement.cs @@ -0,0 +1,43 @@ +namespace NAPS2.Images; + +internal class ReplaceRangeUndoElement( + UiImageList imageList, + List beforeRange, + List afterRange) + : IUndoElement +{ + public void ApplyUndo() => imageList.Mutate( + new ListMutation.ReplaceRange(afterRange, beforeRange), + updateUndoStack: false); + + public void ApplyRedo() => imageList.Mutate( + new ListMutation.ReplaceRange(beforeRange, afterRange), + updateUndoStack: false); + + public static ReplaceRangeUndoElement? FromFullList( + UiImageList imageList, List before, List after) + { + if (!new HashSet(before).SetEquals(after)) + { + // Can't undo/redo additions or deletions + return null; + } + int firstDelta = -1; + int lastDelta = -1; + for (int i = 0; i < before.Count; i++) + { + if (before[i] != after[i]) + { + if (firstDelta == -1) firstDelta = i; + lastDelta = i; + } + } + if (firstDelta != -1) + { + var beforeRange = before.Skip(firstDelta).Take(lastDelta - firstDelta + 1).ToList(); + var afterRange = after.Skip(firstDelta).Take(lastDelta - firstDelta + 1).ToList(); + return new ReplaceRangeUndoElement(imageList, beforeRange, afterRange); + } + return null; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Images/SplitUndoElement.cs b/NAPS2.Lib/Images/SplitUndoElement.cs new file mode 100644 index 0000000000..933227de8c --- /dev/null +++ b/NAPS2.Lib/Images/SplitUndoElement.cs @@ -0,0 +1,46 @@ +namespace NAPS2.Images; + +public class SplitUndoElement( + UiImageList imageList, + UiImage image1, + UiImage image2, + TransformState oldTransforms, + CropTransform transform1, + CropTransform transform2) + : IUndoElement +{ + public void ApplyUndo() + { + if (imageList.Images.Contains(image1) && imageList.Images.Contains(image2) && + image1.TransformState == oldTransforms.AddOrSimplify(transform1) && + image2.TransformState == oldTransforms.AddOrSimplify(transform2)) + { + image1.ReplaceTransformState(image1.TransformState, oldTransforms); + image2.ReplaceTransformState(image2.TransformState, oldTransforms); + if (imageList.Selection.Contains(image1)) + { + imageList.AddToSelection(image2); + } + imageList.Mutate(new ListMutation.DeleteSelected(), ListSelection.Of(image1), + updateUndoStack: false, disposeDeleted: false); + image1.GetImageWeakReference().ProcessedImage.Dispose(); + } + } + + public void ApplyRedo() + { + if (imageList.Images.Contains(image2) && !imageList.Images.Contains(image1) && + image2.TransformState == oldTransforms && !image1.IsDisposed) + { + image1.ReplaceInternalImage(image2.GetClonedImage()); + image1.AddTransform(transform1); + image2.AddTransform(transform2); + imageList.Mutate(new ListMutation.InsertBefore(image1, image2), ListSelection.Empty(), + updateUndoStack: false); + if (imageList.Selection.Contains(image2)) + { + imageList.AddToSelection(image1); + } + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Images/ThumbnailController.cs b/NAPS2.Lib/Images/ThumbnailController.cs index ac7cea6c20..9191467a02 100644 --- a/NAPS2.Lib/Images/ThumbnailController.cs +++ b/NAPS2.Lib/Images/ThumbnailController.cs @@ -1,4 +1,5 @@ -using NAPS2.EtoForms; +using Eto.Drawing; +using NAPS2.EtoForms.Widgets; namespace NAPS2.Images; @@ -50,7 +51,7 @@ public int VisibleSize { var thumbnailSize = ThumbnailSizes.Validate(value); _config.User.Set(c => c.ThumbnailSize, thumbnailSize); - if (ListView?.ImageSize == thumbnailSize) + if (ListView?.ImageSize.Width == thumbnailSize) { // Same size so no resizing needed return; @@ -75,7 +76,7 @@ public void Reload() if (ListView != null) { // Adjust the visible thumbnail display with the new size - ListView.ImageSize = VisibleSize; + ListView.ImageSize = new Size(VisibleSize, VisibleSize); ListView.RegenerateImages(); } diff --git a/NAPS2.Lib/Images/ThumbnailRenderQueue.cs b/NAPS2.Lib/Images/ThumbnailRenderQueue.cs index 366a1bf469..04d36b1d04 100644 --- a/NAPS2.Lib/Images/ThumbnailRenderQueue.cs +++ b/NAPS2.Lib/Images/ThumbnailRenderQueue.cs @@ -75,7 +75,7 @@ private void RenderThumbnails() // TODO: Make this run as async? // TODO: Verify WorkerFactory is not null? Or handle this better for tests? bool useWorker = PlatformCompat.System.RenderInWorker; - var worker = useWorker ? _scanningContext.WorkerFactory?.Create() : null; + var worker = useWorker ? _scanningContext.CreateWorker(WorkerType.Native) : null; var fallback = new ExpFallback(100, 60 * 1000); while (true) { @@ -99,7 +99,7 @@ private void RenderThumbnails() { var thumb = worker != null ? RenderThumbnailWithWorker(worker, imageToRender, thumbnailSize) - : _thumbnailRenderer.Render(imageToRender, thumbnailSize); + : _thumbnailRenderer.Render(imageToRender, thumbnailSize).Result; if (!ThumbnailStillNeedsRendering(next, thumbnailSize)) { @@ -118,7 +118,7 @@ private void RenderThumbnails() if (worker != null) { worker.Dispose(); - worker = _scanningContext.WorkerFactory?.Create(); + worker = _scanningContext.CreateWorker(WorkerType.Native); } Thread.Sleep(fallback.Value); fallback.Increase(); @@ -127,6 +127,7 @@ private void RenderThumbnails() _renderThumbnailsCompleteHandle.Set(); _renderThumbnailsWaitHandle.WaitOne(); } + worker?.Dispose(); } private IMemoryImage RenderThumbnailWithWorker(WorkerContext worker, ProcessedImage imageToRender, diff --git a/NAPS2.Sdk/Images/ThumbnailSizes.cs b/NAPS2.Lib/Images/ThumbnailSizes.cs similarity index 97% rename from NAPS2.Sdk/Images/ThumbnailSizes.cs rename to NAPS2.Lib/Images/ThumbnailSizes.cs index b53295a1ac..e20a51cd84 100644 --- a/NAPS2.Sdk/Images/ThumbnailSizes.cs +++ b/NAPS2.Lib/Images/ThumbnailSizes.cs @@ -3,7 +3,7 @@ namespace NAPS2.Images; public static class ThumbnailSizes { public const int MIN_SIZE = 64; - public const int DEFAULT_SIZE = 128; + public const int DEFAULT_SIZE = 256; public static int MAX_SIZE = 1024; public static int Validate(int inputSize) diff --git a/NAPS2.Lib/Images/TransformImagesUndoElement.cs b/NAPS2.Lib/Images/TransformImagesUndoElement.cs new file mode 100644 index 0000000000..59a512d807 --- /dev/null +++ b/NAPS2.Lib/Images/TransformImagesUndoElement.cs @@ -0,0 +1,50 @@ +namespace NAPS2.Images; + +internal class TransformImagesUndoElement( + List images, + List beforeTransforms, + List afterTransforms) + : IUndoElement +{ + public void ApplyUndo() => ReplaceTransforms(afterTransforms, beforeTransforms); + + public void ApplyRedo() => ReplaceTransforms(beforeTransforms, afterTransforms); + + private void ReplaceTransforms(List toReplace, List replaceWith) + { + for (int i = 0; i < images.Count; i++) + { + if (!images[i].IsDisposed) + { + images[i].ReplaceTransformState(toReplace[i], replaceWith[i]); + } + } + } + + public static TransformImagesUndoElement? FromFullList( + List before, List allBeforeTransforms, + List after, List allAfterTransforms) + { + if (!before.SequenceEqual(after)) + { + return null; + } + var images = new List(); + var beforeTransforms = new List(); + var afterTransforms = new List(); + for (int i = 0; i < before.Count; i++) + { + if (allBeforeTransforms[i] != allAfterTransforms[i]) + { + images.Add(before[i]); + beforeTransforms.Add(allBeforeTransforms[i]); + afterTransforms.Add(allAfterTransforms[i]); + } + } + if (images.Count > 0) + { + return new TransformImagesUndoElement(images, beforeTransforms, afterTransforms); + } + return null; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Images/UiImage.cs b/NAPS2.Lib/Images/UiImage.cs index 858910ad5b..9bea6b6f1a 100644 --- a/NAPS2.Lib/Images/UiImage.cs +++ b/NAPS2.Lib/Images/UiImage.cs @@ -9,13 +9,13 @@ public class UiImage : IDisposable private ProcessedImage _processedImage; private IMemoryImage? _thumbnail; private TransformState? _thumbnailTransformState; - private bool _disposed; - - public UiImage(ProcessedImage image) + private bool _saved; + + public UiImage(ProcessedImage image, bool useThumbnail = true) { _processedImage = image; var ppd = _processedImage.PostProcessingData; - if (ppd.Thumbnail != null && ppd.ThumbnailTransformState != null) + if (useThumbnail && ppd.Thumbnail != null && ppd.ThumbnailTransformState != null) { _thumbnail = _processedImage.PostProcessingData.Thumbnail; _thumbnailTransformState = _processedImage.PostProcessingData.ThumbnailTransformState; @@ -45,32 +45,41 @@ public ProcessedImage.WeakReference GetImageWeakReference() return _processedImage.GetWeakReference(); } } - + + public void MarkSaved(ProcessedImage state) + { + lock (this) + { + if (Equals(state, _processedImage)) + { + _saved = true; + } + } + } + public void Dispose() { lock (this) { - if (_disposed) + if (IsDisposed) { return; } - _disposed = true; + IsDisposed = true; _processedImage.Dispose(); - + EditorSessions.DisposeAll(); + if (_thumbnail != null) { _thumbnail.Dispose(); _thumbnail = null; } - - // TODO: This shouldn't be here, OCR cancellation needs to be figured out - FullyDisposed?.Invoke(this, EventArgs.Empty); } } public void AddTransform(Transform transform, IMemoryImage? prerenderedThumbnail = null) { - AddTransforms(new[] { transform }, prerenderedThumbnail); + AddTransforms([transform], prerenderedThumbnail); } public void AddTransforms(IEnumerable transforms, IMemoryImage? prerenderedThumbnail = null) @@ -93,6 +102,7 @@ public void AddTransforms(IEnumerable transforms, IMemoryImage? prere _thumbnail = prerenderedThumbnail; _thumbnailTransformState = _processedImage.TransformState; } + _saved = false; } ThumbnailInvalidated?.Invoke(this, EventArgs.Empty); if (prerenderedThumbnail != null) @@ -101,17 +111,42 @@ public void AddTransforms(IEnumerable transforms, IMemoryImage? prere } } - public void ResetTransforms() + public void ReplaceTransformState(TransformState oldTransformState, TransformState newTransformState) { lock (this) { - if (_processedImage.TransformState.IsEmpty) + if (TransformState != oldTransformState) { return; } - var newImage = _processedImage.WithNoTransforms(); + _processedImage = _processedImage.WithTransformState(newTransformState, true); + _saved = false; + } + ThumbnailInvalidated?.Invoke(this, EventArgs.Empty); + } + + public void ReplaceInternalImage(ProcessedImage newImage) + { + lock (this) + { _processedImage.Dispose(); _processedImage = newImage; + _thumbnailTransformState = null; + _saved = false; + } + ThumbnailInvalidated?.Invoke(this, EventArgs.Empty); + } + + public void ResetTransforms() + { + lock (this) + { + if (_processedImage.TransformState.IsEmpty) + { + return; + } + _processedImage = _processedImage.WithNoTransforms(true); + _saved = false; } ThumbnailInvalidated?.Invoke(this, EventArgs.Empty); } @@ -151,15 +186,20 @@ public void SetThumbnail(IMemoryImage image, TransformState transformState) ThumbnailChanged?.Invoke(this, EventArgs.Empty); } + public bool IsDisposed { get; private set; } + public bool IsThumbnailDirty => _thumbnailTransformState != _processedImage.TransformState; + public bool HasUnsavedChanges => !_saved; + + public TransformState TransformState => _processedImage.TransformState; + + public List EditorSessions { get; } = new(); + public EventHandler? ThumbnailChanged; public EventHandler? ThumbnailInvalidated; - // TODO: Maybe delete depending on how we handle ocr cancellation - public EventHandler? FullyDisposed; - public ImageRenderState GetImageRenderState() { lock (this) @@ -167,4 +207,4 @@ public ImageRenderState GetImageRenderState() return new ImageRenderState(_processedImage.GetWeakReference(), _thumbnailTransformState, _thumbnail, this); } } -} +} \ No newline at end of file diff --git a/NAPS2.Lib/Images/UiImageList.cs b/NAPS2.Lib/Images/UiImageList.cs index d26ef1625d..abe94962c3 100644 --- a/NAPS2.Lib/Images/UiImageList.cs +++ b/NAPS2.Lib/Images/UiImageList.cs @@ -1,18 +1,23 @@ using System.Collections.Immutable; -using System.Threading; namespace NAPS2.Images; public class UiImageList { - private StateToken _savedState = new(ImmutableList.Empty); + private const int MAX_UNDO_LENGTH = 20; + + public static UiImageList FromImages(List images) => new(images); + + private readonly UndoStack _undoStack = new(MAX_UNDO_LENGTH); + private ListSelection _selection; + private StateToken _savedState = new(ImmutableList.Empty); - public UiImageList() : this(new List()) + public UiImageList() : this([]) { } - public UiImageList(List images) + private UiImageList(List images) { Images = images.ToImmutableList(); _selection = ListSelection.Empty(); @@ -22,19 +27,24 @@ public UiImageList(List images) public StateToken CurrentState => new(Images.Select(x => x.GetImageWeakReference()).ToImmutableList()); - public StateToken SavedState - { - get => _savedState; - set => _savedState = value ?? throw new ArgumentNullException(nameof(value)); - } - - // TODO: We should make this selection maintain insertion order, or otherwise guarantee that for things like FDesktop.SavePDF we actually get the images in the right order public ListSelection Selection { get => _selection; private set => _selection = value ?? throw new ArgumentNullException(nameof(value)); } + // The logic around HasUnsavedChanges has to consider a few things: + // 1. Has the state of the world changed since the last save operation? For example, images have been moved around. + // 2. Are there any individual images that have not been saved? For example, the user might have selected and saved + // only a few images. + public bool HasUnsavedChanges => _savedState != CurrentState || Images.Any(x => x.HasUnsavedChanges); + + public bool CanUndo => _undoStack.CanUndo; + + public bool CanRedo => _undoStack.CanRedo; + + public void ClearUndoStack() => _undoStack.ClearBoth(); + public event EventHandler? SelectionChanged; public event EventHandler? ImagesUpdated; @@ -43,31 +53,68 @@ public ListSelection Selection public event EventHandler? ImagesThumbnailInvalidated; + public void AddToSelection(UiImage image) + { + UpdateSelection(ListSelection.From(Images.Where(x => x == image || Selection.Contains(x)))); + } + public void UpdateSelection(ListSelection newSelection) { Selection = newSelection; SelectionChanged?.Invoke(this, EventArgs.Empty); } + public void MarkSaved(StateToken priorState, IEnumerable savedImages) + { + var savedImagesSet = savedImages.ToHashSet(); + lock (this) + { + _savedState = priorState; + foreach (var image in Images) + { + var imageState = image.GetImageWeakReference().ProcessedImage; + if (savedImagesSet.Contains(imageState)) + { + image.MarkSaved(imageState); + } + } + } + } + + public void MarkAllSaved() + { + lock (this) + { + _savedState = CurrentState; + foreach (var image in Images) + { + var imageState = image.GetImageWeakReference().ProcessedImage; + image.MarkSaved(imageState); + } + } + } + public void Mutate(ListMutation mutation, ListSelection? selectionToMutate = null, - bool isPassiveInteraction = false) + bool isPassiveInteraction = false, bool updateUndoStack = true, bool disposeDeleted = true) { - MutateInternal(mutation, selectionToMutate, isPassiveInteraction); + MutateInternal(mutation, selectionToMutate, isPassiveInteraction, updateUndoStack, disposeDeleted); } public async Task MutateAsync(ListMutation mutation, ListSelection? selectionToMutate = null, - bool isPassiveInteraction = false) + bool isPassiveInteraction = false, bool updateUndoStack = true, bool disposeDeleted = true) { - await Task.Run(() => MutateInternal(mutation, selectionToMutate, isPassiveInteraction)); + await Task.Run(() => + MutateInternal(mutation, selectionToMutate, isPassiveInteraction, updateUndoStack, disposeDeleted)); } private void MutateInternal(ListMutation mutation, ListSelection? selectionToMutate, - bool isPassiveInteraction) + bool isPassiveInteraction, bool updateUndoStack, bool disposeDeleted) { lock (this) { var currentSelection = _selection; - var before = new HashSet(Images); + var before = Images.ToList(); + var beforeTransforms = before.Select(img => img.TransformState).ToList(); var mutableImages = Images.ToList(); if (!ReferenceEquals(selectionToMutate, null)) { @@ -78,52 +125,98 @@ private void MutateInternal(ListMutation mutation, ListSelection(Images); + var after = Images.ToList(); + var afterTransforms = after.Select(img => img.TransformState).ToList(); - foreach (var added in after.Except(before)) + var allAdded = after.Except(before).ToList(); + foreach (var added in allAdded) { added.ThumbnailChanged += ImageThumbnailChanged; added.ThumbnailInvalidated += ImageThumbnailInvalidated; } - foreach (var removed in before.Except(after)) + var allRemoved = before.Except(after).ToList(); + foreach (var removed in allRemoved) { removed.ThumbnailChanged -= ImageThumbnailChanged; removed.ThumbnailInvalidated -= ImageThumbnailInvalidated; - removed.Dispose(); + if (disposeDeleted) + { + removed.Dispose(); + } + } + currentSelection = ListSelection.From(currentSelection.Except(allRemoved)); + + // Generally we want to allow the mutation to be undone, but if we're mutating the list as part of undoing + // or redoing a previous mutation, then we'll leave the undo stack intact. + if (updateUndoStack) + { + MaybeAddToUndoStack(before, beforeTransforms, after, afterTransforms); } if (currentSelection != _selection) { - UpdateSelectionOnUiThread(currentSelection); + UpdateSelection(currentSelection); } } ImagesUpdated?.Invoke(this, new ImageListEventArgs(isPassiveInteraction)); } - private void UpdateSelectionOnUiThread(ListSelection currentSelection) + private void MaybeAddToUndoStack( + List before, List beforeTransforms, + List after, List afterTransforms) { - // TODO: This won't work right as SyncContext.Current is only set on the UI thread anyway - var syncContext = SynchronizationContext.Current; - if (syncContext != null) + if (ReplaceRangeUndoElement.FromFullList(this, before, after) + is { } replaceRangeUndoElement) { - syncContext.Post(_ => UpdateSelection(currentSelection), null); + _undoStack.Push(replaceRangeUndoElement); } - else + if (TransformImagesUndoElement.FromFullList(before, beforeTransforms, after, afterTransforms) + is { } replaceTransformsUndoElement) { - UpdateSelection(currentSelection); + _undoStack.Push(replaceTransformsUndoElement); } } private void ImageThumbnailChanged(object? sender, EventArgs args) { // A thumbnail change indicates rendering which is a passive interaction. - ImagesThumbnailChanged?.Invoke(this, new ImageListEventArgs(false)); + ImagesThumbnailChanged?.Invoke(this, new ImageListEventArgs(true)); } private void ImageThumbnailInvalidated(object? sender, EventArgs args) { // A thumbnail invalidation indicates an image edit which is an active interaction. - ImagesThumbnailInvalidated?.Invoke(this, new ImageListEventArgs(true)); + ImagesThumbnailInvalidated?.Invoke(this, new ImageListEventArgs(false)); + } + + public async Task Undo() + { + await Task.Run(() => + { + lock (this) + { + _undoStack.Undo(); + } + }); + } + + public async Task Redo() + { + await Task.Run(() => + { + lock (this) + { + _undoStack.Redo(); + } + }); + } + + public void PushUndoElement(IUndoElement undoElement) + { + lock (this) + { + _undoStack.Push(undoElement); + } } /// diff --git a/NAPS2.Lib/Images/UiThumbnailProvider.cs b/NAPS2.Lib/Images/UiThumbnailProvider.cs index 73726747f4..bd42a0b4e9 100644 --- a/NAPS2.Lib/Images/UiThumbnailProvider.cs +++ b/NAPS2.Lib/Images/UiThumbnailProvider.cs @@ -9,11 +9,12 @@ namespace NAPS2.Images; public class UiThumbnailProvider { private readonly ImageContext _imageContext; - private IMemoryImage? _placeholder; + private readonly ColorScheme _colorScheme; - public UiThumbnailProvider(ImageContext imageContext) + public UiThumbnailProvider(ImageContext imageContext, ColorScheme colorScheme) { _imageContext = imageContext; + _colorScheme = colorScheme; } public IMemoryImage GetThumbnail(UiImage img, int thumbnailSize) @@ -27,7 +28,7 @@ public IMemoryImage GetThumbnail(UiImage img, int thumbnailSize) } if (img.IsThumbnailDirty) { - thumb = EtoPlatform.Current.DrawHourglass(_imageContext, thumb); + thumb = EtoPlatform.Current.DrawHourglass(thumb); } return thumb; } @@ -35,16 +36,9 @@ public IMemoryImage GetThumbnail(UiImage img, int thumbnailSize) private IMemoryImage RenderPlaceholder(int thumbnailSize) { - lock (this) - { - if (_placeholder?.Width == thumbnailSize) - { - return _placeholder; - } - _placeholder?.Dispose(); - _placeholder = _imageContext.Create(thumbnailSize, thumbnailSize, ImagePixelFormat.RGB24); - _placeholder = EtoPlatform.Current.DrawHourglass(_imageContext, _placeholder); - return _placeholder; - } + var placeholder = _imageContext.Create(thumbnailSize, thumbnailSize, ImagePixelFormat.RGB24); + placeholder.Fill(_colorScheme.BackgroundColor); + placeholder = EtoPlatform.Current.DrawHourglass(placeholder); + return placeholder; } } \ No newline at end of file diff --git a/NAPS2.Lib/Images/UndoStack.cs b/NAPS2.Lib/Images/UndoStack.cs new file mode 100644 index 0000000000..e6abeb3772 --- /dev/null +++ b/NAPS2.Lib/Images/UndoStack.cs @@ -0,0 +1,67 @@ +namespace NAPS2.Images; + +public class UndoStack +{ + private readonly int _maxLength; + private readonly List _stack = []; + private int _position = 0; + + public UndoStack(int maxLength) + { + _maxLength = maxLength; + } + + public bool Push(IUndoElement element) + { + ClearRedo(); + _stack.Insert(0, element); + if (_stack.Count > _maxLength) + { + _stack.RemoveRange(_maxLength, _stack.Count - _maxLength); + } + return true; + } + + public void ClearRedo() + { + _stack.RemoveRange(0, _position); + _position = 0; + } + + public void ClearUndo() + { + _stack.RemoveRange(_position, _stack.Count - _position); + } + + public void ClearBoth() + { + ClearRedo(); + ClearUndo(); + } + + public bool CanUndo => _position < _stack.Count; + + public bool CanRedo => _position > 0; + + public bool Undo() + { + if (CanUndo) + { + _position++; + _stack[_position - 1].ApplyUndo(); + return true; + } + return false; + } + + public bool Redo() + { + if (CanRedo) + { + _position--; + _stack[_position].ApplyRedo(); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/ImportExport/AutoSaver.cs b/NAPS2.Lib/ImportExport/AutoSaver.cs index 9e662d770e..995385b892 100644 --- a/NAPS2.Lib/ImportExport/AutoSaver.cs +++ b/NAPS2.Lib/ImportExport/AutoSaver.cs @@ -1,6 +1,7 @@ using NAPS2.EtoForms; +using NAPS2.EtoForms.Notifications; using NAPS2.ImportExport.Images; -using NAPS2.ImportExport.Pdf; +using NAPS2.Pdf; using NAPS2.Scan; namespace NAPS2.ImportExport; @@ -11,14 +12,15 @@ public class AutoSaver private readonly DialogHelper _dialogHelper; private readonly OperationProgress _operationProgress; private readonly ISaveNotify _notify; - private readonly IPdfExporter _pdfExporter; + private readonly PdfExporter _pdfExporter; private readonly IOverwritePrompt _overwritePrompt; private readonly Naps2Config _config; private readonly ImageContext _imageContext; + private readonly UiImageList _imageList; public AutoSaver(ErrorOutput errorOutput, DialogHelper dialogHelper, - OperationProgress operationProgress, ISaveNotify notify, IPdfExporter pdfExporter, - IOverwritePrompt overwritePrompt, Naps2Config config, ImageContext imageContext) + OperationProgress operationProgress, ISaveNotify notify, PdfExporter pdfExporter, + IOverwritePrompt overwritePrompt, Naps2Config config, ImageContext imageContext, UiImageList imageList) { _errorOutput = errorOutput; _dialogHelper = dialogHelper; @@ -28,6 +30,7 @@ public AutoSaver(ErrorOutput errorOutput, DialogHelper dialogHelper, _overwritePrompt = overwritePrompt; _config = config; _imageContext = imageContext; + _imageList = imageList; } public IAsyncEnumerable Save(AutoSaveSettings settings, IAsyncEnumerable images) @@ -39,12 +42,10 @@ public IAsyncEnumerable Save(AutoSaveSettings settings, IAsyncEn { await foreach (var img in images) { - // TODO: We should assume the returned sink may dispose what we give it, therefore we should make - // a clone before sending it out, and then dispose the clone when we're done with it imageList.Add(img); if (!settings.ClearImagesAfterSaving) { - produceImage(img); + produceImage(img.Clone()); } } } @@ -58,6 +59,13 @@ public IAsyncEnumerable Save(AutoSaveSettings settings, IAsyncEn produceImage(img); } } + else + { + foreach (var img in imageList) + { + img.Dispose(); + } + } } }); } @@ -71,17 +79,22 @@ private async Task InternalSave(AutoSaveSettings settings, List InternalSave(AutoSaveSettings settings, List _dialogHelper.PromptToSavePdfOrImage(subPath, out newPath))) { subPath = placeholders.Substitute(newPath!, true, i); } + else + { + return (false, null); + } } // TODO: This placeholder handling is complex and wrong in some cases (e.g. FilePerScan with ext = "jpg") // TODO: Maybe have initial placeholders that replace date, then rely on the ops to increment the file num diff --git a/NAPS2.Lib/ImportExport/DirectImportOperation.cs b/NAPS2.Lib/ImportExport/DirectImportOperation.cs index e824755b3c..a66268e524 100644 --- a/NAPS2.Lib/ImportExport/DirectImportOperation.cs +++ b/NAPS2.Lib/ImportExport/DirectImportOperation.cs @@ -5,7 +5,7 @@ namespace NAPS2.ImportExport; -public class DirectImportOperation : OperationBase +internal class DirectImportOperation : OperationBase { private readonly ScanningContext _scanningContext; private readonly WorkerPool _workerPool; @@ -54,9 +54,11 @@ await Pipeline ProcessedImage newImg; try { - newImg = _workerPool.Use(ctx => - ctx.Service.ImportPostProcess(_scanningContext, img, thumbnailSize, - new BarcodeDetectionOptions())); + newImg = _workerPool.Use( + WorkerType.Native, + ctx => + ctx.Service.ImportPostProcess(_scanningContext, img, thumbnailSize, + new BarcodeDetectionOptions())); } catch (Exception) { diff --git a/NAPS2.Lib/ImportExport/Email/EmailProviderController.cs b/NAPS2.Lib/ImportExport/Email/EmailProviderController.cs new file mode 100644 index 0000000000..739a5ce253 --- /dev/null +++ b/NAPS2.Lib/ImportExport/Email/EmailProviderController.cs @@ -0,0 +1,155 @@ +using NAPS2.EtoForms; +using NAPS2.EtoForms.Ui; +using NAPS2.ImportExport.Email.Oauth; +using NAPS2.Scan; + +namespace NAPS2.ImportExport.Email; + +internal class EmailProviderController +{ + private readonly IFormFactory _formFactory; + private readonly Naps2Config _config; + private readonly ISystemEmailClients _systemEmailClients; + private readonly GmailOauthProvider _gmailOauthProvider; + private readonly OutlookWebOauthProvider _outlookWebOauthProvider; + private readonly ThunderbirdEmailProvider _thunderbirdProvider; + private readonly IEmailProviderFactory _emailProviderFactory; + + public EmailProviderController(IFormFactory formFactory, Naps2Config config, ISystemEmailClients systemEmailClients, + GmailOauthProvider gmailOauthProvider, + OutlookWebOauthProvider outlookWebOauthProvider, ThunderbirdEmailProvider thunderbirdProvider, + IEmailProviderFactory emailProviderFactory) + { + _formFactory = formFactory; + _config = config; + _systemEmailClients = systemEmailClients; + _gmailOauthProvider = gmailOauthProvider; + _outlookWebOauthProvider = outlookWebOauthProvider; + _thunderbirdProvider = thunderbirdProvider; + _emailProviderFactory = emailProviderFactory; + } + + public List GetWidgets() + { + var providerWidgets = new List(); + var userSetup = _config.Get(c => c.EmailSetup); + + var systemClientNames = _systemEmailClients.GetNames(); + var defaultSystemClientName = _systemEmailClients.GetDefaultName(); + + foreach (var clientName in systemClientNames.OrderBy(x => + x == userSetup.SystemProviderName ? 0 : x == defaultSystemClientName ? 1 : 2)) + { + providerWidgets.Add(GetWidget(EmailProviderType.System, clientName)); + } + + void MaybeAddWidget(EmailProviderType type, bool condition = true) + { + if (condition) + { + var provider = _emailProviderFactory.Create(type); + if (provider.ShowInList) + { + var widget = GetWidget(type); + widget.Enabled = provider.CanSelectInList; + providerWidgets.Add(widget); + } + } + } + + // For Windows we expect Thunderbird to be used through MAPI. For Linux we need to handle it specially. + MaybeAddWidget(EmailProviderType.Thunderbird, OperatingSystem.IsLinux()); + MaybeAddWidget(EmailProviderType.AppleMail, OperatingSystem.IsMacOS()); + MaybeAddWidget(EmailProviderType.OutlookNew); + MaybeAddWidget(EmailProviderType.Gmail); + MaybeAddWidget(EmailProviderType.OutlookWeb); + + // Sort the currently-selected provider to the top + return providerWidgets.OrderBy(widget => widget.ProviderType == userSetup.ProviderType ? 0 : 1).ToList(); + } + + private EmailProviderWidget GetWidget(EmailProviderType type, string? clientName = null) + { + return type switch + { + EmailProviderType.System => new EmailProviderWidget + { + ProviderType = EmailProviderType.System, + ProviderIcon = _systemEmailClients.LoadIcon(clientName!)?.ToEtoImage(), + ProviderIconName = "mail_yellow", + ProviderName = clientName!, + Choose = () => ChooseSystem(clientName!) + }, + EmailProviderType.Thunderbird => new EmailProviderWidget + { + ProviderType = EmailProviderType.Thunderbird, + ProviderIconName = "thunderbird", + ProviderName = EmailProviderType.Thunderbird.Description(), + Choose = () => ChooseProviderType(EmailProviderType.Thunderbird), + }, + EmailProviderType.AppleMail => new EmailProviderWidget + { + ProviderType = EmailProviderType.AppleMail, + ProviderIconName = "apple_mail", + ProviderName = EmailProviderType.AppleMail.Description(), + Choose = () => ChooseProviderType(EmailProviderType.AppleMail), + }, + EmailProviderType.Gmail => new EmailProviderWidget + { + ProviderType = EmailProviderType.Gmail, + ProviderIconName = "gmail", + ProviderName = EmailProviderType.Gmail.Description(), + Choose = () => ChooseOauth(_gmailOauthProvider) + }, + EmailProviderType.OutlookNew => new EmailProviderWidget + { + ProviderType = EmailProviderType.OutlookNew, + ProviderIconName = "outlooknew", + ProviderName = EmailProviderType.OutlookNew.Description(), + Choose = () => ChooseProviderType(EmailProviderType.OutlookNew), + }, + EmailProviderType.OutlookWeb => new EmailProviderWidget + { + ProviderType = EmailProviderType.OutlookWeb, + ProviderIconName = "outlookweb", + ProviderName = EmailProviderType.OutlookWeb.Description(), + Choose = () => ChooseOauth(_outlookWebOauthProvider) + }, + // EmailProviderType.CustomSmtp => new EmailProviderWidget + // { + // ProviderType = EmailProviderType.CustomSmtp, + // ProviderIconName = "email_setting", + // ProviderName = EmailProviderType.CustomSmtp.Description(), + // Choose = ChooseCustomSmtp + // }, + _ => throw new ArgumentException() + }; + } + + private bool ChooseSystem(string clientName) + { + var transact = _config.User.BeginTransaction(); + transact.Remove(c => c.EmailSetup); + transact.Set(c => c.EmailSetup.SystemProviderName, clientName); + transact.Set(c => c.EmailSetup.ProviderType, EmailProviderType.System); + transact.Commit(); + return true; + } + + private bool ChooseProviderType(EmailProviderType providerType) + { + var transact = _config.User.BeginTransaction(); + transact.Remove(c => c.EmailSetup); + transact.Set(c => c.EmailSetup.ProviderType, providerType); + transact.Commit(); + return true; + } + + private bool ChooseOauth(OauthProvider provider) + { + var authForm = _formFactory.Create(); + authForm.OauthProvider = provider; + authForm.ShowModal(); + return authForm.Result; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/ImportExport/Email/EmailProviderType.cs b/NAPS2.Lib/ImportExport/Email/EmailProviderType.cs index cb30170906..e3c163434d 100644 --- a/NAPS2.Lib/ImportExport/Email/EmailProviderType.cs +++ b/NAPS2.Lib/ImportExport/Email/EmailProviderType.cs @@ -9,6 +9,12 @@ public enum EmailProviderType CustomSmtp, [LocalizedDescription(typeof(SettingsResources), "EmailProviderType_Gmail")] Gmail, + [LocalizedDescription(typeof(SettingsResources), "EmailProviderType_OutlookNew")] + OutlookNew, [LocalizedDescription(typeof(SettingsResources), "EmailProviderType_OutlookWeb")] - OutlookWeb + OutlookWeb, + [LocalizedDescription(typeof(SettingsResources), "EmailProviderType_Thunderbird")] + Thunderbird, + [LocalizedDescription(typeof(SettingsResources), "EmailProviderType_AppleMail")] + AppleMail } \ No newline at end of file diff --git a/NAPS2.Lib/ImportExport/Email/EmailProviderWidget.cs b/NAPS2.Lib/ImportExport/Email/EmailProviderWidget.cs new file mode 100644 index 0000000000..7188516889 --- /dev/null +++ b/NAPS2.Lib/ImportExport/Email/EmailProviderWidget.cs @@ -0,0 +1,13 @@ +using Eto.Drawing; + +namespace NAPS2.ImportExport.Email; + +public class EmailProviderWidget +{ + public required EmailProviderType ProviderType { get; init; } + public Bitmap? ProviderIcon { get; init; } + public required string ProviderIconName { get; init; } + public required string ProviderName { get; init; } + public required Func Choose { get; init; } + public bool Enabled { get; set; } = true; +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Email/EmailSettings.cs b/NAPS2.Lib/ImportExport/Email/EmailSettings.cs similarity index 100% rename from NAPS2.Sdk/ImportExport/Email/EmailSettings.cs rename to NAPS2.Lib/ImportExport/Email/EmailSettings.cs diff --git a/NAPS2.Lib/ImportExport/Email/IAppleMailEmailProvider.cs b/NAPS2.Lib/ImportExport/Email/IAppleMailEmailProvider.cs new file mode 100644 index 0000000000..b915938a1f --- /dev/null +++ b/NAPS2.Lib/ImportExport/Email/IAppleMailEmailProvider.cs @@ -0,0 +1,5 @@ +namespace NAPS2.ImportExport.Email; + +internal interface IAppleMailEmailProvider : IEmailProvider +{ +} \ No newline at end of file diff --git a/NAPS2.Lib/ImportExport/Email/IEmailProviderFactory.cs b/NAPS2.Lib/ImportExport/Email/IEmailProviderFactory.cs index bdf9b411a4..a6f7d3e56b 100644 --- a/NAPS2.Lib/ImportExport/Email/IEmailProviderFactory.cs +++ b/NAPS2.Lib/ImportExport/Email/IEmailProviderFactory.cs @@ -1,6 +1,6 @@ namespace NAPS2.ImportExport.Email; -public interface IEmailProviderFactory +internal interface IEmailProviderFactory { IEmailProvider Create(EmailProviderType type); diff --git a/NAPS2.Lib/ImportExport/Email/ISystemEmailClients.cs b/NAPS2.Lib/ImportExport/Email/ISystemEmailClients.cs new file mode 100644 index 0000000000..de4c6e2588 --- /dev/null +++ b/NAPS2.Lib/ImportExport/Email/ISystemEmailClients.cs @@ -0,0 +1,8 @@ +namespace NAPS2.ImportExport.Email; + +public interface ISystemEmailClients +{ + string[] GetNames(); + string? GetDefaultName(); + IMemoryImage? LoadIcon(string clientName); +} \ No newline at end of file diff --git a/NAPS2.Lib/ImportExport/Email/MimeEmailProvider.cs b/NAPS2.Lib/ImportExport/Email/MimeEmailProvider.cs index c7d339745d..469ac074c0 100644 --- a/NAPS2.Lib/ImportExport/Email/MimeEmailProvider.cs +++ b/NAPS2.Lib/ImportExport/Email/MimeEmailProvider.cs @@ -3,7 +3,7 @@ namespace NAPS2.ImportExport.Email; -public abstract class MimeEmailProvider : IEmailProvider +internal abstract class MimeEmailProvider : IEmailProvider { public async Task SendEmail(EmailMessage emailMessage, ProgressHandler progress = default) { @@ -38,4 +38,8 @@ private void CopyRecips(List recips, EmailRecipientType type, In outputList.Add(new MailboxAddress(Encoding.UTF8, recip.Name, recip.Address)); } } + + public abstract bool ShowInList { get; } + + public virtual bool CanSelectInList => true; } \ No newline at end of file diff --git a/NAPS2.Lib/ImportExport/Email/Oauth/GmailEmailProvider.cs b/NAPS2.Lib/ImportExport/Email/Oauth/GmailEmailProvider.cs index 466a2c93e5..bbc76ef3d1 100644 --- a/NAPS2.Lib/ImportExport/Email/Oauth/GmailEmailProvider.cs +++ b/NAPS2.Lib/ImportExport/Email/Oauth/GmailEmailProvider.cs @@ -2,7 +2,7 @@ namespace NAPS2.ImportExport.Email.Oauth; -public class GmailEmailProvider : MimeEmailProvider +internal class GmailEmailProvider : MimeEmailProvider { private readonly GmailOauthProvider _gmailOauthProvider; @@ -22,7 +22,10 @@ protected override async Task SendMimeMessage(MimeMessage message, ProgressHandl { var userEmail = _gmailOauthProvider.User; // Open the draft in the user's browser - Process.Start($"https://mail.google.com/mail/?authuser={userEmail}#drafts?compose={draft.MessageId}"); + ProcessHelper.OpenUrl( + $"https://mail.google.com/mail/?authuser={userEmail}#drafts?compose={draft.MessageId}"); } } + + public override bool ShowInList => _gmailOauthProvider.HasClientCreds; } \ No newline at end of file diff --git a/NAPS2.Lib/ImportExport/Email/Oauth/GmailOauthProvider.cs b/NAPS2.Lib/ImportExport/Email/Oauth/GmailOauthProvider.cs index 6c131e5bba..03bf7c45f8 100644 --- a/NAPS2.Lib/ImportExport/Email/Oauth/GmailOauthProvider.cs +++ b/NAPS2.Lib/ImportExport/Email/Oauth/GmailOauthProvider.cs @@ -44,14 +44,20 @@ protected override OauthClientCreds ClientCreds protected override void SaveToken(OauthToken token, bool refresh) { - var emailSetup = _config.Get(c => c.EmailSetup); - emailSetup.GmailToken = token; - if (!refresh) + if (refresh) { - emailSetup.GmailUser = GetEmailAddress(); - emailSetup.ProviderType = EmailProviderType.Gmail; + _config.User.Set(c => c.EmailSetup.GmailToken, token); + } + else + { + var transact = _config.User.BeginTransaction(); + transact.Remove(c => c.EmailSetup); + transact.Set(c => c.EmailSetup.GmailToken, token); + transact.Set(c => c.EmailSetup.ProviderType, EmailProviderType.Gmail); + transact.Commit(); + // We need to commit the token before we can read the email address + _config.User.Set(c => c.EmailSetup.GmailUser, GetEmailAddress()); } - _config.User.Set(c => c.EmailSetup, emailSetup); } #endregion diff --git a/NAPS2.Lib/ImportExport/Email/Oauth/OauthProvider.cs b/NAPS2.Lib/ImportExport/Email/Oauth/OauthProvider.cs index 8c9225a170..5fe7f552bd 100644 --- a/NAPS2.Lib/ImportExport/Email/Oauth/OauthProvider.cs +++ b/NAPS2.Lib/ImportExport/Email/Oauth/OauthProvider.cs @@ -50,7 +50,7 @@ public void AcquireToken(CancellationToken cancelToken) // Open the user interface (which will redirect to our localhost listener) var url = $"{CodeEndpoint}?scope={Scope}&response_type=code&state={state}&redirect_uri={redirectUri}&client_id={ClientCreds.ClientId}"; - Process.Start(url); + ProcessHelper.OpenUrl(url); // Wait for the authorization code to be sent to the local socket string code; @@ -169,6 +169,12 @@ protected async Task PostAuthorized(string url, string body, string con return JObject.Parse(response); } + protected async Task PostAuthorizedNoResponse(string url) + { + using var client = AuthorizedClient(); + await client.UploadStringTaskAsync(url, "POST", ""); + } + private WebClient AuthorizedClient() { var client = new WebClient(); diff --git a/NAPS2.Lib/ImportExport/Email/Oauth/OutlookWebEmailProvider.cs b/NAPS2.Lib/ImportExport/Email/Oauth/OutlookWebEmailProvider.cs index 3b1ad47507..40c764a776 100644 --- a/NAPS2.Lib/ImportExport/Email/Oauth/OutlookWebEmailProvider.cs +++ b/NAPS2.Lib/ImportExport/Email/Oauth/OutlookWebEmailProvider.cs @@ -2,7 +2,7 @@ namespace NAPS2.ImportExport.Email.Oauth; -public class OutlookWebEmailProvider : IEmailProvider +internal class OutlookWebEmailProvider : IEmailProvider { private readonly OutlookWebOauthProvider _outlookWebOauthProvider; @@ -15,26 +15,34 @@ public async Task SendEmail(EmailMessage emailMessage, ProgressHandler pro { var messageObj = new JObject { - { "Subject", emailMessage.Subject }, - { "Body", new JObject + ["subject"] = emailMessage.Subject, + ["body"] = new JObject { - { "ContentType", "Text" }, - { "Content", emailMessage.BodyText } - }}, - { "ToRecipients", Recips(emailMessage, EmailRecipientType.To) }, - { "CcRecipients", Recips(emailMessage, EmailRecipientType.Cc) }, - { "BccRecipients", Recips(emailMessage, EmailRecipientType.Bcc) }, - { "Attachments", new JArray(emailMessage.Attachments.Select(attachment => new JObject + ["contentType"] = "text", + ["content"] = emailMessage.BodyText + }, + ["toRecipients"] = Recips(emailMessage, EmailRecipientType.To), + ["ccRecipients"] = Recips(emailMessage, EmailRecipientType.Cc), + ["bccRecipients"] = Recips(emailMessage, EmailRecipientType.Bcc), + ["attachments"] = new JArray(emailMessage.Attachments.Select(attachment => new JObject { - { "@odata.type", "#Microsoft.OutlookServices.FileAttachment" }, - { "Name", attachment.AttachmentName }, - { "ContentBytes", Convert.ToBase64String(File.ReadAllBytes(attachment.FilePath)) } - }))} + ["@odata.type"] = "#microsoft.graph.fileAttachment", + ["name"] = attachment.AttachmentName, + ["contentBytes"] = Convert.ToBase64String(File.ReadAllBytes(attachment.FilePath)) + })) }; - var respUrl = await _outlookWebOauthProvider.UploadDraft(messageObj.ToString(), progress); + var draft = await _outlookWebOauthProvider.UploadDraft(messageObj.ToString(), progress); - // Open the draft in the user's browser - Process.Start(respUrl + "&ispopout=0"); + + if (emailMessage.AutoSend) + { + await _outlookWebOauthProvider.SendDraft(draft.MessageId); + } + else + { + // Open the draft in the user's browser + ProcessHelper.OpenUrl(draft.WebLink + "&ispopout=0"); + } return true; } @@ -43,11 +51,17 @@ private JToken Recips(EmailMessage message, EmailRecipientType type) { return new JArray(message.Recipients.Where(recip => recip.Type == type).Select(recip => new JObject { - { "EmailAddress", new JObject { - { "Address", recip.Address }, - { "Name", recip.Name } - }} + "emailAddress", new JObject + { + { "address", recip.Address }, + { "name", recip.Name } + } + } })); } + + public bool ShowInList => _outlookWebOauthProvider.HasClientCreds; + + public bool CanSelectInList => true; } \ No newline at end of file diff --git a/NAPS2.Lib/ImportExport/Email/Oauth/OutlookWebOauthProvider.cs b/NAPS2.Lib/ImportExport/Email/Oauth/OutlookWebOauthProvider.cs index 02df26e0d6..296ca2f0ca 100644 --- a/NAPS2.Lib/ImportExport/Email/Oauth/OutlookWebOauthProvider.cs +++ b/NAPS2.Lib/ImportExport/Email/Oauth/OutlookWebOauthProvider.cs @@ -33,7 +33,7 @@ protected override OauthClientCreds ClientCreds } } - protected override string Scope => "https://outlook.office.com/mail.readwrite https://outlook.office.com/mail.send https://outlook.office.com/user.read offline_access"; + protected override string Scope => "mail.readwrite mail.send user.read offline_access"; protected override string CodeEndpoint => "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; @@ -41,14 +41,20 @@ protected override OauthClientCreds ClientCreds protected override void SaveToken(OauthToken token, bool refresh) { - var emailSetup = _config.Get(c => c.EmailSetup); - emailSetup.OutlookWebToken = token; - if (!refresh) + if (refresh) { - emailSetup.OutlookWebUser = GetEmailAddress(); - emailSetup.ProviderType = EmailProviderType.OutlookWeb; + _config.User.Set(c => c.EmailSetup.OutlookWebToken, token); + } + else + { + var transact = _config.User.BeginTransaction(); + transact.Remove(c => c.EmailSetup); + transact.Set(c => c.EmailSetup.OutlookWebToken, token); + transact.Set(c => c.EmailSetup.ProviderType, EmailProviderType.OutlookWeb); + transact.Commit(); + // We need to commit the token before we can read the email address + _config.User.Set(c => c.EmailSetup.OutlookWebUser, GetEmailAddress()); } - _config.User.Set(c => c.EmailSetup, emailSetup); } #endregion @@ -57,15 +63,24 @@ protected override void SaveToken(OauthToken token, bool refresh) public string GetEmailAddress() { - var resp = GetAuthorized("https://outlook.office.com/api/v1.0/me"); - return resp.Value("Id") ?? throw new InvalidOperationException("Could not get Id from Outlook profile response"); + var resp = GetAuthorized("https://graph.microsoft.com/v1.0/me"); + return resp.Value("mail") ?? throw new InvalidOperationException("Could not get Id from Outlook profile response"); + } + + public async Task UploadDraft(string messageRaw, ProgressHandler progress = default) + { + var resp = await PostAuthorized("https://graph.microsoft.com/v1.0/me/messages", messageRaw, "application/json", progress); + var webLink = resp.Value("webLink") ?? throw new InvalidOperationException("Could not get WebLink from Outlook messages response"); + var messageId = resp.Value("id") ?? throw new InvalidOperationException("Could not get ID from Outlook messages response"); + return new DraftInfo(webLink, messageId); } - public async Task UploadDraft(string messageRaw, ProgressHandler progress = default) + public async Task SendDraft(string draftMessageId) { - var resp = await PostAuthorized("https://outlook.office.com/api/v1.0/me/messages", messageRaw, "application/json", progress); - return resp.Value("WebLink") ?? throw new InvalidOperationException("Could not get WebLink from Outlook messages response"); + await PostAuthorizedNoResponse($"https://graph.microsoft.com/v1.0/me/messages/{draftMessageId}/send"); } + public record DraftInfo(string WebLink, string MessageId); + #endregion } \ No newline at end of file diff --git a/NAPS2.Lib/ImportExport/Email/OutlookNewEmailProvider.cs b/NAPS2.Lib/ImportExport/Email/OutlookNewEmailProvider.cs new file mode 100644 index 0000000000..da0fbfafe0 --- /dev/null +++ b/NAPS2.Lib/ImportExport/Email/OutlookNewEmailProvider.cs @@ -0,0 +1,92 @@ +using Microsoft.Win32; +using MimeKit; +using NAPS2.Scan; + +namespace NAPS2.ImportExport.Email; + +/// +/// The "New" Outlook. https://support.microsoft.com/en-us/office/getting-started-with-the-new-outlook-for-windows-656bb8d9-5a60-49b2-a98b-ba7822bc7627 +/// +internal class OutlookNewEmailProvider : MimeEmailProvider +{ + private readonly ScanningContext _scanningContext; + private readonly ErrorOutput _errorOutput; + + public OutlookNewEmailProvider(ScanningContext scanningContext, ErrorOutput errorOutput) + { + _scanningContext = scanningContext; + _errorOutput = errorOutput; + } + + private string? ExecutablePath + { + get + { + if (!OperatingSystem.IsWindows()) return null; + using var key = + Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\App Paths\olk.exe"); + var path = key?.GetValue(null)?.ToString(); + if (path == null || !File.Exists(path)) return null; + return path; + } + } + + public override bool ShowInList => ExecutablePath != null; + + protected override async Task SendMimeMessage(MimeMessage message, ProgressHandler progress, bool autoSend) + { + await Task.Run(async () => + { + if (progress.CancelToken.IsCancellationRequested) return; + + string? emlPath = null; + try + { + var exePath = ExecutablePath ?? + throw new InvalidOperationException("New outlook executable not available"); + + // Add header so that Outlook opens the message as a draft to edit/send + message.Headers.Add("X-Unsent", "1"); + + emlPath = Path.Combine(_scanningContext.TempFolderPath, Path.GetRandomFileName() + ".eml"); + await message.WriteToAsync(emlPath, progress.CancelToken); + + var process = + Process.Start(new ProcessStartInfo(exePath, emlPath) + { + RedirectStandardOutput = true, + RedirectStandardError = true + }); + if (process == null) + { + throw new InvalidOperationException("Could not start Outlook (new) process"); + } + await Task.WhenAny( + process.WaitForExitAsync(), + Task.Delay(TimeSpan.FromSeconds(30)), + progress.CancelToken.WaitHandle.WaitOneAsync()); + } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + _errorOutput.DisplayError(MiscResources.EmailError, ex); + } + finally + { + if (emlPath != null) + { + try + { + File.Delete(emlPath); + } + catch (Exception) + { + // Ignore + } + } + } + }); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/ImportExport/Email/StubSystemEmailClients.cs b/NAPS2.Lib/ImportExport/Email/StubSystemEmailClients.cs new file mode 100644 index 0000000000..07e0a1fb7c --- /dev/null +++ b/NAPS2.Lib/ImportExport/Email/StubSystemEmailClients.cs @@ -0,0 +1,8 @@ +namespace NAPS2.ImportExport.Email; + +public class StubSystemEmailClients : ISystemEmailClients +{ + public string[] GetNames() => []; + public string? GetDefaultName() => null; + public IMemoryImage? LoadIcon(string clientName) => null; +} \ No newline at end of file diff --git a/NAPS2.Lib/ImportExport/Email/ThunderbirdEmailProvider.cs b/NAPS2.Lib/ImportExport/Email/ThunderbirdEmailProvider.cs new file mode 100644 index 0000000000..b163283f0e --- /dev/null +++ b/NAPS2.Lib/ImportExport/Email/ThunderbirdEmailProvider.cs @@ -0,0 +1,97 @@ +using System.Text; + +namespace NAPS2.ImportExport.Email; + +internal class ThunderbirdEmailProvider : IEmailProvider +{ + private readonly ErrorOutput _errorOutput; + + public ThunderbirdEmailProvider(ErrorOutput errorOutput) + { + _errorOutput = errorOutput; + } + + // Even if Thunderbird isn't installed, we will show it (disabled) to hint to the user it's supported + public bool ShowInList => true; + + // Note we can't really support the Flatpak version of Thunderbird as it won't have access to attachment files from + // the sandbox. + public bool CanSelectInList => ProcessHelper.TryRun("thunderbird", "-v", 1000); + + public Task SendEmail(EmailMessage message, ProgressHandler progress = default) + { + return Task.Run(async () => + { + var arguments = new List(); + string? bodyFile = null; + try + { + if (message.Attachments.Any()) + { + // TODO: Need to use name if different than path (i.e. copy to temp) + arguments.Add($"attachment='{string.Join(",", message.Attachments.Select(x => x.FilePath))}'"); + } + if (!string.IsNullOrEmpty(message.BodyText)) + { + bodyFile = Path.Combine(Paths.Temp, Path.GetRandomFileName() + ".txt"); + File.WriteAllText(bodyFile, message.BodyText, Encoding.UTF8); + arguments.Add($"message='{bodyFile}'"); + } + if (!string.IsNullOrEmpty(message.Subject)) + { + // There doesn't seem to be a way to escape "'," but it shouldn't be common + arguments.Add($"subject='{message.Subject!.Replace("',", "' ,")}'"); + } + if (message.Recipients.Any(x => x.Type == EmailRecipientType.To)) + { + arguments.Add( + $"to='{string.Join(",", message.Recipients.Where(x => x.Type == EmailRecipientType.To).Select(x => x.Address))}'"); + } + if (message.Recipients.Any(x => x.Type == EmailRecipientType.Cc)) + { + arguments.Add( + $"cc='{string.Join(",", message.Recipients.Where(x => x.Type == EmailRecipientType.Cc).Select(x => x.Address))}'"); + } + if (message.Recipients.Any(x => x.Type == EmailRecipientType.Bcc)) + { + arguments.Add( + $"bcc='{string.Join(",", message.Recipients.Where(x => x.Type == EmailRecipientType.Bcc).Select(x => x.Address))}'"); + } + // This escaping isn't perfect but it should be good enough. I can't identify clear rules used by the + // thunderbird parser anyway. + var composeArgs = string.Join(",", arguments.Select(x => x.Replace("\"", "\\\""))); + var process = + Process.Start(new ProcessStartInfo("thunderbird", $"-compose \"{composeArgs}\"") + { + RedirectStandardOutput = true, + RedirectStandardError = true + }); + if (process == null) + { + throw new InvalidOperationException("Could not start Thunderbird process"); + } + await process.WaitForExitAsync(); + } + catch (Exception ex) + { + _errorOutput.DisplayError(MiscResources.EmailError, ex); + return false; + } + finally + { + if (bodyFile != null) + { + try + { + File.Delete(bodyFile); + } + catch (Exception) + { + // Ignore + } + } + } + return true; + }); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/ExportController.cs b/NAPS2.Lib/ImportExport/ExportController.cs similarity index 54% rename from NAPS2.Lib/EtoForms/ExportController.cs rename to NAPS2.Lib/ImportExport/ExportController.cs index 76e4413468..388d06b323 100644 --- a/NAPS2.Lib/EtoForms/ExportController.cs +++ b/NAPS2.Lib/ImportExport/ExportController.cs @@ -1,9 +1,11 @@ -using NAPS2.ImportExport; +using NAPS2.EtoForms; +using NAPS2.EtoForms.Notifications; +using NAPS2.EtoForms.Ui; using NAPS2.ImportExport.Email; using NAPS2.ImportExport.Images; -using NAPS2.ImportExport.Pdf; +using NAPS2.Pdf; -namespace NAPS2.EtoForms; +namespace NAPS2.ImportExport; public class ExportController : IExportController { @@ -28,28 +30,29 @@ public ExportController(DialogHelper dialogHelper, IOperationFactory operationFa public async Task SavePdf(ICollection uiImages, ISaveNotify notify) { using var images = GetSnapshots(uiImages); - if (images.Any()) + if (!images.Any()) { - string savePath; + return false; + } - var defaultFileName = _config.Get(c => c.PdfSettings.DefaultFileName); - if (_config.Get(c => c.PdfSettings.SkipSavePrompt) && Path.IsPathRooted(defaultFileName)) - { - savePath = defaultFileName!; - } - else + string savePath; + var defaultFileName = _config.Get(c => c.PdfSettings.DefaultFileName); + if (_config.Get(c => c.PdfSettings.SkipSavePrompt) && Path.IsPathRooted(defaultFileName)) + { + savePath = defaultFileName!; + } + else + { + if (!_dialogHelper.PromptToSavePdf(GetDefaultPath(defaultFileName, uiImages, true), out savePath!)) { - if (!_dialogHelper.PromptToSavePdf(defaultFileName, out savePath!)) - { - return false; - } + return false; } + } - if (await DoSavePdf(images, notify, savePath)) - { - MaybeDeleteAfterSaving(uiImages); - return true; - } + if (await DoSavePdf(images, notify, savePath)) + { + MaybeDeleteAfterSaving(uiImages); + return true; } return false; } @@ -57,28 +60,30 @@ public async Task SavePdf(ICollection uiImages, ISaveNotify notif public async Task SaveImages(ICollection uiImages, ISaveNotify notify) { using var images = GetSnapshots(uiImages); - if (images.Any()) + if (!images.Any()) { - string savePath; + return false; + } - if (_config.Get(c => c.ImageSettings.SkipSavePrompt) && - Path.IsPathRooted(_config.Get(c => c.ImageSettings.DefaultFileName))) - { - savePath = _config.Get(c => c.ImageSettings.DefaultFileName)!; - } - else + string savePath; + var defaultFileName = _config.Get(c => c.ImageSettings.DefaultFileName); + if (_config.Get(c => c.ImageSettings.SkipSavePrompt) && + Path.IsPathRooted(defaultFileName)) + { + savePath = defaultFileName!; + } + else + { + if (!_dialogHelper.PromptToSaveImage(GetDefaultPath(defaultFileName, uiImages, false), out savePath!)) { - if (!_dialogHelper.PromptToSaveImage(_config.Get(c => c.ImageSettings.DefaultFileName), out savePath!)) - { - return false; - } + return false; } + } - if (await DoSaveImages(images, notify, savePath)) - { - MaybeDeleteAfterSaving(uiImages); - return true; - } + if (await DoSaveImages(images, notify, savePath)) + { + MaybeDeleteAfterSaving(uiImages); + return true; } return false; } @@ -87,13 +92,32 @@ public async Task SavePdfOrImages(ICollection uiImages, ISaveNoti { // Note this path bypasses some of the pdf/image save options (e.g. default file name) using var images = GetSnapshots(uiImages); - if (!_dialogHelper.PromptToSavePdfOrImage(null, out var savePath)) + + string savePath; + var pdfDefaultFileName = _config.Get(c => c.PdfSettings.DefaultFileName); + var imageDefaultFileName = _config.Get(c => c.ImageSettings.DefaultFileName); + if (_config.Get(c => c.PdfSettings.SkipSavePrompt) && Path.IsPathRooted(pdfDefaultFileName)) { - return false; + savePath = pdfDefaultFileName!; } - if (Path.GetExtension(savePath!).ToLowerInvariant() == ".pdf" - ? await DoSavePdf(images, notify, savePath!) - : await DoSaveImages(images, notify, savePath!)) + else if (_config.Get(c => c.ImageSettings.SkipSavePrompt) && Path.IsPathRooted(imageDefaultFileName)) + { + savePath = imageDefaultFileName!; + } + else + { + var defaultFileName = string.IsNullOrWhiteSpace(pdfDefaultFileName) + ? imageDefaultFileName + : pdfDefaultFileName; + if (!_dialogHelper.PromptToSavePdfOrImage(GetDefaultPath(defaultFileName, uiImages, null), out savePath!)) + { + return false; + } + } + + if (Path.GetExtension(savePath).ToLowerInvariant() == ".pdf" + ? await DoSavePdf(images, notify, savePath) + : await DoSaveImages(images, notify, savePath)) { MaybeDeleteAfterSaving(uiImages); return true; @@ -109,16 +133,16 @@ public async Task EmailPdf(ICollection uiImages) return false; } - // TODO: What? - // if (_config == null) - // { - // // First run; prompt for a - // var form = _formFactory.Create(); - // if (form.ShowDialog() != DialogResult.OK) - // { - // return false; - // } - // } + if (!_config.User.Has(c => c.EmailSetup.ProviderType)) + { + // First email attempt; prompt for a provider + var form = _formFactory.Create(); + Invoker.Current.Invoke(() => form.ShowModal()); + if (!form.Result) + { + return false; + } + } var invalidChars = new HashSet(Path.GetInvalidFileNameChars()); var attachmentName = new string(_config.Get(c => c.EmailSettings.AttachmentName) @@ -145,10 +169,10 @@ private async Task DoSavePdf(IList images, ISaveNotify not { var subSavePath = Placeholders.All.Substitute(savePath); var state = _imageList.CurrentState; - if (await RunSavePdfOperation(subSavePath, images, false, null)) + if (await RunSavePdfOperation(subSavePath, images, originalFilename: savePath)) { - _imageList.SavedState = state; - notify?.PdfSaved(subSavePath); + _imageList.MarkSaved(state, images); + notify.PdfSaved(subSavePath); return true; } return false; @@ -158,13 +182,14 @@ private async Task DoSaveImages(IList images, ISaveNotify { var op = _operationFactory.Create(); var state = _imageList.CurrentState; - if (op.Start(savePath, Placeholders.All.WithDate(DateTime.Now), images, _config.Get(c => c.ImageSettings))) + if (op.Start(savePath, Placeholders.All.WithDate(DateTime.Now), images, _config.Get(c => c.ImageSettings), + savePath)) { _operationProgress.ShowProgress(op); } if (await op.Success) { - _imageList.SavedState = state; + _imageList.MarkSaved(state, images); notify.ImagesSaved(images.Count, op.FirstFileSaved!); return true; } @@ -180,14 +205,9 @@ private async Task DoEmailPdf(IList images, string attachm string targetPath = Path.Combine(tempFolder.FullName, attachmentName); var state = _imageList.CurrentState; - var message = new EmailMessage + if (await RunSavePdfOperation(targetPath, images, new EmailMessage())) { - Attachments = { new EmailAttachment(targetPath, attachmentName) } - }; - - if (await RunSavePdfOperation(targetPath, images, true, message)) - { - _imageList.SavedState = state; + _imageList.MarkSaved(state, images); return true; } } @@ -198,13 +218,13 @@ private async Task DoEmailPdf(IList images, string attachm return false; } - private async Task RunSavePdfOperation(string filename, IList images, bool email, - EmailMessage? emailMessage) + private async Task RunSavePdfOperation(string filename, IList images, + EmailMessage? emailMessage = null, string? originalFilename = null) { var op = _operationFactory.Create(); if (op.Start(filename, Placeholders.All.WithDate(DateTime.Now), images, _config.Get(c => c.PdfSettings), - _config.DefaultOcrParams(), email, emailMessage)) + _config.DefaultOcrParams(), emailMessage, originalFilename ?? filename)) { _operationProgress.ShowProgress(op); } @@ -223,4 +243,23 @@ private void MaybeDeleteAfterSaving(ICollection uiImages) _imageList.Mutate(new ImageListMutation.DeleteSelected(), ListSelection.From(uiImages)); } } + + private string? GetDefaultPath(string? defaultFileName, ICollection uiImages, bool? pdf) + { + if (!string.IsNullOrEmpty(defaultFileName)) + { + return defaultFileName; + } + var originalFilePaths = uiImages + .Select(x => x.GetImageWeakReference().ProcessedImage.PostProcessingData.OriginalFilePath) + .WhereNotNull() + .Where(x => pdf == null || pdf == (Path.GetExtension(x).ToLowerInvariant() == ".pdf")) + .Distinct() + .ToList(); + if (originalFilePaths.Count == 1) + { + return originalFilePaths[0]; + } + return null; + } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/IExportController.cs b/NAPS2.Lib/ImportExport/IExportController.cs similarity index 83% rename from NAPS2.Lib/EtoForms/IExportController.cs rename to NAPS2.Lib/ImportExport/IExportController.cs index f696b4faa3..9af2c710c7 100644 --- a/NAPS2.Lib/EtoForms/IExportController.cs +++ b/NAPS2.Lib/ImportExport/IExportController.cs @@ -1,4 +1,6 @@ -namespace NAPS2.EtoForms; +using NAPS2.EtoForms.Notifications; + +namespace NAPS2.ImportExport; public interface IExportController { diff --git a/NAPS2.Lib/ImportExport/IScannedImagePrinter.cs b/NAPS2.Lib/ImportExport/IScannedImagePrinter.cs index 8a7a91134b..487143aa46 100644 --- a/NAPS2.Lib/ImportExport/IScannedImagePrinter.cs +++ b/NAPS2.Lib/ImportExport/IScannedImagePrinter.cs @@ -1,3 +1,5 @@ +using Eto.Forms; + namespace NAPS2.ImportExport; public interface IScannedImagePrinter @@ -8,5 +10,5 @@ public interface IScannedImagePrinter /// The full list of images to print. /// The list of selected images. If non-empty, the user will be presented an option to print selected. /// True if the print completed, false if there was nothing to print or the user cancelled. - Task PromptToPrint(IList images, IList selectedImages); + Task PromptToPrint(Window parentWindow, IList images, IList selectedImages); } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/ImageClipboard.cs b/NAPS2.Lib/ImportExport/ImageClipboard.cs similarity index 82% rename from NAPS2.Lib/EtoForms/ImageClipboard.cs rename to NAPS2.Lib/ImportExport/ImageClipboard.cs index 6158410dd7..10c06c6549 100644 --- a/NAPS2.Lib/EtoForms/ImageClipboard.cs +++ b/NAPS2.Lib/ImportExport/ImageClipboard.cs @@ -1,21 +1,13 @@ using System.Text; using Eto.Forms; +using NAPS2.EtoForms; using NAPS2.ImportExport.Images; -namespace NAPS2.EtoForms; +namespace NAPS2.ImportExport; public class ImageClipboard { - private readonly ImageTransfer _imageTransfer; - - public ImageClipboard() : this(new ImageTransfer()) - { - } - - public ImageClipboard(ImageTransfer imageTransfer) - { - _imageTransfer = imageTransfer; - } + private readonly ImageTransfer _imageTransfer = new(); public async Task Write(IEnumerable images, bool includeBitmap) { @@ -32,12 +24,14 @@ public async Task Write(IEnumerable images, bool includeBitmap) { // Slow path for more full-featured copying, expensive parts not run on UI thread using var firstBitmap = await Task.Run(() => imageList[0].Render()); - EtoPlatform.Current.SetClipboardImage(Clipboard.Instance, firstBitmap.ToEtoImage()); - var encodedRtf = await Task.Run(() => RtfEncodeImages(firstBitmap, imageList)); - if (encodedRtf != null) - { - Clipboard.Instance.SetString(encodedRtf, "Rich Text Format"); - } + EtoPlatform.Current.SetClipboardImage(Clipboard.Instance, imageList[0], firstBitmap); + // TODO: Do we want to re-enable this at all? Or make it configurable somehow? The problem is that it's + // TODO: slow and memory-hungry. + // var encodedRtf = await Task.Run(() => RtfEncodeImages(firstBitmap, imageList)); + // if (encodedRtf != null) + // { + // Clipboard.Instance.SetString(encodedRtf, "Rich Text Format"); + // } } } diff --git a/NAPS2.Lib/ImportExport/Images/ImageTransfer.cs b/NAPS2.Lib/ImportExport/Images/ImageTransfer.cs index b4e0fb0fea..c83370cbec 100644 --- a/NAPS2.Lib/ImportExport/Images/ImageTransfer.cs +++ b/NAPS2.Lib/ImportExport/Images/ImageTransfer.cs @@ -2,7 +2,7 @@ namespace NAPS2.ImportExport.Images; -public class ImageTransfer : TransferHelper, ImageTransferData> +internal class ImageTransfer : TransferHelper, ImageTransferData> { protected override ImageTransferData AsData(IEnumerable images) { diff --git a/NAPS2.Lib/ImportExport/Images/SaveImagesOperation.cs b/NAPS2.Lib/ImportExport/Images/SaveImagesOperation.cs index 51123767e1..43a579a383 100644 --- a/NAPS2.Lib/ImportExport/Images/SaveImagesOperation.cs +++ b/NAPS2.Lib/ImportExport/Images/SaveImagesOperation.cs @@ -1,7 +1,6 @@ namespace NAPS2.ImportExport.Images; -// TODO: Cross-platform TIFF -public class SaveImagesOperation : OperationBase +internal class SaveImagesOperation : OperationBase { private readonly IOverwritePrompt _overwritePrompt; private readonly ImageContext _imageContext; @@ -25,9 +24,11 @@ public SaveImagesOperation(IOverwritePrompt overwritePrompt, ImageContext imageC /// The name of the file to save. For multiple images, this is modified by appending a number before the extension. /// /// The collection of images to save. + /// /// + /// public bool Start(string fileName, Placeholders placeholders, IList images, - ImageSettings imageSettings, bool batch = false) + ImageSettings imageSettings, string? overwriteFile = null, bool batch = false) { Status = new OperationStatus { @@ -46,12 +47,17 @@ public bool Start(string fileName, Placeholders placeholders, IList filesToImport, Action imageCallba { Status.StatusText = string.Format(MiscResources.ImportingFormat, Path.GetFileName(fileName)); InvokeStatusChanged(); - var images = _scannedImageImporter.Import(fileName, importParams, oneFile ? ProgressHandler : CancelToken); + var images = _fileImporter.Import(fileName, importParams, oneFile ? ProgressHandler : CancelToken); await foreach (var image in images) { imageCallback(image); diff --git a/NAPS2.Lib/ImportExport/Profiles/ProfileTransfer.cs b/NAPS2.Lib/ImportExport/Profiles/ProfileTransfer.cs index fcbc0b2481..fb15bf4cca 100644 --- a/NAPS2.Lib/ImportExport/Profiles/ProfileTransfer.cs +++ b/NAPS2.Lib/ImportExport/Profiles/ProfileTransfer.cs @@ -3,7 +3,7 @@ namespace NAPS2.ImportExport.Profiles; -public class ProfileTransfer : TransferHelper +internal class ProfileTransfer : TransferHelper { protected override ProfileTransferData AsData(ScanProfile profile) { diff --git a/NAPS2.Sdk/ImportExport/SaveSeparator.cs b/NAPS2.Lib/ImportExport/SaveSeparator.cs similarity index 100% rename from NAPS2.Sdk/ImportExport/SaveSeparator.cs rename to NAPS2.Lib/ImportExport/SaveSeparator.cs diff --git a/NAPS2.Sdk/ImportExport/SaveSeparatorHelper.cs b/NAPS2.Lib/ImportExport/SaveSeparatorHelper.cs similarity index 92% rename from NAPS2.Sdk/ImportExport/SaveSeparatorHelper.cs rename to NAPS2.Lib/ImportExport/SaveSeparatorHelper.cs index 75a2dc5736..eeba0d8430 100644 --- a/NAPS2.Sdk/ImportExport/SaveSeparatorHelper.cs +++ b/NAPS2.Lib/ImportExport/SaveSeparatorHelper.cs @@ -1,6 +1,6 @@ namespace NAPS2.ImportExport; -public static class SaveSeparatorHelper +internal static class SaveSeparatorHelper { /// /// Given a list of scans (each of which is a list of 1 or more images), @@ -41,13 +41,13 @@ public static IEnumerable> SeparateScans(IEnumerable 0) { yield return images; - images = new List(); + images = []; } } else diff --git a/NAPS2.Lib/Lang/ConsoleResources/ConsoleResources.Designer.cs b/NAPS2.Lib/Lang/ConsoleResources/ConsoleResources.Designer.cs index afa81ce91e..caa1e3e586 100644 --- a/NAPS2.Lib/Lang/ConsoleResources/ConsoleResources.Designer.cs +++ b/NAPS2.Lib/Lang/ConsoleResources/ConsoleResources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -105,6 +104,15 @@ internal static string BeginningScan { } } + /// + /// Looks up a localized string similar to Cancelling... Press Ctrl+C again to terminate.. + /// + internal static string Cancelling { + get { + return ResourceManager.GetString("Cancelling", resourceCulture); + } + } + /// /// Looks up a localized string similar to The specified component is already installed.. /// @@ -141,6 +149,15 @@ internal static string CouldntLoadEncryptionConfig { } } + /// + /// Looks up a localized string similar to The page size could not be parsed.. + /// + internal static string CouldntParsePageSize { + get { + return ResourceManager.GetString("CouldntParsePageSize", resourceCulture); + } + } + /// /// Looks up a localized string similar to You don't have permission to save files at this location.. /// @@ -150,6 +167,15 @@ internal static string DontHavePermission { } } + /// + /// Looks up a localized string similar to The --driver option must be set to use the --device or --listdevices option. Possible values: {0}. + /// + internal static string DriverRequired { + get { + return ResourceManager.GetString("DriverRequired", resourceCulture); + } + } + /// /// Looks up a localized string similar to Emailing.... /// @@ -313,6 +339,15 @@ internal static string Installing { } } + /// + /// Looks up a localized string similar to The driver option was not valid. Possible values: {0}. + /// + internal static string InvalidDriver { + get { + return ResourceManager.GetString("InvalidDriver", resourceCulture); + } + } + /// /// Looks up a localized string similar to NAPS2. /// @@ -322,6 +357,15 @@ internal static string NAPS2 { } } + /// + /// Looks up a localized string similar to No device was specified. Either use "--profile" to specify a profile with a device, or use "--device" to choose a particular device (and "--listdevices" to see available choices).. + /// + internal static string NoDeviceSpecified { + get { + return ResourceManager.GetString("NoDeviceSpecified", resourceCulture); + } + } + /// /// Looks up a localized string similar to No scanned pages to email.. /// @@ -385,6 +429,15 @@ internal static string PagesScanned { } } + /// + /// Looks up a localized string similar to Press Enter to scan.. + /// + internal static string PressEnterToScan { + get { + return ResourceManager.GetString("PressEnterToScan", resourceCulture); + } + } + /// /// Looks up a localized string similar to The specified profile is unavailable or ambiguous. ///Use the --profile option to specify a profile by name.. diff --git a/NAPS2.Lib/Lang/ConsoleResources/ConsoleResources.resx b/NAPS2.Lib/Lang/ConsoleResources/ConsoleResources.resx index 629f6753cb..79219275e1 100644 --- a/NAPS2.Lib/Lang/ConsoleResources/ConsoleResources.resx +++ b/NAPS2.Lib/Lang/ConsoleResources/ConsoleResources.resx @@ -147,6 +147,9 @@ Waiting {0}ms... + + Press Enter to scan. + Starting scan {0} of {1}... @@ -251,4 +254,19 @@ Use the "--importpassword" option. Installing {0}... + + The page size could not be parsed. + + + The driver option was not valid. Possible values: {0} + + + No device was specified. Either use "--profile" to specify a profile with a device, or use "--device" to choose a particular device (and "--listdevices" to see available choices). + + + The --driver option must be set to use the --device or --listdevices option. Possible values: {0} + + + Cancelling... Press Ctrl+C again to terminate. + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/LanguageNames.Designer.cs b/NAPS2.Lib/Lang/LanguageNames.Designer.cs index bffecc5895..d9489e04c5 100644 --- a/NAPS2.Lib/Lang/LanguageNames.Designer.cs +++ b/NAPS2.Lib/Lang/LanguageNames.Designer.cs @@ -95,6 +95,15 @@ internal static string bn { } } + /// + /// Looks up a localized string similar to Bosanski. + /// + internal static string bs { + get { + return ResourceManager.GetString("bs", resourceCulture); + } + } + /// /// Looks up a localized string similar to Català. /// diff --git a/NAPS2.Lib/Lang/LanguageNames.resx b/NAPS2.Lib/Lang/LanguageNames.resx index 22186803c0..fc94e4b91a 100644 --- a/NAPS2.Lib/Lang/LanguageNames.resx +++ b/NAPS2.Lib/Lang/LanguageNames.resx @@ -129,6 +129,9 @@ বাংলা + + Bosanski + Català diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.Designer.cs b/NAPS2.Lib/Lang/Resources/MiscResources.Designer.cs index e9db361680..41c4c03ba3 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.Designer.cs +++ b/NAPS2.Lib/Lang/Resources/MiscResources.Designer.cs @@ -123,7 +123,7 @@ internal static string AutoSaveError { } /// - /// Looks up a localized string similar to An unknown error ocurred during the batch scan.. + /// Looks up a localized string similar to An unknown error occurred during the batch scan.. /// internal static string BatchError { get { @@ -788,6 +788,15 @@ internal static string InstallFailedTitle { } } + /// + /// Looks up a localized string similar to Leave a Review. + /// + internal static string LeaveAReview { + get { + return ResourceManager.GetString("LeaveAReview", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} ({1}x{2} {3}). /// @@ -986,6 +995,15 @@ internal static string ResetImage { } } + /// + /// Looks up a localized string similar to Like NAPS2?. + /// + internal static string ReviewPrompt { + get { + return ResourceManager.GetString("ReviewPrompt", resourceCulture); + } + } + /// /// Looks up a localized string similar to Running OCR.... /// @@ -1158,7 +1176,7 @@ internal static string UpdateCheckDisabled { } /// - /// Looks up a localized string similar to An error occured when trying to install the update.. + /// Looks up a localized string similar to An error occurred when trying to install the update.. /// internal static string UpdateError { get { diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.af.resx b/NAPS2.Lib/Lang/Resources/MiscResources.af.resx index 469f9fd37e..48ca394e4f 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.af.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.af.resx @@ -13,28 +13,28 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Chọn thuộc tính + Kies Profiel - Xóa + Maak skoon - Bạn có chắc chắn bạn muốn xóa {0} sản phẩm (s)? + Verseker dat jy {0} item(s) wil verwyder? - Bạn có chắc chắn muốn xóa {0} sản phẩm (s)? + Verseker dat jy {0} item(s) wil uitvee? - Bạn có chắc chắn muốn xóa {0} hồ sơ? + Verseker dat jy {0} profiele wil uitvee? - Bạn có chắc chắn bạn muốn xóa hồ sơ {0}? + Verseker dat jy profiel {0} wil uitvee? The file {0} already exists. Do you want to overwrite it? - Xóa + Vee uit The selected scanner could not be found. @@ -43,28 +43,28 @@ The selected scanner is offline. - You don't have permission to save files at this location. + Jy het nie toestemming om lêers hier te stoor nie. - Lỗi + Fout - Bitmap Files (*.bmp) + Bitmap Leêrs (*.bmp) - Enhanced Windows MetaFile (*.emf) + Verbeterde Windows MetaFile (*.emf) - Exchangeable Image File (*.exif) + Uitruilbare beeldlêer (*.exif) - GIF File (*.gif) + GIF Lêer (*.gif) - JPEG File (*.jpg, *.jpeg) + JPEG Lêer (*.jpg, *.jpeg) - PDF dokument (* .pdf) + PDF dokument (*.pdf) PNG-lêer (* png) @@ -73,7 +73,7 @@ TIFF File (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG2000 Lêer (*.jp2, *.jpx) Noem ontbreek. @@ -88,49 +88,49 @@ Geen skandering toestel gevind. - Oorskryf Lêer + Skryf Oor Lêer - {0} of {1} + {0} van {1} - Scanned Image + Geskandeerde Beeld - Select a profile before clicking Scan. + Kies 'n profiel voordat u Skandeer klik. - Có lỗi xảy ra với trình điều khiển máy quét. + Daar was n fout met die skandeer drywer. - Version {0} + Weergawe {0} - Có lỗi xảy ra trong khi đang cố gắng để gửi một email. + Daar was 'n fout tydens die e-pos versending. - Cài đặt thành công + Installasie Afgehandel - Cài đặt hoàn tất. Bạn có muốn khởi động lại NAPS2 bây giờ? + Installasie Afgehandel. Herlaai NAPS2? - Cài đặt thất bại. + Installasie het misluk. - Cài đặt bị lỗi + Installasie het misluk - Tất cả ({0}) + Alles ({0}) - Selected ({0}) + Gekies ({0}) - Ước tính kích thước download: {0} MB + Beraamde Aflaai grootte: {0} MB - {0} / {1} files + {0} / {1} leêrs {0} / {1} MB @@ -142,37 +142,37 @@ The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported. - Tải Về lỗi + Aflaai Misluk Een of meer lêers kon nie afgelaai word. - Tùy chỉnh ({0}x{1} {2}) + Eie Groote ({0}x{1} {2}) - Scan + Skandeer Bạn có chắc chắn bạn muốn hoàn tác các thay đổi đến {0} hình ảnh (s)? - Reset Image + Stel Beeld terug - You have unsaved changes. Are you sure you want to exit and discard those changes? + Jy het ongebergde veranderinge. Is jy seker jy wil die program verlaat en veranderinge verloor? Die program is tans besig met 'n taak. Is jy seker dat jy die program wil verlaat en die taak kanselleer? - Unsaved Changes + Ongebergde Veranderings - Operation in Progress + Besig met handeling - Scanning page {0} + Skandeer bladsy {0} The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. @@ -181,46 +181,46 @@ Geen bladsye is in die voerder. - You do not have permission to copy content from the file '{0}'. + Jy het nie toestemming om inhoud vanaf die lêer '{0}' te kopieer nie. - Có lỗi xảy ra khi cố gắng để lưu các tập tin. + Daar was 'n fout tydens leêr-stoor. - of {0} + van {0} - xảy ra một lỗi không rõ trong đợt quét. + Obekende fout tydens bondel skandeer. - Hủy Scan hàng loạt. + Bondel gekanseleer. - Đang dừng.... + Besig om te kanseleer.... - Scan hàng loạt thành công. + Bondel suksesvol afgehandel. Scan hàng loạt dừng lại do lỗi. - Scanning page {0}... + Skandeer tans bladsy {0}... - Scanning page {0} (scan {1})... + Skandeer tans bladsy {0} (skandeer {1})... - Waiting for scan {0}... + Wag tans vir skandering {0}... - Hủy + Kanselleer - Đóng + Maak toe - Saving batch results... + Stoor tans bondelresultate... The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. @@ -229,58 +229,58 @@ {0} / {1} - Đang chèn {0}... + Intrek Besig {0}... - Chèn xong + Intrek Vordering - Herstel... + Besig om te Herstel... - Recovery Progress + Herwinningsvordering - Save Images + Stoor Beelde - Save Images Progress + Beeldstoor Vordering - Save PDF + Stoor PDF - Save PDF Progress + PDF-Stoor Vordering - Saving {0}... + Stoor tans {0}... Afdruk - Đang sao chép... + Besig om te kopieer... - Sao chép Tiến trình + Kopieëringvordering - Đang chèn... + Intrek Besig... - Email PDF + ePosPDF - Email PDF Progress + ePos PDF Vordering - Có lỗi xảy ra khi cố gắng tự động Lưu. + Daar was 'n fout tydens outo-stoor. - Tất cả các tập tin + Alle Lêers - TỆP ẢNH + Beeld Lêers The selected scanner is busy. @@ -295,49 +295,55 @@ The scanner is warming up. - Một bản cập nhật để OCR có sẵn. + Nuwe OCR weergawe is beskikbaar. Lưu ảnh. - {0} images saved. + {0} Beelde gestoor. - PDF gered. + PDF gestoor. {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Verseker dat jy "{0}" wil uitvee? The file could not be overwritten because it is currently in use. - Deskewing... + Besig om reguit te trek... - Deskew Progress + Reguit-trek Vordering - Download Needed + Aflaai Benodig - An additional component is needed to import this PDF file. Would you like to download it now? + Addisionele komponent benodig om hierdie PDF in te trek. Wil jy dit nou aflaai? - Are you sure you want to cancel the batch scan? + Bevestig bondel-skandeer kanselasie? - Cancel Batch + Kanseleer Bondel - Donate + Skenk - NAPS2 is completely free. Consider making a donation. + NAPS2 is heeltemal gratis. Oorweeg dit om 'n skenking te maak. + + + Leave a Review + + + Like NAPS2? The SANE driver is not available. Make sure to install the required packages: @@ -349,48 +355,48 @@ The selected driver is not supported on this system. - OCR Progress + OCR-vordering - Running OCR... + Besig om OCR uit te voer... - Update Progress + Opdateer-vordering - Updating... + Besig om nuwe weergawe te instaleeri... - No updates available. + Geen nuuter weergawes beskikbaar nie. - Checking... + Besig om te kyk... - Update checking is disabled. + Nuwe weergawe soek is afgeskakel. - 'n Nuwe weergawe is beskikbaar. + Daar was 'n fout met installasie van die nuwe weergawe. - Install {0} + Installeer {0} - An update is available. + Nuwe weergawe is beskikbaar. - An error occurred when trying to authorize. + Magtigings-fout het voorgekom. - Uploading email... + Besig om ePos op te laai... - Daar was a 'n fout tydens die e-pos versending. + Daar was 'n fout tydens die e-pos versending. - Scanning page {0}... + Skandeer tans bladsy {0}... - Thu thập dữ liệu... + Data word ingetrek... \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.ar.resx b/NAPS2.Lib/Lang/Resources/MiscResources.ar.resx index 9876f5ce93..1305975f62 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.ar.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.ar.resx @@ -100,7 +100,7 @@ حدد ملف شخصي قبل النقر فوق مسح ضوئي. - حدث خطأ مع تعريف المسح الضوئي.. + حدث خطأ مع تعريف المسح الضوئي. الإصدارة {0} @@ -109,7 +109,7 @@ حدث خطأ أثناء محاولة إرسال بريد إلكتروني. - اكتمل التثبيت. + اكتمل التثبيت اكتمل التثبيت. هل تريد إعادة تشغيل NAPS2 الآن؟ @@ -163,7 +163,7 @@ لديك تغييرات غير محفوظة. هل أنت متأكد من أنك تريد الخروج وتجاهل هذه التغييرات؟ - هناك عملية قيد التنفيذ الآن. هل أنت متأكد أنك تريد إلغاء العملية والخروج؟‏ + هناك عملية قيد التنفيذ الآن. هل أنت متأكد أنك تريد إلغاء العملية والخروج؟ التغييرات التي لم يتم حفظها @@ -190,7 +190,7 @@ من {0} - حدث خطأ غير معروف أثناء المسح الضوئي على دفعات. + حدث خطأ غير معروف أثناء مسح الدفعة. الدفعات تم إلغائها. @@ -310,7 +310,7 @@ {0} ({1} × {2} {3}) - Are you sure you want to delete "{0}"? + هل أنت متأكد أنك تريد حذف "{0}"؟ The file could not be overwritten because it is currently in use. @@ -325,7 +325,7 @@ تحميل اللوازم - قوائم الأرشفة هى نتاج ماتم إنشاءه سابقا فى الإعدادات العامة (التراميز) والمجموعات بشقيها الرئيسي والفرعى والقوالب وهيكلها بعد أن تم إنشاءها لتظهرلنا على الشكل المراد إظهارها عليه : + قوائم الأرشفة هى نتاج ماتم إنشاءه سابقا فى الإعدادات العامة (التراميز) والمجموعات بشقيها الرئيسي والفرعى والقوالب وهيكلها بعد أن تم إنشاءها لتظهرلنا على الشكل المراد إظهارها عليه؟ هل أنت متأكد من أنك تريد الغاء عملية المسح ؟ @@ -339,6 +339,12 @@ NAPS2 is completely free. Consider making a donation. + + Leave a Review + + + Like NAPS2? + The SANE driver is not available. Make sure to install the required packages: @@ -364,7 +370,7 @@ لا يوجد تحديثات. - يجري الفحص...... + يجري الفحص... Update checking is disabled. diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.bg.resx b/NAPS2.Lib/Lang/Resources/MiscResources.bg.resx index faedab3070..1abe643e67 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.bg.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.bg.resx @@ -55,7 +55,7 @@ Подобрен Windows MetaFile (*.emf) - Exchangeable Image File (*.exif) + стандарт за метаданни в графични и звукови файлове (*.exif) GIF файл (*.gif) @@ -73,7 +73,7 @@ TIFF файл (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG-2000 файл (*.jp2, *.jpx) Липсва име. @@ -97,7 +97,7 @@ Сканирано изображение - Изберете профил преди да натиснете \"Сканиране\". + Изберете профил преди да натиснете "Сканиране". Проблем с драйвъра на скенера. @@ -163,13 +163,13 @@ Има незаписани промени. Сигурни ли сте, че искате да затворите програмата и да отхвърлите тези промени? - An operation is in progress. Are you sure you want to exit and cancel the operation? + Операцията е в развитие. Сигурни ли сте, че искате да излезете и да отмените операцията? Незаписани промени - Operation in Progress + Операцията е в ход Сканиране на страница {0} @@ -190,7 +190,7 @@ от {0} - Възникна неизвестна грешка при групово сканиране. + Възникна неизвестна грешка по време на груповото сканиране. Груповата задача е отменена. @@ -295,7 +295,7 @@ Подготовка на скенера. - Наличен е ъпдейт за OCR модула. + Наличен е актуализация за OCR модула. Изображението е записано. @@ -310,22 +310,22 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Сигурни ли сте, че искате да изтриете {0}? Файлът не може да бъде презаписан, защото в момента се ползва. - Deskewing... + Изправяне... - Deskew Progress + Напредък на изправянето - Download Needed + Необходимо изтегляне - An additional component is needed to import this PDF file. Would you like to download it now? + Необходим е допълнителен компонент за импортиране на този PDF файл. Искате ли да го изтеглите сега? Сигурен ли сте, че искате да прекъснете поточното сканиране? @@ -334,58 +334,64 @@ Спри групата - Donate + Даряване - NAPS2 is completely free. Consider making a donation. + NAPS2 е напълно безплатен. Обмислете дарение. + + + Leave a Review + + + Like NAPS2? - The SANE driver is not available. Make sure to install the required packages: + Драйверът SANE не е наличен. Уверете се, че сте инсталирали необходимите пакети: - The OCR engine is not available. Make sure to install the required package: + OCR двигател не е наличен. Уверете се, че сте инсталирали необходимия пакет: - The selected driver is not supported on this system. + Избраният драйвер не се поддържа от тази система. - OCR Progress + Напредък на оптичното разпознаване - Running OCR... + Започва оптичното разпознаване... - Update Progress + Напредък на актуализацията - Updating... + Актуализиране... - No updates available. + Няма налични актуализации. Проверяване... - Update checking is disabled. + Проверката за актуализация е деактивирана. - An error occured when trying to install the update. + Грешка при опит за инталация на обновление. - Install {0} + Инсталиране {0} Налично обновяване. - An error occurred when trying to authorize. + Възникна грешка при опит за упълномощаване. - Uploading email... + Качване на имейл... - An error occurred when trying to send the email. + Възникна грешка при опит за изпращане на имейла. Сканиране на страница {0}... diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.bs.resx b/NAPS2.Lib/Lang/Resources/MiscResources.bs.resx new file mode 100644 index 0000000000..b1e8767ac7 --- /dev/null +++ b/NAPS2.Lib/Lang/Resources/MiscResources.bs.resx @@ -0,0 +1,402 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Izaberite profil + + + Očisti + + + Sigurni ste da želite očistiti {0} stavke(i)? + + + Sigurni ste da želite izbrisati {0} stavke(i)? + + + Sigurni ste da želite izbrisati {0} profil(a)? + + + Sigurni ste da želite izbrisati profil {0}? + + + Datoteka {0} već postoji. Da li želite da pišete preko iste? + + + Izbriši + + + Izabrani skener se ne može pronaći. + + + Izabrani skener je isključen. + + + Nemate dozvolu za čuvanje datoteka na ovoj lokaciji. + + + Greška + + + Bitmap datoteke (*.bmp) + + + Poboljšana Windows metadatoteka (*.emf) + + + Zamjenjiva slikovna datoteka (*.exif) + + + GIF datoteka (*.gif) + + + JPEG datoteka (*.jpg, *.jpeg) + + + PDF dokument (*.pdf) + + + PNG daoteka (*.png) + + + TIFF datoteka (*.tiff, *.tif) + + + JPEG2000 datoteka (*.jp2, *.jpx) + + + Nedostaje naziv. + + + NAPS2 + + + Nije izabran uređaj. + + + Nije pronađen uređaj za skeniranje. + + + Prepisati preko datoteke + + + {0} od {1} + + + Skenirana slika + + + Izaberite profil prije klika na "Skeniraj". + + + Greška se pojavila kod drivera za skeniranje. + + + Verzija {0} + + + Greška se pojavila pri pokušaju slanja nekog e-maila. + + + Instalacija završena + + + Instalacija završena. Da li želite sada restartovati NAPS2? + + + Instalacija nije uspjela. + + + Instalacija nije uspjela + + + Sve ({0}) + + + Izabrano ({0}) + + + Procjenjena veličina preuzimanja: {0} MB + + + {0} / {1} datoteke-a + + + {0} / {1} MB + + + Datoteka '{0}' se ne može uvesti. + + + Datoteka '{0}' se ne može uvesti. Samo PDF datoteke nastale u NAPS2 se mogu uvesti. + + + Greška pri preuzimanju + + + Jedna ili više datoteka se ne može preuzeti. + + + Prilagođeno ({0}x{1} {2}) + + + Skeniraj + + + Sigurni ste da želite vratiti promjene na {0} slike(a)? + + + Resetuj sliku + + + Niste sačuvali promjene. Da li želite izaći i odbaciti ove promjene? + + + Radnja je u toku. Da li sigurno želite otkazati radnju i izaći? + + + Nesačuvane promjene + + + Radnja u toku + + + Skeniranje stranice {0} + + + Izabrani skener nema podršku za korištenje ulagača papira. Ako skener ima ulagač papira, pokušajte s korištenjem drugog drajvera. + + + Nema papira u ulagaču. + + + Nemate dozvolu za kopiranje sadržaja iz datoteke '{0}'. + + + Greška se pojavila pri pokušaju snimanja datoteke. + + + od {0} + + + Nepoznata greška se pojavila tokom serijskog skeniranja. + + + Serija otkazana. + + + Otkazivanje.... + + + Serija kompletirana uspješno. + + + Serijsko skeniranje zaustavljeno zbog greške. + + + Skeniranje stranice {0}... + + + Skeniranje stranice {0} (skeniraj {1})... + + + Čekanje na skeniranje {0}... + + + Otkaži + + + Zatvori + + + Čuvanje serijskih rezultata... + + + Izabrani skener nema podršku za korištenje dupleksa - dvostranog skeniranja. Ako skener treba da podržava dupleks, pokušajte s korištenjem drugog drajvera. + + + {0} / {1} + + + Uvoženje {0}... + + + Napredak uvoza + + + Oporavljanje... + + + Napredak oporavka + + + Saćuvaj slike + + + Saćuvaj napredak slika + + + Sačuvaj PDF + + + Sačuvaj napredak PDF-ova + + + Spašavam {0}... + + + Štampanje + + + Kopiranje... + + + Napredak kopiranja + + + Uvoženje... + + + Pošalji PDF e-mailom + + + Pošalji napredak PDF-ova + + + Greška se pojavila pri pokušaju automatskog snimanja. + + + Sve datoteke + + + Slikovne datoteke + + + Izabrani skener je zauzet. + + + Poklopac skenera je otvoren. + + + Papir je zaglavio u skeneru. + + + Skener se zagrijava. + + + Dostupno je ažuriranje za OPZ. + + + Slika je sačuvana. + + + {0} slike(a) snimljene(o). + + + PDF sačuvan. + + + {0} ({1}x{2} {3}) + + + Sigurni ste da želite izbrisati "{0}"? + + + Ne može se pisati preko datoteke, jer se ona trenutno koristi. + + + Ravnanje... + + + Napredak ravnanja + + + Preuzimanje je potrebno + + + Dodatna komponenta je potrebna za uvoz ove PDF datoteke. Da li je želite preuzeti? + + + Sigurni ste da želite otkazati serijsko skeniranje? + + + Otkaži seriju + + + Donirajte + + + NAPS2 je potpuno besplatan. Razmotrite donaciju kao podršku. + + + Napišite recenziju + + + Sviđa Vam se NAPS2? + + + SANE drajver nije dostupan. Pobrinite se da instalirate potrebne pakete: + + + Mehanizam OPZ-a nije dostupan. Pobrinite se da instalirate potreban paket: + + + Izabrani drajver nije podržan na ovom sistemu. + + + Napredak OPZ-a + + + OPZ u toku... + + + Napredak ažuriranja + + + Ažuriranje... + + + Nema dostupnih ažuriranja. + + + Provjeravanje... + + + Provjera ažuriranja je isključena. + + + Greška se pojavila pri pokušaju instaliranja ažuriranja. + + + Instaliraj {0} + + + Dostupno je ažuriranje. + + + Greška se pojavila pri pokušaju autorizacije. + + + Prijenos e-pošte... + + + Greška se pojavila pri pokušaju slanja e-maila. + + + Skeniranje stranice {0}... + + + Pribavljanje podataka... + + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.ca.resx b/NAPS2.Lib/Lang/Resources/MiscResources.ca.resx index aed1782306..3fc9cced2f 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.ca.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.ca.resx @@ -19,31 +19,31 @@ Neteja - Segur de voler buidar {0} pagina(s)? + Segur que voleu buidar {0} pàgines? - Esteu segur que voleu suprimir {0} element(s)? + Segur que voleu suprimir {0} elements? - Esteu segur que voleu suprimir {0} perfils? + Segur que voleu suprimir {0} perfils? - Esteu segur que voleu suprimir el perfil {0}? + Segur que voleu suprimir el perfil {0}? El fitxer {0} ja existeix. Voleu sobreescriure'l? - Esborra + Suprimeix - El scanner seleccionat no s'ha pogut trobar . + No s'ha pogut trobar l'escàner seleccionat. - El scaner seleccionat esta apagat. + L'escàner seleccionat està apagat. - No teniu permís per desar fitxers en aquesta ubicació. + No teniu prou permisos per a desar fitxers en aquesta ubicació. Error @@ -55,13 +55,13 @@ Metafitxer millorat (*.emf) - Exchangeable Image File (*.exif) + Fitxer d'intercanvi d'imatge (*.exif) - GIF File (*.gif) + Fitxer GIF (*.gif) - JPEG File (*.jpg, *.jpeg) + Fitxer JPEG (*.jpg, *.jpeg) Document PDF (*.pdf) @@ -73,10 +73,10 @@ Fitxer TIFF (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + Fitxer JPEG2000 (*.jp2, *.jpx) - Manca el nom. + Falta el nom. NAPS2 @@ -97,16 +97,16 @@ Imatge escanejada - Selecciona un perfil abans de fer click a Scanejar. + Seleccioneu un perfil abans de fer clic a Escaneja. - S'ha produït un error amb el driver del escaner. + S'ha produït un error amb el controlador de l'escàner. Versió {0} - S'ha produït un error mentre s'enviava el correu-e . + S'ha produït un error en enviar un correu electrònic. Instal·lació completa @@ -124,7 +124,7 @@ Tot ({0}) - Seleccionat ({0}) + Selecció ({0}) Mida estimada de la baixada: {0} MB @@ -181,7 +181,7 @@ No hi ha cap pàgina a l'alimentador. - No teniu prou permisos per copiar el contingut del fitxer '{0}'. + No teniu prou permisos per a copiar el contingut del fitxer '{0}'. S'ha produït un error en desar el fitxer. @@ -196,7 +196,7 @@ S'ha cancel·lat l'escaneig per lots. - S'està cancel·lant....... + S'està cancel·lant... L'escaneig per lots ha finalitzat correctament. @@ -220,7 +220,7 @@ Tanca - Resultats del desat en lot... + Resultats del desament en lot... L'escàner seleccionat no suporta dúplex (escaneig per dues cares). Si el vostre escàner suporta aquesta tecnologia, seleccioneu un controlador diferent. @@ -235,7 +235,7 @@ Progrés de la importació - S'està recuperant...... + S'està recuperant... Progrés de la recuperació @@ -244,13 +244,13 @@ Desa les imatges - Progrés del desat d'imatges + Progrés del desament d'imatges Desa (PDF) - Progrés del desat en PDF + Progrés del desament en PDF S'està desant {0}... @@ -259,7 +259,7 @@ Imprimeix - S'està copiant...... + S'està copiant... Progrés de la còpia @@ -268,22 +268,22 @@ S'està important... - Email PDF + PDF per correu-e Progrés d'enviament del PDF - S'ha produït un error amb el desat automàtic. + S'ha produït un error amb el desament automàtic. Tots els fitxers - Imatges + Fitxers d'imatge - L'escàner seleccionat esta ocupat. + L'escàner seleccionat està ocupat. La tapa de l'escàner està oberta. @@ -292,7 +292,7 @@ L'escàner té un embús de paper. - S'està posant a punt l'escàner.... + L'escàner s'està escalfant. Hi ha una actualització de l'OCR disponible. @@ -310,13 +310,13 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Segur que voleu suprimir {0} elements? No es pot sobreescriure el fitxer perquè està en ús. - S'està alineant...... + S'està alineant... Progrés de l'alineació @@ -325,7 +325,7 @@ Baixada necessària - Es requereix un complement extra per importar aquest fitxer PDF. Voleu baixar-ho ara? + Es requereix un complement extra per a importar aquest fitxer PDF. Voleu baixar-ho ara? Segur que voleu cancel·lar el procés d'escaneig per lots? @@ -339,6 +339,12 @@ El NAPS2 és completament gratuït. Considereu fer una donació. + + Deixeu una ressenya + + + Us agrada el NAPS2? + No s'ha trobat el controlador SANE. Assegureu-vos d'instal·lar les dependències necessàries: @@ -346,7 +352,7 @@ No s'ha trobat el motor OCR. Assegureu-vos d'instal·lar el paquet necessari: - El controlador seleccionat no està suportat en aquest sistema. + El controlador seleccionat no és compatible amb aquest sistema. Progrés de l'OCR @@ -358,16 +364,16 @@ Progrés de l'actualització - S'està actualitzant...... + S'està actualitzant... No hi ha actualitzacions disponibles. - S'està comprovant...... + S'està comprovant... - Update checking is disabled. + La comprovació d'actualitzacions està inhabilitada. S'ha produït un error en instal·lar l'actualització. @@ -391,6 +397,6 @@ S'està escanejant la pàgina {0}... - S'estan obtenint les dades...... + S'estan obtenint les dades... \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.cs.resx b/NAPS2.Lib/Lang/Resources/MiscResources.cs.resx index 7da9fea655..8d7cb0d470 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.cs.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.cs.resx @@ -25,7 +25,7 @@ Opravdu si přejete smazat položky {0}? - Jste si jisti, že chcete smazat profily {0}? + Jste si jisti, že chcete smazat profily {0}? Jste si jisti, že chcete smazat profil {0}? @@ -61,7 +61,7 @@ Soubor GIF (*.gif) - Soubor JPEG (*.jpg, *.jpeg) + Soubor JPEG (*.jpg, *.jpeg) PDF dokument (*.pdf) @@ -73,7 +73,7 @@ Soubor TIFF (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + Soubor JPEG2000 (*.jp2, *.jpx) Název chybí. @@ -307,10 +307,10 @@ PDF uloženo. - {0} ({1} × {2} {3}) + {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Opravdu chcete smazat "{0}"? Soubor nemůže být přepsán, protože je právě používán. @@ -339,6 +339,12 @@ NAPS2 je zcela zdarma. Zvažte poskytnutí daru. + + Napsat recenzi + + + Líbí se vám NAPS2? + Ovladač SANE není dostupný. Ověřte instalaci požadovaných balíčků: @@ -367,10 +373,10 @@ Vyhledávání... - Update checking is disabled. + Kontrola aktualizací je vypnuta. - Při pokusu o aktualizaci došlo k chybě.. + Při pokusu o aktualizaci došlo k chybě. Instalovat {0} diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.da.resx b/NAPS2.Lib/Lang/Resources/MiscResources.da.resx index e3b6ee4d64..a127d4cfc3 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.da.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.da.resx @@ -37,10 +37,10 @@ Slet - Den valgte skanner kan ikke findes. + Den valgte scanner blev ikke fundet. - Den valgte skanner er offline. + Den valgte scanner er offline. Du har ikke rettigheder til gemme filer på dette sted. @@ -58,22 +58,22 @@ Exchangeable Image File (*.exif) - GIF File (*.gif) + GIF-fil (*.gif) - JPEG File (*.jpg, *.jpeg) + JPEG- Fil (*.jpg, *.jpeg) PDF dokument (*.pdf) - PNG File (*.png) + PNG-fil (*.png) - TIFF File (*.tiff, *.tif) + TIFF-fil (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG2000-fil (*.jp2, *.jpx) Navn mangler. @@ -85,7 +85,7 @@ Ingen enheder valgt. - Ingen skannerenheder blev fundet. + Ingen scannincsenheder blev fundet. Overskriv fil @@ -94,10 +94,10 @@ {0} af {1} - Skannet billede + Scannet billede - Vælg profil før klik på skan. + Vælg en profil før du klikker på scan. Der opstod en fejl med scanner driveren. @@ -106,7 +106,7 @@ Version {0} - En fejl opstod under forsøg på at sende en email. + Der opstod en fejl under forsøget på at sende en e-mail. Installation er færdig @@ -151,7 +151,7 @@ Tilpasset ({0}x{1} {2}) - Skan + Scan Er du sikker på at du vil fortryde din ændring(er) af {0} billede(r)? @@ -163,13 +163,13 @@ Du har ændringer, der ikke er gemt. Er du sikker på, at du vil afslutte uden at gemme ændringerne? - An operation is in progress. Are you sure you want to exit and cancel the operation? + En handling er i gang. Er du sikker på, at du vil afslutte og annullere handlingen? Ikke-gemte ændringer - Operation in Progress + Handling er i gang Scanner side {0} @@ -190,7 +190,7 @@ af {0} - En ukendt fejl opstod under batch scanning. + En ukendt fejl opstod under batch-scanning. Batch annulleret. @@ -256,7 +256,7 @@ Gemmer {0}... - Print + Udskriv Kopierer... @@ -268,10 +268,10 @@ Importerer... - Email PDF + E-mail til PDF - Email PDF fremskridt + E-mail PDF fremskridt En fejl opstod under forsøg på på at gemme automatisk. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Vil du virkelig slette {0} element(er)? Filen kunne ikke overskrives, fordi den er i brug. @@ -328,64 +328,70 @@ En ekstra komponent er krævet for at importere denne fil. Vil du downloade komponenten nu? - Are you sure you want to cancel the batch scan? + Er du sikker på du vil annullere batch-scanningen? - Cancel Batch + Annuller batch - Donate + Doner - NAPS2 is completely free. Consider making a donation. + NAPS2 er helt gratis. Overvej at foretage en donation. + + + Leave a Review + + + Like NAPS2? - The SANE driver is not available. Make sure to install the required packages: + SANE-driveren er ikke tilgængelig. Kontroller at du har installeret den påkrævede pakke: - The OCR engine is not available. Make sure to install the required package: + OCR-motoren er ikke tilgængelig. Kontroller at du har installeret den påkrævede pakke: - The selected driver is not supported on this system. + Den valgte driver er ikke understøttet på dette system. - OCR Progress + OCR-fremskridt - Running OCR... + Udfører OCR... - Update Progress + Opdateringsstatus - Updating... + Opdaterer... - No updates available. + Der er ingen tilgængelige opdateringer. - Checking... + Søger... - Update checking is disabled. + Opdateringstjek er slået fra. - An error occured when trying to install the update. + Der opstod en fejl under opdateringen. - Install {0} + Installer {0} - An update is available. + En opdatering er tilgængelig. - An error occurred when trying to authorize. + Der opstod en fejl forsøg på godkendelse. - Uploading email... + Uploader e-mail... - An error occurred when trying to send the email. + Der opstod en fejl under forsøget på at sende e-mailen. Scanner side {0}... diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.de.resx b/NAPS2.Lib/Lang/Resources/MiscResources.de.resx index 8b9e9c8759..bc1a541037 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.de.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.de.resx @@ -73,7 +73,7 @@ TIFF Datei (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG2000 Datei (*.jp2, *.jpx) Name fehlt. @@ -91,7 +91,7 @@ Datei überschreiben - {0} of {1} + {0} von {1} Eingelesenes Bild @@ -190,7 +190,7 @@ von {0} - Ein unbekannter Fehler ist während der Stapelverarbeitung aufgetreten. + Beim Stapel-Scan ist ein unbekannter Fehler aufgetreten. Stapelverarbeitung abgebrochen. @@ -268,10 +268,10 @@ Import... - Email PDF + E-Mail als PDF - Email PDF Fortschritt + E-Mail PDF Fortschritt Beim automatischen Speichern der Datei ist ein Fehler aufgetreten. @@ -301,7 +301,7 @@ Bild gespeichert. - {0} bilder gespeichert. + {0} Bilder gespeichert. PDF gespeichert. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Sind Sie sicher, dass sie "{0}" löschen möchten? Die Datei konnte nicht überschrieben werden, da sie im Moment verwendet wird. @@ -331,7 +331,7 @@ Sind Sie sicher, dass Sie den batch-scan abbrechen möchten? - Stapelverarbeitung abbrechen + Stapelverarbeitung abbrechen Spenden @@ -339,6 +339,12 @@ NAPS2 ist vollständig gratis. Unterstützen Sie uns mit einer Spende. + + Eine Bewertung hinterlassen + + + Gefällt Ihnen NAPS2? + Der SANE-Treiber ist nicht verfügbar. Stelle sicher, dass die erforderlichen Pakete installiert sind: @@ -367,7 +373,7 @@ Prüfe... - Update checking is disabled. + Updateprüfung ist deaktiviert. Beim Versuch das Update zu installieren ist ein Fehler aufgetreten. @@ -391,6 +397,6 @@ Seite {0} wird eingescannt... - Daten erfassen... + Datenerfassung... \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.el.resx b/NAPS2.Lib/Lang/Resources/MiscResources.el.resx index acaa897c88..cd24326d93 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.el.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.el.resx @@ -16,7 +16,7 @@ Επιλογή Προφίλ - Ολική Διαγραφή + Εκκαθάριση Θέλετε σίγουρα ολική διαγραφή ({0} αντικείμενα); @@ -49,7 +49,7 @@ Σφάλμα - Αρχεία BMP (*.bmp) + Αρχεία Bitmap (*.bmp) Αρχεία EMF (*.emf) @@ -73,7 +73,7 @@ Αρχεία TIFF (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + Αρχείο JPEG2000 (*.jp2, *.jpx) Απουσιάζει το όνομα. @@ -163,13 +163,13 @@ Υπάρχουν αλλαγές που δεν έχουν αποθηκευτεί. Θέλετε σίγουρα να τερματιστεί η εφαρμογή; - An operation is in progress. Are you sure you want to exit and cancel the operation? + Υπάρχει εργασία σε εξέλιξη. Είστε σίγουρος ότι θέλετε την ακύρωσή της και έξοδο; Μη Αποθηκευμένες Αλλαγές - Operation in Progress + Εργασία σε Εξέλιξη Σάρωση σελίδας {0} @@ -190,10 +190,10 @@ από {0} - Άγνωστο σφάλμα κατά την μαζική σάρωση. + Ένα άγνωστο σφάλμα προέκυψε κατά την μαζική σάρωση. - Η μαζική ακυρώθηκε. + Η μαζική επεξεργασία ακυρώθηκε. Ακύρωση.... @@ -214,7 +214,7 @@ Σε αναμονή για σάρωση {0}... - Άκυρο + Ακύρωση Κλείσιμο @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Θέλετε σίγουρα να διαγραφεί το '{0}'; Το αρχείο είναι σε χρήση και είναι αδύνατο να αντικατασταθεί. @@ -339,23 +339,29 @@ Το NAPS2 είναι ελεύθερο λογισμικό. Αξιολογήστε την επιλογή μιας δωρεάς. + + Leave a Review + + + Like NAPS2? + - The SANE driver is not available. Make sure to install the required packages: + Ο οδηγός SANE δεν είναι διαθέσιμος. Βεβαιωθείτε ότι έχετε εγκαταστήσει τα απαιτούμενα πακέτα: - The OCR engine is not available. Make sure to install the required package: + Η εφαρμογή OCR δεν είναι διαθέσιμη. Βεβαιωθείτε ότι έχετε εγκαταστήσει το απαιτούμενο πακέτο: - The selected driver is not supported on this system. + Ο επιλεγμένος οδηγός δεν υποστηρίζεται σε αυτό το σύστημα. - OCR Progress + Πρόοδος OCR Γίνεται Οπτική Αναγνώριση Χαρακτήρων.... - Update Progress + Εξέλιξη ενημέρωσης Γίνεται ενημέρωση... @@ -367,13 +373,13 @@ Έλεγχος... - Update checking is disabled. + Ο έλεγχος ενημερώσεων είναι απενεργοποιημένος. - An error occured when trying to install the update. + Προέκυψε σφάλμα κατά την εγκατάσταση της ενημέρωσης. - Install {0} + Εγκατάσταση {0} Δεν υπάρχουν διαθέσιμες ενημερώσεις. @@ -382,7 +388,7 @@ Παρουσιάστηκε ένα σφάλμα κατά την προσπάθεια εκτέλεσης του. - Uploading email... + Ανέβασμα email... Δημιουργήθηκε σφάλμα κατά την εκτύπωση του αρχείου. @@ -391,6 +397,6 @@ Σάρωση σελίδας {0}... - Απόκτηση δεδομένων... + Συλλογή δεδομένων... \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.es.resx b/NAPS2.Lib/Lang/Resources/MiscResources.es.resx index 3837052ffa..df55c0efa7 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.es.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.es.resx @@ -73,7 +73,7 @@ Imagen TIFF (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + Archivo JPEG2000 (*.jp2, *.jpx) Falta nombre. @@ -190,7 +190,7 @@ de {0} - Ocurrió un error desconocido durante el escaneo por lotes. + Se ha producido un error desconocido durante el escaneo por lotes. Proceso por lotes cancelado. @@ -226,7 +226,7 @@ El escáner seleccionado no admite el uso dúplex. Si el escáner si admite dúplex, intente utilizar un controlador diferente. - {0} / {1} + {0} / {1} MB Importando {0}... @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + ¿Está seguro de que desea eliminar "{0}"? El archivo no se pudo sobrescribir porque actualmente está en uso. @@ -339,6 +339,12 @@ NAPS2 es completamente gratuito. Anímese a hacer una donación. + + Dejar una reseña + + + ¿Te gusta NAPS2? + El controlador SANE no está disponible. Asegúrese de instalar los paquetes necesarios: @@ -367,7 +373,7 @@ Comprobando... - Update checking is disabled. + La verificación de actualizaciones está desactivada. Ha ocurrido un error al intentar instalar la actualización. diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.et.resx b/NAPS2.Lib/Lang/Resources/MiscResources.et.resx index 80741bc0b5..66755312ca 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.et.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.et.resx @@ -181,7 +181,7 @@ Sööturis ei ole ühtegi lehte. - Teil pole luba sisu kopeerimiseks failist \"{0}\". + Teil pole luba sisu kopeerimiseks failist "{0}". Faili salvestamisel tekkis viga. @@ -190,7 +190,7 @@ of {0} - An unknown error ocurred during the batch scan. + An unknown error occurred during the batch scan. Batch cancelled. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Oled sa kindel, et soovid {0} elementi kustutada? Faili ei saa üle kirjutada, kuna see on praegu kasutusel. @@ -339,6 +339,12 @@ NAPS2 is completely free. Consider making a donation. + + Leave a Review + + + Like NAPS2? + The SANE driver is not available. Make sure to install the required packages: diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.fa.resx b/NAPS2.Lib/Lang/Resources/MiscResources.fa.resx index 6e825e8ae2..fe6a9af84d 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.fa.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.fa.resx @@ -190,7 +190,7 @@ از {0} - خطای ناشناخته‌ای هنگام اسکن دسته‌ای. + An unknown error occurred during the batch scan. اسکن دسته‌ای لغو شد. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + آیا مطمئنید که می‌خواهید {0} حذف کنید؟ پرونده قابل بازنویسی نیست زیاد در حال استفاده است.. @@ -339,6 +339,12 @@ NAPS2 کاملا رایگان است. کمک مالی کنید.. + + Leave a Review + + + Like NAPS2? + درایور SANE موجود نیست. مطمئن شوید بسته‌های مورد نیاز را نصب کرده‌اید.: diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.fi.resx b/NAPS2.Lib/Lang/Resources/MiscResources.fi.resx index a754e4194e..73ca09a33a 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.fi.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.fi.resx @@ -55,7 +55,7 @@ Edistynyt Windows-metatiedosto (*.emf) - Exchangeable Image File (*.exif) + Exif-tiedosto (*.exif) GIF-tiedosto (*.gif) @@ -73,7 +73,7 @@ TIFF-tiedosto (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG2000-tiedosto (*.jp2, *.jpx) Nimi puuttuu. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Haluatko varmasti poistaa {0}? Tiedostoa ei voitu korvata, koska se on käytössä. @@ -339,6 +339,12 @@ NAPS2 on täysin ilmainen. Harkitsethan lahjoitusta. + + Jätä arviosi + + + Pidätkö NAPS2:sta? + SANE - ajuri ei ole käytettävissä. Asenna tarvittava osat: @@ -367,7 +373,7 @@ Tarkistetaan... - Update checking is disabled. + Päivityksen tarkistus on poistettu käytöstä. Virhe päivityksen asennuksessa. diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.fr.resx b/NAPS2.Lib/Lang/Resources/MiscResources.fr.resx index 2ee7529bd1..1c16013878 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.fr.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.fr.resx @@ -16,7 +16,7 @@ Choisir un profil - Tout supprimer + Tout effacer Faut-il vraiment effacer {0} élément(s) ? @@ -25,7 +25,7 @@ Faut-il vraiment supprimer {0} élément(s) ? - Faut-il vraiment supprimer {0} profils ? + Faut-il vraiment supprimer {0} profil(s) ? Faut-il vraiment supprimer le profil « {0} » ? @@ -73,7 +73,7 @@ Fichier TIFF (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + Fichier JPEG2000 (*.jp2, *.jpx) Nom manquant. @@ -145,7 +145,7 @@ Erreur de téléchargement - Au moins un fichier n'a pas pu être téléchargé. + Un ou plusieurs fichiers n'ont pas pu être téléchargés. Personnalisé ({0}x{1} {2}) @@ -157,7 +157,7 @@ Faut-il vraiment défaire les modifications sur {0} image(s) ? - Réinitialisation de l'image + Réinitialiser l'image Il existe des modifications non enregistrées. Faut-il vraiment quitter et les perdre ? @@ -175,13 +175,13 @@ Acquisition de la page {0} - Le scanner sélectionné ne gère pas de dispositif d'alimentation. Si ce scanner en possède un, essayer d'utiliser un autre pilote. + Le scanner sélectionné ne semble pas disposer de dispositif d'alimentation. S'il en possède un, essayer d'utiliser un autre pilote. Il n'y a aucune page dans le dispositif d'alimentation. - Vous n'avez pas la permission de copier le contenu du fichier « {0} ».. + Vous n'avez pas la permission de copier le contenu du fichier « {0} ». Une erreur est survenue en essayant d'enregistrer le fichier. @@ -190,28 +190,28 @@ de {0} - Une erreur inconnue est survenue lors de l'acquisition par lot. + Une erreur inconnue s'est produite lors de l'acquisition par lot. Lot annulé. - Annulation.... + Annulation… Lot terminé avec succès. - Acquisition par lot arrêtée avec l'erreur. + Acquisition par lot arrêtée en raison d'une erreur. Acquisition de la page {0}... - Acquisition page {0} (numérisation {1})... + Acquisition page {0} (numérisation {1})… - En attente de l'acquisition {0}... + En attente de l'acquisition {0}… Annuler @@ -220,22 +220,22 @@ Fermer - Enregistrement des acquisitions par lot... + Enregistrement des acquisitions par lot… - Le scanner sélectionné ne gère pas le recto-verso. Si ce scanner est supposé le gérer, essayer d'utiliser un autre pilote. + Le scanner sélectionné ne semble pas gérer pas le recto-verso (duplex). S'il est supposé le prendre en charge, essayer d'utiliser un autre pilote. {0} / {1} - Importation de « {0} »... + Importation de « {0} »… Progression de l'importation - Récupération... + Récupération… Progression de la récupération @@ -253,19 +253,19 @@ Progression de l'enregistrement en PDF - Enregistrement de « {0} »... + Enregistrement de « {0} »… Imprimer - Copie... + Copie… Progression de la copie - Importation... + Importation… PDF par courriel @@ -301,7 +301,7 @@ Image enregistrée. - {0} images enregistrées. + {0} image(s) enregistrée(s). PDF enregistré. @@ -310,13 +310,13 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Faut-il vraiment supprimer « {0} » ? Le fichier ne peut pas être écrasé car il est en cours d'utilisation. - Réalignement... + Réalignement… Progression du réalignement @@ -334,16 +334,22 @@ Annuler le lot - Don + Faire un don NAPS2 est gratuit, mais il est possible de faire un don. + + Laisser un avis + + + Aimez-vous NAPS2 ? + - Le pilote SANE n'est pas disponible. Merci d'installer les paquets requis: + Le pilote SANE n'est pas disponible. Merci d'installer les paquets nécessaires : - Le moteur OCR n'est pas disponible. Merci d'installer le paquet requis: + Le moteur OCR n'est pas disponible. Merci d'installer le paquet requis : Le pilote sélectionné n'est pas pris en charge sur ce système. @@ -352,22 +358,22 @@ Progression de l'OCR - OCR en exécution... + OCR en cours… Progression de la mise à jour - Mise à jour... + Mise à jour… Aucune mise à jour disponible. - Vérification... + Vérification… - Update checking is disabled. + Vérification des mises à jour désactivée. Une erreur est survenue en essayant d'installer la mise à jour. @@ -376,13 +382,13 @@ Installer {0} - Une mise à jour est disponible. + Mise à jour disponible. - Une erreur est survenue en essayent d'autoriser. + Une erreur est survenue en essayant d'autoriser. - Envoi du courriel... + Envoi du courriel… Une erreur est survenue en essayant d'envoyer le courriel. @@ -391,6 +397,6 @@ Acquisition de la page {0}... - Acquisition des données... + Acquisition des données… \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.he.resx b/NAPS2.Lib/Lang/Resources/MiscResources.he.resx index bbc709181d..9e69f04556 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.he.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.he.resx @@ -13,25 +13,25 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - בחר פרופיל + בחירת פרופיל - מחק הכל + פינוי - בטוחים שתרצו למחוק {0} פריט/ים? + לפנות {0} פריט/ים? - בטוחים שתרצו למחוק {0} פריט/ים? + למחוק {0} פריט/ים? - בטוחים שתרצו למחוק את הפרופיל/ים {0}? + למחוק {0} פרופיל/ים? - בטוחים שתרצו למחוק את הפרופיל {0}? + למחוק את הפרופיל {0}? - הקובץ {0} כבר קיים. האם להחליפו? + הקובץ {0} כבר קיים. להחליפו? מחיקה @@ -40,7 +40,7 @@ הסורק שנבחר לא נמצא. - הסורק שנבחר איננו מקוון (OFFLINE). + הסורק שנבחר איננו מקוון. אין ברשותכם הרשאות מספיקות לשמירת הקובץ במיקום זה. @@ -49,37 +49,37 @@ שגיאה - קובץ מפת סיביות (*.bmp) + קובץ מפת סיביות (‎*.bmp) - קובצי מטא מורחבים (‎*.emf) + קובצי על מורחבים (‎*.emf) - קובץ תמונה בר-החלפה (*.exif) + קובץ תמונה בר־החלפה (‎*.exif) - קובץ GIF (*.gif) + קובץ GIF‏ (‎*.gif) - קובץ JPEG (*.jpg, *.jpeg) + קובץ JPEG‏ (‎*.jpg,‏ ‎*.jpeg) - PDF Document (*.pdf) + מסמך PDF‏ (‎*.pdf) קובץ PNG (*.png) - קובץ TIFF (*.tiff, *.tif) + קובץ TIFF‏ (‎*.tiff,‏ ‎*.tif) - JPEG2000 File (*.jp2, *.jpx) + קובץ JPEG2000‏ (‎*.jp2,‏ ‎*.jpx) שם חסר. - NAPS2 + לא עוד סתם סורק 2 לא נבחר התקן. @@ -106,16 +106,16 @@ גרסה {0} - תקלה בשליחת דוא\"ל. + תקלה בשליחת דוא"ל. ההתקנה הושלמה - ההתקנה הושלמה. האם ברצונך לאתחל את התוכנה? + ההתקנה הושלמה. להפעיל את התוכנה מחדש עכשיו? - תקלה בהתקנה. + ההתקנה נכשלה. תקלה בהתקנה @@ -127,22 +127,22 @@ מסומנים ({0}) - גודל מוערך להורדה: {0} מ\"ב + גודל מוערך להורדה: {0} מ״ב {0} / {1} קבצים - {0} / {1} מ\"ב + {0} / {1} מ״ב - לא ניתן לייבא את הקובץ '{0}'. + לא ניתן לייבא את הקובץ ‚{0}’. - לא ניתן לייבא את הקובץ '{0}'. ניתן לייבא רק קבצים שנוצרו על ידי תוכנה זו. + לא ניתן לייבא את הקובץ ‚{0}’. ניתן לייבא רק קבצים שנוצרו על ידי תוכנה זו. - תקלה בהורדה + שגיאה בהורדה תקלה בהורדת קובץ אחד או יותר. @@ -154,28 +154,28 @@ סריקה - לבטל שינויים ב-{0} תמונה/תמונות? + להחזיר את השינויים שביצעת ב־{0} תמונות? - אפס תמונה + איפוס תמונה ישנם שינויים שלא נשמרו. האם לסגור את התוכנה ולאבד את השינויים הללו? - פעולה מתבצעת. האם אתה בטוח שברצונך לצאת ולבטל את הפעולה? + פעולה מתבצעת. לצאת ולבטל את הפעולה? שינויים שלא נשמרו - Operation in Progress + פעולה מתבצעת - סורק עמוד {0} + עמוד {0} נסרק - הסורק שנבחר איננו תומך במזין מסמכים. אם לסורק יש מזין מסמכים, נסה להשתמש במנהל התקן אחר. + הסורק שנבחר איננו תומך במזין מסמכים. אם לסורק יש מזין מסמכים, כדאי לנסות להשתמש במנהל התקן אחר. אין דפים במזין המסמכים. @@ -190,70 +190,70 @@ מתוך {0} - שגיאה בלתי ידוע אירע בעת סריקת אצווה. + אירעה שגיאה בלתי־ידועה במהלך הסריקה במרוכז. - האצווה בוטלה. + הסריקה במרוכז בוטלה. - Cancelling.... + בהליכי ביטול… - אצווה הושלמה בהצלחה. + סריקה במרוכז הושלמה בהצלחה. - סריקה אצווה נעצרה עקב שגיאה. + סריקה במרוכז נעצרה עקב שגיאה. - סורק עמוד {0}... + עמוד {0} נסרק… - Scanning page {0} (scan {1})... + עמוד {0} נסרק (סריקה {1})… - Waiting for scan {0}... + בהמתנה לסריקה {0}… ביטול - Close + סגירה - Saving batch results... + תוצאות הסריקה במרוכז נשמרות… - The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + הסורק שנבחר לא תומך במצב דופלקס (סריקה דו־צדדית). אם הסורק שלך אמור לתמוך בדופלקס, כדאי לנסות להשתמש במנהל התקן אחר. {0} / {1} - Importing {0}... + מתבצע ייבוא של {0}… - Import Progress + התקדמות ייבוא - Recovering... + מתבצע שחזור… - Recovery Progress + התקדמות שחזור לשמור תמונות - Save Images Progress + התקדמות שמירת תמונות לשמור PDF - Save PDF Progress + התקדמות שמירת PDF - Saving {0}... + {0} נשמר… הדפסה @@ -265,43 +265,43 @@ התקדמות העתקה - מייבא... + מתבצע ייבוא… - שליחת PDF בדוא\"ל + שליחת PDF בדוא״ל - Email PDF Progress + התקדמות שליחת PDF בדוא״ל - אירעה שגיאה בעת שמירת אוטומטי. + אירעה שגיאה בעת שמירה אוטומטית. כל הקבצים - קבצי תמונה + קובצי תמונה - The selected scanner is busy. + הסורק הנבחר עסוק. - The scanner's cover is open. + המכסה של הסורק פתוח. - The scanner has a paper jam. + נתקע דף בסורק. - The scanner is warming up. + הסורק מתחמם. - עדכון ל- OCR זמין. + יש עדכון לזיהוי התווים האופטי. התמונה נשמרה. - {0} images saved. + {0} תמונות נשמרו. PDF נשמר. @@ -310,87 +310,93 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + למחוק את „{0}”? - The file could not be overwritten because it is currently in use. + לא ניתן לדרוס את הקובץ כיוון שהוא בשימוש כרגע. - Deskewing... + ההטייה מתוקנת… - Deskew Progress + התקדמות תיקון הטייה - Download Needed + נדרשת הורדה נדרש רכיב נוסף כדי לייבא קובץ PDF זה. האם ברצונך להוריד אותו כעת? - האם אתה בטוח שאתה רוצה לבטל את קבוצות הסריקות? + לבטל את הסריקות במרוכז? - Cancel Batch + ביטול סריקה במרוכז - תרום + תרומה - NAPS2 הוא לגמרי בחינם. שקול לבצע תרומה. + NAPS2 הוא לגמרי בחינם. נא לשקול לתרום לנו. + + + כתיבת ביקורת + + + אהבת את NAPS2? - The SANE driver is not available. Make sure to install the required packages: + מנהל התקן ה־SANE לא זמין. נא לוודא שהתקנת את החבילות הנדרשות: - The OCR engine is not available. Make sure to install the required package: + מנוע זיהוי התווים האופטי לא זמין. נא לוודא שהתקנת את החבילות הנדרשות: - The selected driver is not supported on this system. + מנהל ההתקן הנבחר לא נתמך במערכת הזאת. - OCR Progress + התקדמות זיהוי תווים אופטי - Running OCR... + זיהוי התווים האופטי רץ… - Update Progress + התקדמות העדכון - Updating... + מתבצע עדכון… - No updates available. + אין עדכונים זמינים. - Checking... + מתבצעת בדיקה… - Update checking is disabled. + בדיקת עדכונים מושבתת. אירעה שגיאה בעת ניסיון להתקין את העדכון. - Install {0} + התקנת {0} קיים עדכון זמין. - An error occurred when trying to authorize. + אירעה שגיאה בניסיון לאימות. - Uploading email... + ההודעה נשלחת בדוא״ל… - שגיאה התרחשה בעת הנסיון לשלוח מייל.. + שגיאה התרחשה בעת הנסיון לשלוח דוא״ל. - סורק עמוד {0}... + עמוד {0} נסרק… - מייבא נתונים... + נתונים מתקבלים… \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.hi.resx b/NAPS2.Lib/Lang/Resources/MiscResources.hi.resx new file mode 100644 index 0000000000..5c3a502016 --- /dev/null +++ b/NAPS2.Lib/Lang/Resources/MiscResources.hi.resx @@ -0,0 +1,402 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + प्रोफ़ाइल चुनें + + + क्लियर करें + + + क्या आप वाकई {0} आइटम साफ़ करना चाहते हैं? + + + क्या आप वाकई {0} आइटम हटाना चाहते हैं? + + + क्या आप वाकई {0} प्रोफ़ाइल हटाना चाहते हैं? + + + क्या आप वाकई प्रोफ़ाइल {0} को हटाना चाहते हैं? + + + The file {0} already exists. Do you want to overwrite it? + + + नष्ट करें + + + The selected scanner could not be found. + + + The selected scanner is offline. + + + You don't have permission to save files at this location. + + + त्रुटि + + + बिटमैप फ़ाइलें (*.bmp) + + + Enhanced Windows MetaFile (*.emf) + + + Exchangeable Image File (*.exif) + + + GIF File (*.gif) + + + JPEG File (*.jpg, *.jpeg) + + + PDF Document (*.pdf) + + + PNG File (*.png) + + + TIFF File (*.tiff, *.tif) + + + JPEG2000 File (*.jp2, *.jpx) + + + नाम गायब है। + + + NAPS2 + + + कोई उपकरण चयनित नहीं। + + + कोई स्कैनिंग उपकरण नहीं मिला। + + + Overwrite File + + + {0} of {1} + + + Scanned Image + + + Select a profile before clicking Scan. + + + स्कैनिंग ड्राइवर के साथ कोई त्रुटि उत्पन्न हुई. + + + Version {0} + + + ईमेल भेजने का प्रयास करते समय एक त्रुटि उत्पन्न हुई. + + + अधिस्थापना पूर्ण + + + अधिस्थापना पूर्ण। क्या आप अब NAPS2 को पुनः आरंभ करना चाहते हैं? + + + अधिस्थापना विफल। + + + अधिस्थापना विफल + + + सभी ({0}) + + + Selected ({0}) + + + Estimated download size: {0} MB + + + {0} / {1} files + + + {0} / {1} MB + + + The file '{0}' could not be imported. + + + The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported. + + + डाउनलोड एरर + + + One or more files could not be downloaded. + + + पसंद के अनुसार ({0}x{1} {2}) + + + स्कैन करें + + + क्या आप वाकई {0} छवि(छवियों) में अपने परिवर्तन पूर्ववत करना चाहते हैं? + + + Reset Image + + + You have unsaved changes. Are you sure you want to exit and discard those changes? + + + एक ऑपरेशन चल रहा है. क्या आप वाकई बाहर निकलना और ऑपरेशन रद्द करना चाहते हैं? + + + Unsaved Changes + + + Operation in Progress + + + Scanning page {0} + + + The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. + + + फीडर में कोई पेज नहीं है। + + + You do not have permission to copy content from the file '{0}'. + + + फ़ाइल को सहेजने का प्रयास करते समय एक त्रुटि उत्पन्न हुई. + + + of {0} + + + बैच स्कैन के दौरान एक अज्ञात त्रुटि उत्पन्न हुई. + + + बैच रद्द कर दिया गया. + + + रद्द किया जा रहा है... + + + बैच सफलतापूर्वक पूरा हुआ. + + + त्रुटि के कारण बैच स्कैन रुक गया। + + + Scanning page {0}... + + + Scanning page {0} (scan {1})... + + + Waiting for scan {0}... + + + रद्द करें + + + बंद करे + + + खेप परिणाम सेव कारा जा रहा है... + + + The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + + + {0} / {1} + + + आयत हो रहा है {0}... + + + आयात की प्रगति + + + पुनर्प्राप्त हो रही है... + + + Recovery Progress + + + Save Images + + + Save Images Progress + + + पीडीएफ सेव करें + + + पीडीएफ की प्रगति सेव करें + + + सेव कारा जा रहा है {0}... + + + Print + + + कॉपी करी जा रही है | + + + कॉपी प्रगति + + + आयत हो रहा है + + + पीडीएफ(PDF) ईमेल करे + + + Email PDF Progress + + + स्वतः सहेजने का प्रयास करते समय एक त्रुटि उत्पन्न हुई. + + + सभी फाइलें + + + इमेज फाइल + + + The selected scanner is busy. + + + The scanner's cover is open. + + + The scanner has a paper jam. + + + The scanner is warming up. + + + OCR का अपडेट उपलब्ध है. + + + Image saved. + + + {0} images saved. + + + PDF saved. + + + {0} ({1}x{2} {3}) + + + क्या आप {0} को डिलीट करना चाहते हैं? + + + The file could not be overwritten because it is currently in use. + + + तिरछापन दूर किया जा रहा है... + + + तिरछापन दूर करने की प्रगति + + + डाउनलोड की आवश्यकता है + + + इस पीडीएफ फ़ाइल को आयात करने के लिए एक अतिरिक्त घटक की आवश्यकता है। क्या आप इसे अभी डाउनलोड करना चाहेंगे? + + + क्या आप बैच स्कैन रद्द करना चाहते हैं? + + + बैच रद्द करें + + + दान करें + + + NAPS2 पूरी तरह से मुफ़्त है। दान देने पर विचार करें। + + + Leave a Review + + + Like NAPS2? + + + The SANE driver is not available. Make sure to install the required packages: + + + The OCR engine is not available. Make sure to install the required package: + + + The selected driver is not supported on this system. + + + OCR Progress + + + Running OCR... + + + Update Progress + + + Updating... + + + कोई अपडेट उपलब्ध नहीं है। + + + जांच की जा रही है + + + Update checking is disabled. + + + अद्यतन स्थापित करने का प्रयास करते समय एक त्रुटि उत्पन्न हुई। + + + अधिस्थापित {0} + + + एक अपडेट उपलब्ध है। + + + अधिकृत करने का प्रयास करते समय एक त्रुटि उत्पन्न हुई। + + + Uploading email... + + + ईमेल भेजने का प्रयास करते समय एक त्रुटि उत्पन्न हुई. + + + Scanning page {0}... + + + डेटा प्राप्त किया जा रहा है... + + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.hr.resx b/NAPS2.Lib/Lang/Resources/MiscResources.hr.resx index f820fdbfe3..e3176d62c5 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.hr.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.hr.resx @@ -16,16 +16,16 @@ Odaberite profil - Očisti + Izbriši - Jeste li sigurni da želite ukloniti sledeći broj datoteka: {0}? + Jeste li sigurni da želite obrisati {0} stavku(i)? - Jeste li sigurni da želite obrisati sledeći broj datoteka: {0}? + Jeste li sigurni da želite obrisati sledeći broj stavki: {0}? - Jeste li sigurni da želite obrisati {0} profila? + Jeste li sigurni da želite obrisati {0} profil(a)? Jeste li sigurni da želite obrisati profil {0}? @@ -34,16 +34,16 @@ Datoteka {0} već postoji. Želite li ju presnimiti? - Obriši + Izbriši - Odabrani skener nije pronađen. + Odabrani skener nije moguće pronaći. - Odabrani skener nije uključen. + Odabrani skener je izvan mreže. - Nemate dopuštenja da snimite datoteke na ovu lokaciju. + Nemate dopuštenje za spremanje datoteka na ovu lokaciju. Greška @@ -52,10 +52,10 @@ Bitmap datoteka (*.bmp) - Poboljšani Windows MetaFile (*.emf) + Enhanced Windows MetaFile (*.emf) - Razmenjiva slika (*.exif) + Exchangeable Image File (*.exif) GIF datoteka (*.gif) @@ -64,7 +64,7 @@ JPEG datoteka (*.jpg, *.jpeg) - PDF Document (*.pdf) + PDF dokument (*.pdf) PNG datoteka (*.png) @@ -73,7 +73,7 @@ TIFF datoteka (*.tiff, *tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG2000 datoteka (*.jp2, *.jpx) Nedostaje ime. @@ -82,10 +82,10 @@ NAPS2 - Uređaj nije odabran. + Nije odabran uređaj. - Uređaj za skeniranje nije pronađen. + Nije pronađen uređaj za skeniranje. Presnjimiti datoteku? @@ -97,22 +97,22 @@ Skenirana slika - Izaberite profil pre nego kliknete dugme za skeniranje. + Odaberite profil prije nego što kliknete Skeniraj. - Pogreška s upravljačkim programom za skener. + Dogodila se pogreška s upravljačkim programom za skeniranje. Verzija {0} - Pogreška pri slanju e-pošte. + Došlo je do pogreške prilikom pokušaja slanja e-pošte. Instalacija završena - Instalacija uspešna. Želite li sada restartovati NAPS2? + Instalacija je završena. Želite li sada ponovno pokrenuti NAPS2? Instalacija nije uspjela. @@ -139,13 +139,13 @@ Datoteka '{0}' nije mogla biti uvezena. - Datoteka '{0}' nije mogla biti uvezena. Samo PDF datoteke generirane od strane NAPS2 mogu biti uvezene. + Datoteka '{0}' nije mogla biti uvezena. Moguće je uvesti samo PDF datoteke generirane pomoću NAPS2. - Greška u preuzimanju + Greška preuzimanja - Jedna ili više datoteka nisu mogle biti skinute. + Jednu ili više datoteka nije bilo moguće preuzeti. Custom ({0}x{1} {2}) @@ -154,43 +154,43 @@ Skeniraj - Jeste li sigurni da želite vratiti promjene na {0} slika? + Jeste li sigurni da želite poništiti promjene na {0} slike(a)? - Reset Image + Resetiraj sliku - You have unsaved changes. Are you sure you want to exit and discard those changes? + Imate nespremljene promjene. Jeste li sigurni da želite izaći i odbaciti te promjene? - An operation is in progress. Are you sure you want to exit and cancel the operation? + Operacija je u tijeku. Jeste li sigurni da želite izaći i otkazati operaciju? - Unsaved Changes + Nespremljene promjene - Operation in Progress + Radnja u tijeku - Scanning page {0} + Skeniranje stranice {0} - The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. + Odabrani skener ne podržava korištenje ulagača. Ako vaš skener ima ulagač, pokušajte upotrijebiti drugi upravljački program. - No pages are in the feeder. + U ulagaču nema stranica. - Nemate dozvolu za kopiranje sadržaja iz datoteke '{0}'. + Nemate dopuštenje za kopiranje sadržaja iz datoteke '{0}'. - Pogreška pri spremanju datoteke. + Došlo je do pogreške prilikom pokušaja spremanja datoteke. - of {0} + od {0} - Nepoznata pogreška prilikom serijskog skeniranja. + An unknown error occurred during the batch scan. Serijsko Skeniranje otkazano. @@ -205,16 +205,16 @@ Serijsko Skeniranje prekinuto zbog pogreške. - Scanning page {0}... + Skeniranje stranice {0}... - Scanning page {0} (scan {1})... + Skeniranje stranice {0} (skeniraj {1})... - Waiting for scan {0}... + Čekam na skeniranje {0}... - Prekini + Odustani Zatvori @@ -223,7 +223,7 @@ Saving batch results... - The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + Odabrani skener ne podržava obostrano skeniranje. Ako bi vaš skener trebao podržavati obostrano skeniranje, pokušajte upotrijebiti drugi upravljački program. {0} / {1} @@ -232,88 +232,88 @@ Importing {0}... - Import Progress + Napredak uvoza - Recovering... + Oporavljam... - Recovery Progress + Napredak oporavka Spremi slike - Save Images Progress + Napredak spremanja slika Spremi PDF - Save PDF Progress + Napredak spremanja PDF-a Saving {0}... - Print + Ispis Kopiranje... - Napredak Kopiranja + Napredak kopiranja Importing... - Email PDF + Pošalji PDF e-poštom - Email PDF Progress + Napredak slanja PDF-a e-poštom - Pogreška pri automatskom spremanju. + Došlo je do pogreške prilikom pokušaja automatskog spremanja. Sve datoteke - Image Files + Slikovne datoteke - The selected scanner is busy. + Odabrani skener je zauzet. - The scanner's cover is open. + Poklopac skenera je otvoren. - The scanner has a paper jam. + U skeneru se zaglavio papir. The scanner is warming up. - Nadogradnja za OCR je dostupna. + Dostupno je ažuriranje za OCR. - Image saved. + Slika je spremljena. - {0} images saved. + {0} slike(a) spremljena(o). - PDF saved. + PDF spremljen. {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Jeste li sigurni da želite izbrisati "{0}"? - The file could not be overwritten because it is currently in use. + Datoteka se ne može prebrisati jer je trenutno u upotrebi. Proces popravljanja iskrivljene perspektive... @@ -322,10 +322,10 @@ Napredak popravljanja iskrivljene perspektive - Potrebno je preuzimanje + Potrebno preuzimanje - Dodatna komponenta je potrebna za uvoz ovog PDF-a. Želite li ju preuzeti? + Za uvoz ove PDF datoteke potrebna je dodatna komponenta. Želite li je sada preuzeti? Jeste li sigurni da želite prekinuti serijsko skeniranje? @@ -337,58 +337,64 @@ Doniraj - NAPS2 is completely free. Consider making a donation. + NAPS2 je potpuno besplatan. Razmotrite mogućnost donacije. + + + Ostavite recenziju + + + Sviđa Vam se NAPS2? - The SANE driver is not available. Make sure to install the required packages: + SANE upravljački program nije dostupan. Provjerite jeste li instalirali potrebne pakete: - The OCR engine is not available. Make sure to install the required package: + OCR program nije dostupan. Provjerite jeste li instalirali potrebni paket: - The selected driver is not supported on this system. + Odabrani upravljački program nije podržan na ovom sustavu. - OCR Progress + Napredak OCR-a Running OCR... - Update Progress + Napredak ažuriranja - Updating... + Ažuriranje... - No updates available. + Nema dostupnih ažuriranja. Checking... - Update checking is disabled. + Provjera ažuriranja je onemogućena. - An error occured when trying to install the update. + Došlo je do pogreške prilikom pokušaja instaliranja ažuriranja. Install {0} - An update is available. + Dostupno je ažuriranje. - An error occurred when trying to authorize. + Došlo je do pogreške prilikom pokušaja autorizacije. Uploading email... - An error occurred when trying to send the email. + Došlo je do pogreške prilikom pokušaja slanja e-pošte. - Scanning page {0}... + Skeniranje stranice {0}... Prikupljam podatke... diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.hu.resx b/NAPS2.Lib/Lang/Resources/MiscResources.hu.resx index 2420b50000..afccb936c5 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.hu.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.hu.resx @@ -16,40 +16,40 @@ Profil kiválasztása - Kép tisztítása + Összes törlése - Biztos hogy tisztítja a(z) {0} képet? + Biztos, hogy törölni szeretné a {0} elemet? - Biztos hogy törli mind a(z) {0} oldalt? + Biztos, hogy törölni szeretne {0} elemet? - Biztos hogy törli mind a(z) {0} profilt? + Biztos, hogy törölni szeretne {0} profilt? - Biztos hogy törli a következő profilt: {0}? + Biztos, hogy törölni szeretné a következő profilt: {0}? - Már létezik {0} nevű fájl, felül szeretné írni? + A következő fájl már létezik: {0}. Szeretné felülírni? - Oldal törlése + Törlés - A kiválasztott képolvasó nem található. + A kiválasztott lapolvasó nem található. - A kiválasztott képolvasó nem elérhető (leválasztva). + A kiválasztott lapolvasó inaktív. - Nincs hozzáférési egedélye az adott helyhez.. + Nincs jogosultsága fájlok mentésére ezen a helyen. Hiba - BMP fájl (*.bmp) + BMP fájlok (*.bmp) EMF fájl (*.emf) @@ -64,7 +64,7 @@ JPEG fájl (*.jpg, *.jpeg) - PDF Document (*.pdf) + PDF dokumentum (*.pdf) PNG fájl (*.png) @@ -73,10 +73,10 @@ TIFF fájl (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG2000 fájl (*.jp2, *.jpx) - Hiányzó név. + A név hiányzik. NAPS2 @@ -85,7 +85,7 @@ Nincs kiválasztott eszköz. - Nincs elérhető képolvasó. + Nem található lapolvasó eszköz. Fájl felülírása @@ -97,22 +97,22 @@ Beolvasott kép - Válasszon profilt a képolvasás elött. + Válasszon ki egy profilt, mielőtt a Beolvasás gombra kattint. - Hiba történt a képolvasó meghajtójával.. + Hiba történt a lapolvasó illesztőprogrammal. Verzió: {0} - Hiba történt az e-mail küldése során.. + Hiba történt e-mail küldése közben. A telepítés befejeződött - A telepítés sikeres. Újra szeretnéd indítani a NAPS2 programot most? + A telepítés befejeződött. Szeretné most újraindítani a NAPS2 programot? A telepítés sikertelen. @@ -121,25 +121,25 @@ A telepítés sikertelen - Mind ({0}) + Összes ({0}) - Kiválasztva ({0}) + Kiválasztott ({0}) Becsült letöltési méret: {0} MB - {0} / {1} files + {0} / {1} fájl {0} / {1} MB - A '{0}' fájl nem importálható.. + A következő fájlt nem lehetett importálni: '{0}'. - A '{0}' fájlt nem lehet importálni. Csak a NAPS2 által generált PDF fájlokat lehet importálni. + A következő fájlt nem lehetett importálni: '{0}'. Csak a NAPS2 által generált PDF fájlok importálhatók. Letöltési hiba @@ -148,22 +148,22 @@ Egy vagy több fájlt nem sikerült letölteni. - Custom ({0}x{1} {2}) + Egyéni ({0}x{1} {2}) - Képolvasás + Beolvasás - Are you sure you want undo your changes to {0} image(s)? + Biztos benne, hogy vissza szeretné vonni {0} kép módosítását? Kép visszaállítása - Nem mentett módosításai vannak, amik elvesznek ha bezárja a programot. Biztosan kilép? + Vannak el nem mentett módosításai. Biztos, hogy ki akar lépni és el akarja vetni ezeket a módosításokat? - An operation is in progress. Are you sure you want to exit and cancel the operation? + Egy művelet folyamatban van. Biztos, hogy ki szeretne lépni és meg szeretné szakítani a műveletet? Nem mentett módosítások @@ -172,46 +172,46 @@ Művelet folyamatban - Scanning page {0} + {0}. oldal beolvasása - The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. + A kiválasztott lapolvasó nem támogatja az lapadagoló használatát. Ha a lapolvasója rendelkezik lapadagolóval, próbáljon meg másik illesztőprogramot használni. - Nincs lap a beolvasóban. + Nincs lap a lapadagolóban. - You do not have permission to copy content from the file '{0}'. + Nincs jogosultsága a következő fájl tartalmát másolni: '{0}'. - An error occurred when trying to save the file. + Hiba történt a fájl mentésekor. - of {0} + / {0} - An unknown error ocurred during the batch scan. + Ismeretlen hiba történt a kötegelt lapolvasás során. - Batch cancelled. + A kötegelt feldolgozás törlésre került. Visszavonás.... - A folyamat befejeződött. + A kötegelt feldolgozás sikeresen befejeződött. - Hiba történt a művelet során, a folyamat megszakadt. + A kötegelt beolvasás hiba miatt megszakadt. - Scanning page {0}... + {0}. oldal beolvasása... - Scanning page {0} (scan {1})... + {0}. oldal beolvasása ({1}. beolvasás)... - Waiting for scan {0}... + Várakozás a beolvasásra {0}... Mégse @@ -220,10 +220,10 @@ Bezárás - Saving batch results... + A kötegelt feldolgozás eredményének mentése... - The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + A kiválasztott lapolvasó nem támogatja a kétoldalas használatot. Ha a lapolvasó feltehetőleg támogatja a kétoldalas használatot, próbáljon meg másik illesztőprogramot használni. {0} / {1} @@ -232,76 +232,76 @@ Importálás {0}... - Import Progress + Importálás folyamata Helyreállítás... - Helyreállítási folyamat + Helyreállítás folyamata Képek mentése - Save Images Progress + Képmentés folyamata - PDF mentése + PDF mentés - Save PDF Progress + PDF mentésének folyamata - Saving {0}... + Mentés {0}... Nyomtatás - Másolás folyamatban... + Másolás... - Folyamatban + Másolás folyamata Importálás... - PDF küldése Email-ben + PDF küldése e-mailben - Email PDF Progress + PDF e-mailben történő elküldésének folyamata - An error occurred when trying to auto save. + Hiba történt az automatikus mentés során. - Minden fájl + Összes fájl Képfájlok - A kiválasztott képolvasó nem elérhető (foglalt). + A kiválasztott lapolvasó foglalt. - The scanner's cover is open. + A lapolvasó fedele nyitva van. - The scanner has a paper jam. + A lapolvasóban papírelakadás van. - The scanner is warming up. + A lapolvasó felmelegedik. - An update to OCR is available. + Elérhető egy frissítés a karakterfelismeréshez. Kép mentve. - {0} images saved. + {0} kép mentve. PDF mentve. @@ -310,87 +310,93 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Biztos, hogy törölni szeretne "{0}" elemet? - The file could not be overwritten because it is currently in use. + A fájlt nem lehetett felülírni, mert jelenleg használatban van. - Visszatorzítás... + Kiegyenesítés... - Visszatorzítás folyamatban + Kiegyenesítés folyamata Letöltés szükséges - További bővitmény szükséges, hogy ezt a PDF fájlt importálhassa. Szeretné most letölteni? + A PDF-fájl importálásához egy további összetevőre van szükség. Szeretné letölteni most? - Are you sure you want to cancel the batch scan? + Biztos, hogy meg szeretné szakítani a kötegelt beolvasást? - Cancel Batch + A kötegelt feldolgozás törlése - Támogatás + Adományozás - NAPS2 is completely free. Consider making a donation. + A NAPS2 teljesen ingyenes. Fontolja meg az adományozást. + + + Hagyjon egy értékelést + + + Kedveli a NAPS2-t? - The SANE driver is not available. Make sure to install the required packages: + A SANE illesztőprogram nem áll rendelkezésre. Győződjön meg róla, hogy telepítette a szükséges csomagokat: - The OCR engine is not available. Make sure to install the required package: + Az karakterfelismerő motor nem áll rendelkezésre. Győződjön meg róla, hogy telepítette a szükséges csomagot: - The selected driver is not supported on this system. + A kiválasztott illesztőprogram nem támogatott ezen a rendszeren. - OCR Progress + Karakterfelismerés folyamata - OCR futtatása... + Karakterfelismerés futtatása... - Update Progress + Frissítés folyamata - Updating... + Frissítés... - Nem érhető el frissítés. + Nem érhetők el frissítések. - Checking... + Ellenőrzés... - Update checking is disabled. + A frissítés ellenőrzése le van tiltva. - Hiba történt a frissités telepitésekor. + Hiba történt a frissítés telepítésekor. - Install {0} + {0} telepítése - An update is available. + Elérhető egy frissítés. - An error occurred when trying to authorize. + Hiba történt az engedélyezés során. - Uploading email... + E-mail feltöltése... - An error occurred when trying to send the email. + Hiba történt az e-mail elküldésekor. - Scanning page {0}... + {0}. oldal beolvasása... - Beolvasás... + Adatgyűjtés... \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.id.resx b/NAPS2.Lib/Lang/Resources/MiscResources.id.resx new file mode 100644 index 0000000000..0941e21a65 --- /dev/null +++ b/NAPS2.Lib/Lang/Resources/MiscResources.id.resx @@ -0,0 +1,402 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Pilih Profil + + + Bersihkan + + + Lakukan pengosongan {0} item(s), anda yakin? + + + Lakukan penghapusan {0} item(s), anda yakin? + + + Lakukan penghapusan profile(s) {0}, anda yakin? + + + Lakukan penghapusan profil {0}, anda yakin? + + + The file {0} already exists. Do you want to overwrite it? + + + Hapus + + + The selected scanner could not be found. + + + The selected scanner is offline. + + + You don't have permission to save files at this location. + + + Kesalahan + + + berkas Bitmap (*.bmp) + + + Tingkatkan Windows MetaFile (*.emf) + + + Exchangeable Image File (*.exif) + + + GIF File (*.gif) + + + JPEG File (*.jpg, *.jpeg) + + + PDF Document (*.pdf) + + + PNG File (*.png) + + + TIFF File (*.tiff, *.tif) + + + JPEG2000 File (*.jp2, *.jpx) + + + Name missing. + + + NAPS2 + + + No device selected. + + + No scanning device was found. + + + Overwrite File + + + {0} of {1} + + + Scanned Image + + + Select a profile before clicking Scan. + + + Galat ditemukan didalam driver piranti. + + + Version {0} + + + Galat ditemukan saat proses pengiriman melalui surel berlangsung. + + + Installation Complete + + + Installation complete. Do you want to restart NAPS2 now? + + + Installation failed. + + + Installation Failed + + + seluruhnya ({0}) + + + Selected ({0}) + + + Estimasi ukuran unduhan: {0} MB + + + {0} / {1} files + + + {0} / {1} MB + + + The file '{0}' could not be imported. + + + The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported. + + + Galat Unduh + + + One or more files could not be downloaded. + + + Custom ({0}x{1} {2}) + + + Scan + + + Lakukan Undo perubahan terhadap gambar: {0}, anda yakin? + + + Reset Image + + + You have unsaved changes. Are you sure you want to exit and discard those changes? + + + Tindakan kegiatan sedang berlangsung. Yakin untuk keluar dan membatalkan kegiatan? + + + Unsaved Changes + + + Operation in Progress + + + Scanning page {0} + + + The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. + + + No pages are in the feeder. + + + You do not have permission to copy content from the file '{0}'. + + + Galat ditemukan saat proses menyimpan berkas. + + + of {0} + + + Galat tak diketahui ditemukan dalam proses pindai berkas tumpak. + + + Tumpak dibatalkan. + + + Membatalkan.... + + + Tumpak diselesaikan dengan sukses. + + + Pindai Tumpak terhenti dikarenakan galat. + + + Scanning page {0}... + + + Scanning page {0} (scan {1})... + + + Waiting for scan {0}... + + + Batal + + + Tutup + + + Saving batch results... + + + The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + + + {0} / {1} + + + Importing {0}... + + + Import Progress + + + Recovering... + + + Recovery Progress + + + Save Images + + + Save Images Progress + + + Save PDF + + + Save PDF Progress + + + Saving {0}... + + + Print + + + Menyalin... + + + Proses Salin + + + Importing... + + + Emailkan PDF + + + Emailkan Progres PDF + + + Galat ditemukan ketika menyimpan otomatis. + + + Semua Berkas + + + Image Files + + + The selected scanner is busy. + + + The scanner's cover is open. + + + The scanner has a paper jam. + + + The scanner is warming up. + + + Pemutakhiran aplikasi untuk fitur OCR telah tersedia. + + + Image saved. + + + {0} images saved. + + + PDF saved. + + + {0} ({1}x{2} {3}) + + + Lakukan penghapusan {0} item(s), anda yakin? + + + The file could not be overwritten because it is currently in use. + + + ngelurusin halaman... + + + Proses Deskew - miring mereng + + + Butuh mengunduh + + + Dibutuhkan komponen tambahan untuk dapat mengimport berkas PDF ini. Apakah anda ingin mengunduhnya sekarang? + + + Batalkan Pemindaian Tumpak, anda yakin? + + + Batalkan Tumpak + + + Donasi + + + NAPS2 is completely free. Consider making a donation. + + + Leave a Review + + + Like NAPS2? + + + The SANE driver is not available. Make sure to install the required packages: + + + The OCR engine is not available. Make sure to install the required package: + + + The selected driver is not supported on this system. + + + OCR Progress + + + Running OCR... + + + Update Progress + + + Updating... + + + No updates available. + + + Memeriksa... + + + Update checking is disabled. + + + Galat ditemukan ketika proses installasi update. + + + Install {0} + + + Pemutakhiran aplikasi NAPS telah tersedia. + + + Galat ditemukan ketika memproses autorisasi. + + + Uploading email... + + + Galat ditemukan ketika proses pengiriman melalui surel. + + + Scanning page {0}... + + + Mengumpulkan Data... + + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.it.resx b/NAPS2.Lib/Lang/Resources/MiscResources.it.resx index ede555ac7b..3f3b100b5e 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.it.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.it.resx @@ -13,19 +13,19 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Scegli Profilo + Scegli profilo - Cancella + Azzera - Vuoi cancellare il(i) {0} oggetto(i)? + Vuoi eliminare {0} elemento/i? - Vuoi eliminare {0} oggetto(i)? + Vuoi eliminare {0} elemento/i? - Vuoi eliminare {0} profilo(i)? + Vuoi eliminare {0} profilo/i? Vuoi eliminare il profilo {0}? @@ -40,40 +40,40 @@ Lo scanner selezionato non è stato trovato. - Lo scanner selezionato è disconnesso. + Lo scanner selezionato è offline. - Non hai i permessi per salvare i files in questo percorso. + Non hai i permessi per salvare i file in questo percorso. Errore - Bitmap Files (*.bmp) + File bitmap (*.bmp) - Enhanced Windows MetaFile (*.emf) + Enhanced MetaFile di Windows (*.emf) Exchangeable Image File (*.exif) - GIF File (*.gif) + File GIF (*.gif) - JPEG File (*.jpg, *.jpeg) + File JPEG (*.jpg, *.jpeg) Documento PDF (*.pdf) - PNG File (*.png) + File PNG (*.png) File TIFF (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + File JPEG2000 (*.jp2, *.jpx) Nome mancante. @@ -85,7 +85,7 @@ Nessun dispositivo selezionato. - Nessuno scanner non trovato. + Nessuno scanner trovato. Sovrascrivi file @@ -97,10 +97,10 @@ Immagine scansionata - Seleziona profilo prima di scansionare. + Seleziona profilo prima della scansione. - Errore del driver di scansione. + Si è verificato un errore con il driver di scansione. Versione {0} @@ -127,10 +127,10 @@ Selezionato ({0}) - Dimensione stimata del download: {0} MB + Dimensione stimata download: {0} MB - {0} / {1} files + {0} / {1} file {0} / {1} MB @@ -139,13 +139,13 @@ Il file '{0}' non può essere importato. - Il file '{0}' non può essere importato. Solo i files PDF generati da NAPS2 possono essere importati. + Il file '{0}' non può essere importato. Solo i file PDF generati da NAPS2 possono essere importati. - Errore nel download + Errore download - Uno o più files non possono essere scaricati. + Uno o più file non possono essere scaricati. Personalizzato ({0}x{1} {2}) @@ -154,16 +154,16 @@ Scansiona - Sicuro di voler annullare i cambiamenti a {0} immagine(i)? + Vuoi annullare le modifiche a {0} immagine/i? - Resetta Immagine + Ripristina immagine - Ci sono modifiche non salvate. Sei sicuro di voler uscire ed abbandonare tali modifiche? + Ci sono modifiche non salvate. Sei sicuro di voler uscire e abbandonare tali modifiche? - E' in corso un'operazione. Desideri uscire e cancellare tale operazione? + È in corso un'operazione. Sicuro di volere uscire e annullare questa operazione? Modifiche non salvate @@ -172,13 +172,13 @@ Operazione in corso - Digitalizzazione pagina {0} + Scansione pagina {0} - Lo scanner selezionato non supporta l'uso di un alimentatore. Se il tuo scanner ha un alimentatore, prova ad usare un driver differente. + Lo scanner selezionato non supporta l'uso di un alimentatore automatico. Se lo scanner ha un alimentatore automatico, prova a usare un driver differente. - Non ci sono fogli nel caricatore. + Non ci sono fogli nell'alimentatore automatico. Non hai i permessi per copiare il contenuto dal file '{0}'. @@ -190,7 +190,7 @@ di {0} - Errore sconosciuto durante la scansione batch. + Si è verificato un errore sconosciuto durante la scansione in serie. Scansione in serie annullata. @@ -199,13 +199,13 @@ Annullamento.... - Serie completata. + Scansione in serie completata. Scansione in serie arrestata a causa di un errore. - Digitalizzazione pagina {0}... + Scansione pagina {0}... Scansione pagina {0} (scansione {1})... @@ -220,10 +220,10 @@ Chiudi - Salvataggio risultati della serie... + Salvataggio risultati operazioni in serie... - Lo scanner selezionato non supporta l'uso del duplex. Se lo scanner ha un supporto duplex, prova ad usare un driver differente. + Lo scanner selezionato non supporta la scansione fronte retro. Se lo scanner dovrebbe supportare la scansione fronte retro, prova a usare un driver differente. {0} / {1} @@ -238,19 +238,19 @@ Ripristino... - Progresso del ripristino + Progresso ripristino - Salva Immagine + Salva immagini - Progresso del salvataggio immagini + Progresso salvataggio immagini Salva PDF - Progresso del salvataggio PDF + Progresso salvataggio PDF Salvataggio {0}... @@ -259,16 +259,16 @@ Stampa - Copia in corso... + Copia... - Progresso della copia + Progresso copia Importazione... - Email PDF + Invia PDF via email Progresso invio PDF via email @@ -280,22 +280,22 @@ Tutti i file - Files immagini + File immagini Lo scanner selezionato è occupato. - Coperchio dello scanner aperto. + Il coperchio dello scanner è aperto. Carta inceppata nello scanner. - Surriscaldamento scanner. + Lo scanner si sta riscaldando. - E' disponibile un aggiornamento per l'OCR. + È disponibile un aggiornamento per l'OCR. Immagine salvata. @@ -310,10 +310,10 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Vuoi eliminare "{0}"? - Il file non può essere sovrascritto perchè è attualmente in uso. + Il file non può essere sovrascritto perché è attualmente in uso. Allineamento... @@ -322,23 +322,29 @@ Progresso allineamento - Necessario download + Scaricamento necessario Per importare questo file PDF è necessario un componente aggiuntivo. Vuoi scaricarlo ora? - Sei sicuro di voler annullare la scansione batch? + Vuoi annullare la scansione in serie? - Annulla batch + Annulla operazione in serie - Fai una donazione. + Dona NAPS2 è totalmente gratuito. Considera la possibilità di fare una donazione. + + Scrivi una recensione + + + Ti piace NAPS2? + Il driver SANE non è disponibile. Assicurati di aver installato il pacchetto richiesto: @@ -346,16 +352,16 @@ Il motore OCR non è disponibile. Assicurati di aver installato il pacchetto richiesto: - Il driver selezionato non è supportato in questo sistema. + Il driver selezionato non è supportato da questo sistema. - OCR in corso + Progresso OCR - OCR in esecuzione... + Esecuzione OCR... - Avanzamento dell'aggiornamento + Progresso aggiornamento Aggiornamento... @@ -364,22 +370,22 @@ Nessun aggiornamento disponibile. - Verifica... + Verifica in corso... - Update checking is disabled. + Il controllo degli aggiornamenti è disabilitato. Si è verificato un errore durante l'installazione dell'aggiornamento. - Installazione {0} + Installazione di {0} - E' disponibile un aggiornamento. + È disponibile un aggiornamento. - Si è verificato un errore provando ad autorizzare. + Si è verificato un errore durante il tentativo di autorizzazione. Caricamento email... @@ -388,7 +394,7 @@ Si è verificato un errore durante l'invio dell'email. - Digitalizzazione pagina {0}... + Scansione pagina {0}... Acquisizione dati... diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.ja.resx b/NAPS2.Lib/Lang/Resources/MiscResources.ja.resx index 34fd769143..2fce76218e 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.ja.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.ja.resx @@ -190,7 +190,7 @@ {0} の - 一括スキャン中に不明なエラーが発生しました. + An unknown error occurred during the batch scan. 一括スキャンはキャンセルされました. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + "{0}"を削除しますか? 使用中ファイルのため、上書きできませんでした。. @@ -339,6 +339,12 @@ NAPS2はフリーウェアです。気に入ったら寄付をお願いします。. + + Leave a Review + + + Like NAPS2? + SANEドライバが利用不可能です。必要なパッケージをインストールしてください。: diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.ko.resx b/NAPS2.Lib/Lang/Resources/MiscResources.ko.resx index 594db2c960..0a66520183 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.ko.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.ko.resx @@ -16,7 +16,7 @@ 프로파일 선택 - 비우기 + 지우기 {0} 개의 이미지를 비우시겠습니까? @@ -37,10 +37,10 @@ 삭제 - 선택된 스캐너를 찾을 수 없습니다.. + 선택된 스캐너를 찾을 수 없습니다. - 선택된 스캐너의 연결이 끊겼습니다. . + 선택된 스캐너의 연결이 끊겼습니다. 이 위치에 파일을 쓸 수 있는 권한을 가지고 있지 않습니다.. @@ -52,10 +52,10 @@ 비트맵 파일 (*.bmp) - 확장 메타 파일 (*.emf) + 확장 메타파일 (*.emf) - 교환이미지 파일 (*.exif) + 교환 가능한 이미지 파일 (*.exif) GIF 파일 (*.gif) @@ -73,7 +73,7 @@ TIFF 파일 (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG2000 파일 (*.jp2, *.jpx) 이름이 없습니다. @@ -85,7 +85,7 @@ 선택된 장치가 없습니다.. - 스캔 장치를 찾을 수 없습니다.. + 스캔 장치를 찾을 수 없습니다. 파일 덮어쓰기 @@ -97,13 +97,13 @@ 스캔된 이미지 - 스캔을 클릭하기 전에 프로파일을 선택 해 주세요.. + 스캔을 클릭하기 전에 프로파일을 선택 해 주세요. 스캔 중 오류가 발생했습니다.. - {0} 버전 + 버전 {0} 이메일을 보내는 중 오류가 발생했습니다.. @@ -124,7 +124,7 @@ 전체 ({0}) - {0} 개 선택됨 + 선택된 항목 ({0}) 예상 다운로드 용량: {0} MB @@ -136,16 +136,16 @@ {0} / {1} MB - '{0}' 파일을 불러올 수 없습니다.. + '{0}' 파일을 불러올 수 없습니다. - '{0}' 파일을 불러올 수 없습니다. NAPS2 는 PDF 파일만 제작이 가능합니다.. + '{0}' 파일을 불러올 수 없습니다. NAPS2에서 제작한 PDF 파일만 불러올 수 있습니다. 다운로드 오류 - 한개 또는 그 이상의 파일을 다운로드 할 수 없습니다.. + 하나 이상의 파일을 다운로드 할 수 없습니다. 사용자 설정 ({0}x{1} {2}) @@ -163,22 +163,22 @@ 저장되지 않은 변경사항이 있습니다. 변경사항을 무시하고 종료하시겠습니까? - An operation is in progress. Are you sure you want to exit and cancel the operation? + 작업이 진행 중입니다. 진행중인 작업을 취소하고 종료하시겠습니까? 저장되지 않은 변경 사항 - Operation in Progress + 작업 진행 중 {0} 페이지 스캔 중 - 선택된 스캐너에서는 지급기를 사용할 수 없습니다. 만약 지급기가 있는 스캐너라면, 다른 드라이버를 선택 해 주세요.. + 선택된 스캐너에서는 지급기를 사용할 수 없습니다. 만약 지급기가 있는 스캐너라면, 다른 드라이버를 선택 해 주세요. - 공급장치에 용지가 없습니다.. + 공급장치에 용지가 없습니다. '{0}' 파일의 내용을 복사하기 위한 권한을 가지고 있지 않습니다.. @@ -187,31 +187,31 @@ 파일을 저장하는 중 오류가 발생했습니다.. - of {0} + 중 {0} - 일괄 스캔 중 오류가 발생했습니다.. + 일괄 스캔 중 알 수 없는 오류가 발생했습니다. - 일괄 작업이 취소되었습니다.. + 일괄 작업이 취소되었습니다. - 취소하기.... + 취소하는 중... - 일괄 작업이 완료되었습니다.. + 일괄 작업이 완료되었습니다. - 오류로 인해 일괄 작업이 중지되었습니다.. + 오류로 인해 일괄 작업이 중지되었습니다. {0} 페이지 스캔 중... - {0} 페이지 스캔 중 ({1} 스캔)... + {0} 페이지 스캔 중 (스캔 {1})... - {0} 스캔 대기중... + 스캔 {0} 대기중... 취소 @@ -220,10 +220,10 @@ 닫기 - 일괄 작업 결과 저장하기... + 일괄 작업 결과 저장 중... - 선택된 스캐너에서는 양면 기능을 사용할 수 없습니다. 만약 스캐너가 양면 기능을 지원한다면, 다른 드라이버를 선택 해 주세요.. + 선택된 스캐너에서는 양면 기능을 사용할 수 없습니다. 만약 스캐너가 양면 기능을 지원한다면, 다른 드라이버를 선택 해 주세요. {0} / {1} @@ -232,25 +232,25 @@ {0} 가져오는 중... - 가져오기 진행 상태 + 가져오기 진행 중 복구 중... - 복구 진행 상황 + 복구 진행 중 이미지 저장 - 이미지 저장 상황 + 이미지 저장 중 PDF 저장 - PDF 저장 상황 + PDF 저장 중 {0} 저장 중... @@ -262,16 +262,16 @@ 복사 중... - 복사 진행상태 + 복사 진행 중 가져오는 중입니다... - 이메일 PDF + PDF로 이메일 보내기 - 이메일 PDF 진행상황 + PDF로 이메일 보내기 진행 중 파일을 자동으로 저장하는 중 오류가 발생했습니다.. @@ -283,7 +283,7 @@ 이미지 파일 - 선택된 스캐너가 사용 중 입니다.. + 선택된 스캐너가 사용 중 입니다. 스캐너 커버 열림. @@ -295,7 +295,7 @@ 스캐너 예열 중. - OCR 업데이트를 사용할 수 있습니다.. + OCR 업데이트를 사용할 수 있습니다. 이미지가 저장되었습니다. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + "{0}" 이미지를 삭제하시겠습니까? 파일이 사용 중이므로 덮어쓰기 할 수 없습니다. @@ -319,7 +319,7 @@ 기울기 보정... - 기울기 보정 + 기울기 보정 중 다운로드가 필요합니다 @@ -328,69 +328,75 @@ 이 PDF 파일을 불러오기 위해서는 추가적인 컴포넌트가 필요합니다. 지금 다운로드 하시겠습니까? - Are you sure you want to cancel the batch scan? + 일괄 스캔을 취소하시겠습니까? - Cancel Batch + 일괄 작업 취소 - Donate + 후원 - NAPS2 is completely free. Consider making a donation. + NAPS2는 완전 무료로 제공됩니다. 기부를 고려해 보십시오. + + + Leave a Review + + + Like NAPS2? - The SANE driver is not available. Make sure to install the required packages: + SANE 드라이버를 사용할 수 없습니다. 다음 패키지가 설치 되어 있는지 확인해 주십시오: - The OCR engine is not available. Make sure to install the required package: + OCR 엔진을 사용할 수 없습니다. 다음 패키지가 설치 되어 있는지 확인해 주십시오: - The selected driver is not supported on this system. + 선택한 드라이버는 이 시스템에서 사용할 수 없습니다. - OCR Progress + OCR 진행 중 - Running OCR... + OCR 실행 중... - Update Progress + 업데이트 진행 중 - Updating... + 업데이트 중... - No updates available. + 사용 가능한 업데이트가 없습니다. - Checking... + 확인 중... - Update checking is disabled. + 업데이트 확인이 비활성화 되어 있습니다. - An error occured when trying to install the update. + 업데이트를 설치하는 중 오류가 발생했습니다. - Install {0} + {0} 설치 - An update is available. + 업데이트가 있습니다. - An error occurred when trying to authorize. + 인증을 시도하는 중 오류가 발생했습니다. - Uploading email... + 이메일 업로드 중... - An error occurred when trying to send the email. + 이메일을 보내는 중 오류가 발생했습니다. {0} 페이지 스캔 중... - 수집 데이터... + 데이터 수집 중... \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.lt.resx b/NAPS2.Lib/Lang/Resources/MiscResources.lt.resx index 94783d14aa..5ebb3a8fc6 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.lt.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.lt.resx @@ -190,7 +190,7 @@ iš {0} - Įvyko nežinoma paketinio skenavimo klaida. + An unknown error occurred during the batch scan. Paketas atšauktas. @@ -339,6 +339,12 @@ NAPS2 is completely free. Consider making a donation. + + Leave a Review + + + Like NAPS2? + The SANE driver is not available. Make sure to install the required packages: diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.lv.resx b/NAPS2.Lib/Lang/Resources/MiscResources.lv.resx index 2cced4cbb6..fee1f866b8 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.lv.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.lv.resx @@ -52,10 +52,10 @@ Bitmap datnes (*.bmp) - Enhanced Windows MetaFile (*.emf) + Paplašinātais metafails (*.emf) - Exchangeable Image File (*.exif) + Paplašinātais attēlu fails (*.exif) GIF datne (*.gif) @@ -73,7 +73,7 @@ TIFF datne (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG2000 fails (*.jp2, *.jpx) Nav nosaukuma. @@ -190,7 +190,7 @@ no {0} - Notika nezināma kļūda partijas skenēšanas laikā. + Pie partijas skenēšanas notika nezināma kļūda. Partija atcelta. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Vai tiešām vēlaties dzēst "{0}"? Datni nevar pārrakstīt jo tā pašlaik tiek lietota. @@ -339,6 +339,12 @@ NAPS2 ir pilnīgi bez maksas. Apsveriet iespēju ziedot. + + Leave a Review + + + Like NAPS2? + The SANE driver is not available. Make sure to install the required packages: @@ -367,13 +373,13 @@ Pārbauda... - Update checking is disabled. + Jauninājumu pārbaude ir atslēgta. Notikusi kļūda mēģinot instalēt jauninājumu. - Install {0} + Instalēt {0} Pieejams atjauninājums. diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.nb.resx b/NAPS2.Lib/Lang/Resources/MiscResources.nb.resx index 087de02f41..8ee7c69943 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.nb.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.nb.resx @@ -190,7 +190,7 @@ of {0} - En ukjent feil oppstod under serieskanningen. + An unknown error occurred during the batch scan. Serieskanning avbrutt. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Er du sikker på at du vil slette "{0}"? The file could not be overwritten because it is currently in use. @@ -339,6 +339,12 @@ NAPS2 is completely free. Consider making a donation. + + Leave a Review + + + Like NAPS2? + The SANE driver is not available. Make sure to install the required packages: diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.nl.resx b/NAPS2.Lib/Lang/Resources/MiscResources.nl.resx index ffe452d4db..c1659b2641 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.nl.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.nl.resx @@ -58,22 +58,22 @@ Exchangeable Image File (*.exif) - GIF bestand (*.gif) + GIF-bestand (*.gif) - JPEG bestand (*.jpg, *.jpeg) + JPEG-bestand (*.jpg, *.jpeg) - PDF bestand (*.pdf) + PDF-document (*.pdf) - PNG bestand (*.png) + PNG-bestand (*.png) - TIFF bestand (*.tiff, *.tif) + TIFF-bestand (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG2000-bestand(*.jp2, *.jpx) Naam ontbreekt. @@ -169,10 +169,10 @@ Niet-opgeslagen wijzigingen - Bewerking is bezig + Bewerking wordt uitgevoerd - Scannen pagina {0} + Pagina {0} aan het scannen Deze scanner ondersteunt geen automatische documentinvoer. Als uw scanner dit wel heeft, probeer dan een andere driver. @@ -190,13 +190,13 @@ van {0} - Er is een onbekende fout opgetreden tijdens het batch scannen. + Er is een onbekende fout opgetreden tijdens de batch scan. Batch geannuleerd. - Bezig met annuleren.... + Annuleren.... Batch succesvol voltooid. @@ -205,10 +205,10 @@ Batch scan gestopt door fout. - Scannen pagina {0}... + Pagina {0} aan het scannen... - Scant pagina {0} (scan {1})... + Pagina {0} (scan {1}) aan het scannen... Wacht op scan {0}... @@ -235,10 +235,10 @@ Voortgang importeren - Bezig met herstellen... + Herstellen... - Herstelvoortgang + Voortgang herstellen Afbeeldingen opslaan @@ -250,28 +250,28 @@ PDF opslaan - PDF voortgang bewaren + Voortgang PDF opslaan - Bezig met opslaan {0}... + Opslaan {0}... Afdrukken - Bezig met kopiëren... + Kopiëren... Voortgang kopiëren - Bezig met importeren... + Importeren... - PDF mailen + PDF e-mailen - E-mail PDF voortgang + Voortgang PDF e-mailen Er was een fout bij het automatisch opslaan. @@ -295,7 +295,7 @@ De scanner warmt op. - Er is een OCR update beschikbaar. + Er is een update voor OCR beschikbaar. Afbeelding opgeslagen. @@ -310,13 +310,13 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Weet u zeker dat u "{0}" wilt verwijderen? Overschrijven bestand mislukt want het is in gebruik. - Aan het rechtzetten... + Rechtzetten... Voortgang rechtzetten @@ -334,25 +334,31 @@ Annuleer batch - Doneer + Doneren NAPS2 is helemaal gratis. Overweeg een donatie. + + Schrijf een recensie + + + Vind je NAPS2 leuk? + De SANE driver is niet beschikbaar. Installeer de vereiste pakketten: - De OCR engine is niet beschikbaar. Installeer het vereiste pakket: + De OCR-engine is niet beschikbaar. Installeer het vereiste pakket: De gekozen driver wordt niet ondersteund op dit systeem. - OCR voortgang + Voortgang van OCR - OCR draait... + OCR uitvoeren... Voortgang bijwerken @@ -364,10 +370,10 @@ Geen updates beschikbaar. - Controleert... + Controleren... - Update checking is disabled. + Controleren van updates is uitgeschakeld. Er was een fout bij het installeren van de update. @@ -388,9 +394,9 @@ Er was een fout bij het verzenden van de e-mail. - Scannen pagina {0}... + Pagina {0} aan het scannen... - Data wordt opgehaald... + Gegevens ophalen... \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.nn.resx b/NAPS2.Lib/Lang/Resources/MiscResources.nn.resx index e974c24dce..5891f8074b 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.nn.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.nn.resx @@ -73,7 +73,7 @@ TIFF fil (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG2000 fil (*.jp2, *.jpx) Namn manglar. @@ -190,7 +190,7 @@ av {0} - Ukjend feil ved gruppeskanning. + An unknown error occurred during the batch scan. Multiskann avbroten. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Er du sikker på at du vil sletta "{0}"? Fila kunne ikkje overskrivast fordi den er i bruk. @@ -339,6 +339,12 @@ NAPS2 er heilt gratis. Vurder å gje ein donasjon. + + Legg igjen en omtale + + + Liker du: NAPS2? + SANE-drivaren er ikkje tilgjengeleg. Installer den nødvendige pakken: @@ -367,7 +373,7 @@ Sjekker... - Update checking is disabled. + Oppdateringskontroll er deaktivert. Det vart ein feil ved installasjon av oppdatering. diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.pl.resx b/NAPS2.Lib/Lang/Resources/MiscResources.pl.resx index 020dff49a3..fc4284bbea 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.pl.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.pl.resx @@ -160,10 +160,10 @@ Zresetuj obraz - Masz niezapisane zmiany. Czy na pewno chcesz wyjść i odrzucić te zmiany? + Masz niezapisane zmiany. Czy na pewno chcesz wyjść i odrzucić te zmiany? - Operacja jest w toku. Czy na pewno chcesz zakończyć i anulować operację? + Operacja jest w toku. Czy na pewno chcesz zakończyć i anulować operację? Niezapisane zmiany @@ -190,7 +190,7 @@ z {0} - Nieznany błąd wystąpił podczas skanowania wsadowego. + Wystąpił nieznany błąd podczas skanowania wsadowego. Anulowano przetwarzanie. @@ -310,7 +310,7 @@ {0} ({1} x {2} {3}) - Are you sure you want to delete "{0}"? + Czy na pewno chcesz usunąć "{0}"? Plik nie mógł zostać zastąpiony, ponieważ jest obecnie w użyciu. @@ -339,6 +339,12 @@ NAPS2 jest całkowicie bezpłatny. Rozważ przekazanie darowizny. + + Napisz recenzję + + + Lubisz NAPS2? + Sterownik SANE nie jest dostępny. Upewnij się, że zainstalowałeś wymagane pakiety: diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.pt-BR.resx b/NAPS2.Lib/Lang/Resources/MiscResources.pt-BR.resx index 74df6e8bf0..b1f0475eec 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.pt-BR.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.pt-BR.resx @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Confirma a remoção de {0} item(s)? O Arquivo não pode ser sobrescrito porque está atualmente em uso. @@ -339,6 +339,12 @@ NAPS2 é totalmente gratuito. Considere fazer uma doação. + + Deixe uma avaliação + + + Gosta do NAPS2? + O driver SANE não está disponível. Certifique-se de instalar os pacotes necessários: @@ -367,7 +373,7 @@ Verificando... - Update checking is disabled. + Verificação de atualizações está desativada. Ocorreu um erro ao tentar instalar a atualização. diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.pt-PT.resx b/NAPS2.Lib/Lang/Resources/MiscResources.pt-PT.resx index 218ad905e3..01f92d2950 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.pt-PT.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.pt-PT.resx @@ -19,16 +19,16 @@ Limpar - Confirma a limpeza de {0} iten(s)? + Tem a certeza de que pretende remover {0} itens? - Confirma que quer eliminar {0} itens? + Tem a certeza de que pretende eliminar {0} itens? - Confirma que quer eliminar {0} perfis? + Tem a certeza de que pretende eliminar {0} perfis? - Confirma que quer eliminar o perfil {0}? + Tem a certeza de que pretende eliminar o perfil "{0}"? O ficheiro {0} já existe. Deseja substituir? @@ -37,13 +37,13 @@ Eliminar - O scanner escolhido não foi encontrado. + O digitalizador escolhido não foi encontrado. - O scanner escolhido está offline. + O digitalizador escolhido está desligado. - Não tem permissão para salvar o ficheiro neste local. + Não tem permissão para guardar ficheiros neste local. Erro @@ -52,10 +52,10 @@ Ficheiro Bitmap (*.bmp) - Ficheiro EMF (*.emf) + Ficheiro Enhanced Windows MetaFile (*.emf) - Ficheiro EXIF + Ficheiro Exchangeable Image File (*.exif) Ficheiro GIF (*.gif) @@ -64,7 +64,7 @@ Ficheiro JPG (*.jpg, *.jpeg) - PDF Document (*.pdf) + Documento PDF (*.pdf) Ficheiro PNG (*.png) @@ -73,22 +73,22 @@ Ficheiro TIFF (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + Ficheiro JPEG2000 (*.jp2, *.jpx) - Falta o nome. + Nome em falta. NAPS2 - Nenhum dispositivo escolhido. + Nenhum dispositivo selecionado. - Nenhum scanner foi encontrado. + Não foi encontrado um digitalizador. - O ficheiro já existe. Deseja substituir? + Substituir ficheiro {0} de {1} @@ -97,22 +97,22 @@ Imagem digitalizada - Escolha um perfil antes de clicar Scan. + Escolha um perfil antes de Digitalizar. - Ocorreu um erro com o driver do scanner. + Ocorreu um erro com o controlador de digitalização. Versão {0} - Ocorreu um erro enquanto tentava enviar um email. + Ocorreu um erro durante o envio do e-mail. - Instalação completa + Instalação concluída - Instalação completa. Quer reiniciar o NAPS2 agora? + Instalação concluída. Reiniciar NAPS2 agora? Falha na instalação. @@ -124,271 +124,277 @@ Todos ({0}) - Selected ({0}) + Selecionadas ({0}) Tamanho estimado da transferência: {0} MB - {0} / {1} files + {0} / {1} ficheiros {0} / {1} MB - The file '{0}' could not be imported. + Não foi possível importar o ficheiro "{0}". - The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported. + Não foi possível importar o ficheiro "{0}". Apenas pode importar ficheiros PDF gerados por NAPS2. - Erro na Transferência + Erro ao descarregar - Não foi possível transferir um ou mais ficheiros. + Não foi possível descarregar um ou mais ficheiros. - Personalizado ({0}x{1} {2}) + Personalizado ({0} × {1} {2}) Digitalizar - Tem a certeza que quer desfazer as alterações a {0} imagens? + Tem a certeza de que pretende desfazer as alterações a {0} imagens? - Reset Image + Repor imagem - You have unsaved changes. Are you sure you want to exit and discard those changes? + Existem alterações não guardadas. Tem a certeza de que pretende sair e rejeitar as alterações? - An operation is in progress. Are you sure you want to exit and cancel the operation? + Está em curso uma operação. Tem a certeza de que pretende sair e cancelar a operação? - Unsaved Changes + Alterações não guardadas - Operation in Progress + Operação em curso - Scanning page {0} + A digitalizar página {0} - The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. + Aparentemente, o digitalizador escolhido não possui um alimentador. Se o seu digitalizador possuir um alimentador, tente usar um controlador diferente. Não existem folhas no alimentador. - You do not have permission to copy content from the file '{0}'. + Não tem permissão para copiar conteúdo do ficheiro "{0}". - Ocorreu um erro ao tentar gravar o ficheiro. + Ocorreu um erro ao tentar guardar o ficheiro. - of {0} + de {0} Ocorreu um erro desconhecido durante a digitalização em lote. - Batch cancelled. + Lote cancelado. - Cancelling.... + A cancelar... - Batch completed successfully. + Lote concluído com sucesso. - Batch scan stopped due to error. + Digitalização em lote parada devido a um erro. - Scanning page {0}... + A digitalizar página {0}... - Scanning page {0} (scan {1})... + A digitalizar página {0} (digitalização {1})... - Waiting for scan {0}... + A aguardar digitalização {0}... Cancelar - Close + Fechar - Saving batch results... + A guardar lote... - The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + Aparentemente, o digitalizador escolhido não permite digitalização em frente e verso. Se é suposto que o digitalizador o permita, tente usar um controlador diferente. {0} / {1} - Importing {0}... + A importar {0}... - Import Progress + Progresso da importação - Recovering... + A recuperar... - Recovery Progress + Progresso da recuperação - Salvar imagens + Guardar imagens - Save Images Progress + Progresso da gravação de imagens - Salvar PDF + Guardar PDF - Save PDF Progress + Progresso da gravação PDF - Saving {0}... + A guardar {0}... Imprimir - Copying... + A copiar… - Copy Progress + Progresso da cópia - Importing... + A importar... - Enviar PDF por Email + Enviar por e-mail - Email PDF Progress + Progresso da ação de enviar PDF - An error occurred when trying to auto save. + Ocorreu um erro ao tentar guardar automaticamente. Todos os ficheiros - Image Files + Ficheiros de imagem - The selected scanner is busy. + O digitalizador escolhido está ocupado. - The scanner's cover is open. + A tampa do digitalizador está aberta. - The scanner has a paper jam. + Existe papel encravado no digitalizador. - The scanner is warming up. + O digitalizador está a aquecer. - Uma atualização para OCR está disponível. + Está disponível uma atualização para OCR. - Image saved. + Imagem guardada. - {0} images saved. + {0} imagens guardadas. - PDF saved. + PDF guardado. - {0} ({1}x{2} {3}) + {0} ({1} × {2} {3}) - Are you sure you want to delete "{0}"? + Tem a certeza de que pretende eliminar "{0}"? - The file could not be overwritten because it is currently in use. + O ficheiro está a ser utilizado e não pode ser substituído. - Deskewing... + A endireitar... - Deskew Progress + Progresso da ação de endireitar - Download Needed + Descarga necessária - An additional component is needed to import this PDF file. Would you like to download it now? + Um componente adicional é necessário para importar este ficheiro PDF. Descarregar agora? - Are you sure you want to cancel the batch scan? + Tem a certeza de que pretende cancelar a digitalização em lote? - Cancel Batch + Cancelar lote - Donate + Doar - NAPS2 is completely free. Consider making a donation. + NAPS2 é totalmente gratuito. Considere fazer um donativo. + + + Avaliar + + + Gosta de NAPS2? - The SANE driver is not available. Make sure to install the required packages: + O controlador SANE não está disponível. Certifique-se que instala os pacotes necessários: - The OCR engine is not available. Make sure to install the required package: + O motor OCR não está disponível. Certifique-se que instalou o pacote necessário: - The selected driver is not supported on this system. + O controlador escolhido não é suportado neste sistema. - OCR Progress + Progresso de OCR - Running OCR... + A executar OCR... - Update Progress + Progresso da atualização - Updating... + A atualizar... - No updates available. + Não existem atualizações. - Checking... + A verificar... - Update checking is disabled. + A verificação de atualizações está desativada. - An error occured when trying to install the update. + Ocorreu um erro ao tentar instalar a atualização. - Install {0} + Instalar {0} - An update is available. + Existe uma atualização. - An error occurred when trying to authorize. + Ocorreu um erro ao tentar autorizar. - Uploading email... + A enviar e-mail... - An error occurred when trying to send the email. + Ocorreu um erro ao tentar enviar o e-mail. - Scanning page {0}... + A digitalizar página {0}... A obter dados... diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.resx b/NAPS2.Lib/Lang/Resources/MiscResources.resx index 2f22abc14c..5def25e6eb 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.resx @@ -295,7 +295,7 @@ of {0} - An unknown error ocurred during the batch scan. + An unknown error occurred during the batch scan. Batch cancelled. @@ -444,6 +444,12 @@ NAPS2 is completely free. Consider making a donation. + + Leave a Review + + + Like NAPS2? + The SANE driver is not available. Make sure to install the required packages: @@ -475,7 +481,7 @@ Update checking is disabled. - An error occured when trying to install the update. + An error occurred when trying to install the update. Install {0} diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.ro.resx b/NAPS2.Lib/Lang/Resources/MiscResources.ro.resx index b20b73e542..475d394ed2 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.ro.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.ro.resx @@ -55,7 +55,7 @@ Enhanced Windows Metafile (*.emf) - Exchangeable Image File (*.exif) + Fișier imagine interschimbabil (*.exif) Fișier GIF (*.gif) @@ -64,7 +64,7 @@ Fișier JPEG (*.jpg, *.jpeg) - PDF Document (*.pdf) + Document PDF (*.pdf) Fișier PNG (*.png) @@ -73,7 +73,7 @@ Fișier TIFF (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + Fișier JPEG2000 (*.jp2, *.jpx) Lipsește numele. @@ -130,7 +130,7 @@ Mărime estimată de download: {0} MB - 0} / {1} files + {0} / {1} fișiere {0} / {1} MB @@ -187,10 +187,10 @@ A apărut o eroare la salvarea fișierului. - din {0} + Din {0} - A apărut o eroare necunoscută la scanarea multiplă. + A apărut o eroare necunoscută în timpul scanării lotului. Scanare multiplă anulată. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Sigur doriți să ștergeți {0} elemente? Fișierul nu poate fi suprascris deoarece este în uz. @@ -334,13 +334,19 @@ Anuleaza Batch-ul - Donate + Donează - NAPS2 is completely free. Consider making a donation. + NAPS2 este complet gratuit. Luați în considerare să faceți o donație. + + + Leave a Review + + + Like NAPS2? - The SANE driver is not available. Make sure to install the required packages: + Driverul SANE nu este disponibil. Asigurați-vă că instalați pachetele necesare: Motorul OCR nu este disponibil. Asigură-te că ai instalat pachetul necesar.: @@ -349,10 +355,10 @@ Driver-ul selectat nu este suportat de acest sistem.. - OCR Progress + OCR în lucru - Running OCR... + Se rulează OCR... Progres Actualizare @@ -367,7 +373,7 @@ Verificare... - Update checking is disabled. + Verificarea actualizării este dezactivată. A intervenit o eroare la instalarea actualizării. @@ -379,7 +385,7 @@ Este disponibilă o actualizare. - A survenit o eroare la încercarea de autorizare. + A apărut o eroare la încercarea de autorizare. Se încarcă e-mailul ... diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.ru.resx b/NAPS2.Lib/Lang/Resources/MiscResources.ru.resx index 96283e8aff..21116eb9a7 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.ru.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.ru.resx @@ -52,7 +52,7 @@ Bitmap-файлы (*.bmp) - Улучшенный метафайл (*.emf) + Расширенный метафайл (*.emf) Файл метаданных (*.exif) @@ -73,7 +73,7 @@ Файл TIFF (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + Файл JPEG2000 (*.jp2, *.jpx) Не указано имя. @@ -88,7 +88,7 @@ Сканеры не найдены. - Заменить файл + Перезаписать файл {0} из {1} @@ -115,10 +115,10 @@ Установка завершена. Перезапустить NAPS2? - Ошибка установки. + Ошибка при установке. - Ошибка установки + Ошибка при установке Все ({0}) @@ -163,7 +163,7 @@ Есть несохранённые изменения. Вы уверены, что хотите выйти и отказаться от этих изменений? - Выполняется операция. Вы уверены, что хотите выйти и отменить операцию? + Уже выполняется операция. Вы уверены, что хотите выйти и отменить операцию? Несохранённые изменения @@ -175,13 +175,13 @@ Сканирование страницы {0} - Выбранный сканер не поддерживает использование автоподатчика. Если ваш сканер имеет АПД, попробуйте другой драйвер.. + Выбранный сканер не поддерживает использование автоподатчика. Если ваш сканер имеет АПД, попробуйте другой драйвер. В податчике нет листов. - У вас нет разрешения копировать содержимое из файла '{0}'.. + У вас нет разрешения копировать содержимое из файла '{0}'. Ошибка при сохранении файла. @@ -229,7 +229,7 @@ {0} / {1} - Импортируется \"{0}\"... + Импортируется "{0}"... Процесс импорта @@ -265,7 +265,7 @@ Копирование… - Импорт...... + Импортирование... Отправить PDF почтой @@ -295,7 +295,7 @@ Разогрев сканера. - Обновить распознавание. + Доступно обновление средства распознавания текста (OCR). Изображение сохранено. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Вы действительно хотите удалить эти ({0} шт.)? Файл не может быть перезаписан, так как он используется. @@ -337,7 +337,13 @@ Пожертвовать - NAPS2 - бесплатная программа,однако Вы можете пожертвовать деньги на развитие проекта. + NAPS2 - бесплатная программа, однако Вы можете пожертвовать деньги на развитие проекта. + + + Оставить отзыв + + + Нравится NAPS2? Драйвер SANE недоступен. Убедитесь, что необходимые пакеты установлены: @@ -367,7 +373,7 @@ Проверка... - Update checking is disabled. + Проверка обновлений отключена. Произошла ошибка во время установки обновления. @@ -379,7 +385,7 @@ Доступно обновление. - Произошла ошибка во время авторизации. + При попытке авторизации произошла ошибка. Загрузка электронного сообщения... diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.si.resx b/NAPS2.Lib/Lang/Resources/MiscResources.si.resx index 505d515983..0e2a9e02e5 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.si.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.si.resx @@ -13,7 +13,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Choose Profile + පැතිකඩ තෝරන්න Clear @@ -163,7 +163,7 @@ You have unsaved changes. Are you sure you want to exit and discard those changes? - An operation is in progress. Are you sure you want to exit and cancel the operation? + මෙහෙයුමක් ක්‍රියාත්මකයි. මෙහෙයුම අවලංගු කර පිටවීමට අවශ්‍ය ද? Unsaved Changes @@ -190,7 +190,7 @@ of {0} - කාණ්ඩ පරිලෝකනයේදී නාදුනන දෝෂයක් හටගැනින. + An unknown error occurred during the batch scan. Batch cancelled. @@ -214,7 +214,7 @@ Waiting for scan {0}... - Cancel + අවලංගු කරන්න Close @@ -339,6 +339,12 @@ NAPS2 is completely free. Consider making a donation. + + Leave a Review + + + Like NAPS2? + The SANE driver is not available. Make sure to install the required packages: @@ -370,7 +376,7 @@ Update checking is disabled. - An error occured when trying to install the update. + යාවත්කාලීන ස්ථාපනය කිරීමට උත්සාහ කිරීමේදී දෝෂයක් ඇති විය. Install {0} @@ -379,13 +385,13 @@ An update is available. - An error occurred when trying to authorize. + අවසර දීමට උත්සාහ කිරීමේදී දෝෂයක් ඇති විය. Uploading email... - An error occurred when trying to send the email. + ඊමේල් යැවීමට උත්සාහ කිරීමේදී දෝෂයක් ඇති විය. Scanning page {0}... diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.sk.resx b/NAPS2.Lib/Lang/Resources/MiscResources.sk.resx index 8d3efb6c9b..869a6774a9 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.sk.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.sk.resx @@ -73,7 +73,7 @@ Súbor TIFF (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + Súbor JPEG2000 (*.jp2, *.jpx) Chýba názov. @@ -97,7 +97,7 @@ Naskenovaný obrázok - Pred kliknutím na Skenovať najskôr vyberte profil. + Najskôr vyberte profil pred kliknutím na Skenovať. Pri ovládači skenovania sa vyskytla chyba. @@ -136,7 +136,7 @@ {0} / {1} MB - Súbor '{0}' sa nemožno importovať. + Súbor '{0}' nemožno importovať. Súbor '{0}' nemožno importovať. Možno importovať len PDF súbory vygenerované cez NAPS2. @@ -190,7 +190,7 @@ z {0} - Počas dávkového skenovania sa vyskytla neznáma chyba. + Počas dávkového skenovania došlo k neznámej chybe. Dávka zrušená. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Naozaj chcete odstrániť znak "{0}"? Súbor nemožno prepísať, pretože sa momentálne používa. @@ -325,7 +325,7 @@ Potrebné stiahnutie - Na import tohto súboru PDF je potrebný ďalší komponent. Chcete ho stiahnuť teraz? + Na import tohto súboru PDF je potrebný ďalší komponent. Chcete ho teraz stiahnuť? Naozaj chcete zrušiť dávkové skenovanie? @@ -334,11 +334,17 @@ Zrušiť dávku - Obdarovať + Darovať NAPS2 je úplne zadarmo. Zvážte obdarovanie. + + Zanechať recenziu + + + Ako NAPS2? + Ovládač SANE nie je k dispozícii. Preverte požadované balíky pre inštaláciu: @@ -367,7 +373,7 @@ Kontrola... - Update checking is disabled. + Kontrola aktualizácií je vypnutá. Pri pokuse o inštaláciu aktualizácie sa vyskytla chyba. diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.sl.resx b/NAPS2.Lib/Lang/Resources/MiscResources.sl.resx index bf23ac0080..7c31686bc4 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.sl.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.sl.resx @@ -61,7 +61,7 @@ GIF File (*.gif) - JPEG File (*.jpg, *.jpeg) + Datoteka JPEG (*.jpg, *.jpeg) PDF Dokument (*.pdf) @@ -73,7 +73,7 @@ TIFF Datoteka (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + Datoteka JPEG2000 (*.jp2, *.jpx) Manjka ime. @@ -190,7 +190,7 @@ od {0} - Med zaporednim skeniranjem je prišlo do napake. + Med zaporednim skeniranjem je prišlo do neznane napake. Zaporedno skeniranje preklicano. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Ali ste prepričani, da želite izbrisati "{0}"? Datoteke ni bilo mogoče prepisati, ker je odprta v drugem programu. @@ -328,7 +328,7 @@ Za uvoz te PDF datoteke je potrebna dodatna komponenta. Jo želite prenesti sedaj? - Prekini zaporedno skeniranje? + Ali želite prekiniti zaporedno skeniranje? Prekliči paket @@ -339,6 +339,12 @@ NAPS2 je popolnoma brezplačen. Razmislite o donaciji. + + Leave a Review + + + Like NAPS2? + SANE gonilnik ni na voljo. Preverite namestitev potrebnega dodatka: @@ -367,7 +373,7 @@ Preverjanje... - Update checking is disabled. + Preverjanje posodobitev je onemogočeno. Med nameščanjem posodobitve je prišlo do napake. diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.sq.resx b/NAPS2.Lib/Lang/Resources/MiscResources.sq.resx index 6a8be5ce40..cd49459ba4 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.sq.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.sq.resx @@ -73,7 +73,7 @@ Skedar TIFF (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + Skedar JPEG2000 (*.jp2, *.jpx) Emri mungon. @@ -190,7 +190,7 @@ nga {0} - Ndodhi një gabim i panjohur gjatë skanimit në grup. + Ndodhi një gabim i panjohur gjatë ekzekutimit të skanimit në grup. Grupi u anulua. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + A je i sigurt se dëshiron të fshish "{0}"? Skedari nuk mund të mbishkruhej sepse aktualisht është në përdorim. @@ -339,6 +339,12 @@ NAPS2 is tërësisht falas. Mendoni të dhuroni. + + Leave a Review + + + Like NAPS2? + Drejtuesi SANE nuk është i disponueshëm. Sigurohuni të instaloni paketat e duhura: @@ -367,7 +373,7 @@ Po kontrollon... - Update checking is disabled. + Kontrolli i përditësimit është çaktivizuar. Ndodhi një gabim gjatë instalimit të përditësimit. diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.sr-CS.resx b/NAPS2.Lib/Lang/Resources/MiscResources.sr-CS.resx index fb57f1c50b..255f3351ce 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.sr-CS.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.sr-CS.resx @@ -190,7 +190,7 @@ od {0} - Došlo je do nepoznate greške u toku paketnog skeniranja. + An unknown error occurred during the batch scan. Paketna obrada otkazana. @@ -339,6 +339,12 @@ NAPS2 is completely free. Consider making a donation. + + Leave a Review + + + Like NAPS2? + The SANE driver is not available. Make sure to install the required packages: diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.sr.resx b/NAPS2.Lib/Lang/Resources/MiscResources.sr.resx index ef49df60f5..f9d316cc10 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.sr.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.sr.resx @@ -190,7 +190,7 @@ од {0} - Дошло је до непознате грешке у току пакетног скенирања. + An unknown error occurred during the batch scan. Пакетна обрада отказана. @@ -339,6 +339,12 @@ NAPS2 is completely free. Consider making a donation. + + Leave a Review + + + Like NAPS2? + The SANE driver is not available. Make sure to install the required packages: @@ -364,13 +370,13 @@ No updates available. - Checking... + Проверавам... Update checking is disabled. - An error occured when trying to install the update. + An error occurred when trying to install the update. Install {0} diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.sv.resx b/NAPS2.Lib/Lang/Resources/MiscResources.sv.resx index 253dde0c09..08ac46c584 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.sv.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.sv.resx @@ -73,7 +73,7 @@ TIFF-fil (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG2000-fil (*.jp2, *.jpx) Namn saknas. @@ -190,7 +190,7 @@ av {0} - Ett okänt fel inträffade under mängdskanning. + Ett okänt fel uppstod under mängdskanningen. Mängdskanning avbruten. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Vill du verkligen ta bort {0} objekt? Filen kunde inte skrivas över eftersom den används. @@ -337,7 +337,13 @@ Donerar - NAPS2 är helt gratis. Tänk gärna på att donera.. + NAPS2 är helt gratis. Överväg gärna att donera. + + + Skriv en recension + + + Gillar du NAPS2? SANE drivrutin är inte tillgänglig. Kontroller och installera nödvändigt paket: @@ -367,7 +373,7 @@ Kontrollerar... - Update checking is disabled. + Sökning efter uppdateringar är inaktiverad. Ett fel inträffade när uppdateringen försökte installeras. @@ -379,13 +385,13 @@ En uppdatering är tillgänglig. - An error occurred when trying to authorize. + Ett fel uppstod vid försök att auktorisera. Laddar upp e-post... - Ett fel uppstod när man försökte skicka till E-Post. + Ett fel uppstod vid försök att skicka e-post. Skannar sidan {0}... diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.th.resx b/NAPS2.Lib/Lang/Resources/MiscResources.th.resx new file mode 100644 index 0000000000..5a64ebbe58 --- /dev/null +++ b/NAPS2.Lib/Lang/Resources/MiscResources.th.resx @@ -0,0 +1,402 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + เลือกโปรไฟล์ + + + ล้าง + + + Are you sure you want to clear {0} item(s)? + + + Are you sure you want to delete {0} item(s)? + + + Are you sure you want to delete {0} profiles? + + + Are you sure you want to delete the profile {0}? + + + The file {0} already exists. Do you want to overwrite it? + + + ลบ + + + The selected scanner could not be found. + + + The selected scanner is offline. + + + You don't have permission to save files at this location. + + + ผิดพลาด + + + ไฟล์บิตแมป (*.bmp) + + + Enhanced Windows MetaFile (*.emf) + + + Exchangeable Image File (*.exif) + + + GIF File (*.gif) + + + JPEG File (*.jpg, *.jpeg) + + + เอกสาร PDF (*.pdf) + + + PNG File (*.png) + + + TIFF File (*.tiff, *.tif) + + + JPEG2000 File (*.jp2, *.jpx) + + + Name missing. + + + NAPS2 + + + No device selected. + + + No scanning device was found. + + + Overwrite File + + + {0} of {1} + + + Scanned Image + + + Select a profile before clicking Scan. + + + An error occurred with the scanning driver. + + + เวอร์ชั่น {0} + + + An error occurred while trying to send an email. + + + Installation Complete + + + Installation complete. Do you want to restart NAPS2 now? + + + Installation failed. + + + Installation Failed + + + ทั้งหมด ({0}) + + + Selected ({0}) + + + Estimated download size: {0} MB + + + {0} / {1} files + + + {0} / {1} MB + + + ไม่สามารถนำเข้าไฟล์ '{0}' ได้ + + + ไม่สามารถนำเข้าไฟล์ '{0}' ได้ สามารถนำเข้าได้เฉพาะไฟล์ PDF ที่สร้างโดย NAPS2 เท่านั้น + + + การดาวน์โหลดผิดพลาด + + + One or more files could not be downloaded. + + + Custom ({0}x{1} {2}) + + + Scan + + + Are you sure you want undo your changes to {0} image(s)? + + + Reset Image + + + You have unsaved changes. Are you sure you want to exit and discard those changes? + + + An operation is in progress. Are you sure you want to exit and cancel the operation? + + + Unsaved Changes + + + Operation in Progress + + + Scanning page {0} + + + The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. + + + No pages are in the feeder. + + + You do not have permission to copy content from the file '{0}'. + + + An error occurred when trying to save the file. + + + of {0} + + + An unknown error occurred during the batch scan. + + + Batch cancelled. + + + กำลังยกเลิก.... + + + Batch completed successfully. + + + Batch scan stopped due to error. + + + Scanning page {0}... + + + Scanning page {0} (scan {1})... + + + Waiting for scan {0}... + + + ยกเลิก + + + ปิด + + + Saving batch results... + + + The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + + + {0} / {1} + + + กำลังนำเข้า {0}... + + + ความคืบหน้าการนำเข้า + + + Recovering... + + + Recovery Progress + + + บันทึกภาพ + + + Save Images Progress + + + บันทึก PDF + + + ความคืบหน้าการบันทึก PDF + + + Saving {0}... + + + พิมพ์ + + + กำลังคัดลอก... + + + กำลังคัดลอก + + + กำลังนำเข้า.... + + + ส่งอีเมล PDF + + + ความคืบหน้าการส่งอีเมล PDF + + + An error occurred when trying to auto save. + + + ไฟล์ทั้งหมด + + + ไฟล์รูปภาพ + + + The selected scanner is busy. + + + The scanner's cover is open. + + + The scanner has a paper jam. + + + The scanner is warming up. + + + มีการอัปเดต OCR ใหม่ให้ใช้งานแล้ว + + + Image saved. + + + {0} images saved. + + + PDF บันทึกแล้ว + + + {0} ({1}x{2} {3}) + + + คุณต้องการ ลบ "{0}" ใช่หรือไม่ + + + The file could not be overwritten because it is currently in use. + + + กำลังปรับให้ตรง... + + + Deskew Progress + + + Download Needed + + + จำเป็นต้องมีส่วนประกอบเพิ่มเติมเพื่อนำเข้าไฟล์ PDF นี้ คุณต้องการดาวน์โหลดทันทีหรือไม่ + + + Are you sure you want to cancel the batch scan? + + + ยกเลิกคำสั่งชุด + + + บริจาค + + + NAPS2 is completely free. Consider making a donation. + + + Leave a Review + + + Like NAPS2? + + + The SANE driver is not available. Make sure to install the required packages: + + + The OCR engine is not available. Make sure to install the required package: + + + The selected driver is not supported on this system. + + + OCR Progress + + + Running OCR... + + + ความคืบหน้าการอัปเดต + + + Updating... + + + ยังไม่มีเวอร์ชันใหม่ + + + กำลังตรวจสอบ... + + + การตรวจสอบการอัปเดตถูกปิดใช้งาน + + + An error occurred when trying to install the update. + + + Install {0} + + + มีการอัปเดตใหม่พร้อมใช้งาน + + + An error occurred when trying to authorize. + + + Uploading email... + + + An error occurred when trying to send the email. + + + Scanning page {0}... + + + กำลังรับข้อมูล... + + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.tr.resx b/NAPS2.Lib/Lang/Resources/MiscResources.tr.resx index 35c90680da..892eda5b39 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.tr.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.tr.resx @@ -43,7 +43,7 @@ Seçilen tarayıcı çevrim dışı. - Bu konuma kaydetmek için yetkiniz bulunmuyor. + Bu konuma kaydetmek için yetkiniz yok. Hata @@ -73,7 +73,7 @@ TIFF Dosyası (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG2000 Dosyası (*.jp2, *.jpx) Ad eksik. @@ -97,7 +97,7 @@ Taranmış Resim - Taraya tıklamadan önce profil seçiniz. + Taraya tıklamadan önce profil seç. Tarayıcı sürücüsü ile ilgili bir hata oluştu. @@ -127,7 +127,7 @@ Seçilen ({0}) - Tahmini indirme boyutu: {0} MB + Öngörülen indirme boyutu: {0} MB {0} / {1} dosya @@ -190,7 +190,7 @@ / {0} - Yığın tarama sırasında bilinmeyen bir hata oluştu. + Toplu tarama sırasında bilinmeyen bir hata oluştu. Yığın iptal edildi. @@ -295,7 +295,7 @@ Tarayıcı ısınıyor. - OCR için bir güncelleme var. + OKT için güncelleme var. Resim kaydedildi. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + "{0}" ögesini silmek istediğinize emin misiniz? Dosyanın üzerine yazılamaz çünkü şu anda kullanımda. @@ -339,20 +339,26 @@ NAPS2 tümüyle ücretsizdir. Bağış yapmayı düşünün. + + Yorum Yap + + + NAPS2'yi Beğendiniz Mi? + SANE sürücüsü kullanılamıyor. Gerekli paketlerin kuruluduğundan emin olun: - OCR motoru kullanılamıyor. Gerekli paketin kurulduğundan emin olun: + OKT motoru kullanılamıyor. Gerekli paketin kurulduğundan emin olun: Seçilen sürücü bu sistemde desteklenmiyor. - OCR Süreci + OKT Süreci - OCR Çalıştırılıyor... + OKT Çalıştırılıyor... Güncelleme Süreci @@ -367,7 +373,7 @@ Denetleniyor... - Update checking is disabled. + Güncelleme denetimi devre dışıdır. Güncelleme kurulmaya çalışılırken hata oluştu. @@ -391,6 +397,6 @@ {0}. sayfa taranıyor... - Veri elde ediliyor... + Veri ediniliyor... \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.uk.resx b/NAPS2.Lib/Lang/Resources/MiscResources.uk.resx index d6123d61f3..41abd2e2fd 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.uk.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.uk.resx @@ -19,31 +19,31 @@ Очистити - Ви справді бажаєте очистити профіль(лі) {0}? + Дійсно очистити профіль(лі) {0}? - Ви справді бажаєте видалити {0} профілі(в)? + Дійсно вилучити {0} профілі(в)? - Ви справді бажаєте видалити {0} профілів? + Дійсно вилучити {0} профілів? - Ви дійсно хочете видалити профіль {0}? + Дійсно вилучити профіль {0}? Файл {0} вже існує. Перезаписати його? - Видалити + Вилучити Не знайдено вибраний сканер. - Вибраний сканер відключений. + Вибраний сканер вимкнено. - У вас немає прав для створення файлів у цьому каталозі.. + Відсутні права для створення файлів у цьому каталозі.. Помилка @@ -61,19 +61,19 @@ GIF файл (*.gif) - JPEG файл (*.jpg, *.jpeg) + Файл JPEG (*.jpg, *.jpeg) - PDF Document (*.pdf) + Документ PDF (*.pdf) - PNG файл (*.png) + Файл PNG (*.png) - TIFF файл (*.tiff, *.tif) + Файл TIFF (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + Файл JPEG2000 (*.jp2, *.jpx) Відсутнє ім'я. @@ -82,7 +82,7 @@ NAPS2 - Не вибраний пристрій. + Не вибрано пристрій. Не знайдено жодного сканера. @@ -91,7 +91,7 @@ Перезаписати файл - {0} з {1} + {0} із {1} Зіскановане зображення @@ -106,7 +106,7 @@ Версія {0} - При відправці пошти сталася помилка. + Під час надсилання ел. пошти сталася помилка. Встановлення завершено @@ -127,7 +127,7 @@ Вибрані ({0}) - Приблизний об’єм звантаження: {0} МБ + Приблизний розмір звантаження: {0} МБ {0} / {1} файлів @@ -136,16 +136,16 @@ {0} / {1} МБ - Файл '{0}' не може бути імпортований. + Неможливо імпортувати файл '{0}'. - Не можна імпортувати файл '{0}'. Можуть бути імпортовані лише PDF файли створені NAPS2.. + Неможливо імпортувати файл '{0}'. Можливо імпортувати лише файли PDF, які було створено NAPS2.. Помилка звантаження - Один або декілька файлів завантажити не вдалось. + Не вдалося звантажити один або більше файлів. Користувача ({0}x{1} {2}) @@ -154,238 +154,244 @@ Сканувати - Ви бажаєте відмінити зміни в {0} зображеннях ? + Дійсно скасувати зміни в {0} зображеннях ? Перевантажити зображення - Є незбережені зміни. Ви впевнені, що хочете вийти та відмовитись від них? + Є незбережені зміни. Дійсно вийти та відмовитись від них? - An operation is in progress. Are you sure you want to exit and cancel the operation? + Операція в процесі виконання. Дійсно скасувати цю операцію та вийти? Зміни не збережено - Operation in Progress + Виконується операція Сканування сторінки {0} - Цей сканер не підтримує автоматичну подачу паперу. Якщо Ваш сканер має автоматичну подачу паперу, спробуйте використати інший драйвер.. + Вибраний сканер не підтримує автоматичну подачу паперу. Якщо ваш сканер все ж має автоматичну подачу паперу, спробуйте вибрати інший драйвер.. - В пристрої відсутній папір. + У пристрої відсутній папір. - Ви не можете копіювати вміст файлу '{0}'. + Відсутні права на копіювання вмісту файлу '{0}'. Під час збереження файлу трапилась помилка. - з {0} + із {0} - An unknown error ocurred during the batch scan. + Під час пакетного сканування трапилася невідома помилка. - Batch cancelled. + Пакетне сканування скасовано. - Cancelling.... + Скасування... - Batch completed successfully. + Пакетне сканування успішно виконано. - Batch scan stopped due to error. + Пакетне сканування зупинено через помилку. Сканування сторінки {0}... - Scanning page {0} (scan {1})... + Сканування сторінки {0} (скан {1})... - Waiting for scan {0}... + Очікування на скан {0}... Скасувати - Close + Закрити - Saving batch results... + Збереження результатиів пакетного сканування... - The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + Вибраний сканер не підтримує двостороннє сканування. Якщо ваш сканер все ж підтримує двостороннє сканування, спробуйте вибрати інший драйвер. {0} / {1} - Importing {0}... + Виконується імпорт {0}... - Import Progress + Імпорт - Recovering... + Відновлення... - Recovery Progress + Виконується відновлення Зберегти як зображення - Save Images Progress + Виконується збереження зображень - Зберегти PDF + Зберегти у PDF - Save PDF Progress + Виконується збереження PDF - Saving {0}... + Збереження {0}... Друк - Copying... + Виконується копіювання... - Copy Progress + Копіювання - Importing... + Виконується імпорт... PDF поштою - Email PDF Progress + Надсилання PDF ел. поштою - An error occurred when trying to auto save. + Під час спроби автоматичного збереження трапилася помилка. Всі файли - Image Files + Файли зображень - The selected scanner is busy. + Вибраний сканер зайнятий. - The scanner's cover is open. + Відкрито кришку сканеру. - The scanner has a paper jam. + Зминання паперу у сканері. - The scanner is warming up. + Прогрівання сканеру. - An update to OCR is available. + Доступне оновлення пакету розпізнавання. - Image saved. + Зображення збережено. - {0} images saved. + {0} зображень збережено. - PDF saved. + Файл PDF збережено. {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Дійсно вилучити "{0}"? - The file could not be overwritten because it is currently in use. + Неможливо перезаписати файл, оскільки він зараз використовується. - Deskewing... + Вирівнювання виконується... - Deskew Progress + Вирівнювання перекосу - Download Needed + Потрібно звантажити - An additional component is needed to import this PDF file. Would you like to download it now? + Для імпорту цього файлу PDF потрбно встановити додатковий компонент. Чи завантажити його зараз? - Are you sure you want to cancel the batch scan? + Дійсно скасувати пакетне сканування - Cancel Batch + Скасувати пакетне сканування - Donate + Ваш внесок - NAPS2 is completely free. Consider making a donation. + NAPS2 є вільним програмним забезпеченням. Запрошуємо зробити ваш внесок. + + + Leave a Review + + + Подобається NAPS2? - The SANE driver is not available. Make sure to install the required packages: + Драйвер SANE не доступний. Перевірте, чи встановлено такі пакети: - The OCR engine is not available. Make sure to install the required package: + Рушій розпізнавання (OCR) не доступний. Перевірте, чи встановлено такі пакети:: - The selected driver is not supported on this system. + Вибраний драйвер не підтримується системою. - OCR Progress + Виконується розпізнавання - Running OCR... + Виконується розпізнавання... - Update Progress + Виконується оновлення - Updating... + Оновлення... - No updates available. + Відсутні оновлення - Checking... + Виконується перевірка... - Update checking is disabled. + Перевірку оновлення вимкнено. - An error occured when trying to install the update. + Під час встановлення оновлення трапилася помилка. - Install {0} + Встановити {0} - An update is available. + Доступне оновлення. - An error occurred when trying to authorize. + Під час спроби авторизації трапилася помилка. - Uploading email... + Завантаження ел.пошти... - An error occurred when trying to send the email. + Під час надсилання ел. пошти сталася помилка. Сканування сторінки {0}... diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.vi.resx b/NAPS2.Lib/Lang/Resources/MiscResources.vi.resx index 176d51f46e..9cfeae97a1 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.vi.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.vi.resx @@ -190,7 +190,7 @@ of {0} - Có lỗi khi scan hàng loạt. + An unknown error occurred during the batch scan. Hủy Scan hàng loạt. @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + Bạn có chắc chắn muốn xóa "{0}"? Các tập tin có thể không được ghi đè bởi vì nó hiện đang sử dụng. @@ -339,6 +339,12 @@ NAPS2 miễn phí hoàn toàn. Bạn có thể ủng hộ tùy ý.. + + Leave a Review + + + Like NAPS2? + The SANE driver is not available. Make sure to install the required packages: diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.zh-CN.resx b/NAPS2.Lib/Lang/Resources/MiscResources.zh-CN.resx index 6eeaf70eba..f25adb3d6f 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.zh-CN.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.zh-CN.resx @@ -22,13 +22,13 @@ 您确定要清除 {0} 个项目吗? - 您确定要删除{0}个项目吗? + 您确定要删除{0} 个项目吗? - 您确定要删除{0}个配置吗? + 您确定要删除{0} 个配置吗? - 您确定要删除这个{0}配置吗? + 您确定要删除这个{0} 配置吗? 文件 {0} 已存在,是否覆盖? @@ -73,7 +73,7 @@ TIFF 文件 (*.tiff, *.tif) - JPEG2000 File (*.jp2, *.jpx) + JPEG 文件(*.jp2, *.jpx) 请输入配置名称. @@ -121,7 +121,7 @@ 安装失败 - 全部 ({0}) + 所有 ({0}) 已选择 ({0}) @@ -154,7 +154,7 @@ 扫描 - 您确定要撤销您对{0}个图像的修改吗? + 您确定要撤销您对{0} 个图像的修改吗? 重置图像 @@ -163,7 +163,7 @@ 您有未保存的修改,确定要放弃这些修改并退出吗? - 操作进行中,是否取消操作并退出? + 一个操作正在进行中。您确定要退出并取消操作吗? 未保存的更改 @@ -310,7 +310,7 @@ {0} ({1}x{2} {3}) - Are you sure you want to delete "{0}"? + 您确定要删除“{0}”吗? 文件正在使用,无法覆盖. @@ -339,6 +339,12 @@ NAPS2 是完全免费的。请考虑捐款资助. + + 撰写评论 + + + 喜欢NAPS2吗? + SANE 驱动不可用。请确认已安装所需的软件包: @@ -367,7 +373,7 @@ 正在检查... - Update checking is disabled. + 更新检查已禁用 尝试安装更新时发生错误. diff --git a/NAPS2.Lib/Lang/Resources/MiscResources.zh-TW.resx b/NAPS2.Lib/Lang/Resources/MiscResources.zh-TW.resx index 32d051731e..6d199c21b5 100644 --- a/NAPS2.Lib/Lang/Resources/MiscResources.zh-TW.resx +++ b/NAPS2.Lib/Lang/Resources/MiscResources.zh-TW.resx @@ -190,7 +190,7 @@ / {0} - An unknown error ocurred during the batch scan. + An unknown error occurred during the batch scan. Batch cancelled. @@ -339,6 +339,12 @@ NAPS2 is completely free. Consider making a donation. + + Leave a Review + + + Like NAPS2? + The SANE driver is not available. Make sure to install the required packages: @@ -370,7 +376,7 @@ Update checking is disabled. - An error occured when trying to install the update. + An error occurred when trying to install the update. Install {0} diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.Designer.cs b/NAPS2.Lib/Lang/Resources/SettingsResources.Designer.cs index 39417b586c..90f506d1b7 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.Designer.cs +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.Designer.cs @@ -60,7 +60,7 @@ internal SettingsResources() { } /// - /// Looks up a localized string similar to Black & White. + /// Looks up a localized string similar to Black and White. /// internal static string BitDepth_1BlackAndWhite { get { @@ -87,110 +87,74 @@ internal static string BitDepth_8Grayscale { } /// - /// Looks up a localized string similar to 100 dpi. + /// Looks up a localized string similar to {0} dpi. /// - internal static string Dpi_100 { + internal static string DpiFormat { get { - return ResourceManager.GetString("Dpi_100", resourceCulture); + return ResourceManager.GetString("DpiFormat", resourceCulture); } } /// - /// Looks up a localized string similar to 1200 dpi. - /// - internal static string Dpi_1200 { - get { - return ResourceManager.GetString("Dpi_1200", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to 150 dpi. - /// - internal static string Dpi_150 { - get { - return ResourceManager.GetString("Dpi_150", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to 200 dpi. - /// - internal static string Dpi_200 { - get { - return ResourceManager.GetString("Dpi_200", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to 300 dpi. - /// - internal static string Dpi_300 { - get { - return ResourceManager.GetString("Dpi_300", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to 400 dpi. + /// Looks up a localized string similar to No provider selected.. /// - internal static string Dpi_400 { + internal static string EmailProvider_NotSelected { get { - return ResourceManager.GetString("Dpi_400", resourceCulture); + return ResourceManager.GetString("EmailProvider_NotSelected", resourceCulture); } } /// - /// Looks up a localized string similar to 600 dpi. + /// Looks up a localized string similar to Apple Mail. /// - internal static string Dpi_600 { + internal static string EmailProviderType_AppleMail { get { - return ResourceManager.GetString("Dpi_600", resourceCulture); + return ResourceManager.GetString("EmailProviderType_AppleMail", resourceCulture); } } /// - /// Looks up a localized string similar to 800 dpi. + /// Looks up a localized string similar to Custom SMTP. /// - internal static string Dpi_800 { + internal static string EmailProviderType_CustomSmtp { get { - return ResourceManager.GetString("Dpi_800", resourceCulture); + return ResourceManager.GetString("EmailProviderType_CustomSmtp", resourceCulture); } } /// - /// Looks up a localized string similar to No provider selected.. + /// Looks up a localized string similar to Gmail. /// - internal static string EmailProvider_NotSelected { + internal static string EmailProviderType_Gmail { get { - return ResourceManager.GetString("EmailProvider_NotSelected", resourceCulture); + return ResourceManager.GetString("EmailProviderType_Gmail", resourceCulture); } } /// - /// Looks up a localized string similar to Custom SMTP. + /// Looks up a localized string similar to Outlook (new). /// - internal static string EmailProviderType_CustomSmtp { + internal static string EmailProviderType_OutlookNew { get { - return ResourceManager.GetString("EmailProviderType_CustomSmtp", resourceCulture); + return ResourceManager.GetString("EmailProviderType_OutlookNew", resourceCulture); } } /// - /// Looks up a localized string similar to Gmail. + /// Looks up a localized string similar to Outlook Web Access. /// - internal static string EmailProviderType_Gmail { + internal static string EmailProviderType_OutlookWeb { get { - return ResourceManager.GetString("EmailProviderType_Gmail", resourceCulture); + return ResourceManager.GetString("EmailProviderType_OutlookWeb", resourceCulture); } } /// - /// Looks up a localized string similar to Outlook Web Access. + /// Looks up a localized string similar to Thunderbird. /// - internal static string EmailProviderType_OutlookWeb { + internal static string EmailProviderType_Thunderbird { get { - return ResourceManager.GetString("EmailProviderType_OutlookWeb", resourceCulture); + return ResourceManager.GetString("EmailProviderType_Thunderbird", resourceCulture); } } @@ -383,6 +347,51 @@ internal static string PdfCompat_PdfA3U { } } + /// + /// Looks up a localized string similar to Custom.... + /// + internal static string Resolution_Custom { + get { + return ResourceManager.GetString("Resolution_Custom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Always Prompt. + /// + internal static string SaveButtonDefaultAction_AlwaysPrompt { + get { + return ResourceManager.GetString("SaveButtonDefaultAction_AlwaysPrompt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prompt If Selected. + /// + internal static string SaveButtonDefaultAction_PromptIfSelected { + get { + return ResourceManager.GetString("SaveButtonDefaultAction_PromptIfSelected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save All. + /// + internal static string SaveButtonDefaultAction_SaveAll { + get { + return ResourceManager.GetString("SaveButtonDefaultAction_SaveAll", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save Selected. + /// + internal static string SaveButtonDefaultAction_SaveSelected { + get { + return ResourceManager.GetString("SaveButtonDefaultAction_SaveSelected", resourceCulture); + } + } + /// /// Looks up a localized string similar to 1:1. /// @@ -419,6 +428,24 @@ internal static string Scale_1_8 { } } + /// + /// Looks up a localized string similar to Always Prompt. + /// + internal static string ScanButtonDefaultAction_AlwaysPrompt { + get { + return ResourceManager.GetString("ScanButtonDefaultAction_AlwaysPrompt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Scan With Default Profile. + /// + internal static string ScanButtonDefaultAction_ScanWithDefaultProfile { + get { + return ResourceManager.GetString("ScanButtonDefaultAction_ScanWithDefaultProfile", resourceCulture); + } + } + /// /// Looks up a localized string similar to Duplex. /// @@ -446,6 +473,33 @@ internal static string Source_Glass { } } + /// + /// Looks up a localized string similar to Dark. + /// + internal static string Theme_Dark { + get { + return ResourceManager.GetString("Theme_Dark", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Default. + /// + internal static string Theme_Default { + get { + return ResourceManager.GetString("Theme_Default", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Light. + /// + internal static string Theme_Light { + get { + return ResourceManager.GetString("Theme_Light", resourceCulture); + } + } + /// /// Looks up a localized string similar to Auto. /// @@ -501,7 +555,7 @@ internal static string TwainImpl_Legacy { } /// - /// Looks up a localized string similar to Alternative Transfer. + /// Looks up a localized string similar to Memory Transfer. /// internal static string TwainImpl_MemXfer { get { @@ -509,6 +563,15 @@ internal static string TwainImpl_MemXfer { } } + /// + /// Looks up a localized string similar to Native Transfer. + /// + internal static string TwainImpl_NativeXfer { + get { + return ResourceManager.GetString("TwainImpl_NativeXfer", resourceCulture); + } + } + /// /// Looks up a localized string similar to Old DSM. /// diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.af.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.af.resx index 2cd1bd0100..0a169f8c93 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.af.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.af.resx @@ -13,64 +13,52 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Trắng & Đen + Swart en Wit - 24-bit Color + 24-bis Kleur - Grayscale + Grys - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi - Custom SMTP + Pasgemaakte SMTP Gmail + + Outlook (new) + - Outlook Web Access + Outlook Web Toegang + + + Thunderbird + + + Apple Pos - No provider selected. + Geen verskaffer gkies nie. - Canh giữa + Senter - Trái + Links - Right + Regs cm - in + duim mm @@ -97,10 +85,13 @@ US Legal (8.5x14 in) - US Letter (8.5x11 in) + US Legal (8.5x14 in) + + + Tùy chỉnh... - Mặc định + Verstek PDF/A-1b @@ -115,16 +106,16 @@ PDF/A-3u - Hai mặt + Duplex - Feeder + Voerder - Kính + Glas - Auto + Outo CCITT4 @@ -133,16 +124,19 @@ LZW - None + Geen - Mặc định + Verstek - Legacy (UI bản địa chỉ) + Tradisioneel (Slegs inheemse UI) + + + Inheemse Oordrag - Alternative Transfer + Geheue Oordrag Ou DSM @@ -151,12 +145,39 @@ x64 - Mặc định + Verstek - Fast + Vinnig - Best + Beste + + + Stoor Alles + + + Stoor gekiesde + + + Vra Altyd + + + Vra indien gekies + + + Skandeer met verstekprofiel + + + Vra Altyd + + + Verstek + + + Light + + + Dark \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.ar.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.ar.resx index fde57dacf8..2584d9c654 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.ar.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.ar.resx @@ -21,29 +21,8 @@ تدرج الرمادي - - 100 نقطة لكل بوصة - - - 1200 نقطة لكل بوصة - - - 150 نقطة لكل بوصة - - - 200 نقطة لكل بوصة - - - 300 نقطة لكل بوصة - - - 400 نقطة لكل بوصة - - - 600 نقطة لكل بوصة - - - 800 نقطة لكل بوصة + + {0} نقطة لكل بوصة بروتوكول نقل البريد البسيط المخصص @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + No provider selected. @@ -99,6 +87,9 @@ US رسالة (8.5x11 بوصة) + + مخصص... + الإفتراضي @@ -141,8 +132,11 @@ وراثي (واجهة المستخدم الأصلية فقط) + + Native Transfer + - التحويل البديل + Memory Transfer DSM القديم @@ -159,4 +153,31 @@ Best + + Save All + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + الإفتراضي + + + Light + + + Dark + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.bg.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.bg.resx index bdeb76cd00..dc0e17aa44 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.bg.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.bg.resx @@ -21,41 +21,29 @@ Степени на сивото - - 100 dpi - - - 1200 dpi - - - 100 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi - Custom SMTP + Персонализиран SMTP Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple имейл + - No provider selected. + Няма избран доставчик. Център @@ -99,6 +87,9 @@ US писмо (8.5x11 инч) + + По избор... + По подразбиране @@ -130,19 +121,22 @@ CCITT4 - LZW + LZW - алгоритъм за компресиране на данни без загуби - None + Нито един По подразбиране - Legacy (native UI only) + Остаряло (само с присъщия потребителски интерфейс) + + + Местен трансфер - Alternative Transfer + Прехвърляне на паметта Стар DSM @@ -154,9 +148,36 @@ По подразбиране - Fast + Бърз - Best + Най-добър + + + Запазване на всички + + + Запазване на избраното + + + Постоянно напомняне + + + Ако е избрано напомняне + + + Сканиране с профил по подразбиране + + + Постоянно напомняне + + + По подразбиране + + + Light + + + Dark \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.bs.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.bs.resx new file mode 100644 index 0000000000..39aff1c207 --- /dev/null +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.bs.resx @@ -0,0 +1,183 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Crna i bijela boja + + + 24-bitne boje + + + Nijanse sive boje + + + {0} TPI + + + Prilagođeni SMTP + + + Gmail + + + Outlook (novi) + + + Outlook web pristup + + + Thunderbird + + + Apple Mail + + + Nije izabran provajder. + + + centralno + + + Lijevo + + + Desno + + + cm + + + in + + + mm + + + A3 (297x420 mm) + + + A4 (210x297 mm) + + + A5 (148x210 mm) + + + B4 (250x353 mm) + + + B5 (176x250 mm) + + + Prilagođeno... + + + Američki legal (8,5x14 inča) + + + Američko pismo (8,5x11 inča) + + + Prilagođeno... + + + Zadana + + + PDF/A-1b + + + PDF/A-2b + + + PDF/A-3b + + + PDF/A-3u + + + Dvostran + + + Ulagač papira + + + Skenersko staklo + + + Automatska + + + CCITT4 + + + LZW + + + Nijedan + + + Zadana + + + Legat (samo nativni KI) + + + Nativni transfer + + + Memorijski transfer + + + Stari DSM + + + x64 + + + Zadana + + + Brzi + + + Najbolji + + + Sačuvaj sve + + + Sačuvaj izabrano + + + Uvijek upitaj + + + Upitaj ako je izabrano + + + Skeniraj sa zadanim profilom + + + Uvijek upitaj + + + Zadana + + + Svijetla + + + Tamna + + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.ca.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.ca.resx index be7a759480..538a0a926f 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.ca.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.ca.resx @@ -21,29 +21,8 @@ Escala de grisos - - 100 ppp - - - 1200 ppp - - - 150 ppp - - - 1200 ppp - - - 300 ppp - - - 400 ppp - - - 600 ppp - - - 800 ppp + + {0} ppp SMTP personalitzat @@ -51,9 +30,18 @@ Gmail + + Outlook (nou) + Accés web de l'Outlook + + Thunderbird + + + Apple Mail + No s'ha seleccionat cap proveïdor. @@ -91,13 +79,16 @@ B5 (176x250 mm) - Personalitzat... + Personalitzada... US Legal (8.5x14 in) - US Letter (8.5x11 in) + Carta US (8.5x11 in) + + + Personalitzada... Per defecte @@ -124,7 +115,7 @@ Vidre - Automàtic + Automàtica CCITT4 @@ -139,10 +130,13 @@ Per defecte - Legacy (native UI only) + Heretat (només interfície nativa) + + + Transferència nativa - Baixada opcional + Transferència en memòria DSM antic @@ -154,9 +148,36 @@ Per defecte - Fast + Ràpid - Best + Millor + + + Desa-ho tot + + + Desa la selecció + + + Demana sempre + + + Demana en seleccionar + + + Escaneja amb el perfil per defecte + + + Demana sempre + + + Per defecte + + + Clar + + + Fosc \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.cs.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.cs.resx index 5caac10929..8aa3c31cd8 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.cs.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.cs.resx @@ -21,29 +21,8 @@ Stupně šedi - - 100 dpi - - - 1 200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Vlastní SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (nový) + Outlook Web Access + + Thunderbird + + + Apple Mail + Nebyl vybrán poskytovatel. @@ -99,6 +87,9 @@ US Letter (8.5 × 11 in) + + Vlastní... + Výchozí @@ -141,8 +132,11 @@ Legacy (pouze nativní UI) + + Nativní přenos + - Alternativní přenos + Přenos paměti, Staré DSM @@ -154,9 +148,36 @@ Výchozí - Fast + Rychlý - Best + Nejlepší + + + Uložit vše + + + Uložit vybrané + + + Vždy se zeptat + + + Výzva Pokud je vybráno + + + Skenovat pomocí výchozího profilu + + + Vždy se zeptat + + + Výchozí + + + Světlý + + + Tmavý \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.da.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.da.resx index 4151a714cd..2527b68383 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.da.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.da.resx @@ -21,41 +21,29 @@ Gråskala - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi - Custom SMTP + Brugerdefineret SMTP Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + - No provider selected. + Ingen udbyder valgt. Centrér @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Brugertilpasset... + Standard @@ -133,7 +124,7 @@ LZW - None + Ingen Standard @@ -141,8 +132,11 @@ Arv (kun oprindelig brugergrænseflade) + + Indbygget overførsel + - Alternativ overførsel + Overførsel af hukommelse Gammel DSM @@ -154,9 +148,36 @@ Standard - Fast + Hurtig - Best + Bedst + + + Gem alle + + + Gem valgte + + + Spørg altid + + + Spørg hvis valgt + + + Scan med standardprofil + + + Spørg altid + + + Standard + + + Light + + + Dark \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.de.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.de.resx index f412505191..d60dd56c27 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.de.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.de.resx @@ -21,29 +21,8 @@ Graustufen - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Benutzerdefinierter SMTP @@ -51,8 +30,17 @@ GMail + + Outlook (neu) + - Outlook Web Access + Outlook im Web + + + Thunderbird + + + Apple Mail Kein Anbieter ausgewählt. @@ -94,10 +82,13 @@ Benutzerdefiniert... - US Legal (8.5x14 in) + US Legal (8.5x14 Zoll) - US Letter (8.5x11 in) + US Letter (8.5x11 Zoll) + + + Benutzerdefiniert... Standard @@ -139,10 +130,13 @@ Standard - Altsystem (nur ursprüngliche Oberfläche) + Altsystem (nur native Oberfläche) + + + Native Übertragung - Alternative Übertragung + Gepufferte Übertragung Alte Datenquellenverwaltung @@ -154,9 +148,36 @@ Standard - Fast + Schnell - Best + Am besten + + + Alles speichern + + + Auswahl speichern + + + Immer nachfragen + + + Nachfrage, wenn ausgewählt + + + Mit Standardprofil scannen + + + Immer nachfragen + + + Standard + + + Hell + + + Dunkel \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.el.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.el.resx index 934b44bbbb..0af993a9c7 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.el.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.el.resx @@ -21,41 +21,29 @@ Κλίμακα του Γκρι - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi - Custom SMTP + Προσαρμοσμένο SMTP Gmail + + Outlook (new) + - Outlook Web Access + Outlook Πρόσβαση μέσω Web + + + Thunderbird + + + Apple Mail - No provider selected. + Δεν έχει επιλεγεί πάροχος. Κέντρο @@ -99,6 +87,9 @@ US Letter (8.5x11 ιν.) + + Προσαρμογή... + Προεπιλεγμένο @@ -141,8 +132,11 @@ Παρωχημένο (εγγενές περιβάλλον μόνο) + + Native Transfer + - Εναλλακτική Μεταφορά + Memory Transfer Παλαιός Διαχειριστής Πηγής Δεδομένων (DSM) @@ -154,9 +148,36 @@ Προεπιλεγμένο - Fast + Γρήγορα - Best + Βέλτιστα + + + Αποθήκευση Όλων + + + Αποθήκευση Επιλεγμένων + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + Προεπιλεγμένο + + + Light + + + Dark \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.es.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.es.resx index 43c5ec3912..9ea7c96c75 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.es.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.es.resx @@ -21,38 +21,26 @@ Escala de grises - - 100 ppp - - - 1200 ppp - - - 150 ppp - - - 200 ppp - - - 300 ppp - - - 400 ppp - - - 600 ppp - - - 800 ppp + + {0} ppp SMTP personalizado - Gmail + GMail + + + Outlook (nuevo) - Outlook Web Access + Acceso web de Outlook + + + Thunderbird + + + Apple Mail No se ha seleccionado ningún proveedor. @@ -99,6 +87,9 @@ Carta (279 x 216 mm) + + Personalizar... + Predeterminado @@ -141,8 +132,11 @@ Heredado (solo interfaz nativa) + + Transferencia nativa + - Transferencia alternativa + Transferencia de memoria Viejo DSM @@ -154,9 +148,36 @@ Predeterminado - Fast + Rápido - Best + Mejor + + + Guardar todo + + + Guardar seleccionado + + + Preguntar siempre + + + Preguntar si se selecciona + + + Escanear con perfil predeterminado + + + Preguntar siempre + + + Predeterminado + + + Claro + + + Oscuro \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.et.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.et.resx index 5f311b84fc..4043da0949 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.et.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.et.resx @@ -21,29 +21,8 @@ Halltoonid - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Custom SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + Ükski pakkuja pole valitud. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Custom... + Vaikimisi @@ -141,8 +132,11 @@ Legacy (native UI only) + + Native Transfer + - Alternative Transfer + Memory Transfer Old DSM @@ -159,4 +153,31 @@ Best + + Save All + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + Vaikimisi + + + Light + + + Dark + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.fa.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.fa.resx index 019cb027ab..cdf561ecea 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.fa.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.fa.resx @@ -21,29 +21,8 @@ بی‌رنگ - - ۱۰۰ نقطه در اینچ - - - ۱۲۰۰ نقطه در اینچ - - - ۱۵۰ نقطه در اینچ - - - ۲۰۰ نقطه در اینچ - - - ۳۰۰ نقطه در اینچ - - - ۴۰۰ نقطه در اینچ - - - ۶۰۰ نقطه در اینچ - - - ۸۰۰ نقطه در اینچ + + {0} نقطه در اینچ SMTP خاص @@ -51,9 +30,18 @@ پست الکترونیکی جی‌میل + + Outlook (new) + دسترسی وب Outlook + + Thunderbird + + + Apple Mail + هیچ فراهم‌کننده‌ای‌ انتخاب نشده. @@ -99,6 +87,9 @@ US Letter (۸٫۵x۱۱ اینچ) + + سفارشی... + پیش‌فرض @@ -141,8 +132,11 @@ ارث (فقط رابط کاربری بومی) + + Native Transfer + - روش انتقال جایگزین + Memory Transfer DSM قدیمی @@ -159,4 +153,31 @@ Best + + Save All + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + پیش‌فرض + + + Light + + + Dark + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.fi.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.fi.resx index ea2aa30919..ffc315d27a 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.fi.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.fi.resx @@ -21,29 +21,8 @@ Harmaasävy - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 800 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Mukautettu SMTP @@ -51,8 +30,17 @@ Gmail + + Outlook (new) + - Outlook Web Access + Outlook Web-käyttö + + + Thunderbird + + + Apple Sähköposti Tapaa ei ole valittu. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Mukautettu... + Oletus @@ -141,8 +132,11 @@ Perinteinen (vain natiivikäyttöliittymä) + + Natiivi Siirto + - Vaihtoehtoinen muunnos + Memory Transfer Vanha DSM @@ -154,9 +148,36 @@ Oletus - Fast + Nopea - Best + Paras + + + Tallenna kaikki + + + Tallenna valitut + + + Kysy aina + + + Kysy jos valittu + + + Skannaa Oletusprofiililla + + + Kysy aina + + + Oletus + + + Light + + + Dark \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.fr.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.fr.resx index 104aa9eee7..d474006bfa 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.fr.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.fr.resx @@ -21,29 +21,8 @@ Niveaux de gris - - 100 ppp - - - 1200 ppp - - - 150 ppp - - - 200 ppp - - - 300 ppp - - - 400 ppp - - - 600 ppp - - - 800 ppp + + {0} ppp SMTP personnalisé @@ -51,9 +30,18 @@ Gmail + + Outlook (nouveau) + Accès Web pour Outlook + + Thunderbird + + + Apple Mail + Aucun fournisseur sélectionné. @@ -61,10 +49,10 @@ Centré - Gauche + À gauche - Droite + À droite cm @@ -91,7 +79,7 @@ B5 (176x250 mm) - Personnalisé... + Personnalisée… US Legal (8.5x14 p) @@ -99,6 +87,9 @@ US Letter (8.5x11 p) + + Personnalisée… + Prédéfinie @@ -118,7 +109,7 @@ Recto-verso - Dispositif d'alimentation + Chargeur de documents Vitre @@ -141,8 +132,11 @@ Ancienne (interface native seulement) + + Transfert natif + - Transfert alternatif + Transfert en mémoire Ancien DSM @@ -154,9 +148,36 @@ Prédéfinie - Fast + Plus rapide - Best + Plus précis + + + Tout enregistrer + + + Enregistrer la sélection + + + Toujours demander + + + Demander si sélectionné + + + Numériser avec le profil par défaut + + + Toujours demander + + + Prédéfinie + + + Clair + + + Sombre \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.he.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.he.resx index dbc8b38f9a..7bab1ec330 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.he.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.he.resx @@ -21,41 +21,29 @@ גווני אפור - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} נק׳ לאינטש - Custom SMTP + SMTP בהתאמה אישית Gmail + + Outlook (חדש) + Outlook Web Access + + Thunderbird + + + דוא״ל Apple + - No provider selected. + לא נבחר ספק. ממורכז @@ -67,37 +55,40 @@ ימין - ס\"מ + ס״מ אינטש - מ\"מ + מ״מ - A3 (297x240 מ\"מ) + A3 (297x240 מ״מ) - A4 (210x297 מ\"מ) + A4 (210x297 מ״מ) - A5 (148x210 מ\"מ) + A5 (148x210 מ״מ) - B4 (250x353 מ\"מ) + B4 (250x353 מ״מ) - B5 (176x250 מ\"מ) + B5 (176x250 מ״מ) - מותאם אישית... + התאמה אישית… - נייר US Legal (8.5x14 in) + נייר US Legal (8.5x14 אינטש) - נייר US Legal (8.5x14 in) + נייר US Letter (8.5x11 אינטש) + + + התאמה אישית… ברירת מחדל @@ -115,7 +106,7 @@ PDF/A-3u - דופלקס (דו-צדדי) + דופלקס (דו־צדדי) מזין מסמכים @@ -133,19 +124,22 @@ LZW - None + אין ברירת מחדל - Legacy (native UI only) + מיושן (ממשק משתמש מקורי בלבד) + + + העברה טבעית - Alternative Transfer + העברת זיכרון - Old DSM + מנהל מקורות נתונים ישן x64 @@ -154,9 +148,36 @@ ברירת מחדל - Fast + מהיר - Best + מיטבי + + + שמירה של הכול + + + שמירת הנבחרים + + + תמיד לבקש אישור + + + לבקש אישור אם נבחר + + + לסרוק עם פרופיל ברירת המחדל + + + תמיד לבקש אישור + + + ברירת מחדל + + + בהיר + + + כהה \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.hi.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.hi.resx new file mode 100644 index 0000000000..8628f840a7 --- /dev/null +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.hi.resx @@ -0,0 +1,183 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + काला और सफेद + + + 24-बिट रंग + + + ग्रेस्केल + + + {0} dpi + + + स्वयं का एसएमटीपी(SMTP) + + + जीमेल + + + Outlook (new) + + + Outlook Web Access + + + Thunderbird + + + एप्पल मेल + + + कोई प्रदाता चयनित नहीं। + + + केन्द्र + + + बाएं + + + Right + + + cm + + + in + + + mm + + + A3 (297x420 मिलीमीटर) + + + A4 (210x297 मिलीमीटर) + + + A5 (148x210 मिलीमीटर) + + + बी4 (250x353 मिलीमीटर) + + + बी5 (176x250 मिलीमीटर) + + + कस्टम... + + + US Legal (8.5x14 in) + + + US Letter (8.5x11 in) + + + कस्टम... + + + डिफ़ॉल्ट (न्यून) + + + PDF/A-1b + + + PDF/A-2b + + + PDF/A-3b + + + PDF/A-3u + + + डुप्लेक्स + + + Feeder + + + Glass + + + स्वतः + + + CCITT4 + + + LZW + + + None + + + डिफ़ॉल्ट (न्यून) + + + लीगेसी (केवल मूल यूआई) + + + नेटिव ट्रांसफर + + + मेमोरी ट्रांसफर + + + Old DSM + + + x64 + + + डिफ़ॉल्ट (न्यून) + + + तेज + + + श्रेष्ठ + + + Save All + + + चयनित सेव करें + + + हमेशा पूछें + + + Prompt If Selected + + + Scan With Default Profile + + + हमेशा पूछें + + + डिफ़ॉल्ट (न्यून) + + + Light + + + Dark + + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.hr.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.hr.resx index 250ca0ad6c..027f9623bc 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.hr.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.hr.resx @@ -13,7 +13,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Crno/bijelo + Crno-bijelo 24-bitna Boja @@ -21,41 +21,29 @@ Sivi tonovi - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi - Custom SMTP + Prilagođeni SMTP Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + - No provider selected. + Nije odabran nijedan pružatelj usluga. Sredina @@ -91,7 +79,7 @@ B5 (176x250 mm) - Custom... + Prilagođeno... US Legal (8.5x14 in) @@ -99,8 +87,11 @@ US Letter (8.5x11 in) + + Prilagođeno... + - Default + Zadano PDF/A-1b @@ -118,7 +109,7 @@ Dvostrano - Umetač + Ulagač papira Staklo @@ -136,27 +127,57 @@ None - Default + Zadano Legacy (native UI only) + + Native Transfer + - Alternativni Prijenos + Memory Transfer - Old DSM + Stari DSM x64 - Default + Zadano - Fast + Brzo Best + + Spremi sve + + + Spremi odabrano + + + Uvijek upitaj + + + Pitaj ako je odabrano + + + Skeniraj sa zadanim profilom + + + Uvijek upitaj + + + Zadano + + + Svjetla + + + Tamna + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.hu.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.hu.resx index baf786f3d3..ff90dea4af 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.hu.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.hu.resx @@ -16,46 +16,34 @@ Fekete-fehér - Szines (24 bit) + 24 bites szín - Szürkeárnylatos + Szürkeárnyalatos - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi - Custom SMTP + Egyéni SMTP Gmail + + Outlook (új) + Outlook Web Access + + Thunderbird + + + Apple Mail + - No provider selected. + Nincs szolgáltató kiválasztva. Középre @@ -70,7 +58,7 @@ cm - in + hüvelyk mm @@ -91,13 +79,16 @@ B5 (176 x 250 mm) - Custom... + Egyéni... - US Legal (8.5x14 in) + US Legal (8.5x14 hüvelyk) - US Letter (8.5x11 in) + US Letter (8,5x11 hüvelyk) + + + Egyéni... Alapértelmezett @@ -124,7 +115,7 @@ Síkágy - Auto + Automatikus CCITT4 @@ -133,19 +124,22 @@ LZW - None + Nincs Alapértelmezett - Legacy (native UI only) + Régi (csak natív felhasználói felület) + + + Natív átvitel - Alternative Transfer + Memória átvitel - Old DSM + Régi DSM x64 @@ -154,9 +148,36 @@ Alapértelmezett - Fast + Gyors - Best + Legjobb + + + Összes mentése + + + Kiválasztott mentése + + + Mindig kérdezzen rá + + + Kérdezzen rá, ha kiválasztva + + + Beolvasás az alapértelmezett profillal + + + Mindig kérdezzen rá + + + Alapértelmezett + + + Világos + + + Sötét \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.id.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.id.resx new file mode 100644 index 0000000000..c7ed3522db --- /dev/null +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.id.resx @@ -0,0 +1,183 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Hitam Putih + + + Kedalaman Warna 24-Bit + + + Skala abu-abu + + + {0} dpi + + + SMTP Custom + + + Gmail + + + Outlook (new) + + + Outlook Web Access + + + Thunderbird + + + Apple Mail + + + No provider selected. + + + Center + + + Left + + + Right + + + cm + + + in + + + mm + + + A3 (29.7x42.0 cm) + + + A4 (21.0x29.7 cm) + + + A5 (14.8x21.0 cm) + + + B4 (25.0x35.3 cm) + + + B5 (17.6x25.0 cm) + + + kostumasi... + + + US Legal (8.5x14 in) + + + US Letter (8.5x11 in) + + + kostumasi... + + + Default + + + PDF/A-1b + + + PDF/A-2b + + + PDF/A-3b + + + PDF/A-3u + + + Bolak Balik / 2 muka + + + Pengumpan + + + Kaca + + + Auto + + + CCITT4 + + + LZW + + + None + + + Default + + + Legacy (native UI only) + + + Native Transfer + + + Memory Transfer + + + Old DSM + + + x64 + + + Default + + + Cepat + + + Terbaik + + + Save All + + + Save Selected + + + Selalu tanya + + + Prompt If Selected + + + Scan With Default Profile + + + Selalu tanya + + + Default + + + Light + + + Dark + + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.it.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.it.resx index 0ac043b160..f3e18544b0 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.it.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.it.resx @@ -16,34 +16,13 @@ Bianco e nero - Colore a 24-bit + Colore a 24bit Scala di grigi - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi SMTP personalizzato @@ -51,8 +30,17 @@ Gmail + + Outlook (nuovo) + - Accesso Web di Outlook + Accesso web Outlook + + + Thunderbird + + + Apple Mail Nessun provider selezionato. @@ -70,7 +58,7 @@ cm - in + pollici mm @@ -85,7 +73,7 @@ A5 (148x210 mm) - B4 (250×353 mm) + B4 (250x353 mm) B5 (176x250 mm) @@ -94,10 +82,13 @@ Personalizzato... - Legale USA (8.5x14 in) + Legale USA (8.5x14 pollici) - Lettera USA (8.5x11 in) + Lettera USA (8.5x11 pollici) + + + Personalizzato... Predefinito @@ -115,16 +106,16 @@ PDF/A-3u - Fronte/Retro + Fronte/retro - Caricatore + Alimentatore automatico - Piano + Piano fisso - Auto + Automatico CCITT4 @@ -139,10 +130,13 @@ Predefinito - Eredita (solo UI nativo) + Legacy (solo interfaccia grafica nativa) + + + Trasferimento nativo - E' neccessario un componente aggiuntivo per importare questo fle PDF. Vuoi scaricarlo ora? + Trasferimento in memoria Vecchio DSM @@ -154,9 +148,36 @@ Predefinito - Fast + Veloce - Best + Migliore + + + Salva tutti + + + Salva selezionati + + + Chiedi sempre + + + Chiedi se selezionato + + + Scansiona con Profilo Predefinito + + + Chiedi sempre + + + Predefinito + + + Chiaro + + + Scuro \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.ja.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.ja.resx index 9ed02e22c0..685e574eac 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.ja.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.ja.resx @@ -21,29 +21,8 @@ グレースケール - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi カスタムSMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outlookへのウェブアクセス + + Thunderbird + + + Apple Mail + プロバイダが選択されていません。. @@ -99,6 +87,9 @@ US Legal (8.5x14 インチ) + + カスタム... + デフォルト @@ -141,8 +132,11 @@ スキャナ付属のUIのみを利用 + + Native Transfer + - Alternative Transfer + Memory Transfer Old DSM @@ -159,4 +153,31 @@ Best + + Save All + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + デフォルト + + + Light + + + Dark + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.ko.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.ko.resx index 83492168df..cb8d04a6ee 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.ko.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.ko.resx @@ -21,41 +21,29 @@ 회색조 - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi - Custom SMTP + 사용자 설정 SMTP Gmail + + Outlook (new) + - Outlook Web Access + 웹용 Outlook + + + Thunderbird + + + Apple 메일 - No provider selected. + 제공자 선택되지 않음 중앙 @@ -70,7 +58,7 @@ cm - in + 인치 mm @@ -94,10 +82,13 @@ 사용자 설정... - US Legal (8.5x14 in) + US Legal (8.5x14 인치) - US Letter (8.5x11 in) + US Letter (8.5x11 인치) + + + 사용자 설정... 기본값 @@ -124,7 +115,7 @@ 평판 - Auto + 자동 CCITT4 @@ -133,7 +124,7 @@ LZW - None + 없음 기본값 @@ -141,8 +132,11 @@ 레거시 (네이티브 UI 만 가능) + + 네이티브 전송 + - 대체 전송 + 메모리 전송 구 DSM @@ -154,9 +148,36 @@ 기본값 - Fast + 빠르게 - Best + 최고 품질 + + + 모두 저장 + + + 선택된 항목 저장 + + + 항상 묻기 + + + 선택한 경우 묻기 + + + 기본 프로파일을 사용하여 스캔하기 + + + 항상 묻기 + + + 기본값 + + + Light + + + Dark \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.lt.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.lt.resx index b31beb8a65..a0252045fb 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.lt.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.lt.resx @@ -21,29 +21,8 @@ Pilkumo atspalviai - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Custom SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + No provider selected. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Pritaikytas... + Numatytasis @@ -141,8 +132,11 @@ Paveldėjimas (tik skenerio sąsajai) + + Native Transfer + - Alternatyvus perdavimas + Memory Transfer Old DSM @@ -159,4 +153,31 @@ Best + + Save All + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + Numatytasis + + + Light + + + Dark + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.lv.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.lv.resx index 475457a839..8ccb368010 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.lv.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.lv.resx @@ -21,29 +21,8 @@ Pelēktoņu - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Izvēles SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outllok WEB pieeja + + Thunderbird + + + Apple Mail + Nav izvēlēts provaideris. @@ -99,6 +87,9 @@ US Letter (8.5x11 collas) + + Pielāgots... + Noklusētais @@ -124,7 +115,7 @@ Stikls - Auto + Automātiski CCITT4 @@ -141,8 +132,11 @@ Savietojams (tikai standarta saskarne) + + Vecā tipa nosūtīšana + - Alternatīvā pārnešana + Nosūtīšana izmantojot atmiņu Vecais DSM @@ -154,9 +148,36 @@ Noklusētais - Fast + Ātrais - Best + Vislabākais + + + Saglabāt visu + + + Saglabāt izvēlēto + + + Vienmēr atgādināt + + + Prasīt, ja izvēlēts + + + Skenēt ar noklusējuma profilu + + + Vienmēr atgādināt + + + Noklusētais + + + Light + + + Dark \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.nb.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.nb.resx index e4e59e2d7d..824a53f236 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.nb.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.nb.resx @@ -21,29 +21,8 @@ Gråtoner - - 100 ppt - - - 1200 ppt - - - 150 ppt - - - 200 ppt - - - 300 ppt - - - 400 ppt - - - 600 ppt - - - 800 ppt + + {0} ppt Custom SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + No provider selected. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Egendefinert... + Standard @@ -141,8 +132,11 @@ Legacy (native UI only) + + Native Transfer + - Alternativ overføring + Memory Transfer Old DSM @@ -159,4 +153,31 @@ Best + + Save All + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + Standard + + + Light + + + Dark + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.nl.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.nl.resx index 3e91156ecf..853922a758 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.nl.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.nl.resx @@ -21,29 +21,8 @@ Grijswaarden - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Aangepaste SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (nieuw) + Outlook Web Access + + Thunderbird + + + Apple Mail + Geen provider gekozen. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Aangepast... + Standaard @@ -124,7 +115,7 @@ Glasplaat - Auto + Automatisch CCITT4 @@ -141,8 +132,11 @@ Legacy (alleen eigen interface) + + Oorspronkelijke overdracht + - Alternatieve overdracht + Geheugen overdracht Oude DSM @@ -154,9 +148,36 @@ Standaard - Fast + Snel - Best + Beste + + + Alles opslaan + + + Selectie bewaren + + + Altijd vragen + + + Vragen indien geselecteerd + + + Scannen met standaardprofiel + + + Altijd vragen + + + Standaard + + + Licht + + + Donker \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.nn.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.nn.resx index 6729ffa17a..e0b2095957 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.nn.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.nn.resx @@ -21,29 +21,8 @@ Gråskala - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Eigen SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + Ingen leverandør vald. @@ -70,7 +58,7 @@ cm - in + tomme mm @@ -79,25 +67,28 @@ A3 (297x420 mm) - A4 (210x297 mm) + A4 (210 x 297 mm) - A5 (148x210 mm) + A5 (148 x 210 mm) - B4 (250x353 mm) + B4 (250 x 353 mm) - B5 (176x250 mm) + B5 (176 x 250 mm) Eigendefinert... - US Legal (8.5x14 in) + US Legal (8,5x14 in) - US Letter (8.5x11 in) + US Letter (8,5x11 in) + + + Eigendefinert... Standard @@ -141,22 +132,52 @@ Gammal (bare maskinen sitt oppsettvindu) + + Native Transfer + - Alternativ overføring + Memory Transfer Gammal DSM - x64 + X64 Standard - Fast + Kjapp - Best + Beste + + + Save All + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + Standard + + + Lyst + + + Mørkt \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.pl.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.pl.resx index 2db7fbc9db..43697d1181 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.pl.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.pl.resx @@ -21,29 +21,8 @@ Odcienie szarości - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Niestandardowy SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (nowy) + Outlook Web Access + + Thunderbird + + + Apple Mail + Nie wybrano dostawcy. @@ -99,6 +87,9 @@ US Letter (8.5 x 11 in) + + Niestandardowy... + Domyślna @@ -141,8 +132,11 @@ Starsza (tylko natywny interfejs) + + Transfer natywny + - Alternatywny transfer + Transfer pamięciowy Stare DSM @@ -159,4 +153,31 @@ Najlepsza + + Zapisz wszystkie + + + Zapisz zaznaczone + + + Zawsze pytaj + + + Pytaj, jeśli zaznaczone + + + Skanuj za pomocą domyślnego profilu + + + Zawsze pytaj + + + Domyślna + + + Jasny + + + Ciemny + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.pt-BR.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.pt-BR.resx index e5f5a30137..c122a6c0f5 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.pt-BR.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.pt-BR.resx @@ -21,29 +21,8 @@ Escala de cinza - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi SMTP personalizado @@ -51,9 +30,18 @@ Gmail + + Outlook (novo) + Outlook Web Access + + Thunderbird + + + Apple Mail + Nenhum provedor selecionado. @@ -99,6 +87,9 @@ Ofício (8.5x11 pol) + + Personalizado... + Padrão @@ -141,8 +132,11 @@ Legado (UI nativa apenas) + + Transferência Nativa + - Transferência Alternativa + Transferência de Memória DSM Antigo @@ -154,9 +148,36 @@ Padrão - Fast + Rápido - Best + Melhor + + + Salvar Tudo + + + Salvar Selecionados + + + Sempre Perguntar + + + Perguntar se selecionado + + + Digitalizar com o perfil padrão + + + Sempre Perguntar + + + Padrão + + + Claro + + + Escuro \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.pt-PT.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.pt-PT.resx index 5e6f47fd6a..7f62ec0f0b 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.pt-PT.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.pt-PT.resx @@ -13,52 +13,40 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Preto e Branco + Preto e branco - cor de 24 bits + Cor de 24 bits Escala de cinza - - 100 ppp - - - 1200 ppp - - - 150 ppp - - - 200 ppp - - - 300 ppp - - - 400 ppp - - - 600 ppp - - - 800 ppp + + {0} ppp - Custom SMTP + SMTP personalizado Gmail + + Outlook (novo) + - Outlook Web Access + Outlook na Web + + + Thunderbird + + + Apple Mail - No provider selected. + Serviço não selecionado. - Centrar + Centro Esquerda @@ -70,37 +58,40 @@ cm - in + pol mm - A3 (297x420 mm) + A3 (297 × 420 mm) - A4 (210x297 mm) + A4 (210 × 297 mm) - A5 (148x210 mm) + A5 (148 × 210 mm) - B4 (250x353 mm) + B4 (250 × 353 mm) - B5 (176x250 mm) + B5 (176 × 250 mm) Personalizar... - US Legal (8.5x14 pol) + US Legal (8,5 × 14 pol.) - US Letter (8.5x11 pol) + US Letter (8,5 × 11 pol.) + + + Personalizar... - Default + Padrão PDF/A-1b @@ -115,7 +106,7 @@ PDF/A-3u - Duplex + Frente e verso Alimentador @@ -124,7 +115,7 @@ Vidro - Auto + Automático CCITT4 @@ -133,30 +124,60 @@ LZW - None + Nenhuma - Default + Padrão - Legacy (native UI only) + Legado (interface nativa) + + + Transferência nativa - Alternative Transfer + Transferência de memória - Old DSM + DSM antigo x64 - Default + Padrão - Fast + Rápido - Best + Melhor + + + Guardar tudo + + + Guardar seleção + + + Perguntar sempre + + + Perguntar se selecionado + + + Digitalizar com o perfil padrão + + + Perguntar sempre + + + Padrão + + + Claro + + + Escuro \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.resx index 30daf38af9..c760293c72 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Black & White + Black and White 24-bit Color @@ -126,29 +126,8 @@ Grayscale - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Custom SMTP @@ -156,9 +135,18 @@ Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + No provider selected. @@ -204,6 +192,9 @@ US Letter (8.5x11 in) + + Custom... + Default @@ -258,8 +249,11 @@ Legacy (native UI only) + + Native Transfer + - Alternative Transfer + Memory Transfer Old DSM @@ -282,4 +276,31 @@ Best + + Save All + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + Default + + + Light + + + Dark + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.ro.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.ro.resx index 46178cdad8..3e5bb62165 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.ro.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.ro.resx @@ -16,34 +16,13 @@ Alb-negru - Color pe 24 biți + Culoare 24 biți Tonuri de gri - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Adresă server SMTP personalizată @@ -51,11 +30,20 @@ Gmail + + Outlook (new) + - Outlook Web Access + Acces Outlook Web + + + Pasăre tunet + + + Apple Mail - No provider selected. + Niciun furnizor selectat. Centrează @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Personalizat... + Implicit @@ -139,10 +130,13 @@ Implicit - Legacy (native UI only) + Vechi (numai interfața de utilizare nativă) + + + Transfer nativ - Transfer alternativ + Transfer de memorie DSM vechi @@ -154,9 +148,36 @@ Implicit - Fast + Rapid - Best + Cel mai bun + + + Salvează tot + + + Salvează selecția + + + Solicită întotdeauna + + + Solicită dacă este selectat + + + Scanare cu profilul implicit + + + Solicită întotdeauna + + + Implicit + + + Light + + + Dark \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.ru.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.ru.resx index 912d25c402..05b48d82f5 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.ru.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.ru.resx @@ -21,29 +21,8 @@ Оттенки серого - - 100 dpi - - - 1200 точек/дюйм - - - 150 точек/дюйм - - - 200 точек/дюйм - - - 300 точек/дюйм - - - 400 точек/дюйм - - - 600 точек/дюйм - - - 800 точек/дюйм + + {0} точек/дюйм Свой SMTP сервер @@ -51,11 +30,20 @@ Gmail + + Outlook (новый) + Outlook Web Access + + Thunderbird + + + Apple Mail + - Поставщик не выбран. + Поставщик эл. почты не выбран. Центрировать @@ -94,11 +82,14 @@ Другой... - US Legal (8.5x14 дюймов) + US Letter (8.5x11 дюймов) US Letter (8.5x11 дюймов) + + Другой... + По умолчанию @@ -139,10 +130,13 @@ По умолчанию - Наследие (только пользовательский интерфейс) + Устаревшая (только нативный UI) + + + Нативная передача - Альтернативная передача + Передача через память Старый DSM @@ -154,9 +148,36 @@ По умолчанию - Fast + Быстрое - Best + Лучшее + + + Сохранить всё + + + Сохранить выбранные + + + Всегда спрашивать + + + Спрашивать при выборе + + + Сканировать с профилем по умолчанию + + + Всегда спрашивать + + + По умолчанию + + + Светлая + + + Темная \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.si.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.si.resx index 61bda6a8d9..4c8125641e 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.si.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.si.resx @@ -13,7 +13,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Black & White + Black and White බිටු 24 වර්ණ @@ -21,29 +21,8 @@ Grayscale - - අඟලට තිත් 100 - - - අඟලට තිත් 1200 - - - අඟලට තිත් 150 - - - අඟලට තිත් 200 - - - අඟලට තිත් 300 - - - අඟලට තිත් 400 - - - අඟලට තිත් 600 - - - අඟලට තිත් 800 + + අඟලට තිත් {0} Custom SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + No provider selected. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Custom... + Default @@ -141,8 +132,11 @@ Legacy (native UI only) + + Native Transfer + - විකල්ප මාරුව + Memory Transfer Old DSM @@ -159,4 +153,31 @@ Best + + Save All + + + Save Selected + + + සෑමවිටම පෙන්වන්න + + + Prompt If Selected + + + Scan With Default Profile + + + සෑමවිටම පෙන්වන්න + + + Default + + + Light + + + Dark + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.sk.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.sk.resx index c7d0912765..4e3df5b9fd 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.sk.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.sk.resx @@ -21,29 +21,8 @@ Odtiene sivej - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Vlastné SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (nové) + Služba Outlook Web Access + + Thunderbird + + + Pošta Apple + Nie je vybratý žiadny poskytovateľ. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Vlastné... + Pôvodné @@ -141,8 +132,11 @@ Staršie (len natívne užívateľské rozhranie) + + Natívny prenos + - Alternatívny prenos + Presun pamäte Staršie DSM @@ -154,9 +148,36 @@ Pôvodné - Fast + Rýchlo - Best + Najlepšie + + + Uložiť všetko + + + Uložiť vybrané + + + Vždy sa spýtať + + + Spýtať sa, ak je vybrané + + + Skenovať pomocou predvoleného profilu + + + Vždy sa spýtať + + + Pôvodné + + + Light + + + Dark \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.sl.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.sl.resx index 5832ac6b57..9705eb51de 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.sl.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.sl.resx @@ -16,34 +16,13 @@ Črno-belo - 24-bit Barvno + 24-bit barvno Sivine - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi SMTP po meri @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outlook Splet + + Thunderbird + + + Apple Mail + Ponudnik ni izbran. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Po meri... + Privzeto @@ -141,8 +132,11 @@ Opuščeno (samo osnovni UI) + + Izvorni prenos + - Alternativni Prenos + Prenos pomnilnika Star DSM @@ -154,9 +148,36 @@ Privzeto - Fast + Hitro - Best + Najboljše + + + Shrani vse + + + Shrani izbrano + + + Vedno vprašaj + + + Vprašaj, če je izbrano + + + Skeniraj s privzetim profilom + + + Vedno vprašaj + + + Privzeto + + + Light + + + Dark \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.sq.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.sq.resx index 828ed8505d..9141aa8450 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.sq.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.sq.resx @@ -21,29 +21,8 @@ E hirtë - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi SMTP e personalizuar @@ -51,9 +30,18 @@ Gmail + + Outlook (i ri) + Hyrja nga Uebi në Outlook + + Thunderbird + + + Apple Mail + Nuk u zgjodh asnjë ofrues. @@ -76,13 +64,13 @@ mm - A3 (297 x 420 mm) + A3 (297x420 mm) - A4 (210 x 297 mm) + A4 (210x297 mm) - A5 (148 x 210 mm) + A5 (148x210 mm) B4 (250x353 mm) @@ -99,6 +87,9 @@ Letër SHBA (8.5 x 11 inç) + + E personalizuar... + E paracaktuar @@ -141,8 +132,11 @@ E vjetër (vetëm ndërfaqja e përdoruesit origjinale) + + Transferim nativ + - Transfertë alternative + Transferim në kujtesë DSM e vjetër @@ -154,9 +148,36 @@ E paracaktuar - Fast + I shpejtë - Best + Më e mira + + + Ruaji të gjitha + + + Ruaj pjesën e zgjedhur + + + Всегда спрашивать + + + Kërkoje nëse është e zgjedhur + + + Skano me profilin e paracaktuar + + + Всегда спрашивать + + + E paracaktuar + + + E zbardhur + + + E errët \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.sr-CS.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.sr-CS.resx index 3fd5d0a869..741bb18626 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.sr-CS.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.sr-CS.resx @@ -21,29 +21,8 @@ Nijanse sive - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Custom SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + No provider selected. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Proizvoljno... + Podrazumevano @@ -141,8 +132,11 @@ Legacy (native UI only) + + Native Transfer + - Alternativni Prenos + Memory Transfer Old DSM @@ -159,4 +153,31 @@ Best + + Save All + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + Podrazumevano + + + Light + + + Dark + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.sr.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.sr.resx index de28cfdc88..e6179d8437 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.sr.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.sr.resx @@ -21,29 +21,8 @@ Нијансе сиве - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Custom SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + No provider selected. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Произвољно... + Подразумевано @@ -141,8 +132,11 @@ Легат (изворни кориснички интерфејс) + + Native Transfer + - Alternative Transfer + Memory Transfer Old DSM @@ -159,4 +153,31 @@ Best + + Save All + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + Подразумевано + + + Light + + + Dark + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.sv.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.sv.resx index ff70d3b76f..9cb7aaebd5 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.sv.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.sv.resx @@ -21,29 +21,8 @@ Gråskala - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Modifierad SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (ny) + Outlook Web Access + + Thunderbird + + + Apple Mail + Ingen leverantör vald. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Anpassad... + Standard @@ -139,10 +130,13 @@ Standard - Inbyggd (endast inbyggt användargränssnitt) + Äldre (endast inbyggt gränssnitt) + + + Inbyggd överföring - Annan överföring + Minnesöverföring Gammal DSM @@ -154,9 +148,36 @@ Standard - Fast + Snabb - Best + Bäst + + + Spara alla + + + Spara markerade + + + Fråga alltid + + + Fråga vid markering + + + Skanna med standardprofil + + + Fråga alltid + + + Standard + + + Ljust + + + Mörkt \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.th.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.th.resx new file mode 100644 index 0000000000..6fb3391028 --- /dev/null +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.th.resx @@ -0,0 +1,183 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ขาวดำ + + + สี 24 บิต + + + โทนสีเทา + + + {0} dpi + + + Custom SMTP + + + Gmail + + + Outlook (new) + + + Outlook Web Access + + + Thunderbird + + + Apple Mail + + + No provider selected. + + + กึ่งกลาง + + + ซ้าย + + + Right + + + ซม. + + + นิ้ว + + + มม. + + + A3 (297x420 มม.) + + + A4 (210x297 มม.) + + + A5 (148x210 มม.) + + + B4 (250x353 มม.) + + + B5 (176x250 มม.) + + + Custom... + + + US Legal (8.5x14 in) + + + US Letter (8.5x11 in) + + + Custom... + + + ค่าเริ่มต้น + + + PDF/A-1b + + + PDF/A-2b + + + PDF/A-3b + + + PDF/A-3u + + + Duplex + + + ตัวป้อนอัตโนมัติ + + + หน้ากระจก + + + อัตโนมัติ + + + CCITT4 + + + LZW + + + None + + + ค่าเริ่มต้น + + + Legacy (native UI only) + + + Native Transfer + + + Memory Transfer + + + Old DSM + + + x64 + + + ค่าเริ่มต้น + + + Fast + + + ดีที่สุด + + + บันทึกทั้งหมด + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + ค่าเริ่มต้น + + + สว่าง + + + มืด + + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.tr.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.tr.resx index 18a335ceca..72df83a61f 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.tr.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.tr.resx @@ -16,34 +16,13 @@ Siyah & Beyaz - 24 Bit Renk + 24-bit Renk - Gri tonlama + Gri Tonlama - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Özel SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (yeni) + Outlook Web Erişimi + + Thunderbird + + + Apple Mail + Sağlayıcı seçilmedi. @@ -94,10 +82,13 @@ Özel... - US Legal (8.5x14 in) + US Legal (8.5x14 inç) - US Letter (8.5x11 in) + US Letter (8.5x11 inç) + + + Özel... Öntanımlı @@ -141,8 +132,11 @@ Eski (yalnızca yerli kullanıcı arayüzü) + + Yerli Aktarım + - Alternatif Aktarım + Bellek Aktarımı Eski DSM @@ -154,9 +148,36 @@ Öntanımlı - Fast + Hızlı - Best + En iyi + + + Tümünü Kaydet + + + Seçilenleri Kaydet + + + Her Zaman Sor + + + Seçildiyse Sor + + + Öntanımlı Profille Tara + + + Her Zaman Sor + + + Öntanımlı + + + Açık + + + Koyu \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.uk.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.uk.resx index 0e7d978038..135ad48de2 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.uk.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.uk.resx @@ -16,46 +16,34 @@ Чорно-білий - Кольорове 24-біти + Кольорове 24 біти Відтінки сірого - - 100 т/д - - - 1200 т/д - - - 150 т/д - - - 200 т/д - - - 300 т/д - - - 400 т/д - - - 600 т/д - - - 800 т/д + + {0} т/д - Custom SMTP + Користувацький SMTP Gmail + + Outlook (новий) + Outlook Web Access + + Thunderbird + + + Пошта Apple + - No provider selected. + Відсутній постачальник послуг По центру @@ -99,8 +87,11 @@ US Letter (8.5x11 дюймів) + + Інший... + - Default + Типово PDF/A-1b @@ -124,7 +115,7 @@ Зі скла - Auto + Автоматично CCITT4 @@ -133,30 +124,60 @@ LZW - None + Нічого - Default + Типово - Legacy (native UI only) + Режим сумісности (лише рідний інтерфейс користувача) + + + Передача засобами програми - Alternative Transfer + Передача засобами пам'яти - Old DSM + Застарілий DSM x64 - Default + Типово - Fast + Швидко - Best + Найкраще + + + Зберегти все + + + Зберегти вибране + + + Завжди запитувати + + + Запит на продовження, якщо вибрано + + + Сканувати з типовим профілем + + + Завжди запитувати + + + Типово + + + Світла + + + Темна \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.vi.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.vi.resx index 3208a2efa6..3adeba207d 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.vi.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.vi.resx @@ -21,29 +21,8 @@ Grayscale - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi SMTP tự điền @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + Chưa chọn nhà cung cấp.. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + Tùy chỉnh... + Mặc định @@ -141,8 +132,11 @@ Legacy (native UI only) + + Native Transfer + - Cách chuyển khác + Memory Transfer DSN Cũ @@ -159,4 +153,31 @@ Best + + Save All + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + Mặc định + + + Light + + + Dark + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.zh-CN.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.zh-CN.resx index 31fef0bfe4..ae7882c45e 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.zh-CN.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.zh-CN.resx @@ -21,29 +21,8 @@ 灰阶 - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi 自定义 SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (新) + Outlook 网页访问 + + Thunderbird + + + Apple Mail + 未选择邮件服务商. @@ -76,28 +64,31 @@ 毫米 - A3 (297x420 mm) + A3(297 x 420 毫米) - A4 (210x297 mm) + A4 (210 x 297 毫米) - A5 (148x210 mm) + A5(149 x 210 毫米) - B4 (250x353 mm) + B4 (250x353 毫米) - B5 (176x250 mm) + B5 (176x250 毫米) 自定义... - US Legal (8.5x14 in) + US Legal (8.5x14 英寸) - US Letter (8.5x11 in) + US Legal (8.5x14 英寸) + + + 自定义... 默认 @@ -141,8 +132,11 @@ 传统(仅限本地界面) + + 本地传输 + - 替代传输 + 内存传输 旧版 DSM @@ -154,9 +148,36 @@ 默认 - Fast + 快速 - Best + 最佳 + + + 保存所有 + + + 保存所选 + + + 总是提示 + + + 当选中时提示 + + + 使用默认配置文件扫描 + + + 总是提示 + + + 默认 + + + 浅色 + + + 深色 \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/SettingsResources.zh-TW.resx b/NAPS2.Lib/Lang/Resources/SettingsResources.zh-TW.resx index a3b32ed861..7da6f3cccb 100644 --- a/NAPS2.Lib/Lang/Resources/SettingsResources.zh-TW.resx +++ b/NAPS2.Lib/Lang/Resources/SettingsResources.zh-TW.resx @@ -21,29 +21,8 @@ 灰階 - - 100 dpi - - - 1200 dpi - - - 150 dpi - - - 200 dpi - - - 300 dpi - - - 400 dpi - - - 600 dpi - - - 800 dpi + + {0} dpi Custom SMTP @@ -51,9 +30,18 @@ Gmail + + Outlook (new) + Outlook Web Access + + Thunderbird + + + Apple Mail + No provider selected. @@ -99,6 +87,9 @@ US Letter (8.5x11 in) + + 自訂... + Default @@ -141,8 +132,11 @@ Legacy (native UI only) + + Native Transfer + - Alternative Transfer + Memory Transfer Old DSM @@ -159,4 +153,31 @@ Best + + Save All + + + Save Selected + + + Always Prompt + + + Prompt If Selected + + + Scan With Default Profile + + + Always Prompt + + + Default + + + Light + + + Dark + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.Designer.cs b/NAPS2.Lib/Lang/Resources/UiStrings.Designer.cs index d4417dd614..80cb3e39d0 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.Designer.cs +++ b/NAPS2.Lib/Lang/Resources/UiStrings.Designer.cs @@ -77,6 +77,15 @@ internal static string AboutFormTitle { } } + /// + /// Looks up a localized string similar to Action. + /// + internal static string Action { + get { + return ResourceManager.GetString("Action", resourceCulture); + } + } + /// /// Looks up a localized string similar to Advanced. /// @@ -185,6 +194,15 @@ internal static string AltInterleave { } } + /// + /// Looks up a localized string similar to Always Ask. + /// + internal static string AlwaysAsk { + get { + return ResourceManager.GetString("AlwaysAsk", resourceCulture); + } + } + /// /// Looks up a localized string similar to Apple Driver. /// @@ -194,6 +212,15 @@ internal static string AppleDriver { } } + /// + /// Looks up a localized string similar to Application. + /// + internal static string Application { + get { + return ResourceManager.GetString("Application", resourceCulture); + } + } + /// /// Looks up a localized string similar to Apply to all {0} selected images. /// @@ -203,6 +230,15 @@ internal static string ApplyToSelected { } } + /// + /// Looks up a localized string similar to Assign. + /// + internal static string Assign { + get { + return ResourceManager.GetString("Assign", resourceCulture); + } + } + /// /// Looks up a localized string similar to Attachment Name:. /// @@ -374,6 +410,15 @@ internal static string Cancel { } } + /// + /// Looks up a localized string similar to Can't find your scanner? Read about limitations of the NAPS2 Flatpak.. + /// + internal static string CantFindScannerFlatpak { + get { + return ResourceManager.GetString("CantFindScannerFlatpak", resourceCulture); + } + } + /// /// Looks up a localized string similar to Change. /// @@ -428,6 +473,15 @@ internal static string ClearAll { } } + /// + /// Looks up a localized string similar to Combine. + /// + internal static string Combine { + get { + return ResourceManager.GetString("Combine", resourceCulture); + } + } + /// /// Looks up a localized string similar to Compatibility. /// @@ -446,6 +500,33 @@ internal static string CompressionLabel { } } + /// + /// Looks up a localized string similar to Are you sure you want to stop sharing {0}?. + /// + internal static string ConfirmDeleteSharedDevice { + get { + return ResourceManager.GetString("ConfirmDeleteSharedDevice", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connect. + /// + internal static string Connect { + get { + return ResourceManager.GetString("Connect", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connection error.. + /// + internal static string ConnectionError { + get { + return ResourceManager.GetString("ConnectionError", resourceCulture); + } + } + /// /// Looks up a localized string similar to Contrast:. /// @@ -572,6 +653,15 @@ internal static string DeskewScannedPages { } } + /// + /// Looks up a localized string similar to 1 device found.. + /// + internal static string DeviceFoundSingular { + get { + return ResourceManager.GetString("DeviceFoundSingular", resourceCulture); + } + } + /// /// Looks up a localized string similar to Device:. /// @@ -581,6 +671,15 @@ internal static string DeviceLabel { } } + /// + /// Looks up a localized string similar to {0} devices found.. + /// + internal static string DevicesFound { + get { + return ResourceManager.GetString("DevicesFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to Dimensions. /// @@ -644,6 +743,15 @@ internal static string DownloadProgressFormTitle { } } + /// + /// Looks up a localized string similar to Dpi. + /// + internal static string Dpi { + get { + return ResourceManager.GetString("Dpi", resourceCulture); + } + } + /// /// Looks up a localized string similar to Edit. /// @@ -662,6 +770,33 @@ internal static string EditProfileFormTitle { } } + /// + /// Looks up a localized string similar to Edit with.... + /// + internal static string EditWith { + get { + return ResourceManager.GetString("EditWith", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Edit with {0}. + /// + internal static string EditWithAppName { + get { + return ResourceManager.GetString("EditWithAppName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Edit with.... + /// + internal static string EditWithFormTitle { + get { + return ResourceManager.GetString("EditWithFormTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to .... /// @@ -752,6 +887,15 @@ internal static string EnableAutoSave { } } + /// + /// Looks up a localized string similar to Enable debug logging. + /// + internal static string EnableDebugLogging { + get { + return ResourceManager.GetString("EnableDebugLogging", resourceCulture); + } + } + /// /// Looks up a localized string similar to The following file is encrypted and requires a password to open:. /// @@ -788,6 +932,42 @@ internal static string ErrorFormTitle { } } + /// + /// Looks up a localized string similar to Error starting application {0}. + /// + internal static string ErrorStartingApplication { + get { + return ResourceManager.GetString("ErrorStartingApplication", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ESCL Driver. + /// + internal static string EsclDriver { + get { + return ResourceManager.GetString("EsclDriver", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ESCL Network Driver. + /// + internal static string EsclNetworkDriver { + get { + return ResourceManager.GetString("EsclNetworkDriver", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ESCL USB Driver. + /// + internal static string EsclUsbDriver { + get { + return ResourceManager.GetString("EsclUsbDriver", resourceCulture); + } + } + /// /// Looks up a localized string similar to Estimated download size: {0} MB. /// @@ -833,6 +1013,15 @@ internal static string Flip { } } + /// + /// Looks up a localized string similar to Flip back sides of duplex pages. + /// + internal static string FlipBackSidesOfDuplexPages { + get { + return ResourceManager.GetString("FlipBackSidesOfDuplexPages", resourceCulture); + } + } + /// /// Looks up a localized string similar to Flip duplexed pages. /// @@ -932,6 +1121,15 @@ internal static string Import { } } + /// + /// Looks up a localized string similar to Interface. + /// + internal static string Interface { + get { + return ResourceManager.GetString("Interface", resourceCulture); + } + } + /// /// Looks up a localized string similar to Interleave. /// @@ -941,6 +1139,15 @@ internal static string Interleave { } } + /// + /// Looks up a localized string similar to IP/Host. + /// + internal static string IpHost { + get { + return ResourceManager.GetString("IpHost", resourceCulture); + } + } + /// /// Looks up a localized string similar to Jpeg Quality. /// @@ -959,6 +1166,33 @@ internal static string JpegQualityHelp { } } + /// + /// Looks up a localized string similar to Keep images across sessions. + /// + internal static string KeepSession { + get { + return ResourceManager.GetString("KeepSession", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keyboard Shortcuts. + /// + internal static string KeyboardShortcuts { + get { + return ResourceManager.GetString("KeyboardShortcuts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keyboard Shortcuts. + /// + internal static string KeyboardShortcutsFormTitle { + get { + return ResourceManager.GetString("KeyboardShortcutsFormTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Keywords:. /// @@ -995,6 +1229,24 @@ internal static string MakePdfsSearchable { } } + /// + /// Looks up a localized string similar to Manual IP. + /// + internal static string ManualIp { + get { + return ResourceManager.GetString("ManualIp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Manual IP. + /// + internal static string ManualIpFormTitle { + get { + return ResourceManager.GetString("ManualIpFormTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Maximum quality (large files). /// @@ -1058,6 +1310,15 @@ internal static string MoveUp { } } + /// + /// Looks up a localized string similar to Multiple Languages.... + /// + internal static string MultipleLanguages { + get { + return ResourceManager.GetString("MultipleLanguages", resourceCulture); + } + } + /// /// Looks up a localized string similar to Multiple scans (fixed delay between scans). /// @@ -1139,6 +1400,15 @@ internal static string Next { } } + /// + /// Looks up a localized string similar to No devices found.. + /// + internal static string NoDevicesFound { + get { + return ResourceManager.GetString("NoDevicesFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to Not Now. /// @@ -1202,6 +1472,24 @@ internal static string OcrModeLabel { } } + /// + /// Looks up a localized string similar to Multiple Languages. + /// + internal static string OcrMultiLangFormTitle { + get { + return ResourceManager.GetString("OcrMultiLangFormTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fix white balance and remove noise. + /// + internal static string OcrPreProcessing { + get { + return ResourceManager.GetString("OcrPreProcessing", resourceCulture); + } + } + /// /// Looks up a localized string similar to Select one or more languages:. /// @@ -1364,6 +1652,15 @@ internal static string PlaceholdersFormTitle { } } + /// + /// Looks up a localized string similar to Port. + /// + internal static string Port { + get { + return ResourceManager.GetString("Port", resourceCulture); + } + } + /// /// Looks up a localized string similar to Post-processing. /// @@ -1373,6 +1670,15 @@ internal static string PostProcessing { } } + /// + /// Looks up a localized string similar to Pre-emptively run OCR after scanning. + /// + internal static string PreemptivelyOcrAfterScanning { + get { + return ResourceManager.GetString("PreemptivelyOcrAfterScanning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Press Start when ready.. /// @@ -1499,6 +1805,24 @@ internal static string RecoverPrompt { } } + /// + /// Looks up a localized string similar to Redo. + /// + internal static string Redo { + get { + return ResourceManager.GetString("Redo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Redo {0}. + /// + internal static string RedoFormat { + get { + return ResourceManager.GetString("RedoFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Remember these settings. /// @@ -1526,6 +1850,15 @@ internal static string Reset { } } + /// + /// Looks up a localized string similar to Custom Resolution. + /// + internal static string ResolutionFormTitle { + get { + return ResourceManager.GetString("ResolutionFormTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Resolution:. /// @@ -1553,6 +1886,24 @@ internal static string Reverse { } } + /// + /// Looks up a localized string similar to Reverse All. + /// + internal static string ReverseAll { + get { + return ResourceManager.GetString("ReverseAll", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reverse Selected. + /// + internal static string ReverseSelected { + get { + return ResourceManager.GetString("ReverseSelected", resourceCulture); + } + } + /// /// Looks up a localized string similar to Revert. /// @@ -1652,6 +2003,15 @@ internal static string SaveAllAsPdf { } } + /// + /// Looks up a localized string similar to "Save" button default action:. + /// + internal static string SaveButtonDefaultAction { + get { + return ResourceManager.GetString("SaveButtonDefaultAction", resourceCulture); + } + } + /// /// Looks up a localized string similar to Save Images. /// @@ -1724,6 +2084,15 @@ internal static string ScaleLabel { } } + /// + /// Looks up a localized string similar to Scale With Window. + /// + internal static string ScaleWithWindow { + get { + return ResourceManager.GetString("ScaleWithWindow", resourceCulture); + } + } + /// /// Looks up a localized string similar to Scan. /// @@ -1733,6 +2102,24 @@ internal static string Scan { } } + /// + /// Looks up a localized string similar to "Scan" button default action:. + /// + internal static string ScanButtonDefaultAction { + get { + return ResourceManager.GetString("ScanButtonDefaultAction", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "Scan" menu changes default profile. + /// + internal static string ScanChangesDefaultProfile { + get { + return ResourceManager.GetString("ScanChangesDefaultProfile", resourceCulture); + } + } + /// /// Looks up a localized string similar to Scan Configuration. /// @@ -1742,6 +2129,69 @@ internal static string ScanConfig { } } + /// + /// Looks up a localized string similar to Scanner Sharing. + /// + internal static string ScannerSharing { + get { + return ResourceManager.GetString("ScannerSharing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Scanner Sharing. + /// + internal static string ScannerSharingFormTitle { + get { + return ResourceManager.GetString("ScannerSharingFormTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings.. + /// + internal static string ScannerSharingIntro { + get { + return ResourceManager.GetString("ScannerSharingIntro", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Scan With Default Profile. + /// + internal static string ScanWithDefaultProfile { + get { + return ResourceManager.GetString("ScanWithDefaultProfile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Scan With New Profile. + /// + internal static string ScanWithNewProfile { + get { + return ResourceManager.GetString("ScanWithNewProfile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Scan With Profile {0}. + /// + internal static string ScanWithProfile { + get { + return ResourceManager.GetString("ScanWithProfile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Searching for devices.... + /// + internal static string SearchingForDevices { + get { + return ResourceManager.GetString("SearchingForDevices", resourceCulture); + } + } + /// /// Looks up a localized string similar to Second (00-59). /// @@ -1769,6 +2219,15 @@ internal static string SelectAll { } } + /// + /// Looks up a localized string similar to Select Device. + /// + internal static string SelectDevice { + get { + return ResourceManager.GetString("SelectDevice", resourceCulture); + } + } + /// /// Looks up a localized string similar to Select Source. /// @@ -1796,6 +2255,51 @@ internal static string SetDefault { } } + /// + /// Looks up a localized string similar to Settings. + /// + internal static string Settings { + get { + return ResourceManager.GetString("Settings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings. + /// + internal static string SettingsFormTitle { + get { + return ResourceManager.GetString("SettingsFormTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Share. + /// + internal static string Share { + get { + return ResourceManager.GetString("Share", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Share even when NAPS2 is closed. + /// + internal static string ShareAsService { + get { + return ResourceManager.GetString("ShareAsService", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shared Scanner Settings. + /// + internal static string SharedDeviceFormTitle { + get { + return ResourceManager.GetString("SharedDeviceFormTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Sharpen. /// @@ -1805,6 +2309,15 @@ internal static string Sharpen { } } + /// + /// Looks up a localized string similar to Shortcut. + /// + internal static string Shortcut { + get { + return ResourceManager.GetString("Shortcut", resourceCulture); + } + } + /// /// Looks up a localized string similar to Show. /// @@ -1814,6 +2327,42 @@ internal static string Show { } } + /// + /// Looks up a localized string similar to Show native TWAIN progress. + /// + internal static string ShowNativeTwainProgress { + get { + return ResourceManager.GetString("ShowNativeTwainProgress", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show page numbers. + /// + internal static string ShowPageNumbers { + get { + return ResourceManager.GetString("ShowPageNumbers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show "Profiles" toolbar. + /// + internal static string ShowProfilesToolbar { + get { + return ResourceManager.GetString("ShowProfilesToolbar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only allow a single NAPS2 instance. + /// + internal static string SingleInstanceDesc { + get { + return ResourceManager.GetString("SingleInstanceDesc", resourceCulture); + } + } + /// /// Looks up a localized string similar to Single page files. /// @@ -1841,6 +2390,15 @@ internal static string SkipSavePrompt { } } + /// + /// Looks up a localized string similar to Split. + /// + internal static string Split { + get { + return ResourceManager.GetString("Split", resourceCulture); + } + } + /// /// Looks up a localized string similar to Start. /// @@ -1850,6 +2408,15 @@ internal static string Start { } } + /// + /// Looks up a localized string similar to Stop Scanner Sharing. + /// + internal static string StopScannerSharing { + get { + return ResourceManager.GetString("StopScannerSharing", resourceCulture); + } + } + /// /// Looks up a localized string similar to Stretch to page size. /// @@ -1877,6 +2444,15 @@ internal static string TechnicalDetails { } } + /// + /// Looks up a localized string similar to Theme:. + /// + internal static string ThemeLabel { + get { + return ResourceManager.GetString("ThemeLabel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Tiff Options. /// @@ -1904,6 +2480,24 @@ internal static string TitleLabel { } } + /// + /// Looks up a localized string similar to Sidebar. + /// + internal static string ToggleSidebar { + get { + return ResourceManager.GetString("ToggleSidebar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tools. + /// + internal static string Tools { + get { + return ResourceManager.GetString("Tools", resourceCulture); + } + } + /// /// Looks up a localized string similar to TWAIN Driver. /// @@ -1922,6 +2516,33 @@ internal static string TwainImplLabel { } } + /// + /// Looks up a localized string similar to Unassign. + /// + internal static string Unassign { + get { + return ResourceManager.GetString("Unassign", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Undo. + /// + internal static string Undo { + get { + return ResourceManager.GetString("Undo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Undo {0}. + /// + internal static string UndoFormat { + get { + return ResourceManager.GetString("UndoFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use native UI. /// @@ -2039,6 +2660,15 @@ internal static string Zoom { } } + /// + /// Looks up a localized string similar to Zoom Actual. + /// + internal static string ZoomActual { + get { + return ResourceManager.GetString("ZoomActual", resourceCulture); + } + } + /// /// Looks up a localized string similar to Zoom In. /// diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.af.resx b/NAPS2.Lib/Lang/Resources/UiStrings.af.resx index f48886a3e4..32f6b767f9 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.af.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.af.resx @@ -13,22 +13,22 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Thông Tin Phần Mềm + Meer omtrent NAPS2 - Copyright {0} NAPS2 Contributors + Kopiereg {0} NAPS2-bydraers - Icons from: + Ikone van: - Check for updates + Kyk vir nuwe weergawes Goed - Donate + Skenk Profiele @@ -37,34 +37,46 @@ Nuwe - Sửa + Wysig - Xóa + Vee uit - Scan + Skandeer - Set Default + Stel verstek - Sao chép + Kopieer Plak + + Undo + + + Doen oor + + + Undo {0} + + + Doen {0} oor + - Xong + Klaar - Mặc định + Verstek Nuwe Profiel - Scan hàng loạt + Bondel Skandeer OCR @@ -73,169 +85,229 @@ Profiele - Chèn + Intrek - Save PDF + Stoor PDF PDF Stellings - Save Images + Stoor Beelde - Cài đặt ảnh + Beeld Instellings - Email PDF + ePosPDF - Cài đặt Email + E-posinstellings Afdruk - Ảnh + Beeld - View + Aansig - Chỉnh sửa + Knip - Brightness / Contrast + Helderheid / Kontras - Hue / Saturation + Tint / Versadiging - Trắng & Đen + Swart en Wit - Sharpen + Verskerp - Document Correction + Dokument Regstelling - Reset + Stel terug - Rotate + Roteer - Rotate Left + Roteer Links - Rotate Right + Roteer Regs - Lật + Flip - Deskew + Reguit-trek - Tùy chỉnh xoay + Pasgemaakte Rotasie Skuif Op - Af Beweeg + Skuif af - Reorder + Herrangskik - Interleave + Stygende Interblad - Deinterleave + Dalende Interblad - Alternate Interleave + Dalende Interblad - Alternate Deinterleave + Hersorteer Dalende Interblad - Reverse + Keer om + + + Keer Alles om + + + Keer gekiesde om - Xóa + Maak skoon - Clear All + Verwyder Alles - Ngôn ngữ + Taal + + + Stellings + + + Koppelvlak + + + "Skandeer" keuselys verander verstek profiel + + + Wys "Profiele" nutsbalk + + + Wys bladsy nommers + + + "Skandeeer" knoppie verstek aksie: + + + "Stoor"knoppie verstek aksie: + + + Toepassing + + + Laat slegs 'n enkele NAPS2-instansie toe - Thông Tin Phần Mềm + Meer omtrent NAPS2 Zoom - Zoom In + Vergroot - Zoom Out + Verklein + + + Werklike grootte + + + Skaal Met Venster - Save + Stoor + + + Stoor Alles + + + Stoor gekiesde - Save All as PDF + Stoor Alles as PDF - Save Selected as PDF + Stoor Gekiesde as PDF - Save All as Images + Stoor Alles as Beelde - Save Selected as Images + Stoor Gekiesde as Beelde + + + ePos Alles + + + ePos Gekies - Email All as PDF + ePos Alles as PDF - Email Selected as PDF + ePos Gekose as PDF Profiel Stellings - Hiển thị tên: + Vertoon Naam: - WIA Driver + WIA Drywer TWAIN Driver + + ESCL Drywer + + + ESCL Netwerk Drywer + + + ESCL USB Drywer + - Apple Driver + Apple Drywer - SANE Driver + SANE Drywer - Thiết bị: + Toestel: - Chọn Ổ Đĩa + Kies Toestel - Use predefined settings + Gebruik vooraf gedefinieerde instellings - Use native UI + Gebruik inheemse UI Bladsy bron: @@ -244,43 +316,49 @@ Bladsy grootte: - Resolution: + Resolusie: Độ sáng: - Chiều sâu: + Bisdiepte: - Chỉnh chiều ngang: + Horisontale plasing: - Scale: + Skaal: - Tương phản: + Kontras: - Kích hoạt Lưu tự động + Aktiveer Outo-stoor - Cài đặt Tự động Lưu + Outo-stoor + + + Outo-stoor - Nâng cao + Gevorderd - Hủy + Kanselleer - Select + Kies - Select Source + Kies Bron + + + Kies Toestel - Run in Background + Hardloop in agtergrond NAPS2 @@ -289,43 +367,49 @@ NAPS2 - {0} - Not Another PDF Scanner + Netta A+ PDF Skandeerder OCR Opstel - Hãy tìm kiếm các file PDF sử dụng OCR + Maak PDF's soekbaar met OCR OCR taal: - OCR mode: + OCR-modus: + + + Maak witbalans reg en verwyder geraas - Automatically run OCR after scanning + Outo-OCR na skandering + + + Doen voorlopige OCR na skandering - Thêm Ngôn ngữ + Kry meer tale OCR Aflaai - Using OCR requires you to download each language you want to scan. + Die gebruik van OCR vereis dat jy elke taal wat jy wil skandeer, aflaai. - Select one or more languages: + Kies een of meer tale: - Ước tính kích thước download: {0} MB + Beraamde Aflaai grootte: {0} MB - Tải Về + Aflaai - Tiến độ tải về + Aflaai Vordering Voorskou @@ -337,15 +421,468 @@ Vorige - {0} of {1} + {0} van {1} - Revert + Keer terug - Áp dụng cho tất cả {0} hình ảnh được chọn + Pas toe op al {0} gekiesde beelde Kies alles + + Herstel + + + Nie Nou Nie + + + Herstel Geskandeerde Beelde + + + {0} beeld(e) geskandeer op {1} teen {2} mag dalk nie gestoor het nie, en is herwinbaar. Wiljy dit herwin? + + + Plekhouers + + + Slaan stoor-boodskap oor + + + Enkel bladsy lêers + + + Enkripteer PDF + + + Wys + + + Herstel verstekwaardes + + + PDF Stellings + + + Magtig Druk + + + Magtig Voll Kwaliteit Druk + + + Magtig Dokument Veranderings + + + Magtig Dokument Saamstelling + + + Magtig Inhoud-kopieer + + + Magtig Inhoud-kopieer vir Toeganklikheid + + + Magtig Notas + + + Magtig Vorm Invul + + + Verstek lêeradres: + + + Đường dẫn: + + + Titel: + + + Skrywer: + + + Subject: + + + Sleutelwoorde: + + + Eienaar Wagwoord: + + + Gebruiker Wagwoord: + + + Onthou hierdie instellings + + + Metadata + + + Enkripsie + + + Aanpasbaarheid + + + Plekhouers + + + Jaar + + + Jaar (00-99) + + + Maand + + + Dag(01-31) + + + Uur (0-23) + + + Minuut + + + Sekonde (00-59) + + + Outo-inkrement nommer (4 syfers) + + + Outo-inkrement nommer (3 syfers) + + + Outo-inkrement nommer (2 syfers) + + + Outo-inkrement nommer (1 syfer) + + + Tên tệp + + + Voorskou: + + + Vir hoë JPEG-eienskappe (80+), verhoog ook beeldkwaliteit in jou profiel vir die beste resultate. + + + Jpeg Kwaliteit + + + Tiff keuses + + + Samepersing: + + + Beeld Instellings + + + E-posinstellings + + + Stellings + + + Verskaffer + + + Verander + + + Aanhangsel Naam: + + + Kies e-pos Verskaffer + + + Magtig + + + Wag vir magtiging... + + + Fout + + + Technical Details + + + Wagwoord + + + The following file is encrypted and requires a password to open: + + + Vra vir lêeradres + + + Een lêer per bladsy + + + Een lêer per skandering + + + Skei lêers met Patch-T + + + Meer inligting + + + Verwyder Beelde na stoor + + + Behoue beelde oor sessies + + + Pasgemaakte Bladsy Grootte + + + Naam (opsioneel) + + + Dimensies + + + Custom Resolution + + + Dpi + + + Wag tans vir TWAIN om te voltooi... + + + Gevorderde Profiel Instellings + + + Beeld Kwaliteit + + + Maksimum Kwaliteit (Groot Lêers) + + + Leë bladsye + + + Sluit leë bladsye uit + + + Wit Drumpel + + + Dekkingsdrempel + + + Agterna-verwerking + + + Trek geskandeerde blaaie reguit + + + Pas helderheid / kontras aan na skandering + + + Afstand breedte gebaseer op belyning (WIA) + + + Stretch to page size + + + Knip na bladsygrootte + + + Flip duplex bladsye + + + Blaai agterkante van dupleksbladsye om + + + WIA Weergawe: + + + Twain Implementation: + + + Bondel Skandeer + + + Druk Begin wanneer gereed. + + + Skandeer opstelling + + + Uitset + + + Profiel: + + + Enkel skandeering + + + Veelvuldige skanderings (vinnige tussen skanderings) + + + Veelvuldige skanderings (vaste vertraging tussen skanderings) + + + Aantal skanderings: + + + Time between scans (seconds): + + + Laai Beelde in tot NAPS2 + + + Stoor na 'n enkele lêer + + + Stoor na veelvuldige lêers + + + Start + + + Volgende Skandering + + + Gereed vir skandering {0}. + + + Maak Vouer oop + + + Tools + + + Aktiveer ontfout-aantekening + + + Deel + + + Skandeerder Deel + + + Skandeerder Deel + + + Gedeelde skandeerders kan vanaf ander rekenaars op die plaaslike netwerk gebruik word deur "ESCL Driver" in die ander rekenaar se NAPS2-profielinstellings te kies. + + + Gedeelde skandeer-instellings + + + Bevestig stop van deel {0}? + + + Deel selfs wanneer NAPS2 toe is + + + Veelvuldige Tale... + + + Veelvuldige Tale + + + Wys inheemse TWAIN-vordering + + + Split + + + Kombineer + + + Hand-ingevoerde IP + + + Hand-ingevoerde IP + + + IP/Gasheer + + + Poort + + + Koppel + + + Verbindingsfout. + + + Vra Altyd + + + Soek tans vir toestelle... + + + {0} Toestelle gevind. + + + 1 Toestel gevind. + + + Geen toestel gevind nie. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Skandeer met verstekprofiel + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.ar.resx b/NAPS2.Lib/Lang/Resources/UiStrings.ar.resx index cf88ac4ada..2f5d0bb378 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.ar.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.ar.resx @@ -22,7 +22,7 @@ الأيقونات من: - التحقق من وجود تحديثات: + التحقق من وجود تحديثات حسناً @@ -54,6 +54,18 @@ لصق + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + تم @@ -165,6 +177,12 @@ عكس + + Reverse All + + + Reverse Selected + محو @@ -174,6 +192,33 @@ اللغة + + Settings + + + Interface + + + القائمة "المسح" تغير الملف الشخصي الافتراضي + + + Show "Profiles" toolbar + + + Show page numbers + + + الإجراء الافتراضي لزر "مسح": + + + إجراء "حفظ" الافتراضي: + + + Application + + + Only allow a single NAPS2 instance + حول @@ -186,9 +231,21 @@ تصغير + + التكبير الفعلي + + + تحجيم مع النافذة + Save + + Save All + + + Save Selected + Save All as PDF @@ -201,6 +258,12 @@ Save Selected as Images + + Email All + + + Email Selected + Email All as PDF @@ -219,6 +282,15 @@ تعريف TWAIN + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -267,6 +339,9 @@ إعدادات الحفظ التلقائي + + إعدادات الحفظ التلقائي + متقدم @@ -279,6 +354,9 @@ اختيار مصدر + + Select Device + Run in Background @@ -303,9 +381,15 @@ OCR mode: + + Fix white balance and remove noise + مباشرة اقلاع قارئ الحروف بعد الفحص + + Pre-emptively run OCR after scanning + الحصول على المزيد من اللغات @@ -348,4 +432,457 @@ اختيار الكل + + استرداد + + + ليس الآن + + + استرداد الصور الممسوحة ضوئياً + + + {0} صورة/صور ممسوحة ضوئياً على {1} في {2} قد لم يتم حفظها، وهي قابلة للاسترداد. هل تريد استعادتها؟ + + + عناصر نائبة + + + تخطي مطالبة الحفظ + + + صفحه واحده + + + تشفير الـ PDF + + + إظهار + + + استعادة الإعدادات الافتراضية + + + إعدادات PDF + + + السماح بالطباعة + + + السماح بالطباعة بالجودة الكاملة + + + السماح بتعديل الملف + + + السماح بتجميع المستندات + + + اسمح بنسخ المحتوى + + + السماح بنسخ المحتوى لسهولة الوصول + + + اسمح بالتعليقات التوضيحية Annotations + + + السماح بملء الحقول + + + مسار الملف الإفتراضي: + + + مسار الملف: + + + العنوان: + + + المؤلف: + + + الموضوع: + + + الكلمات المفتاحية: + + + كلمة مرور المالك: + + + كلمة مرور المستخدم: + + + تذكر هذه الإعدادات + + + بيانات وصفية + + + التشفير + + + التوافق + + + عناصر نائبة + + + سنة + + + سنة (00-99) + + + شهر (01-12) + + + اليوم (01-31) + + + ساعة (0-23) + + + دقيقة (00-59) + + + ثانية (00-59) + + + رقم متزايد تلقائياً (4 أعداد) + + + رقم متزايد تلقائياً (3 أعداد) + + + رقم متزايد تلقائياً (عددين) + + + Auto-incrementing number (1 digits) + + + اسم الملف + + + معاينة: + + + For high JPEG qualities (80+), also increase Image Quality in your profile for best results. + + + جودة Jpeg + + + خيارات Tiff + + + الضغط: + + + إعدادات الصورة + + + إعدادات البريد الإلكتروني + + + Settings + + + مزود + + + تغيير + + + اسم المرفق: + + + اختر مقدم خدمة الاميل + + + رَخّص + + + انتظر التفويض... + + + خطأ + + + تفاصيل تقنيه + + + كلمة المرور + + + The following file is encrypted and requires a password to open: + + + Prompt for file path + + + ملف واحد لكل صفحة + + + ملف واحد لكل عملية مسح ضوئي + + + Separate files by Patch-T + + + المزيد من المعلومات + + + محو الصور بعد الحفظ + + + Keep images across sessions + + + حجم صفحة مخصص + + + الاسم (اختياري) + + + الأبعاد + + + Custom Resolution + + + Dpi + + + في انتظار TWAIN للإكتمال... + + + إعدادات الملف الشخصي المتقدمة + + + جودة الصورة + + + الجودة القصوى (ملفات كبيرة) + + + صفحات فارغة + + + استبعاد الصفحات الفارغة + + + White Threshold + + + تغطية العتبة + + + Post-processing + + + Deskew scanned pages + + + تطبيق السطوع/التباين بعد المسح الضوئي + + + إزاحة العرض استناداً إلى المحاذاة (WIA) + + + Stretch to page size + + + اقتصاص لحجم الصفحة + + + قلب الصفحات المزدوجة + + + Flip back sides of duplex pages + + + Wia Version: + + + Twain Implementation: + + + مسح ضوئي على دفعات + + + اضغط بدأ عندما تكون جاهزاً. + + + تكوين المسح الضوئي + + + الإخراج + + + الملف الشخصي: + + + مسح ضوئي فردي + + + مسوح ضوئية متعددة (مطالبة بين عمليات المسح) + + + مسوح ضوئية متعددة (توقيت تأخير ثابت بين عمليات المسح) + + + عدد عمليات المسح الضوئي: + + + الوقت بين عمليات المسح الضوئي (ثواني): + + + تحميل صور إلى NAPS2 + + + حفظ إلى ملف واحد + + + حفظ إلى ملفات متعددة + + + بدء + + + المسح الضوئي التالي + + + جاهز للمسح الضوئي {0}. + + + فتح المجلد + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + اسأل دائما + + + Searching for devices... + + + {0} devices found. + + + تم العثور على جهاز 1. + + + لم يتم العثور على أي أجهزة. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.bg.resx b/NAPS2.Lib/Lang/Resources/UiStrings.bg.resx index 2932de8738..a2638634bf 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.bg.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.bg.resx @@ -16,7 +16,7 @@ Относно - Copyright {0} NAPS2 Contributors + Авторско право {0} на сътрудници на NAPS2 Икони от: @@ -25,10 +25,10 @@ Проверка за обновяване - OK + Да - Donate + Даряване Профили @@ -54,6 +54,18 @@ Постави + + Отмяна на извършено действие + + + Възстановяване на отменено действие + + + Отмяна на извършено действие {0} + + + Възстановяване на отменено действие {0} + Готово @@ -67,7 +79,7 @@ Групово сканиране - OCR + Оптично разпознаване на думи Профили @@ -109,16 +121,16 @@ Яркост / Контраст - Hue / Saturation + Нюанс / Насищане Черно и бяло - Sharpen + Изостряне - Document Correction + Корекция на документа Изчистване @@ -136,7 +148,7 @@ Обръщане - Deskew + Изправяне Завъртане по избор @@ -151,29 +163,62 @@ Пренареждане - Interleave + Редуване - Deinterleave + Смесване - Alternate Interleave + Заместващо редуване - Alternate Deinterleave + Редуващо разпръскване Обръщане + + Обръщане на всички + + + Обратно на избраното + Изчистване - Clear All + Изчистване на всичко Език + + Настройки + + + Интерфейс + + + Сканиране + + + Показване на лентата с инструменти "Профили" + + + Показване на номерата на страниците + + + Сканиране: + + + Запазване: + + + Приложение + + + Позволяване на само един екземпляр на NAPS2 + Относно @@ -186,26 +231,44 @@ Намаляване + + Действително увеличение + + + Мащабиране според прозореца + - Save + Запазване + + + Запазване на всички + + + Запазване на избраното - Save All as PDF + Запазване на всички като PDF - Save Selected as PDF + Запазване на избраното като PDF - Save All as Images + Запазване на всички като изображения - Save Selected as Images + Запазване на избраното като изображения + + + Изпращане на имейла на всички + + + Имейлът е избран - Email All as PDF + Изпращане на имейла към всички като PDF - Email Selected as PDF + Имейлът е избран като PDF Настройки на профила @@ -219,11 +282,20 @@ TWAIN драйвър + + ESCL драйвер + + + ESCL мрежов драйвер + + + ESCL USB драйвер + - Apple Driver + Apple драйвер - SANE Driver + SANE драйвер Устройство: @@ -267,6 +339,9 @@ Автоматично запазване на настройките + + Автоматично запазване на настройките + Допълнителни настройки @@ -279,8 +354,11 @@ Избери източник + + Избиране на Устройство + - Run in Background + Изпълнение във фонов режим NAPS2 @@ -289,7 +367,7 @@ NAPS2 - {0} - Not Another PDF Scanner + Не е като други PDF скенери Настройки на OCR @@ -301,11 +379,17 @@ Език за OCR: - OCR mode: + OCR режим: + + + Коригиране баланса на бялото и премахване на шума Автоматично стартиране на Оптично разпознаване след сканиране + + Стартирайте превантивно OCR след сканиране + Още езици @@ -348,4 +432,457 @@ Избери всички + + Възстановяване + + + Не сега + + + Възстановяване на сканираните изображения + + + {0} изображение/я, сканирани от {1} на {2} вероятно не са записани, но могат да бъдат възстановени. Искате ли да ги възстановите? + + + Заместители + + + Пропусни запитването за запис + + + Файлове с една страница + + + Шифроване на PDF + + + Показване + + + Възстановяване на настройки по подразбиране + + + Настройки на PDF + + + Разреши принтиране + + + Разрешаване на принтиране с високо качество + + + Разрешаване на промяна на документа + + + Разреши конструиране на документ + + + Разрешаване копиране на съдържанието + + + Разреши копиране на съдържание за достъпност + + + Разрешаване на анотации + + + Разрешаване на попълване на формуляри + + + Директория по подразбиране: + + + Местоположение на файла: + + + Заглавие: + + + Автор: + + + Тема: + + + Ключови думи: + + + Парола на собственика: + + + Потребителска парола: + + + Запомни тези настройки + + + Допълнителни данни + + + Шифроване + + + Съвместимост + + + Заместители + + + Година + + + Година (00-99) + + + Месец (01-12) + + + Ден (01-31) + + + Час (0-23) + + + Минути (00-59) + + + Секунди (00-59) + + + Автоматично отброяване (4 цифри) + + + Автоматично отброяване (3 цифри) + + + Автоматично отброяване (2 цифри) + + + Автоматично нарастване (с 1 цифра) + + + Име на файл + + + Преглед: + + + За високо качество на JPEG (80+), също така увеличете и качеството на изображението във вашия профил за най-добри резултати. + + + Jpeg качество + + + Опции на Tiff + + + Компресиране: + + + Настройки на изображението + + + Настройки за имейл + + + Настройки + + + Доставчик + + + Промяна + + + Име на прикачения файл: + + + Избери е-майл доставчик + + + Упълномощи + + + Чака се позволение... + + + Грешка + + + Технически данни + + + Парола + + + Следният файл е шифрован и изисква парола за отваряне: + + + Напомняне за път на файла + + + Всяка страница в отделен файл + + + Всяко сканиране в отделен файл + + + Разделяне на файловете чрез Patch-T + + + Повече информация + + + Изчисти изображенията след запис + + + Съхраняване на изображенията по време на сесиите + + + Нестандартен размер на страница + + + Име (незадължително) + + + Размери + + + Custom Resolution + + + Dpi + + + Изчакване TWAIN да приключи задачата... + + + Допълнителни настройки на профила + + + Качество на изображението + + + Максимално качество (по-големи файлове) + + + Празни страници + + + Пропусни празните страници + + + Праг на бялото + + + Праг на покритие + + + Последваща обработка + + + Изправяне на сканираните страници + + + Приложи яркост/контраст след сканиране + + + Компенсирай широчината на база подравняване (WIA) + + + Разтягане до размера на страницата + + + Изрязване до размера на страницата + + + Обърни двустранно сканираните страници + + + Обърнете обратните страни на страниците + + + Wia версия: + + + Twain реализация: + + + Групово сканиране + + + Натиснете Старт, когато сте готови. + + + Настройки на сканирането + + + Резултат + + + Профили: + + + Единично сканиране + + + Многократно сканиране (запитване между сканиранията) + + + Многократно сканиране (фиксирано забавяне между сканиранията) + + + Брой сканирания: + + + Интервал между сканиранията (в секунди): + + + Внасяне на изображения в NAPS2 + + + Запис в един файл + + + Запис в отделни файлове + + + Старт + + + Следващо сканиране + + + Готовност за сканиране {0}. + + + Отвори папка + + + Инструменти + + + Активиране на регистрирането на грешки + + + Споделяне + + + Споделяне на скенер + + + Споделяне на скенер + + + Споделените скенери могат да се използват от други компютри в локалната мрежа, като изберете "ESCL драйвер" в настройките на NAPS2 профила на другия компютър. + + + Споделени настройки на скенера + + + Сигурни ли сте, че искате да спрете споделянето на {0}? + + + Споделяне дори когато NAPS2 е затворен + + + Множество езици... + + + Множество езици + + + Показване на напредъка на TWAIN + + + Разделяне + + + Комбиниране + + + Ръчно IP + + + Ръчно IP + + + IP/Host + + + Порт + + + Свързване + + + Грешка при свързване. + + + Винаги питай + + + Търсят се устройства... + + + {0} намерени устройства. + + + Намерено е 1 устройство. + + + Няма намерени устройства. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Сканиране с профил по подразбиране + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.bs.resx b/NAPS2.Lib/Lang/Resources/UiStrings.bs.resx new file mode 100644 index 0000000000..fa01966831 --- /dev/null +++ b/NAPS2.Lib/Lang/Resources/UiStrings.bs.resx @@ -0,0 +1,888 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + O programu + + + Autorska prava zaštićena {0} NAPS2 saradnici + + + Ikonice sa: + + + Provjeri ažuriranja + + + U redu + + + Donirajte + + + Profili + + + Novi + + + Uredi + + + Izbriši + + + Skeniraj + + + Postavi na zadano + + + Kopiraj + + + Zalijepi + + + Poništi radnju + + + Ponovi radnju + + + Poništi {0} + + + Ponovi {0} + + + Gotovo + + + Zadana + + + Novi profil + + + Serijsko skeniranje + + + OPZ (OCR) + + + Profili + + + Uvoz + + + Sačuvaj PDF + + + PDF postavke + + + Saćuvaj slike + + + Postavke slike + + + Pošalji PDF e-mailom + + + Postavke e-maila + + + Štampanje + + + Slika + + + Pogled + + + Izreži + + + Osvjetljenost / Kontrast + + + Nijanse / Zasićenost + + + Crna i bijela boja + + + Izoštri + + + Ispravka dokumenta + + + Resetuj + + + Rotiraj + + + Rotiraj lijevo + + + Rotiraj desno + + + Obrni + + + Ravnanje + + + Prilagođena rotacija + + + Pomjeri gore + + + Pomjeri dolje + + + Preuredi + + + Naizmjenično + + + Izmjenično + + + Zamjeni naizmjenično + + + Zamjeni izmjenično + + + Preokreni + + + Preokreni sve + + + Preokreni izabrano + + + Očisti + + + Očisti sve + + + Jezik + + + Postavke + + + Interfejs + + + Meni za "Skeniranje" mijenja zadani profil + + + Prikaži alatnu traku "Profili" + + + Prikaži brojeve stranica + + + Zadana funckija dugmeta "Skeniraj": + + + Zadana funckija dugmeta "Saćuvaj": + + + Aplikacija + + + Dozvoli samo jednu programsku instancu NAPS2 + + + O programu + + + Zumiranje + + + Uvećaj + + + Smanji + + + Aktualno zumiranje + + + Skaliraj s prozorom + + + Saćuvaj + + + Sačuvaj sve + + + Sačuvaj izabrano + + + Sačuvaj sve kao PDF + + + Sačuvaj izabrano kao PDF + + + Sačuvaj sve kao slike + + + Sačuvaj izabrano kao slike + + + Pošalji sve e-mailom + + + Pošalji označeno e-mailom + + + Pošalji e-mailom sve kao PDF + + + Pošalji označeno kao PDF e-mailom + + + Postavke profila + + + Naziv za prikaz: + + + WIA drajver + + + TWAIN drajver + + + ESCL drajver + + + ESCL mrežni drajver + + + ESCL USB drajver + + + Apple Driver + + + SANE drajver + + + Uređaj: + + + Izaberite uređaj + + + Koristi predefinisane postavke + + + Koristi nativni KI + + + Izvor dokumenta: + + + Dimenzija stranice: + + + Rezolucija: + + + Osvjetljenost: + + + Bitni kvalitet: + + + Horizontalno poravnanje: + + + Skaliraj: + + + Kontrast: + + + Omogući auto. čuvanje + + + Postavke automatskog snimanja + + + Postavke automatskog snimanja + + + Napredno + + + Otkaži + + + Izaberi + + + Izaberi izvor + + + Izaberite uređaj + + + Radi u pozadini + + + NAPS2 + + + NAPS2 - {0} + + + Not Another PDF Scanner + + + Postavke za OPZ + + + Omogući pretraživanje u PDF-ovima pomoću OPZ-a + + + Jezik OPZ-a: + + + Način OPZ-a: + + + Popravi balans bijele i ukloni šum + + + Automatski pokreni OPZ nakon skeniranja + + + Preventivno pokreni OPZ nakon skeniranja + + + Preuzmi više jezika + + + Preuzmi jezike za OPZ + + + Korištenje OCR (OPZ) zahtjeva preuzimanje pojedinačnih jezika koje želite skenirati. + + + Izaberite jedan ili više jezika: + + + Procjenjena veličina preuzimanja: {0} MB + + + Preuzmi + + + Preuzimanje u toku + + + Pregled + + + Slijedeći + + + Prethodno + + + {0} od {1} + + + Vrati + + + Primijeni na sve(-ih) {0} izabrane(-ih) slike(-a) + + + Izaberi sve + + + Oporavak + + + Ne sada + + + Oporavi skenirane slike + + + {0} slika(e) skenirane {1} u {2} možda nije(-su) snimljeno(e), i moguće ih je oporaviti. Da li ih želite oporaviti? + + + Šablonski nazivi + + + Preskoči pitanja za čuvanje + + + Jednostrane datoteke + + + Šifriraj PDF + + + Prikaži + + + Vrati na zadano + + + PDF postavke + + + Dozvoli štampanje + + + Dozvoli štampanje u punom kvalitetu + + + Dozvoli izmjene na dokumentu + + + Dozvoli sastavljanje dokumenata + + + Dozvoli kopiranje sadržaja + + + Dozvoli kopiranje sadržaja radi pristupačnosti + + + Dozvoli anotacije + + + Dozvoli ispunjavanje formulara + + + Zadana putanja do datoteke: + + + Putanja do datoteke: + + + Naslov: + + + Autor: + + + Predmet: + + + Ključne riječi: + + + Vlasnička lozinka: + + + Korisnička loznika: + + + Zapamti ove postavke + + + Metapodaci + + + Šifriranje + + + Kompatibilnost + + + Šablonski nazivi + + + Godina + + + Godina (00-99) + + + Mjesec (01-12) + + + Dan (01-31) + + + Sat (0-23) + + + Minuta (00-59) + + + Sekunda (00-59) + + + Autorastući broj (4 cifre) + + + Autorastući broj (3 cifre) + + + Autorastući broj (2 cifre) + + + Autorastući broj (1 cifra) + + + Naziv datoteke: + + + Pregled: + + + Za visoki JPEG kvalitet (80+), za najbolje rezultate potrebno je povećati i kvalitet slika na vašem profilu. + + + Jpeg kvalitet + + + TIFF opcije + + + Kompresija: + + + Postavke slike + + + Postavke e-maila + + + Postavke + + + Provajder + + + Promijeni + + + Naziv priloga: + + + Izaberi e-mail provajdera + + + Autoriziraj + + + Čekanje na autorizaciju... + + + Greška + + + Tehnički detalji + + + Lozinka + + + Slijedeća datoteka je šifrirana i zathijeva lozinku za otvaranje: + + + Pitaj za putanju do datoteke + + + Jedna datoteka po stranici + + + Jedna datoteka po skeniranju + + + Razdvoji datoteke pomoću Patch-T + + + Više informacija + + + Očisti slike nakon snimanja + + + Zadrži slike kroz sesije + + + Prilagođene dimenzije stranice + + + Naziv (neobavezno) + + + Dimenzije + + + Prilagođena rezolucija + + + TPI + + + Čekanje TWAIN-a da završi... + + + Postavke naprednog profila + + + Kvalitet slike + + + Maksimalan kvalitet (velike datoteke) + + + Prazne stranice + + + Izuzmi prazne stranice + + + Prag bijeline + + + Prag pokrivenosti + + + Naknadna obrada + + + Izravnaj skenirane stranice + + + Primijeni osvjetljenje/kontrast nakon skeniranja + + + Širina ofseta (pomaka) na osnovu poravnanja (WIA) + + + Raširi na veličinu stranice + + + Izreži na dimenzije stranice + + + Obrni obostrane listove + + + Obrnite pozadinske stranice obostranih listova + + + Verzija WIA: + + + TWAIN implementacija: + + + Serijsko skeniranje + + + Pritisnite "Start" kada ste spremni. + + + Podešavanje skeniranja + + + Izlaz + + + Profil: + + + Jednostruko skeniranje + + + Višestruko skeniranje (prompt između skeniranja) + + + Višestruko skeniranje (fiksni razmak između skeniranja) + + + Broj skeniranja: + + + Vrijeme između skeniranja (sekunde): + + + Učitaj slike u NAPS2 + + + Sačuvaj u jednu datoteku + + + Sačuvaj u više datoteka + + + Start + + + Slijedeće skeniranje + + + Spremno za skeniranje {0}. + + + Otvori folder + + + Alati + + + Omogući zapise otklanjanja grešaka + + + Dijeli + + + Dijeljenje skenera + + + Dijeljenje skenera + + + Dijeljeni skeneri mogu se koristiti s drugih računara na lokalnoj mreži tako što se izabere "ESCL" drajver u postavkama NAPS2 profila na drugim računarima. + + + Postavke dijeljenog skenera + + + Da li želite zaustaviti dijeljenje {0}? + + + Dijeli i kada je NAPS2 zatvoren + + + Više jezika... + + + Više jezika + + + Prikaži napredak nativnog TWAIN-a + + + Podijeli + + + Kombinuj + + + IP adresa + + + IP adresa + + + IP/Host + + + Port + + + Poveži se + + + Greška u povezivanju. + + + Uvijek pitaj + + + Traženje uređaja... + + + {0} pronađen(a) uređaj(a). + + + Pronađen 1 uređaj. + + + Nema pronađenih uređaja. + + + Bočna traka + + + Ne može se pronaći vaš skener? Pročitajte o ograničenjima NAPS2 flatpak-a. + + + Prečaci tastature + + + Prečaci tastature + + + Skeniraj s profilom {0} + + + Skeniraj sa zadanim profilom + + + Skeniraj s novim profilom + + + Dodijeli + + + Oslobodi + + + Radnja + + + Prečac + + + Tema: + + + Uredi pomoću... + + + Uredi pomoću {0} + + + Uredi pomoću... + + + Greška pri pokretanju aplikacije {0} + + + Zaustavi dijeljenje skenera + + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.ca.resx b/NAPS2.Lib/Lang/Resources/UiStrings.ca.resx index 77c08f7245..74af4133b1 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.ca.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.ca.resx @@ -16,10 +16,10 @@ Quant a - Copyright {0} NAPS2 Contributors + Copyright {0} Contribuïdors del NAPS2 - Icons desde: + Icones de: Comprova si hi ha actualitzacions @@ -40,7 +40,7 @@ Edita - Esborra + Suprimeix Escaneja @@ -54,6 +54,18 @@ Enganxa + + Desfés + + + Refés + + + Desfés {0} + + + Refés {0} + Fet @@ -88,10 +100,10 @@ Opcions de la imatge - Email PDF + PDF per correu-e - Paràmetres del correu electrònic + Configuració del correu electrònic Imprimeix @@ -118,13 +130,13 @@ Nitidesa - Document Correction + Correcció del document Restaura - Rotate + Gira Gira a l'esquerra @@ -151,7 +163,7 @@ Organitza - Interleave + Intercala Desentrellaça @@ -163,17 +175,50 @@ Desintercalat alternatiu - Reverse + Inverteix + + + Inverteix-ho tot + + + Selecció inversa Neteja - Clear All + Neteja-ho tot Idioma + + Configuració + + + Interfície + + + El menú "Escaneja" canvia el perfil per defecte + + + Mostra la barra d'eines «Perfils» + + + Mostra els números de pàgina + + + Acció predeterminada del botó "Escaneja": + + + Acció predeterminada del botó "Desa": + + + Aplicació + + + Permet només una única instància del NAPS2 + Quant a @@ -186,41 +231,68 @@ Allunya + + Escala actual + + + Ajusta a la finestra + - Save + Desa + + + Desa-ho tot + + + Desa la selecció - Save All as PDF + Desa com a PDF - Save Selected as PDF + Desa la selecció com a PDF - Save All as Images + Desa com a imatges - Save Selected as Images + Desa la selecció com a imatges + + + Envia-ho tot + + + Seleccionat - Email All as PDF + Envia-ho tot com a PDF per correu-e - Email Selected as PDF + Envia la selecció com a PDF per correu-e Paràmetres del perfil - Mostra el nom: + Nom mostrat: - Dispositiu WIA + Controlador WIA - Dispositiu TWAIN + Controlador TWAIN + + + Controlador ESCL + + + Controlador de xarxa ESCL + + + Controlador USB ESCL - Apple Driver + Controlador d'Apple Controlador SANE @@ -229,10 +301,10 @@ Dispositiu: - Trieu dispositiu + Trieu un dispositiu - Utiltza els ajustos predeterminats + Utilitza les opcions per defecte Utilitza la interfície nativa @@ -262,10 +334,13 @@ Contrast: - Habilita el desat automàtic + Habilita el desament automàtic - Opcions de desat automàtic + Opcions de desament automàtic + + + Opcions de desament automàtic Avançat @@ -279,6 +354,9 @@ Origen + + Selecció del dispositiu + Executa en rerefons @@ -289,7 +367,7 @@ NAPS2 - {0} - Not Another PDF Scanner + (Not Another PDF Scanner) Configuració de l'OCR @@ -303,11 +381,17 @@ Mode de l'OCR: + + Corregeix el balanç de blancs i suprimeix el soroll + Inicia l'OCR automàticament després d'escanejar + + Inicia l'OCR de forma preventiva després de l'escaneig + - Get more languages + Més idiomes Baixada de l'OCR @@ -348,4 +432,457 @@ Selecciona-ho tot + + Recupera + + + Ara no + + + Recuperació d'imatges escanejades + + + Possiblement {0} imatges escanejades el {1} a les {2} no s'han desat, però es poden recuperar. Voleu continuar amb la recuperació? + + + Marcadors de posició + + + Omet les opcions de desament + + + Un fitxer per cada pàgina + + + Xifra el PDF + + + Mostra + + + Restaura als valors per defecte + + + Opcions del PDF + + + Permet la impressió + + + Permet la impressió en alta resolució + + + Permet la modificació del document + + + Permet el muntatge de documents + + + Permet la còpia del contingut + + + Permet l'accés al contingut per part d'eines d'accessibilitat + + + Permet anotacions + + + Permet l'emplenament de camps de formulari + + + Camí predeterminat: + + + Camí del fitxer: + + + Títol: + + + Autoria: + + + Assumpte: + + + Paraules clau: + + + Contrasenya del propietari: + + + Contrasenya de l'usuari: + + + Recorda aquesta configuració + + + Metadades + + + Xifratge + + + Compatibilitat + + + Marcadors de posició + + + Any + + + Any (00-99) + + + Mes (01-12) + + + Dia (01-31) + + + Hora (0-23) + + + Minut (00-59) + + + Segons (00-59) + + + Increment automàtic del número (4 dígits) + + + Increment automàtic del número (3 dígits) + + + Increment automàtic del número (2 dígits) + + + Increment automàtic del número (1 dígit) + + + Nom del fitxer: + + + Previsualitza: + + + Per a obtenir imatges JPEG d'alta qualitat (80+), incrementeu també la resolució al perfil. + + + Qualitat Jpeg + + + Opcions TIFF + + + Compressió: + + + Opcions de la imatge + + + Configuració del correu electrònic + + + Configuració + + + Proveïdor + + + Canvia + + + Nom de l'adjunt: + + + Trieu el proveïdor de correu + + + Autoritza + + + S'està esperant l'autorització... + + + Error + + + Detalls tècnics + + + Contrasenya + + + Aquest fitxer està xifrat per contrasenya i és necessària per obrir-lo: + + + Demana la ruta de destinació + + + Un fitxer per pàgina + + + Un fitxer per escaneig + + + Separació amb codis Patch-T + + + Més informació + + + Neteja les imatges després de desar + + + Conserva les imatges entre sessions + + + Mida personalitzada de la pàgina + + + Nom (opcional) + + + Mides + + + Resolució personalitzada + + + ppp + + + Esperant a TWAIN per a completar... + + + Configuració avançada del perfil + + + Qualitat de la imatge + + + Qualitat màxima (fitxers grans) + + + Pàgines buides + + + Exclou les pàgines en blanc + + + Llindar del blanc + + + Llindar de cobertura + + + Tractament posterior + + + Alinea les pàgines escanejades + + + Aplica brillantor/contrast després d'escanejar + + + Amplada de desplaçament segons l'alineació (WIA) + + + Ajusta a la mida de la pàgina + + + Escapça a la mida de la pàgina + + + Gira les pàgines doblegades + + + Gireu el revers de les pàgines dúplex + + + Versió Wia: + + + Implementació TWAIN: + + + Escaneig per lots + + + Premeu Inicia per continuar. + + + Configuració de l’escàner + + + Sortida + + + Perfil: + + + Escaneig senzill + + + Escaneig múltiple (demana entre cada escaneig) + + + Escaneig múltiple (retard fix entre escaneig) + + + Nombre d'escanejos: + + + Temps entre escanejos (segons): + + + Carrega les imatges al NAPS2 + + + Desa en un únic fitxer + + + Desa en fitxers múltiples + + + Inicia + + + Proper escaneig + + + Preparat per l'escaneig {0}. + + + Obre una carpeta + + + Eines + + + Activa el Mode depuració + + + Comparteix + + + Compartició d'escàner + + + Compartició d'escàner + + + Els escàners compartits es poden utilitzar des d'altres ordinadors de la xarxa local seleccionant «Controlador ESCL» a la configuració del perfil NAPS2 de l'altre ordinador. + + + Configuració de l'escàner compartit + + + Segur que voleu aturar la compartició de {0}? + + + Comparteix fins i tot quan NAPS2 és tancat + + + Diversos idiomes... + + + Diversos idiomes + + + Mostra la interfície de progrés nativa del TWAIN + + + Divideix + + + Combina + + + IP manual + + + IP manual + + + IP/Amfitrió + + + Port + + + Connecta + + + Error de connexió. + + + Demana sempre + + + S'estan cercant dispositius... + + + {0} dispositius trobats. + + + S'ha trobat 1 dispositiu. + + + No s'ha trobat cap dispositiu. + + + Barra lateral + + + No trobeu el vostre escàner? Informeu-vos de les limitacions del NAPS2 en versió Flatpak. + + + Dreceres de teclat + + + Dreceres de teclat + + + Escaneja amb el perfil {0} + + + Escaneja amb el perfil per defecte + + + Escaneja amb un perfil nou + + + Vincula + + + Desvincula + + + Acció + + + Drecera + + + Mode: + + + Edita amb... + + + Edita amb {0} + + + Edita amb... + + + Error en iniciar l'aplicació {0} + + + Atura la compartició d'escàner + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.cs.resx b/NAPS2.Lib/Lang/Resources/UiStrings.cs.resx index f26ff9fe79..e31270aef2 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.cs.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.cs.resx @@ -16,7 +16,7 @@ O aplikaci - Copyright {0} NAPS2 Contributors + Autorská práva {0} přispěvatelů NAPS2 Použité ikony: @@ -54,6 +54,18 @@ Vložit + + Zpět + + + Znovu + + + Zpět {0} + + + Znovu {0} + Hotovo @@ -73,7 +85,7 @@ Profily - Import + Importovat Uložit PDF @@ -118,7 +130,7 @@ Zaostřit - Document Correction + Korekce dokumentu Resetovat @@ -165,15 +177,48 @@ Obrátit + + Obrátit vše + + + Obrátit vybrané + Vymazat - Clear All + Smazat vše Jazyk + + Nastavení + + + Rozhraní + + + Nabídka "Skenovat" změní výchozí profil + + + Zobrazit panel nástrojů "Profily" + + + Zobrazit čísla stránek + + + Výchozí akce tlačítka "Sken": + + + Výchozí akce tlačítka "Uložit": + + + Aplikace + + + Povolit pouze jednu instanci NAPS2 + O aplikaci @@ -186,26 +231,44 @@ Oddálit + + Aktuální zvětšení + + + Stupnice s oknem + - Save + Uložit + + + Uložit vše + + + Uložit vybrané - Save All as PDF + Uložit vše jako PDF - Save Selected as PDF + Uložit vybrané jako PDF - Save All as Images + Uložit vše jako obrázky - Save Selected as Images + Uložit vybrané jako obrázky + + + Odeslat vše + + + Odeslat vybrané - Email All as PDF + Odeslat vše jako PDF - Email Selected as PDF + Odeslat vybrané jako PDF Nastavení profilu @@ -219,8 +282,17 @@ Ovladač TWAIN + + Ovladač ESCL + + + Síťový ovladač ESCL + + + Ovladač ESCL USB + - Apple Driver + Ovladač Apple SANE ovladač @@ -267,6 +339,9 @@ Nastavení automatického uložení + + Nastavení automatického uložení + Pokročilé @@ -279,6 +354,9 @@ Vybrat zdroj + + Výběr zařízení + Spustit na pozadí @@ -303,9 +381,15 @@ Mód OCR: + + Opravit vyvážení bílé a odstranění šumu + Po skenování automaticky spustit OCR + + Předběžné spuštění OCR po skenování + Instalovat více jazyků @@ -348,4 +432,457 @@ Vybrat vše + + Obnovit + + + Teď ne + + + Obnovit skenované obrázky + + + {0} obrázků skenovaných na {1} v {2} možná nebylo uloženo a je možno je obnovit. Chcete je obnovit? + + + Zástupné znaky + + + Přeskočit dotaz na uložení + + + Jednostránkové soubory + + + Zašifrovat PDF + + + Ukázat + + + Obnovit výchozí + + + Nastavení PDF + + + Povolit tisk + + + Povolit tisk v plné kvalitě + + + Povolit úpravu dokumentu + + + Povolit sestavení dokumentu + + + Povolit kopírování obsahu + + + Povolit kopírování obsahu pro přístupnost + + + Povolit anotace + + + Povolit vyplňování formuláře + + + Výchozí umístění souboru: + + + Cesta k souboru: + + + Titul: + + + Autor: + + + Předmět: + + + Klíčová slova: + + + Heslo vlastníka: + + + Heslo uživatele: + + + Zapamatovat si tato nastavení + + + Metadata + + + Šifrování + + + Kompatibilita + + + Zástupné znaky + + + Rok + + + Rok (00-99) + + + Měsíc (01-12) + + + Den (01-31) + + + Hodina (0-23) + + + Minuta (00-59) + + + Sekunda (00-59) + + + Automatické zvyšování čísla (4 číslice) + + + Automatické zvyšování čísla (3 číslice) + + + Automatické zvyšování čísla (2 číslice) + + + Automatický přírůstek čísla (1 znak) + + + Název souboru: + + + Náhled: + + + Pro nejlepší výsledky při vysokých kvalitách JPEG (80+) zvyšte ve svém profilu Kvalitu obrázku. + + + Kvalita JPEG + + + Možnosti Tiff + + + Komprese: + + + Nastavení obrázku + + + Nastavení e-mailu + + + Nastavení + + + Poskytovatel + + + Změnit + + + Název přílohy: + + + Vybrat poskytovatele mailu + + + Autorizovat + + + Čekání na autorizaci... + + + Chyba + + + Technické detaily + + + Heslo + + + Následující soubor je zašifrován a k otevření vyžaduje heslo: + + + Výzva pro cestu k souboru + + + Jeden soubor na stránku + + + Jeden soubor na sken + + + Oddělit soubory pomocí Patch-T + + + Více informací + + + Vyčistit obrázky po skenování + + + Uchovávat obrázky napříč relacemi + + + Vlastní velikost papíru + + + Název (volitelné) + + + Rozměry + + + Vlastní rozlišení + + + Dpi + + + Čekání na dokončení TWAIN... + + + Pokročilé nastavení profilu + + + Kvalita obrázku + + + Nejlepší kvalita (velký soubor) + + + Prázdné stránky + + + Vyloučit prázdné stránky + + + Práh bílé + + + Práh pokrytí + + + Následné zpracování + + + Odstranit zešikmení skenovaných stránek + + + Použít jas/kontrast po skenování + + + Šířka odsazení založená na zarovnání (WIA) + + + Roztáhnout na velikost stránky + + + Oříznout na velikost stránky + + + Překlopit oboustranné stánky + + + Obrácení zadních stran oboustranných stránek + + + Wia verze: + + + Implementace TWAIN: + + + Dávkové skenování + + + Až budete připraveni, stiskněte Start. + + + Nastavení skenování + + + Výstup + + + Profil: + + + Jeden sken + + + Více skenů (oznámení mezi skeny) + + + Více skenů (fixní pauza mezi skeny) + + + Počet skenů: + + + Čas mezi skeny (vteřiny): + + + Nahrát obrázky do NAPS2 + + + Uložit do jednoho souboru + + + Uložit do více souborů + + + Start + + + Další sken + + + Připraven ke skenu {0}. + + + Otevřít složku + + + Nástroje + + + Povolit protokolování ladění + + + Sdílet + + + Sdílení skeneru + + + Sdílení skeneru + + + Sdílené skenery lze používat z jiných počítačů v místní síti výběrem možnosti "ESCL ovladač" v nastavení profilu NAPS2 jiného počítače. + + + Nastavení sdíleného skeneru + + + Opravdu chcete přestat sdílet {0}? + + + Sdílet, i když je NAPS2 zavřený + + + Více jazyků... + + + Více jazyků + + + Zobrazit nativní průběh TWAIN + + + Rozdělit + + + Spojit + + + Manuální IP + + + Manuální IP + + + IP/Hostitel + + + Port + + + Připojit + + + Chyba připojení. + + + Vždy se zeptat + + + Hledání zařízení... + + + Nalezeno {0} zařízení. + + + Nalezeno 1 zařízení. + + + Zařízení nenalezena. + + + Postranní panel + + + Nemůžete najít skener? Přečtěte si o omezeních Flatpaku NAPS2. + + + Klávesové zkratky + + + Klávesové zkratky + + + Skenovat pomocí profilu {0} + + + Skenovat pomocí výchozího profilu + + + Skenovat pomocí nového profilu + + + Přiřadit + + + Zrušit přiřazení + + + Akce + + + Zkratka + + + Motiv: + + + Upravit pomocí... + + + Upravit pomocí {0} + + + Upravit pomocí... + + + Chyba při spouštění aplikace {0} + + + Zastavit sdílení skeneru + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.da.resx b/NAPS2.Lib/Lang/Resources/UiStrings.da.resx index 6ee6d97a87..a7f718b581 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.da.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.da.resx @@ -22,13 +22,13 @@ Ikoner fra: - Check for updates + Søg efter opdateringer OK - Donate + Doner Profiler @@ -43,7 +43,7 @@ Slet - Skan + Scan Sæt standard @@ -54,6 +54,18 @@ Indsæt + + Fortryd + + + Gentag + + + Fortryd {0} + + + Gentag {0} + Fuldført @@ -64,7 +76,7 @@ Ny profil - Batch Scan + Batch-scanning OCR @@ -73,7 +85,7 @@ Profiler - Importér + Importer Gem PDF @@ -88,13 +100,13 @@ Billedindstillinger - Email PDF + E-mail til PDF - Email indstillinger + E-mail indstillinger - Print + Udskriv Billede @@ -118,19 +130,19 @@ Gør skarpere - Document Correction + Dokumentkorrektion Nulstil - Rotér + Roter - Rotér til venstre + Roter til venstre - Rotér til højre + Roter til højre Spejling @@ -148,7 +160,7 @@ Flyt ned - Sortere + Sorter Indflette @@ -165,15 +177,48 @@ Omvendt + + Omvend alle + + + Omvend markerede + Ryd - Clear All + Ryd alle Sprog + + Indstillinger + + + Grænseflade + + + "Scan" menu ændrer standardprofil + + + Vis værktøjslinjen "Profiler" + + + Vis sidenumre + + + Standardhandling for knappen "Scan": + + + Standardhandling for knappen "Gem": + + + Program + + + Tillad kun en enkelt åben NAPS2 + Om @@ -186,26 +231,44 @@ Zoom ud + + Zoom Aktuel + + + Skalér med vindue + - Save + Gem + + + Gem alle + + + Gem valgte - Save All as PDF + Gem alle som PDF - Save Selected as PDF + Gem valgte som PDF - Save All as Images + Gem alle som billeder - Save Selected as Images + Gem valgte som billeder + + + Send alle via e-mail + + + E-mail valgt - Email All as PDF + Send alle via e-mail som PDF - Email Selected as PDF + Send det valgte via e-mail som PDF Profilindstillinger @@ -214,16 +277,25 @@ Vist navn: - WIA Driver + WIA driver - TWAIN Driver + TWAIN-driver + + + ESCL driver + + + ESCL netværksdriver + + + ESCL USB-driver - Apple Driver + Apple driver - SANE Driver + SANE driver Enhed: @@ -267,6 +339,9 @@ Autogem indstillinger + + Autogem indstillinger + Avanceret @@ -279,8 +354,11 @@ Vælg kilde + + Vælg enhed + - Run in Background + Kør i baggrunden NAPS2 @@ -301,10 +379,16 @@ OCR sprog: - OCR mode: + OCR-tilstand: + + + Ret hvidbalance og fjern støj - Automatically run OCR after scanning + Kør OCR automatisk efter scanning + + + Kør forebyggende OCR efter scanning Hent flere sprog @@ -348,4 +432,457 @@ Markér alle + + Gendan + + + Ikke nu + + + Gendan scannet billede + + + {0} billede(r) scannet på {1} af {2} er måske ikke blevet gemt, men er mulige at genskabe. Vil du genskabe dem? + + + Pladsholdere + + + Spring over gemme-prompt + + + Enkelt side-filer + + + Kryptér PDF + + + Vis + + + Gendan standardindstillinger + + + PDF indstillinger + + + Tillad udskrivning + + + Tillad fuld kvalitetsudskrivning + + + Tillad ændring af dokument + + + Tillad samling af dokumenter + + + Tillad kopiering af indhold + + + Tillad kopiering af indhold for tilgængelighed + + + Tillad Annoteringer + + + Tillad formular udfyldning + + + Standard filsti: + + + Filsti: + + + Titel: + + + Forfatter: + + + Emne: + + + Nøgleord: + + + Ejers password: + + + Bruger adgangskode: + + + Husk disse indstillinger + + + Metadata + + + Kryptering + + + Kompatibilitet + + + Pladsholdere + + + År + + + År (00-99) + + + Måned (01-12) + + + Dag (01-31) + + + Time (0-23) + + + Minut (00-59) + + + Sekund (00-59) + + + Autoforøgelsesnummer (4 cifre) + + + Autoforøgelsesnummer (3 cifre) + + + Autoforøgelsesnummer (2 cifre) + + + Automatisk stigende tal (1 ciffer) + + + Filnavn: + + + Eksempel: + + + For høje JPEG-kvaliteter (80+), bør du også øge billedkvaliteten i din profil for de bedste resultater. + + + Jpeg kvalitet + + + TIFF-indstillinger + + + Komprimering: + + + Billedindstillinger + + + E-mail indstillinger + + + Indstillinger + + + Udbyder + + + Skift + + + Navn på vedhæftning: + + + Vælg e-mail udbyder + + + Autoriser + + + Venter på godkendelse... + + + Fejl + + + Tekniske detaljer + + + Adgangskode + + + Følgende fil er krypteret og kræver en adgangskode, for at åbne den: + + + Spørg efter filsti + + + En fil pr. side + + + En fil pr. scanning + + + Separer filer ved hjælp af Patch-T + + + Mere information + + + Ryd billeder efter gem + + + Behold billeder på tværs af sessioner + + + Tilpasset sidestørrelse + + + Navn (valgfrit) + + + Dimensioner + + + Custom Resolution + + + Dpi + + + Venter på TWAIN for at fortsætte... + + + Avancerede Profil Indstillinger + + + Billedkvalitet + + + Maximum kvalitet (store filer) + + + Blanke sider + + + Udelad blanke sider + + + Hvidgrænse + + + Dækningstærskel + + + Efterprocessering + + + Ret scannede sider op + + + Tilføj lysstyrke/kontrast efter scanning + + + Forskydningsbredde baseret på justering (WIA) + + + Stræk til sidestørrelse + + + Beskær til sidestørrelse + + + Vend duplex sider + + + Vend bagsiden af duplex-sider + + + WIA-version: + + + Twain Implementering: + + + Batch-scanning + + + Tryk start når du er klar. + + + Scan konfiguration + + + Resultat + + + Profil: + + + Enkelt scanning + + + Flere scanninger (prompt mellem scanninger) + + + Flere scanninger (fast forsinkelse mellem scanninger) + + + Antal scanninger: + + + Tid mellem scanninger (sekunder): + + + Hent billeder ind i NAPS2 + + + Gem som en enkelt fil + + + Gem som flere filer + + + Start + + + Næste scanning + + + Klar til scanning {0}. + + + Åben mappe + + + Værktøjer + + + Aktiver logning af fejl + + + Del + + + Deling af scanner + + + Deling af scanner + + + Delte scannere kan bruges fra andre computere på det lokale netværk, ved at vælge "ESCL Driver" i den anden computers NAPS2-profilindstillinger. + + + Delt scanner-indstillinger + + + Er du sikker på, at du vil stoppe med at dele {0}? + + + Del selv når NAPS2 er lukket + + + Flere sprog... + + + Flere sprog + + + Vis indbygget TWAIN-fremskridt + + + Opdel + + + Kombiner + + + Manuel IP + + + Manuel IP + + + IP/vært + + + Port + + + Forbind + + + Forbindelsesfejl. + + + Spørg altid + + + Søger efter enheder... + + + {0} enheder fundet. + + + 1 enhed fundet. + + + Ingen enheder fundet. + + + Sidebar + + + Kan du ikke finde din scanner? Læs om begrænsninger i NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan med standardprofil + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.de.resx b/NAPS2.Lib/Lang/Resources/UiStrings.de.resx index cf58409a00..6aa2c7da4f 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.de.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.de.resx @@ -16,7 +16,7 @@ Über - Copyright {0} NAPS2 Contributors + Copyright {0} NAPS2 Mitwirkende Icons von: @@ -54,6 +54,18 @@ Einfügen + + Rückgängig + + + Wiederholen + + + Rückgängig {0} + + + Wiederholen {0} + Fertig @@ -88,10 +100,10 @@ Bildeinstellungen - Email PDF + E-Mail als PDF - Einstellungen für EMail + E-Mail Einstellungen Drucken @@ -118,7 +130,7 @@ Schärfen - Document Correction + Dokumentkorrektur Zurücksetzen @@ -133,7 +145,7 @@ Rechts drehen - Spiegeln + um 180° drehen Schräglagenkorrektur @@ -151,10 +163,10 @@ Neu ordnen - Interleave + Ineinander-Fächern - Zeilensprünge entfernen + Auseinander-Fächern Alternatives Interleave @@ -165,15 +177,48 @@ Umkehren + + Alle umkehren + + + Auswahl umkehren + Alles Löschen - Clear All + Alles löschen Sprache + + Einstellungen + + + Benutzeroberfläche + + + Menü "Scan" ändert das Standardprofil + + + Zeige "Profile" Symbolleiste + + + Zeige Seitenzahlen + + + "Scan"-Button Standardaktion: + + + "Speichern"-Button Standardaktion: + + + Anwendung + + + Nur eine einzige NAPS2-Instanz erlauben + Über @@ -186,26 +231,44 @@ Verkleinern + + Zoom Originalgröße + + + Skaliere mit Fenster + - Save + Speichern + + + Alles speichern + + + Auswahl speichern - Save All as PDF + Alles als PDF speichern - Save Selected as PDF + Auswahl als PDF speichern - Save All as Images + Alles als Bilder speichern - Save Selected as Images + Auswahl als Bilder speichern + + + E-Mail alle + + + Auswahl per E-Mail versenden - Email All as PDF + E-Mail alle als PDF - Email Selected as PDF + Auswahl als PDF per E-Mail versenden Profileinstellungen @@ -219,8 +282,17 @@ TWAIN Treiber + + ESCL Treiber + + + ESCL Netzwerktreiber + + + ESCL USB-Treiber + - Apple Driver + Apple Treiber SANE-Treiber @@ -235,7 +307,7 @@ Vordefinierte Einstellungen verwenden - Ursprüngliche Oberfäche verwenden + Scanner-Dialog verwenden Papiereinzug: @@ -267,6 +339,9 @@ Einstellungen zum automatischen Speichern + + Einstellungen zum automatischen Speichern + Erweitert @@ -279,6 +354,9 @@ Quelle auswählen + + Gerät auswählen + Ausführung im Hintergrund @@ -303,9 +381,15 @@ OCR-Modus: + + Weißabgleich korrigieren und Rauschen entfernen + OCR nach dem Scannen automatisch starten + + OCR nach dem Scannen automatisch starten + Weitere Sprachen installieren @@ -322,7 +406,7 @@ Geschätzte Download-Größe: {0} MB - Herunterladen + Download Download-Fortschritt @@ -337,7 +421,7 @@ Vorheriger - {0} of {1} + {0} von {1} Rückgängig @@ -348,4 +432,457 @@ Alle auswählen + + Wiederherstellen + + + Jetzt nicht + + + Eingescannte Bilder wiederherstellen + + + {0} Bilder wurden am {1} um {2} gescannt und wurden möglicherweise nicht gespeichert, sind aber wiederherstellbar. Sollen sie wiederhergestellt werden? + + + Platzhalter + + + Überspringe Speicherdialog + + + Dateien mit einer Seite + + + PDF verschlüsseln + + + Anzeigen + + + Standardeinstellungen wiederherstellen + + + PDF-Einstellungen + + + Drucken erlauben + + + Erlaube Druck in höchster Qualität + + + Erlaube Dokumentveränderung + + + Erlaube Dokumentenzusammenführung + + + Kopieren von Inhalten zulassen + + + Kopieren des Inhalts für erleichterte Bedienung erlauben + + + Anmerkungen erlauben + + + Erlaube das Ausfüllen von Formularen + + + Standarddateipfad: + + + Dateipfad: + + + Titel: + + + Autor: + + + Betreff: + + + Schlagwörter: + + + Besitzer-Passwort: + + + Benutzerpasswort: + + + Diese Einstellungen merken + + + Metadaten + + + Verschlüsselung + + + Kompatibilität + + + Platzhalter + + + Jahr + + + Jahr (00-99) + + + Monat (01-12) + + + Tag (01-31) + + + Stunde (0-23) + + + Minute (00-59) + + + Sekunde (00-59) + + + Automatisches Hochzählen (4 Stellen) + + + Automatisches Hochzählen (3 Stellen) + + + Automatisches Hochzählen (2 Stellen) + + + Automatisches Hochzählen (1 Stelle) + + + Dateiname: + + + Vorschau: + + + Für eine hohe JPEG-Qualität (80+) ist ebenfalls die Bildqualität in Ihrem Profil zu erhöhen, um beste Ergebnisse zu erhalten. + + + JPEG-Qualität + + + Tiff Einstellungen + + + Komprimierung: + + + Bildeinstellungen + + + E-Mail Einstellungen + + + Einstellungen + + + Anbieter + + + Ändern + + + Name des Anhangs: + + + E-Mail-Anbieter auswählen + + + Anmelden + + + Warten auf Anmeldung... + + + Fehler + + + Technische Details + + + Passwort + + + Die folgende Datei ist verschlüsselt und benötigt ein Passwort zum Öffnen: + + + Dateipfad bestätigen + + + Eine Datei je Seite + + + Eine Datei je Scan + + + Dateien per Patch-T trennen + + + Mehr Info + + + Entferne Bilder nach dem Speichern + + + Bilder über Sitzungen hinweg behalten + + + Benutzerdefinierte Seitengröße + + + Name (optional) + + + Abmessungen + + + Angepasste Auflösung + + + DPI + + + Warte auf TWAIN zur Fertigstellung... + + + Erweiterte Profileinstellungen + + + Bildqualität + + + Maximale Qualität (große Dateien) + + + Leere Seiten + + + Leerseiten unterdrücken + + + Weiß Schwellwert + + + Deckungsgrad + + + Nachbearbeitung + + + Schräglagenkorrektur gescannter Seiten + + + Passe Helligkeit und Kontrast nach dem Scannen an + + + Versatzbreite basierend auf Ausrichtung (WIA) + + + Auf Seitengröße dehnen + + + Auf Seitengröße zuschneiden + + + Doppelseiten umdrehen + + + Rückseite von Duplex-Seiten um 180° drehen + + + Wia Version: + + + Twain Implementierung: + + + Stapel-Scan + + + Start drücken wenn bereit. + + + Scan-Einstellungen + + + Ausgabe + + + Profil: + + + Einzelner Scan + + + Mehrere Scans (Zwischen den Scans nachfragen) + + + Mehrere Scans (Konstante Verzögerung zwischen Scans) + + + Anzahl der Scans: + + + Zeit zwischen den Scans (Sekunden): + + + Bilder in NAPS2 laden + + + In eine einzige Datei speichern + + + In mehrere Dateien speichern + + + Start + + + Nächster Scan + + + Bereit für Scan {0}. + + + Ordner öffnen + + + Werkzeuge + + + Fehlerprotokollierung aktivieren + + + Teilen + + + Scanner teilen + + + Scanner teilen + + + Geteilte Scanner können von anderen Computern im lokalen Netzwerk verwendet werden, indem Sie auf dem anderen Computer in den NAPS2 Profileinstellungen "ESCL Treiber" auswählen. + + + Geteilte Scanner Einstellungen + + + Sind Sie sicher, dass Sie {0} nicht mehr teilen möchten? + + + Auch teilen, wenn NAPS2 geschlossen ist + + + Mehrere Sprachen... + + + Mehrere Sprachen + + + Nativen TWAIN-Fortschritt anzeigen + + + Teilen + + + Kombinieren + + + Manuelle IP + + + Manuelle IP + + + IP/Host + + + Port + + + Verbinden + + + Verbindungsfehler. + + + Immer fragen + + + Suche nach Geräten... + + + {0} Geräte gefunden. + + + 1 Gerät gefunden. + + + Keine Geräte gefunden. + + + Seitenleiste + + + Sie können Ihren Scanner nicht finden? Lesen Sie über die Einschränkungen des NAPS2 Flatpaks. + + + Tastaturbefehle + + + Tastaturbefehle + + + Mit Profil {0} scannen + + + Mit Standardprofil scannen + + + Mit neuem Profil scannen + + + Zuweisen + + + Zuweisung aufheben + + + Aktion + + + Tastenkürzel + + + Erscheinungsbild: + + + Bearbeiten mit... + + + Bearbeiten mit {0} + + + Bearbeiten mit... + + + Fehler beim Starten der Anwendung {0} + + + Scanner nicht mehr teilen + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.el.resx b/NAPS2.Lib/Lang/Resources/UiStrings.el.resx index 6b7c26db92..975ebace8d 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.el.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.el.resx @@ -16,13 +16,13 @@ Σχετικά - Copyright {0} NAPS2 Contributors + Πνευματικά δικαιώματα {0} Συνεισφέροντες NAPS2 Προέλευση εικονιδίων: - Ελέγξτε για ενημερώσεις. + Έλεγχος για ενημερώσεις Εντάξει @@ -54,6 +54,18 @@ Επικόλληση + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + Ολοκλήρωση @@ -118,7 +130,7 @@ Οξύτητα - Document Correction + Διόρθωση Κειμένου Αρχικοποίηση @@ -165,15 +177,48 @@ Αντιστροφή + + Αναστροφή Όλων + + + Αναστροφή Επιλεγμένων + - Ολική Διαγραφή + Εκκαθάριση - Clear All + Εκκαθάριση Όλων Γλώσσα + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + "Save" button default action: + + + Application + + + Only allow a single NAPS2 instance + Σχετικά @@ -186,26 +231,44 @@ Σμίκρυνση + + Πραγματικό Μέγεθος + + + Μέγεθος Βάση Παραθύρου + - Save + Αποθήκευση + + + Αποθήκευση Όλων + + + Αποθήκευση Επιλεγμένων - Save All as PDF + Αποθήκευση Όλων ως PDF - Save Selected as PDF + Αποθήκευση Επιλεγμένων ως PDF - Save All as Images + Αποθήκευση Όλων ως Εικόνες - Save Selected as Images + Αποθήκευση Επιλεγμένων ως Εικόνες + + + Αποστολή Όλων + + + Αποστολή Επιλεγμένων - Email All as PDF + Αποστολή Όλων ως PDF - Email Selected as PDF + Αποστολή Επιλεγμένων ως PDF Ρυθμίσεις Προφίλ @@ -219,11 +282,20 @@ Οδηγός TWAIN + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + - Apple Driver + Οδηγός Apple - SANE Driver + Οδηγός SANE Συσκευή: @@ -267,11 +339,14 @@ Ρυθμίσεις Αυτόματης Αποθήκευσης + + Ρυθμίσεις Αυτόματης Αποθήκευσης + Για προχωρημένους - Άκυρο + Ακύρωση Επιλογή @@ -279,8 +354,11 @@ Επιλογή Προέλευσης + + Select Device + - Run in Background + Εκτέλεση στο Παρασκήνιο NAPS2 @@ -289,7 +367,7 @@ NAPS2 - {0} - Not Another PDF Scanner + Όχι άλλος σαρωτής PDF Διαμόρφωση OCR (Οπτική Αναγνώριση Χαρακτήρων) @@ -301,10 +379,16 @@ Γλώσσα OCR: - OCR mode: + Λειτουργία OCR: + + + Fix white balance and remove noise - Automatically run OCR after scanning + Αυτόματη εκτέλεση OCR μετά την σάρωση + + + Pre-emptively run OCR after scanning Περισσότερες γλώσσες @@ -348,4 +432,457 @@ Επιλογή Όλων + + Ανάκτηση + + + Αργότερα + + + Ανάκτηση Σαρωμένων Εικόνων + + + Υπάρχουν {0} εικόνες που σαρώθηκαν στις {1} και ώρα {2} οι οποίες ενδέχεται να μην έχουν αποθηκευτεί. Θέλετε να ανακτηθούν; + + + Δεσμευτικά Θέσεων (Placeholders) + + + Χωρίς προτροπή αποθήκευσης + + + Αρχεία μιας σελίδας + + + Κρυπτογραφημένο PDF + + + Εμφάνιση + + + Επαναφορά Προεπιλογών + + + Ρυθμίσεις PDF + + + Να Επιτρέπεται Εκτύπωση + + + Να Επιτρέπεται Εκτύπωση Μέγιστης Ποιότητας + + + Να Επιτρέπεται Τροποποίηση Εγγράφου + + + Να Επιτρέπεται Συναρμολόγηση Εγγράφου + + + Να Επιτρέπεται Αντιγραφή Περιεχομένου + + + Να Επιτρέπεται Αντιγραφή Περιεχομένου για Προσβασιμότητα + + + Να Επιτρέπεται Σχολιασμός + + + Να Επιτρέπεται Συμπλήρωση Φορμών + + + Προεπιλεγμένη Διαδρομή Αρχείου: + + + Διαδρομή αρχείου: + + + Τίτλος: + + + Συντάκτης: + + + Θέμα: + + + Λέξεις Κλειδιά: + + + Κωδικός Κατόχου: + + + Κωδικός Χρήστη: + + + Απομνημόνευση αυτών των ρυθμίσεων + + + Μεταδεδομένα (Metadata) + + + Κρυπτογράφηση + + + Συμβατότητα + + + Δεσμευτικά Θέσεων (Placeholders) + + + Έτος + + + Έτος (00-99) + + + Μήνας (01-12) + + + Ημέρα (01-31) + + + Ώρα (0-23) + + + Λεπτά (00-59) + + + Δευτερόλεπτα (00-59) + + + Αυτόματη αρίθμηση (4 ψηφία) + + + Αυτόματη αρίθμηση (3 ψηφία) + + + Αυτόματη αρίθμηση (2 ψηφία) + + + Αυτόματη αρίθμηση (1 ψηφίο) + + + Όνομα Αρχείου: + + + Προεπισκόπηση: + + + Για υψηλές ποιότητες JPEG (80+), η Ποιότητα Εικόνας μπορεί να αυξηθεί από τις ρυθμίσεις προφίλ. + + + Ποιότητα Jpeg + + + Επιλογές Tiff + + + Συμπίεση: + + + Ρυθμίσεις Εικόνας + + + Ρυθμίσεις Email + + + Settings + + + Πάροχος + + + Αλλαγή + + + Όνομα Επισύναψης: + + + Επιλέξτε πάροχο Email + + + Εξουσιοδότηση + + + Αναμονή για εξουσιοδότηση... + + + Σφάλμα + + + Τεχνικές Λεπτομέρειες + + + Κωδικός + + + Το ακόλουθο αρχείο είναι κρυπτογραφημένο και απαιτεί συνθηματικό για να ανοίξει: + + + Προτροπή για διαδρομή αρχείου + + + Ένα αρχείο ανά σελίδα + + + Ένα αρχείο ανά σάρωση + + + Διαχωρισμός αρχείων με Patch-T + + + Περισσότερες πληροφορίες + + + Διαγραφή εικόνων μετά την αποθήκευση + + + Keep images across sessions + + + Προσαρμογή Μεγέθους Σελίδας + + + Όνομα (προαιρετικό) + + + Διαστάσεις + + + Custom Resolution + + + Dpi + + + Αναμονή ολοκλήρωσης TWAIN... + + + Προηγμένες Ρυθμίσεις Προφίλ + + + Ποιότητα Εικόνας + + + Μέγιστη ποιότητα (μεγάλα αρχεία) + + + Κενές Σελίδες + + + Εξαίρεση κενών σελίδων + + + Κατώτατο Όριο Λευκού + + + Κατώτατο Όριο Κάλυψης + + + Μετά την επεξεργασία + + + Διόρθωση ασυμμετρίας σαρωμένων σελίδων + + + Εφαρμογή φωτεινότητας/αντίθεσης μετά την σάρωση + + + Πλάτος απόκλισης βάση στοίχισης (WIA) + + + Επέκταση στο μέγεθος σελίδας + + + Περικοπή στο μέγεθος σελίδας + + + Αναστροφή σελίδων διπλής όψης + + + Flip back sides of duplex pages + + + Έκδοση WIA: + + + Υλοποίηση Twain: + + + Μαζική Σάρωση + + + Σε αναμονή για κλικ στο Έναρξη. + + + Διαμόρφωση Σάρωσης + + + Αποτέλεσμα + + + Προφίλ: + + + Μία σάρωση + + + Πολλαπλές σαρώσεις (προτροπή μεταξύ σαρώσεων) + + + Πολλαπλές σαρώσεις (χρονοκαθυστέρηση μεταξύ σαρώσεων) + + + Αριθμός σαρώσεων: + + + Χρόνος μεταξύ σαρώσεων (δευτερόλεπτα): + + + Φόρτωση εικόνων στο NAPS2 + + + Αποθήκευση σε ένα αρχείο + + + Αποθήκευση σε πολλαπλά αρχεία + + + Έναρξη + + + Επόμενη Σάρωση + + + Σε ετοιμότητα για σάρωση {0}. + + + Άνοιγμα Φακέλου + + + Εργαλεία + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + 1 device found. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.es.resx b/NAPS2.Lib/Lang/Resources/UiStrings.es.resx index a53d578bdf..ccb79fe976 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.es.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.es.resx @@ -16,7 +16,7 @@ Acerca de - Copyright {0} NAPS2 Contributors + Copyright {0} colaboradores NAPS2 Iconos de: @@ -54,6 +54,18 @@ Pegar + + Deshacer + + + Rehacer + + + Deshacer {0} + + + Rehacer {0} + Terminado @@ -67,7 +79,7 @@ Escanear por lotes - Reconocimiento óptico de caracteres (OCR) + OCR Perfiles @@ -118,7 +130,7 @@ Enfoque - Document Correction + Corrección de documento Reiniciar @@ -160,20 +172,53 @@ Entrelazado alternativo - Desentrelazado alternativo + Des entrelazado alternativo Invertir + + Revertir todo + + + Revertir todo + Limpiar - Clear All + Limpiar todo Idioma + + Configuración + + + Interfaz + + + El menú "Escanear" cambia el perfil por defecto + + + Mostrar la barra de herramientas "Perfiles" + + + Mostrar números de página + + + Acción predeterminada del botón "Escanear": + + + Acción predeterminada del botón "Guardar": + + + Aplicación + + + Permitir sólo una sola instancia de NAPS2 + Acerca de @@ -186,26 +231,44 @@ Alejarse + + Zoom actual + + + Escalar con ventana + - Save + Guardar + + + Guardar todo + + + Guardar seleccionado - Save All as PDF + Guardar todo como PDF - Save Selected as PDF + Guardar seleccionado como PDF - Save All as Images + Guardar todo como imágenes - Save Selected as Images + Guardar seleccionado como imágenes + + + Enviar a todos + + + Enviar seleccionado - Email All as PDF + Enviar todo como PDF - Email Selected as PDF + Enviar seleccionado como PDF Configuración de perfil @@ -219,8 +282,17 @@ Controlador TWAIN + + Dispositivo ESCL + + + Dispositivo red ESCL + + + Dispositivo ESCL USB + - Apple Driver + Controlador de Apple Controlador SANE @@ -267,6 +339,9 @@ Configuración de autoguardado + + Configuración de autoguardado + Avanzadas @@ -279,6 +354,9 @@ Seleccione el origen + + Seleccionar dispositivo + Ejecutar en segundo plano @@ -289,7 +367,7 @@ NAPS2 - {0} - Not Another PDF Scanner + No es otro escáner PDF Ajustes de OCR @@ -303,9 +381,15 @@ Modo OCR: + + Corregir el balance de blancos y eliminar el ruido + Ejecutar OCR automáticamente después del escaneo + + Ejecutar OCR automáticamente después de escanear + Obtener más idiomas @@ -348,4 +432,457 @@ Seleccionar todo + + Recuperar + + + Ahora no + + + Recuperar imágenes digitalizadas + + + {0} imagen(es) escaneada(s) el {1} a las {2} quizás no se ha(n) guardado y se puede(n) recuperar. ¿Deseas recuperarla(s)? + + + Marcadores de posición + + + Omitir aviso de guardado + + + Archivos de una única página + + + Cifrar PDF + + + Mostrar + + + Restaurar los valores predeterminados + + + Configuración de PDF + + + Permitir impresión + + + Permitir impresión de calidad total + + + Permitir modificación del documento + + + Permitir montaje del documento + + + Permitir copia de contenido + + + Permitir copia de contenido para accesibilidad + + + Permitir anotaciones + + + Permitir rellenar el formulario + + + Ruta predeterminada: + + + Ruta del archivo: + + + Título: + + + Autor: + + + Sujeto: + + + Palabras clave: + + + Contraseña de propietario: + + + Contraseña de usuario: + + + Recuerde esta configuración + + + Metadatos + + + Cifrado + + + Compatibilidad + + + Marcadores de posición + + + Año + + + Año (00-99) + + + Mes (01-12) + + + Día (01-31) + + + Hora (0-23) + + + Minuto (00-59) + + + Segundo (00-59) + + + Número autoincremental (4 dígitos) + + + Número autoincremental (3 dígitos) + + + Número autoincremental (2 dígitos) + + + Número autoincremental (1 dígitos) + + + Nombre de archivo: + + + Vista previa: + + + Para calidades JPEG altas (más de 80), incremente también la calidad de imagen en su perfil para obtener los mejores resultados. + + + Calidad Jpeg + + + Opciones Tiff + + + Compresión: + + + Configuración de la imagen + + + Configuración del correo electrónico + + + Configuración + + + Proveedor + + + Cambiar + + + Nombre del archivo adjunto: + + + Elegir proveedor de correo electrónico + + + Autorizar + + + Esperando la autorización... + + + Error + + + Detalles técnicos + + + Contraseña + + + El archivo siguiente está encriptado y requiere una contraseña para abrir: + + + Preguntar ruta de archivo + + + Un archivo por página + + + Un archivo por escaneo + + + Separar archivos usando Patch-T + + + Más información + + + Borrar las imágenes después de guardar + + + Mantener las imágenes entre sesiones + + + Tamaño de página personalizado + + + Nombre (opcional) + + + Dimensiones + + + Resolución predeterminada + + + PPP + + + Esperando que TWAIN finalice... + + + Configuración de perfil avanzada + + + Calidad de imagen + + + Máxima calidad (ficheros grandes) + + + Páginas en blanco + + + Excluir las páginas en blanco + + + Umbral de blanco + + + Umbral de cobertura + + + Posprocesamiento + + + Corregir sesgo de las páginas escaneadas + + + Aplicar el brillo/contraste tras el escaneo + + + Ancho de desplazamiento basado en alineación (WIA) + + + Estirar hasta tamaño de página + + + Recortar a tamaño de página + + + Voltear las páginas a doble cara + + + Voltea el reverso de las páginas dúplex + + + Versión Wia: + + + Implementación Twain: + + + Escanear por lotes + + + Cuando esté listo pulse Iniciar. + + + Configuración del escaneo + + + Destino + + + Perfil: + + + Escaneo sencillo + + + Múltiples escaneos (preguntar entre escaneos) + + + Múltiples escaneos (pausa fija entre escaneos) + + + Número de escaneos: + + + Tiempo entre escaneos (segundos): + + + Cargar imágenes en NAPS2 + + + Guardar en un único archivo + + + Guardar en múltiples archivos + + + Iniciar + + + Siguiente escaneo + + + Listo para el escaneo {0}. + + + Abrir carpeta + + + Herramientas + + + Activar registro de depuración + + + Compartir + + + Compartir escáner + + + Compartir escáner + + + Los escáneres compartidos se pueden utilizar desde otros ordenadores de la red local seleccionando "ESCL Driver" en la configuración del perfil NAPS2 del resto de equipos. + + + Configuración de Escáner Compartido + + + ¿Estás seguro de que quieres dejar de compartir {0}? + + + Compartir incluso cuando NAPS2 esté cerrado + + + Varios idiomas... + + + Varios idiomas + + + Mostrar progreso nativo de TWAIN + + + Dividir + + + Combinar + + + IP manual + + + IP manual + + + IP/Host + + + Port + + + Conectar + + + Error de conexión. + + + Preguntar siempre + + + Buscando dispositivo... + + + {0} Dispositivos encontrados. + + + 1 dispositivo encontrado. + + + No se han encontrado dispositivos. + + + Barra lateral + + + ¿No encuentras tu escáner? Lee las limitaciones del Flatpak NAPS2. + + + Atajos del teclado + + + Atajos del teclado + + + Escanear con perfil {0} + + + Escanear con perfil predeterminado + + + Escanear con perfil nuevo + + + Asignar + + + Sin asignar + + + Action + + + Atajo + + + Tema: + + + Editar con... + + + Editar con {0} + + + Editar con... + + + Error al iniciar la aplicación {0} + + + Detener escáner compartido + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.et.resx b/NAPS2.Lib/Lang/Resources/UiStrings.et.resx index a6b5262ba2..57f9276414 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.et.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.et.resx @@ -54,6 +54,18 @@ Aseta + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + Valmis @@ -165,6 +177,12 @@ Reverse + + Reverse All + + + Reverse Selected + Puhasta @@ -174,6 +192,33 @@ Keel + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + "Save" button default action: + + + Application + + + Only allow a single NAPS2 instance + Meist @@ -186,9 +231,21 @@ Zoom Out + + Zoom Actual + + + Scale With Window + Save + + Save All + + + Save Selected + Save All as PDF @@ -201,6 +258,12 @@ Save Selected as Images + + Email All + + + Email Selected + Email All as PDF @@ -219,6 +282,15 @@ TWAIN draiver + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -267,6 +339,9 @@ Automaatselt salvestamise seaded + + Automaatselt salvestamise seaded + Profiili lisavalikud @@ -279,6 +354,9 @@ Vali allikas + + Select Device + Käivita taustal @@ -303,9 +381,15 @@ OCR mode: + + Fix white balance and remove noise + Automatically run OCR after scanning + + Pre-emptively run OCR after scanning + Hankige rohkem keeli @@ -348,4 +432,457 @@ Vali kõik + + Taasta + + + Mitte praegu + + + Taasta skännitud pildid + + + {0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them? + + + Kohahoidjad + + + Skip save prompt + + + Ühelehelised failid + + + Krüpti PDF + + + Näita + + + Taasta algsed sätted + + + PDF seaded + + + Luba printimine + + + Luba täiskvaliteetne printimine + + + Luba dokumendi muutmine + + + Allow Document Assembly + + + Luba sisu kopeerimine + + + Allow Content Copying for Accessibility + + + Luba märkused + + + Luba vormi täitmine + + + Vaikimisi faili asukoht: + + + Faili asukoht: + + + Pealkiri: + + + Autor: + + + Teema: + + + Märksõnad: + + + Omaniku parool: + + + Kasutaja parool: + + + Mälesta neid seadeid + + + Metaandmed + + + Krüpteering + + + Ühilduvus + + + Kohahoidjad + + + Aasta + + + Aasta (00-99) + + + Kuu (01-12) + + + Päev (01-31) + + + Tund (0-23) + + + Minutid (00-59) + + + Sekund (00-59) + + + Auto-incrementing number (4 digits) + + + Auto-incrementing number (3 digits) + + + Auto-incrementing number (2 digits) + + + Auto-incrementing number (1 digits) + + + Faili nimi + + + Eelvaade: + + + For high JPEG qualities (80+), also increase Image Quality in your profile for best results. + + + Jpeg Quality + + + Tiff Options + + + Tihendus: + + + Pildi seadistused + + + E-posti sätted + + + Settings + + + Pakkuja + + + Muuda + + + Manuse nimi: + + + Valige e-posti pakkuja + + + Autoriseeri + + + Waiting for authorization... + + + Viga + + + Tehnilised detailid + + + Parool + + + The following file is encrypted and requires a password to open: + + + Prompt for file path + + + Üks fail lehe kohta + + + Üks fail skaneerimise kohta + + + Separate files by Patch-T + + + Rohkem infot + + + Clear images after saving + + + Keep images across sessions + + + Custom Page Size + + + Nimi (valikuline) + + + Dimensions + + + Custom Resolution + + + Dpi + + + Waiting for TWAIN to complete... + + + Profiili lisavalikud + + + Pildi kvaliteet + + + Maximum quality (large files) + + + Tühjad lehed + + + Tühjade lehtede eemaldamine + + + White Threshold + + + Coverage Threshold + + + Post-processing + + + Deskew scanned pages + + + Rakendage heledust / kontrasti pärast skaneerimist + + + Offset width based on alignment (WIA) + + + Venita lehe suuruseni + + + Crop to page size + + + Flip duplexed pages + + + Flip back sides of duplex pages + + + Wia Version: + + + Twain Implementation: + + + Batch Scan + + + Kui olete valmis, vajutage nuppu Start. + + + Skaneerimise seaded + + + Väljund + + + Profiilid: + + + Ühekordne skaneerimine + + + Multiple scans (prompt between scans) + + + Multiple scans (fixed delay between scans) + + + Skaneerimiste arv: + + + Skaneerimiste vaheline aeg (sekundid): + + + Load images into NAPS2 + + + Salvesta ühte faili + + + Salvesta mitmesse faili + + + Start + + + Uus skaneerimine + + + Ready for scan {0}. + + + Ava kaust + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + 1 device found. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.fa.resx b/NAPS2.Lib/Lang/Resources/UiStrings.fa.resx index bd30cf1262..0702bc78d9 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.fa.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.fa.resx @@ -54,6 +54,18 @@ چسباندن + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + انجام شد @@ -165,6 +177,12 @@ معکوس + + Reverse All + + + Reverse Selected + پاک‌کردن @@ -174,6 +192,33 @@ زبان + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + ذخیره: + + + Application + + + Only allow a single NAPS2 instance + درباره @@ -186,9 +231,21 @@ زوم به خارج + + اندازه واقعی + + + بزرگنمایی اندازه پنجره + Save + + Save All + + + Save Selected + Save All as PDF @@ -201,6 +258,12 @@ Save Selected as Images + + Email All + + + Email Selected + Email All as PDF @@ -219,6 +282,15 @@ درایور TWAIN + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -267,6 +339,9 @@ ذخیره خودکار تنظیمات‌ + + ذخیره خودکار تنظیمات‌ + پیشرفته @@ -279,6 +354,9 @@ انتخاب منبع + + Select Device + اجرا در پس‌زمینه @@ -303,9 +381,15 @@ حالت OCR: + + Fix white balance and remove noise + پس از اسکن OCR بصورت خودکار اجرا شود. + + Pre-emptively run OCR after scanning + دریافت زبان‌های بیشتر @@ -348,4 +432,457 @@ انتخاب همه + + بازیابی + + + الان نه + + + در حال بازیابی + + + {0} تصویر اسکن شده از {1} روی {2} ذخیره نشده‌اند و قابل بازیابی هستندُ آیا مایل به بازیابی آنها هستید؟ + + + جانگهدار + + + اعلان ذخیره‌سازی را نادیده بگیر + + + پرونده‌های تک صفحه‌ای + + + رمزگذاری PDF + + + نمایش + + + تنظيمات اوليه + + + تنظیمات پی‌دی‌اف + + + اجازه چاپ + + + اجازه چاپ با کیفیت کامل + + + اجازه تغییر سند + + + اجازه مونتاژ سند + + + اجازه کپی محتوا + + + اجازه کپی محتوا برای دستیابی پذیری + + + اجازه یادداشت + + + اجازه پرکردن فرم + + + مسیر پیش‌فرض: + + + مسیر پرونده: + + + عنوان: + + + مؤلف: + + + موضوع: + + + کلیدواژه‌ها: + + + گذرواژه مالک: + + + گذرواژه کاربر: + + + این تنظیمات را بخاطر بسپار + + + اَبَرداده + + + رمزگذاری + + + سازگاری + + + جانگهدار + + + سال + + + سال (۹۹-۰۰) + + + ماه (۱۲-۰۱) + + + روز (۳۱-۰) + + + ساعت (۰-۲۳) + + + دقیقه (۵۹-۰۰) + + + ثانیه (۰۰-۵۹) + + + افزایش خودکار شماره ( ۴ رقم) + + + افزایش خودکار شماره ( ۳ رقم) + + + افزایش خودکار شماره ( ۲ رقم) + + + Auto-incrementing number (1 digits) + + + نام پرونده + + + پیش‌نمایش: + + + برای JPEG کیفیت بالا (۸۰+)، همچنین افزایش کیفیت تصویر پروفایل شما با بهترین نتیجه. + + + کیفیت Jpeg + + + گزینه‌های Tiff + + + فشردگی: + + + تنظیمات تصویر + + + تنظیمات رایانامه + + + Settings + + + ارائه‌دهنده + + + تغییر + + + نام های ضمیمه: + + + یک پست الکترونیکی انتخاب کنید + + + اعطای اجازه + + + انتظار برای اجازه... + + + خطا + + + جزئیات فنی + + + گذرواژه + + + The following file is encrypted and requires a password to open: + + + اعلان برای مسیر فایل + + + یک فایل به ازای هر صفحه + + + یک فایل به ازای هر اسکن + + + جداسازی پرونده‌ها با Patch-T + + + اطلاعات بیشتر + + + پاک کردن تصویر پس از ذخیره سازی + + + Keep images across sessions + + + اندازه صفحه سفارشی + + + نام (اختیاری) + + + ابعاد + + + Custom Resolution + + + Dpi + + + منتظر اتمام کار TWAIN... + + + تنظیمات پیشرفته پروفایل + + + کیفیت تصویر + + + حداکثر کیفیت (پرونده‌های بزرگ) + + + صفحات خالی + + + جلوگیری از صفحات خالی + + + آستانه سفید + + + آستانه پوشش + + + پس پردازش + + + مرتب‌سازی صفحات اسکن شده + + + اعمال روشنایی و درخشش پس از اسکن + + + عرض Offset براساس چینش (WIA) + + + کشیدن به اندازه صفحه + + + برش به اندازه صفحه + + + صفحات دورو را قرینه کن + + + Flip back sides of duplex pages + + + Wia Version: + + + پیاده‌سازی Twain: + + + اسکن دسته‌ای + + + وقتی آماده شدید، شروع را فشار دهید. + + + پیکربندی اسکن + + + خروجی + + + پروفایل: + + + تک اسکن + + + اسکن چندتایی (اعلان بین اسکن‌ها) + + + اسکن چندتایی (تاخیر ثابت بین اسکن‌ها) + + + تعداد اسکن‌ها: + + + زمان بین اسکن‌ها (ثانیه): + + + بارگذاری تصویر در NAPS2 + + + ذخیره در یک پرونده + + + ذخیره در چند پرونده + + + شروع + + + اسکن بعدی + + + آماده برای اسکن {0}. + + + بازکردن پوشه + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + 1 device found. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.fi.resx b/NAPS2.Lib/Lang/Resources/UiStrings.fi.resx index adb744e04d..e039b45f3f 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.fi.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.fi.resx @@ -16,7 +16,7 @@ Tietoja - Copyright {0} NAPS2 Contributors + Tekijänoikeudet {0} NAPS2 Avustajat Käytettyjen kuvakkeiden lähde: @@ -54,6 +54,18 @@ Liitä + + Kumoa + + + Tee uudelleen + + + Kumoa {0} + + + Tee Uudelleen {0} + Valmis @@ -118,7 +130,7 @@ Terävöitä - Document Correction + Dokumentin Korjaus Palauta @@ -165,15 +177,48 @@ Taaksepäin + + Vaihda kaikkien arvot + + + Vaihda valittujen arvot + Tyhjennä - Clear All + Poista kaikki Kieli + + Asetukset + + + Käyttöliittymä + + + "Skannaa" valikko muuttaa oletusprofiilia + + + Näytä "Profiilit" työkalupalkki + + + Näytä sivunnumerot + + + "Skannaa" painikkeen oletustoiminto: + + + "Tallenna" painikkeen oletustoiminto: + + + Sovellus + + + Salli vain yksi NAPS2-instanssi + Tietoja @@ -186,26 +231,44 @@ Pienennä + + Alkuperäiseen kokoon + + + Skaalaa ikkunan mukaan + - Save + Tallenna + + + Tallenna kaikki + + + Tallenna valitut - Save All as PDF + Tallenna kaikki PDF-muodossa - Save Selected as PDF + Tallenna valitut PDF-muodossa - Save All as Images + Tallenna kaikki kuvina - Save Selected as Images + Tallenna valitut kuvina + + + Lähetä kaikki sähköpostitse + + + Lähetä valittu sähköpostilla - Email All as PDF + Lähetä kaikki sähköpostilla PDF-muodossa - Email Selected as PDF + Lähetä valittu PDF-tiedostona Profiiliasetukset @@ -219,8 +282,17 @@ TWAIN-ajuri + + ESCL- Ajuri + + + ESCL Verkkoajuri + + + ESCL USB-ajuri + - Apple Driver + Applen Ajuri SANE - ajuri @@ -267,6 +339,9 @@ Automaattisen tallennuksen asetukset + + Automaattisen tallennuksen asetukset + Lisäasetukset @@ -279,6 +354,9 @@ Valitse mistä + + Valitse laite + Aja taustalla @@ -303,9 +381,15 @@ OCR muoto: + + Korjaa valkotasapaino ja poista kohinaa + Tunnista teksti optisesti automaattisesti skannauksen jälkeen + + Pre-emptively run OCR after scanning + Hae lisää kieliä @@ -348,4 +432,457 @@ Valitse kaikki + + Palauta + + + Ei nyt + + + Palauta skannatut kuvat + + + {0} kuva(a) skannattu {1} {2} ei ehkä ole tallennettu ja voidaan palauttaa. Haluatko palauttaa? + + + Paikkamerkit + + + Ohita talleta - kysymys + + + Yhden sivun tiedostot + + + Salaa PDF + + + Näytä + + + Palauta oletusarvoihin + + + PDF - asetukset + + + Salli tulostaminen + + + Salli korkealaatuinen tulostus + + + Salli asiakirjan muokkaus + + + Salli asiakirjan kokoaminen + + + Salli sisällön kopiointi + + + Salli sisällön kopioiminen helppokäyttötoimintoja varten + + + Salli kommentit + + + Salli lomakkeiden täyttäminen + + + Oletuspolku: + + + Tiedostopolku: + + + Otsikko: + + + Tekijä: + + + Aihe: + + + Avainsanat: + + + Omistajan salasana: + + + Käyttäjän salasana: + + + Muista asetukset + + + Metatiedot + + + Salaus + + + Yhteensopivuus + + + Paikkamerkit + + + Vuosi + + + Vuosi (00-99) + + + Kuukausi (01-12) + + + Päivä (01-31) + + + Tunti (0-23) + + + Minuutti (00-59) + + + Sekunti (00-59) + + + Automaattinen lisäys (4 numeroa) + + + Automaattinen lisäys (3 numeroa) + + + Automaattinen lisäys (2 numeroa) + + + Automaattinen lisäys (1 numero) + + + Tiedoston nimi + + + Esikatsele: + + + Lisää myös kuvan laatua profiilissasi laadukkaita JPEG (80+) kuvia varten. + + + Jpeg - laatu + + + Tiff - vaihtoehdot + + + Pakkaus: + + + Kuva-asetukset + + + Sähköpostiasetukset + + + Asetukset + + + Palveluntarjoaja + + + Muuta + + + Liitteen nimi: + + + Valitse sähköpostin lähetystapa + + + Valtuuta + + + Odotetaan valtuutusta... + + + Virhe + + + Tekniset yksityiskohdat + + + Salasana + + + Seuraava tiedosto on salattu ja tarvitsee salasanan avaukseen: + + + Kysy tiedostopolkua + + + Tiedosto joka sivusta + + + Skannauksesta yksi tiedosto + + + Erota tiedostot Patch-T - koodilla + + + Lisätietoja + + + Pyyhi kuvat tallennukset jälkeen + + + Säilytä kuvat eri istunnoissa + + + Mukautettu sivun koko + + + Nimi (valinnainen) + + + Mittasuhteet + + + Custom Resolution + + + Dpi + + + Odotetaan TWAINin valmistumista... + + + Profiilin lisäasetukset + + + Kuvan laatu + + + Korkein laatu (suuret tiedostot) + + + Tyhjät sivut + + + Ohita tyhjät sivut + + + Valkoisen raja-arvo + + + Peitto + + + Jälkikäsittely + + + Suorista skannatut sivut + + + Käytä kirkkautta/kontrastia skannauksen jälkeen + + + Siirtymän suurus kohdistuksen perusteella (WIA) + + + Venytä sivun kokoon + + + Rajaa sivun kokoon + + + Käännä kaksipuoleiset sivut + + + Flip back sides of duplex pages + + + Wia-versio: + + + Twain - toteutus: + + + Sarjaskannaus + + + Paina Aloita, kun olet vlamis. + + + Skannausasetukset + + + Tulos + + + Profiilit: + + + Yksittäisskannaus + + + Monta skannausta (kysy skannausten välillä) + + + Monta skannausta (vakioviive skannausten välillä) + + + Skannauksia: + + + Aika skannausten välillä (sekuntia): + + + Lataa kuvia NAPS2:een + + + Tallenna yhteen tiedostoon + + + Tallenna useaan tiedostoon + + + Aloita + + + Seuraava skannaus + + + Valmis skannaamaan {0}. + + + Avaa kansio + + + Työkalut + + + Ota virheenkorjausloki käyttöön + + + Jaa + + + Skannerin Jakaminen + + + Skannerin Jakaminen + + + Jaettuja skannereita voidaan käyttää muista paikallisessa verkossa olevista tietokoneista valitsemalla "ESCL Driver" toisen tietokoneen NAPS2-profiiliasetuksista. + + + Jaetut Skannerin Asetukset + + + Haluatko varmasti lopettaa jakamisen {0}? + + + Jaa vaikka NAPS2 olisi suljettu + + + Useita kieliä... + + + Useita kieliä + + + Show native TWAIN progress + + + Jaa + + + Yhdistä + + + Manuaalinen IP + + + Manuaalinen IP + + + IP/Isäntä + + + Portti + + + Yhdistä + + + Yhteysvirhe. + + + Kysy aina + + + Etsitään laitteita... + + + {0} laitetta löytyi. + + + 1 laite löytyi. + + + Laitteita ei löytynyt. + + + Sidebar + + + Skanneria ei löytynyt? Lue NAPS2 Flatpakin rajoituksista. + + + Pikanäppäimet + + + Pikanäppäimet + + + Skannaa Profiililla {0} + + + Skannaa Oletusprofiililla + + + Skannaa Uudella Profiili + + + Määritä + + + Poista määritys + + + Toiminto + + + Pikavalinta + + + Theme: + + + Muokkaa sovelluksella... + + + Muokkaa sovelluksella {0} + + + Muokkaa sovelluksella... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.fr.resx b/NAPS2.Lib/Lang/Resources/UiStrings.fr.resx index 12c4a57d74..6acbe3e57d 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.fr.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.fr.resx @@ -16,10 +16,10 @@ À propos - Copyright {0} NAPS2 Contributors + Copyright {0} Contributeurs NAPS2 - Icônes provenant de: + Icônes provenant de : Vérifier les mises à jour @@ -28,7 +28,7 @@ OK - Don + Faire un don Profils @@ -54,6 +54,18 @@ Coller + + Annuler + + + Rétablir + + + Annuler {0} + + + Rétablir {0} + Fermer @@ -115,10 +127,10 @@ Noir et blanc - Accentuation + Améliorer la netteté - Document Correction + Correction de document Réinitialiser @@ -157,23 +169,56 @@ Désappairer [142536>123456] - Appairer alternative. [135642>123456] + Appairer alternativement [135642>123456] - Désappairer alternative. [162534>123456] + Désappairer alternativement [162534>123456] Inverser + + Tout inverser + + + Inverser la sélection + - Tout supprimer + Tout effacer - Clear All + Tout effacer Langue + + Paramètres + + + Interface + + + Modifier le profil par défaut avec le menu « Numériser » + + + Afficher la barre d'outils « Profils » + + + Afficher les numéros de page + + + Action par défaut du bouton « Numériser » : + + + Action par défaut du bouton « Enregistrer » : + + + Application + + + N'autoriser qu'une seule instance de NAPS2 + À propos @@ -186,32 +231,50 @@ Zoom arrière + + Zoom courant + + + Mettre à l'échelle de la fenêtre + - Save + Enregistrer + + + Tout enregistrer + + + Enregistrer la sélection - Save All as PDF + Tout enregistrer en PDF - Save Selected as PDF + Enregistrer la sélection en PDF - Save All as Images + Tout enregistrer en images - Save Selected as Images + Enregistrer la sélection en images + + + Courriel à tous + + + Courriel sélectionné - Email All as PDF + Tout envoyer en PDF par courriel - Email Selected as PDF + Envoyer la sélection en PDF par courriel Paramètres de profil - Nom d'affichage: + Nom d'affichage : Pilote WIA @@ -219,14 +282,23 @@ Pilote TWAIN + + Pilote eSCL + + + Pilote réseau eSCL + + + Pilote USB eSCL + - Apple Driver + Pilote Apple Pilote SANE - Périphérique: + Périphérique Sélectionner @@ -238,28 +310,28 @@ Interface native - Source du papier: + Source du papier : - Taille de la page: + Taille de la page : - Résolution: + Résolution : - Luminosité: + Luminosité : - Profondeur d'échantillonnage: + Profondeur d'échantillonnage : - Alignement horizontal: + Alignement horizontal : - Échelle: + Échelle : - Contraste: + Contraste : Activer l'enregistrement automatique @@ -267,8 +339,11 @@ Paramètres d'enregistrement automatique + + Paramètres d'enregistrement automatique + - Avancés + Avancés… Annuler @@ -279,6 +354,9 @@ Sélectionner une source + + Choisir un périphérique + Exécuter en tâche de fond @@ -298,14 +376,20 @@ Rendre possible la recherche dans les PDF issus d'OCR - Langue OCR: + Langue de l'OCR : - Mode de l'OCR: + Mode de l'OCR : + + + Corriger la balance des blancs et supprimer le bruit Exécuter automatiquement l'OCR après la numérisation + + Exécuter l'OCR immédiatement après la numérisation + Obtenir plus de langues @@ -313,10 +397,10 @@ Téléchargement OCR (reconnaissance optique de caractères) - Télécharger chaque langue OCR appropriée au document numérisé. + Télécharger chaque langue OCR appropriée aux documents numérisés. - Sélectionner une ou plusieurs langues: + Sélectionner une ou plusieurs langues : Taille estimée du téléchargement : {0} Mo @@ -348,4 +432,457 @@ Tout sélectionner + + Récupérer + + + Pas maintenant + + + Récupérer des images numérisées + + + {0} image(s) numérisée(s) le {1} à {2} n'a(ont) peut-être pas été enregistrée(s), et est(sont) récupérable(s). Faut-il procéder à leur récupération ? + + + Chaînes de substitution + + + Ignorer l'invite d'enregistrement + + + Fichiers à page unique + + + Chiffrer le PDF + + + Afficher + + + Valeurs par défaut + + + Paramètres PDF + + + Permettre l'impression + + + Permettre l'impression en qualité maximale + + + Permettre la modification des documents + + + Permettre l'assemblage des documents + + + Permettre la copie du contenu + + + Permettre la copie du contenu pour l'accessibilité + + + Permettre les annotations + + + Permettre l'édition de formulaires + + + Chemin de fichier par défaut : + + + Chemin du fichier : + + + Titre : + + + Auteur : + + + Sujet : + + + Mots-clés : + + + Mot de passe du propriétaire : + + + Mot de passe utilisateur : + + + Mémoriser ces paramètres + + + Métadonnées + + + Chiffrement + + + Compatibilité + + + Chaînes de substitution + + + Année + + + Année (00-99) + + + Mois (01-12) + + + Jour (01-31) + + + Heure (0-23) + + + Minute (00-59) + + + Seconde (00-59) + + + Numéro auto-incrémenté (4 chiffres) + + + Numéro auto-incrémenté (3 chiffres) + + + Numéro auto-incrémenté (2 chiffres) + + + Numéro auto-incrémenté (1 chiffre) + + + Nom du fichier : + + + Aperçu : + + + En JPEG haute qualité (80+), augmenter aussi la Qualité d'Image dans le profil pour de meilleurs résultats. + + + Qualité JPEG + + + Options TIFF + + + Compression : + + + Paramètres d'image + + + Paramètres de messagerie + + + Paramètres + + + Fournisseur + + + Modifier + + + Nom de la pièce jointe : + + + Choisir un fournisseur de messagerie + + + Autoriser + + + En attente d'autorisation... + + + Erreur + + + Détails techniques + + + Mot de passe + + + Le fichier suivant est chiffré et son ouverture nécessite un mot de passe : + + + Demander un chemin pour le fichier + + + Un fichier par page + + + Un fichier par numérisation + + + Délimitation des fichiers par Patch-T + + + Plus d'informations + + + Effacer les images après l'enregistrement + + + Conserver les images d'une session à l'autre + + + Taille de page personnalisée + + + Nom (optionnel) + + + Dimensions + + + Résolution personnalisée + + + PPP + + + En attente de la fin du transfert TWAIN… + + + Paramètres de profil avancés + + + Qualité d'image + + + Qualité maximale (fichiers volumineux) + + + Pages blanches + + + Exclure les pages blanches + + + Seuil du blanc + + + Seuil de couverture + + + Post-traitement + + + Réaligner les pages numérisées + + + Appliquer la luminosité/contraste après acquisition + + + Largeur de décalage basée sur l'alignement (WIA) + + + Étirer aux dimensions de la page + + + Recadrer aux dimensions de la page + + + Retourner les pages recto-verso + + + Retourner les pages recto-verso + + + Version WIA : + + + Implémentation TWAIN : + + + Acquisition par lot + + + Presser « Démarrer » une fois prêt. + + + Configuration d'acquisition + + + Sortie + + + Profil : + + + Acquisition unique + + + Acquisitions multiples (demande à chaque numérisation) + + + Acquisitions multiples (délai fixe entre les numérisations) + + + Nombre de numérisations : + + + Délai entre 2 numérisations (secondes) : + + + Charger les images dans NAPS2 + + + Enregistrer en un fichier multipages unique + + + Enregistrer en fichiers uniques + + + Démarrer + + + Prochaine acquisition + + + Prêt à numériser {0}. + + + Ouvrir un dossier + + + Outils + + + Activer le journal de débogage + + + Partager + + + Partage de scanner + + + Partage de scanner + + + Les scanners partagés peuvent être utilisés à partir d'autres ordinateurs du réseau local en sélectionnant « Pilote eSCL » dans les paramètres de profil NAPS2 de l'autre ordinateur. + + + Paramètres de partage de scanner + + + Faut-il vraiment arrêter de partager « {0} » ? + + + Partager même lorsque NAPS2 est fermé + + + Multilingue… + + + Multilingue + + + Afficher la progression native TWAIN + + + Scinder + + + Combiner + + + IP manuelle + + + IP manuelle + + + IP/Hôte + + + Port + + + Connecter + + + Erreur de connexion. + + + Toujours demander + + + Recherche de périphériques… + + + {0} périphériques trouvés. + + + Un périphérique trouvé. + + + Aucun périphérique trouvé. + + + Barre latérale + + + Scanner introuvable ? Afficher les limites du Flatpak NAPS2. + + + Raccourcis clavier + + + Raccourcis clavier + + + Numériser avec le profil {0} + + + Numériser avec le profil par défaut + + + Numériser avec un nouveau profil + + + Affecter + + + Désaffecter + + + Action + + + Raccourci + + + Thème : + + + Modifier avec... + + + Modifier avec {0} + + + Modifier avec... + + + Erreur lors du démarrage de l’application {0} + + + Arrêt du partage du scanner + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.he.resx b/NAPS2.Lib/Lang/Resources/UiStrings.he.resx index 163d7fcef6..b97aaecc24 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.he.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.he.resx @@ -13,22 +13,22 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - אודות + על אודות - Copyright {0} NAPS2 Contributors + כל הזכויות שמורות {0} לתורמים של לא עוד סתם סורק 2 - צלמיות מתוך: + סמלים מתוך: - בדוק עדכונים + איתור עדכונים אישור - תרום + תרומה פרופילים @@ -46,13 +46,25 @@ סריקה - הגדר ברירת מחדל + הגדרת ברירת מחדל העתקה - הדבק + הדבקה + + + הסגה + + + ביצוע מחדש + + + הסגת {0} + + + ביצוע {0} מחדש אישור @@ -64,10 +76,10 @@ פרופיל חדש - סריקה באצווה + סריקה במרוכז - OCR + זיהוי תווים אופטי פרופילים @@ -88,10 +100,10 @@ הגדרות תמונה - שליחת PDF בדוא\"ל + שליחת PDF בדוא״ל - הגדרות דוא\"ל + הגדרות דוא״ל הדפסה @@ -100,31 +112,31 @@ תמונה - הצג + הצגה חיתוך - Brightness / Contrast + בהירות / ניגודיות - Hue / Saturation + גוון / רוויה שחור/לבן - Sharpen + חידוד - Document Correction + תיקון מסמך - אתחל + איפוס - סובב + סיבוב סיבוב לשמאל @@ -136,7 +148,7 @@ היפוך - Deskew + תיקון הטייה סיבוב מותאם אישית @@ -148,13 +160,13 @@ להזיז למטה - סדר מחדש + סידור מחדש הפרדה - בטל הפרדה + ביטול הפרדה הפרדה מסורג @@ -163,49 +175,100 @@ ביטול הפרדה מסורג - הפוך + היפוך + + + היפוך של הכול + + + היפוך הנבחרים - מחק הכל + פינוי - Clear All + לפנות הכול שפה + + הגדרות + + + מנשק + + + תפריט „סריקה” משנה את פרופיל ברירת המחדל + + + הצגת סרגל כלים „פרופילים” + + + הצגת מספרי עמודים + + + פעולת ברירת מחדל לכפתור „סריקה”: + + + פעולת ברירת מחדל לכפתור „שמירה”: + + + יישום + + + לאפשר רק עותק יחיד של NAPS2 + - אודות + על אודות - זום + תקריב - זום פנימה + התקרבות - זום החוצה + התרחקות + + + תקריב מקורי + + + התאמת גודל עם החלון - Save + שמירה + + + שמירה של הכול + + + שמירת הנבחרים - Save All as PDF + שמירה של הכול כ־PDF - Save Selected as PDF + שמירת הנבחרים כ־PDF - Save All as Images + שמירה של הכול כתמונות - Save Selected as Images + שמירת הנבחרים כתמונות + + + שליחה של הכול בדוא״ל + + + שליחת הנבחרים בדוא״ל - Email All as PDF + שליחה של הכול כ־PDF - Email Selected as PDF + שליחת הנבחרים כ־PDF הגדרות פרופיל @@ -219,23 +282,32 @@ מנהל התקן TWAIN + + מנהל התקן ESCL + + + מנהל התקן רשת מסוג ESCL + + + מנהל התקן USB מסוג ESCL + - Apple Driver + מנהל התקן של Apple - SANE Driver + מנהל התקן SANE התקן: - בחר התקן + בחירת התקן - השתמש בהגדרות שמורות + שימוש בהגדרות שמורות - השתמש בממשק מקורי (נטיבי) + שימוש בממשק מקורי (טבעי) מקור נייר: @@ -262,11 +334,14 @@ ניגודיות: - הפעל שמירה אוטומטית + הפעלת שמירה אוטומטית הגדרות שמירה אוטומטית + + הגדרות שמירה אוטומטית + מתקדם @@ -274,52 +349,61 @@ ביטול - Select + בחירה - בחר מקור + בחירת מקור + + + בחירת מכשיר - Run in Background + הרצה ברקע - NAPS2 + לא עוד סתם סורק 2 - NAPS2 - {0} + לא עוד סתם סורק 2 - {0} - Not Another PDF Scanner + לא עוד סורק PDF - הגדרות OCR + הגדרות זיהוי תווים אופטי - הכן קובצי PDF הניתנים לחיפוש באמצעות OCR + הפקת קובצי PDF שאפשר לחפש בהם עם זיהוי תווים אופטי - שפת OCR: + שפת זיהוי תווים אופטי: - מצב OCR: + מצב זיהוי תווים אופטי: + + + תיקון איזון לבן והסרת רעש - הרץ OCR בצורת אוטומטית בסוף הסריקה + הרצת זיהוי תווים אופטי אוטומטית בסוף הסריקה + + + הרצת זיהוי תווים אופטי מקדים בסוף הסריקה - קבל שפות נוספות + משיכת שפות נוספות - הורדת OCR + הורדת זיהוי תווים אופטי השימוש ב-OCR דורש הורדה של נתוני השפה במסמך הנסרק. - בחר שפה אחת או יותר: + נא לבחור שפה אחת או יותר: - גודל מוערך להורדה: {0} מ\"ב + גודל מוערך להורדה: {0} מ״ב הורדה @@ -340,12 +424,465 @@ {0} מתוך {1} - בטל שינויים + ביטול שינויים - החל על כל {0} התמונות שנבחרו + החלה על כל {0} התמונות שנבחרו - בחר הכל + בחירה בהכול + + + שחזור + + + לא כעת + + + שחזור תמונות שנסרקו + + + {0} תמונה/תמונות שנסרקו ב-{1} ב-{2} לא נשמרו, אך ניתן לשחזר אותן. האם לשחזר אותן? + + + ממלאי מקום + + + דילוג על בקשת שמירה + + + קבצים של עמוד אחד + + + הצפנת PDF + + + הצגה + + + שחזור ברירות מחדל + + + הגדרות PDF + + + לאפשר הדפסה + + + לאפשר הדפסה באיכות מירבית + + + לאפשר שינויים במסמך + + + לאפשר הרכבת מסמך + + + לאפשר העתקת תוכן + + + לאפשר העתקת תוכן עבור נגישות + + + לאפשר ביאורים + + + לאפשר מילוי טופס + + + שם קובץ ברירת מחדל: + + + נתיב קובץ: + + + כותרת: + + + מאת: + + + נושא: + + + מילות מפתח: + + + סיסמת בעלים: + + + סיסמת משתמש: + + + לזכור את ההגדרות + + + נתוני־על + + + הצפנה + + + תאימות + + + ממלאי מקום + + + שנה + + + שנה (00-99) + + + חודש (01-12) + + + יום (01-31) + + + שעה (0-23) + + + דקה (00-59) + + + שניות (00-59) + + + מס׳ מונה אוטומטי (ארבע ספרות) + + + מס׳ מונה אוטומטי (שלוש ספרות) + + + מס׳ מונה אוטומטי (שתי ספרות) + + + מס׳ מונה אוטומטי (ספרה אחת) + + + שם קובץ: + + + תצוגה מקדימה: + + + לקבלת איכויות JPEG גבוהות (80 ומעלה), רצוי להגדיל גם את איכות התמונה בפרופיל שלך לתוצאות מיטביות. + + + איכות Jpeg + + + אפשרויות Tiff + + + דחיסה: + + + הגדרות תמונה + + + הגדרות דוא״ל + + + הגדרות + + + ספק + + + שינוי + + + שם קובץ מצורף: + + + נא לבחור ספק דוא״ל + + + אימות + + + בהמתנה לאימות… + + + שגיאה + + + פרטים טכניים + + + סיסמה + + + הקובץ הבא מוצפן ונדרשת סיסמה על מנת להציגו: + + + לבקש נתיב קובץ + + + קובץ נפרד לכל דף + + + קובץ אחד לכל סריקה + + + הפרדת קבצים בשיטת Patch-T + + + מידע נוסף + + + פינוי תמונות לאחר שמירה + + + לשמור תמונות בין הפעלות + + + גודל נייר מותאם אישית + + + שם (רשות) + + + ממדים + + + רזולוציה בהתאמה אישית + + + נק׳ לאינץ׳ + + + בהמתנה למנהל התקן TWAIN... + + + הגדרות שרת מתקדמות + + + איכות תמונה + + + איכות מירבית (קבצים גדולים) + + + דפים ריקים + + + החרגת דפים ריקים + + + סף לבן + + + סף כיסוי + + + עיבוד סופי + + + תיקון הטיית דפים שנסרקו + + + החלת בהירות/ניגודיות לאחר הסריקה + + + רוחב היסט לפי יישור (WIA) + + + מתיחה לגודל הדף + + + חיתוך לגודל הדף + + + היפוך דפים דו־צדדיים + + + היפוך הצד האחורי בדפים דו־צדדיים + + + גרסת Wia: + + + מימושים של TWAIN: + + + סריקה במרוכז + + + נא ללחוץ על התחלה כשאפשר להתחיל. + + + הגדרות סריקה + + + פלט + + + פרופיל: + + + סריקה בודדת + + + ריבוי סריקות (הצגת בקשה בין הסריקות) + + + ריבוי סריקות (הפרש קבוע בין הסריקות) + + + מספר הסריקות: + + + זמן בין סריקות (שניות): + + + טעינת תמונות לתוך NAPS2 + + + לשמור לקובץ יחיד + + + שמירה למספר קבצים + + + התחלה + + + הסריקה הבאה + + + אפשר להתחיל לסרוק {0}. + + + פתיחת תיקייה + + + כלים + + + הפעלת תיעוד ניפוי שגיאות + + + שיתוף + + + שיתוף סורק + + + שיתוף סורק + + + אפשר להשתמש בסורקים משותפים במחשבים אחרים ברשת המקומית על ידי בחירה ב„מנהל התקן ESCL” בהגדרות הפרופיל של NAPS2 במחשב השני. + + + הגדרות סורק משותף + + + להפסיק לשתף את {0}? + + + לשתף אפילו כש־NAPS2 סגור + + + מגוון שפות… + + + מגוון שפות + + + הצגת תהליך TWAIN טבעי + + + פיצול + + + שילוב + + + IP ידני + + + IP ידני + + + IP/מארח + + + פתחה + + + התחברות + + + שגיאת חיבור. + + + תמיד לשאול + + + מתבצע חיפוש אחר מכשירים… + + + נמצאו {0} מכשירים. + + + נמצא מכשיר. + + + לא נמצאו מכשירים. + + + סרגל צד + + + הסורק לא נמצא? להלן הפרטים על המגבלות של ה־Flatpak של NAPS2. + + + קיצורי מקלדת + + + קיצורי מקלדת + + + סריקה עם פרופיל {0} + + + לסרוק עם פרופיל ברירת המחדל + + + סריקה עם פרופיל חדש + + + הקצאה + + + ביטול הקצאה + + + פעולה + + + קיצור דרך + + + ערכת עיצוב: + + + עריכה עם… + + + עריכה עם {0} + + + עריכה עם… + + + שגיאה בהפעלת היישום {0} + + + הפסקת שיתוף סורק \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.hi.resx b/NAPS2.Lib/Lang/Resources/UiStrings.hi.resx new file mode 100644 index 0000000000..ed452fce05 --- /dev/null +++ b/NAPS2.Lib/Lang/Resources/UiStrings.hi.resx @@ -0,0 +1,888 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + सॉफ्टवेयर के बारे में + + + कॉपीराइट {0} NAPS2 योगदानकर्ता + + + चिह्न प्रदाता: + + + अद्यतन के लिए जाँच + + + OK + + + दान करें + + + Profiles + + + नया + + + संपादित करें + + + नष्ट करें + + + स्कैन करें + + + Set Default + + + प्रतिलिपि बनाएँ + + + Paste + + + Undo + + + फिर से करें + + + Undo {0} + + + Redo {0} + + + पूर्ण + + + डिफ़ॉल्ट (न्यून) + + + नया प्रोफ़ाइल + + + बैच स्कैन + + + OCR + + + Profiles + + + आयात करें + + + पीडीएफ सेव करें + + + PDF Settings + + + Save Images + + + छवि समायोजन + + + पीडीएफ(PDF) ईमेल करे + + + Email Settings + + + Print + + + छवि + + + View + + + क्रॉप करें + + + चमक/विपरीतता + + + रंग / संतृप्ति + + + काला और सफेद + + + Sharpen + + + दस्तावेज़ सुधार + + + Reset + + + Rotate + + + Rotate Left + + + Rotate Right + + + पलटें + + + तिरछापन दूर करें + + + कस्टम घुमाव + + + ऊपर की ओर खिसकाए + + + नीचे की ओर खिसकाए + + + Reorder + + + Interleave + + + बीच में छूड़ाव हटाये + + + वैकल्पिक इंटरलीव + + + वैकल्पिक डीइंटरलीव + + + Reverse + + + Reverse All + + + Reverse Selected + + + क्लियर करें + + + सभी साफ करें + + + भाषा + + + Settings + + + इंटरफेस + + + "स्कैन" मेनू डिफ़ॉल्ट प्रोफ़ाइल को बदल देता है + + + Show "Profiles" toolbar + + + Show page numbers + + + स्कैन करे + + + स्कैन करे + + + एप्लिकेशन + + + Only allow a single NAPS2 instance + + + सॉफ्टवेयर के बारे में + + + Zoom + + + Zoom In + + + Zoom Out + + + Zoom Actual + + + खिड़की के साथ स्केल करें + + + Save + + + Save All + + + चयनित सेव करें + + + Save All as PDF + + + चयनित को पीडीएफ के रूप में सेव करें + + + Save All as Images + + + चयनित को छवियों के रूप में सेव करें + + + Email All + + + Email Selected + + + Email All as PDF + + + Email Selected as PDF + + + Profile Settings + + + प्रदर्शित होने वाला नाम: + + + WIA Driver + + + TWAIN Driver + + + ईएससीएल (ESCL) ड्राइवर + + + ESCL Network Driver + + + ESCL USB Driver + + + एप्पल ड्राइवर + + + SANE Driver + + + उपकरण: + + + डिवाइस का चयन करें + + + Use predefined settings + + + Use native UI + + + Paper source: + + + Page size: + + + Resolution: + + + चमक: + + + बिट गहराई: + + + Horizontal align: + + + पैमाना + + + कन्ट्रास्ट: + + + Enable Auto Save + + + स्वतः सहेजें सेटिंग्स + + + स्वतः सहेजें सेटिंग्स + + + उन्नत विकल्प + + + रद्द करें + + + Select + + + Select Source + + + Select Device + + + Run in Background + + + NAPS2 + + + NAPS2 - {0} + + + Not Another PDF Scanner + + + OCR Setup + + + ओसीआर (OCR) का उपयोग करके पीडीएफ को खोजने योग्य बनाएं + + + OCR language: + + + OCR mode: + + + Fix white balance and remove noise + + + स्कैनिंग के बाद स्वचालित रूप से OCR चलाएँ + + + Pre-emptively run OCR after scanning + + + Get more languages + + + OCR Download + + + Using OCR requires you to download each language you want to scan. + + + Select one or more languages: + + + Estimated download size: {0} MB + + + डाउनलोड करें + + + डाउनलोड प्रगति + + + Preview + + + अगला + + + Previous + + + {0} of {1} + + + Revert + + + सभी {0} चयनित छवियों पर लागू करें + + + Select All + + + पुनर्प्राप्त करें + + + Not Now + + + स्कैन की गई छवियाँ पुनर्प्राप्त करें + + + {0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them? + + + Placeholders + + + Skip save prompt + + + Single page files + + + PDF सुरक्षित करे + + + Show + + + Restore Defaults + + + PDF Settings + + + मुद्रण की अनुमति दें + + + पूर्ण गुणवत्ता मुद्रण की अनुमति दें + + + दस्तावेज़ संशोधन की अनुमति दें + + + दस्तावेज़ संयोजन की अनुमति दें + + + सामग्री प्रतिलिपि बनाने की अनुमति दें + + + पहुंच के लिए सामग्री की प्रतिलिपि बनाने की अनुमति दें + + + एनोटेशन की अनुमति दें + + + फॉर्म भरने की अनुमति दें + + + डिफ़ॉल्ट फ़ाइल पथ + + + File Path: + + + Title: + + + लेखक: + + + Subject: + + + खोजशब्द: + + + Owner Password: + + + User Password: + + + Remember these settings + + + मेटाडाटा + + + सुरक्षा अनुभाग + + + संगतता + + + Placeholders + + + Year + + + Year (00-99) + + + महीना (0-12) + + + दिन (01-31) + + + घंटा (0-23) + + + मिनट (00-59) + + + Second (00-59) + + + स्वत: वृद्धिशील संख्या (4 अंक) + + + स्वत: बढ़ती संख्या (3 अंक) + + + स्वत: बढ़ती संख्या (2 अंक) + + + स्वत: बढ़ती संख्या (1 अंक) + + + फ़ाइल का नाम + + + Preview: + + + For high JPEG qualities (80+), also increase Image Quality in your profile for best results. + + + Jpeg की गुणवत्ता + + + Tiff Options + + + कम्प्रेशन + + + छवि समायोजन + + + Email Settings + + + Settings + + + प्रदाता + + + परिवर्तन + + + अनुलग्नक का नाम: + + + ईमेल प्रदाता चुनें + + + अधिकृत + + + Waiting for authorization... + + + त्रुटि + + + Technical Details + + + Password + + + The following file is encrypted and requires a password to open: + + + Prompt for file path + + + One file per page + + + One file per scan + + + Separate files by Patch-T + + + और जाने + + + सहेजने के बाद छवियाँ साफ़ करें + + + Keep images across sessions + + + कस्टम पेज आकार + + + नाम (वैकल्पिक) + + + आयाम + + + Custom Resolution + + + Dpi + + + Waiting for TWAIN to complete... + + + उन्नत प्रोफ़ाइल सेटिंग्स + + + छवि की गुणवत्ता + + + अधिकतम गुणवत्ता (बड़ी फ़ाइलें) + + + खाली पन्ने + + + खाली पेज रहने दे + + + White Threshold + + + कवरेज सीमा + + + Post-processing + + + स्कैन किए गए पृष्ठों से तिरछापन दूर करें + + + स्कैन के बाद चमक/कंट्रास्ट लागू करें + + + Offset width based on alignment (WIA) + + + Stretch to page size + + + पृष्ठ आकार के अनुसार काटें + + + Flip duplexed pages + + + Flip back sides of duplex pages + + + Wia Version: + + + Twain Implementation: + + + बैच स्कैन + + + Press Start when ready. + + + Scan Configuration + + + Output + + + Profile: + + + Single scan + + + एकाधिक स्कैन (स्कैन के बीच संकेत) + + + एकाधिक स्कैन (स्कैन के बीच निश्चित विलंब) + + + Number of scans: + + + Time between scans (seconds): + + + NAPS2 में छवियाँ लोड करें + + + एकल फ़ाइल में सेव करें + + + एकाधिक फ़ाइलों में सेव करें + + + Start + + + अगला स्कैन + + + Ready for scan {0}. + + + Open Folder + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + क्या आप वाकई शेयरिंग बंद करना चाहते हैं? + + + Share even when NAPS2 is closed + + + एकाधिक भाषाएँ... + + + एकाधिक भाषाएँ + + + Show native TWAIN progress + + + Split + + + मिलाएँ + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + 1 device found. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.hr.resx b/NAPS2.Lib/Lang/Resources/UiStrings.hr.resx index 645488b360..ab653eaddd 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.hr.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.hr.resx @@ -19,10 +19,10 @@ Copyright {0} NAPS2 Contributors - Ikonice iz: + Ikone sa: - Check for updates + Provjeri ažuriranja U redu @@ -31,37 +31,49 @@ Doniraj - Profil + Profili - New + Novo Uredi - Obriši + Izbriši Skeniraj - Set Default + Postavi zadano Kopiraj - Paste + Zalijepi + + + Poništi + + + Ponovi + + + Poništi {0} + + + Ponovi {0} Gotovo - Default + Zadano - New Profile + Novi profil Serijsko Skeniranje @@ -70,40 +82,40 @@ OCR - Profil + Profili - Uvezi + Uvoz Spremi PDF - PDF Settings + PDF postavke Spremi slike - Image Settings + Postavke slike - Email PDF + Pošalji PDF e-poštom - Email Settings + Postavke e-pošte - Print + Ispis - Image + Slika - Pogled + Pogledaj - Crop + Obreži Svjetlina / Kontrast @@ -112,25 +124,25 @@ Hue / Saturation - Crno/bijelo + Crno-bijelo - Sharpen + Izoštri Document Correction - Reset + Resetiraj Rotiraj - Rotiraj levo + Rotirajte ulijevo - Rotiraj desno + Rotiraj udesno Okreni @@ -139,7 +151,7 @@ Popravljanje iskrivljene perspektive - Custom Rotation + Prilagođeno rotiranje Pomakni gore @@ -148,7 +160,7 @@ Pomakni dolje - Resortiraj + Prerasporedi Interleave @@ -165,8 +177,14 @@ Reverse + + Reverse All + + + Reverse Selected + - Očisti + Izbriši Clear All @@ -174,6 +192,33 @@ Jezik (language) + + Postavke + + + Sučelje + + + Izbornik "Skeniraj" mijenja zadani profil + + + Prikaži alatnu traku "Profili" + + + Prikaži brojeve stranica + + + Zadana radnja gumba "Skeniraj": + + + Zadana radnja gumba "Spremi": + + + Aplikacija + + + Dopusti samo jednu NAPS2 instancu + O programu @@ -181,31 +226,49 @@ Uvećaj/Umanji - Zoom In + Uvećaj - Zoom Out + Smanji + + + Stvarna veličina + + + Scale With Window - Save + Spremi + + + Spremi sve + + + Spremi odabrano - Save All as PDF + Spremi sve kao PDF - Save Selected as PDF + Spremi odabrano kao PDF - Save All as Images + Spremi sve kao slike - Save Selected as Images + Spremi odabrano kao slike + + + Pošalji sve e-poštom + + + Pošalji odabrano e-poštom - Email All as PDF + Pošalji sve e-poštom kao PDF - Email Selected as PDF + Pošalji odabrano kao PDF putem e-pošte Postavke profila @@ -214,16 +277,25 @@ Ime za prikaz: - WIA Driver + WIA upravljački program - TWAIN Driver + TWAIN upr. prog. + + + ESCL upr. prog. + + + ESCL mrežni upr. prog. + + + ESCL USB upr. prog. - Apple Driver + Apple upr. prog. - SANE Driver + SANE upr. prog. Uređaj: @@ -235,10 +307,10 @@ Koristi predefinirane postavke - Koristi nativni UI + Koristi izvorno sučelje - Paper source: + Izvor papira: Veličina stranice: @@ -247,40 +319,46 @@ Rezolucija: - Osvjetljenost: + Svjetlina: - Dubina bitova: + Klaliteta boje u bitova: Vodoravno poravnanje: - Razmjer: + Omjer: Kontrast: - Enable Auto Save + Omogući automatsko spremanje - Automatski spremi postavke + Postavke automatskog spremanja + + + Postavke automatskog spremanja Napredno - Prekini + Odustani - Select + Odaberi - Select Source + Odaberi izvor + + + Odaberi uređaj - Run in Background + Izvodi u pozadini NAPS2 @@ -292,7 +370,7 @@ Not Another PDF Scanner - Podesi OCR + OCR postavke Učinite PDF-ove pretraživim koristeći OCR @@ -301,22 +379,28 @@ OCR jezik: - OCR mode: + OCR način: + + + Popravi balans bijele i ukloni šum - Automatically run OCR after scanning + Automatski pokreni OCR nakon skeniranja + + + Preventivno pokretanje OCR-a nakon skeniranja - Instaliraj više jezika + Preuzmite više jezika Preuzmi OCR - Korištenje OCR-a zahteva da preuzmete svaki jezik koji želite da skenirate. + Korištenje OCR-a zahtijeva preuzimanje svakog pojedinog jezika koji želite skenirati. - Izaberite jedan ili više jezika: + Odaberite jedan ili više jezika: Procijenjena veličina preuzimanja: {0} MB @@ -331,21 +415,474 @@ Pregled - Next + Sljedeće - Previous + Prethodno {0} od {1} - Revert + Poništi - Primjeni na sve {0} označene slike + Primijeni na sve odabrane slike ({0}). Odaberi sve + + Obnovi + + + Ne sada + + + Oporavak skeniranih slika + + + {0} slika(e) skenirana(e) {1} u {2} možda nisu spremljene i mogu se oporaviti. Želite li ih oporaviti? + + + Placeholders + + + Preskoči upit za spremanje + + + Datoteke s jednom stranicom + + + Šifriraj PDF + + + Prikaži + + + Vrati zadane postavke + + + PDF postavke + + + Dozvoli ispis + + + Dozvoli ispis u punoj kvaliteti + + + Dozvoli izmjenu dokumenta + + + Dozvoli slaganje dokumenata + + + Dozvoli kopiranje sadržaja + + + Dozvoli kopiranje sadržaja radi pristupačnosti + + + Dozvoli zabilješke + + + Dopusti ispunjavanje obrazaca + + + Zadana putanja datoteke: + + + Putanja datoteke: + + + Naslov: + + + Autor: + + + Predmet: + + + Ključne riječi: + + + Lozinka vlasnika: + + + Lozinka korisnika: + + + Zapamti ove postavke + + + Metapodaci + + + Šifriranje + + + Kompatibilnost + + + Placeholders + + + Godina + + + Godina (00-99) + + + Mjesec (01-12) + + + Dan (01-31) + + + Sat (0-23) + + + Minuta (00-59) + + + Sekunda (00-59) + + + Samo-povećavajući brojač (4 znamenke) + + + Samo-povećavajući brojač (3 znamenke) + + + Samo-povećavajući brojač (2 znamenke) + + + Auto-incrementing number (1 digits) + + + Naziv datoteke: + + + Pretpregled: + + + Za visoku JPEG kvalitetu (80+), za najbolje rezultate potrebno je povećati i kvalitetu slike na svom profilu. + + + JPEG kvaliteta + + + TIFF opcije + + + Sažimanje: + + + Postavke slike + + + Postavke e-pošte + + + Postavke + + + Davatelj usluga + + + Promijeni + + + Naziv privitka: + + + Odaberite pružatelja usluga e-pošte + + + Autoriziraj + + + Čekam na autorizaciju... + + + Greška + + + Tehnički detalji + + + Lozinka + + + Sljedeća datoteka je šifrirana i za otvaranje je potrebna lozinka: + + + Pitaj za putanju datoteke + + + Jedna datoteka po stranici + + + Jedna datoteka po skeniranju + + + Razdvojite datoteke pomoću Patch-T + + + Više informacija + + + Obriši slike nakon spremanja + + + Keep images across sessions + + + Prilagođena veličina stranice + + + Naziv (neobavezno) + + + Dimenzije + + + Prilagođena rezolucija + + + DPI + + + Čekam da TWAIN završi... + + + Napredne postavke profila + + + Kvaliteta slike + + + Maksimalna kvaliteta (velike datoteke) + + + Prazne stranice + + + Izuzmi prazne stranice + + + White Threshold + + + Prag Pokrivanja + + + Naknadna obrada + + + Popravljanje iskrivljene perspektive skeniranih stranica + + + Primjeni podešenje svjetlline/kontrasta nakon skeniranja + + + Širina pomaka na temelju poravnanja (WIA) + + + Raširi na veličinu stranice + + + Izreži na veličinu stranice + + + Okreni obostrane stranice + + + Okrenite stražnje strane obostranih stranica + + + WIA verzija: + + + TWAIN implementacija: + + + Serijsko Skeniranje + + + Pritisnite Započni kada ste spremni. + + + Postavke skeniranja + + + Izlaz + + + Profil: + + + Jedno skeniranje + + + Više skeniranja (upit između skeniranja) + + + Više skeniranja (fiksna odgoda između skeniranja) + + + Broj skeniranja: + + + Vrijeme između skeniranja (sekunde): + + + Učitajte slike u NAPS2 + + + Spremi u jednu datoteku + + + Spremi u više datoteka + + + Započni + + + Slijedeće skeniranje + + + Spremno za skeniranje {0}. + + + Otvori mapu + + + Alati + + + Omogući zapise otklanjanja grešaka + + + Dijeli + + + Dijeljenje skenera + + + Dijeljenje skenera + + + Dijeljeni skeneri mogu se koristiti s drugih računala u lokalnoj mreži odabirom "ESCL upr. prog." u postavkama NAPS2 profila drugog računala. + + + Postavke dijeljenog skenera + + + Jeste li sigurni da želite zaustaviti dijeljenje {0}? + + + Dijeli čak i kada je NAPS2 zatvoren + + + Višestruki jezici... + + + Višestruki jezici + + + Prikaži napredak izvornog TWAIN-a + + + Podijeli + + + Combine + + + Ručna IP adresa + + + Ručna IP adresa + + + IP/Host + + + Priključak + + + Poveži + + + Greška kod povezivanja. + + + Uvijek pitaj + + + Traženje uređaja... + + + {0} pronađen(a) uređaj(a). + + + Pronađen je 1 uređaj. + + + Nisu pronađeni uređaji. + + + Bočni izbornik + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Tipkovni prečaci + + + Tipkovni prečaci + + + Skeniraj s profilom {0} + + + Skeniraj sa zadanim profilom + + + Skeniraj s novim profilom + + + Dodijeli + + + Poništi dodjelu + + + Radnja + + + Prečac + + + Tema: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Greška pri pokretanju aplikacije {0} + + + Zaustavi dijeljenje skenera + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.hu.resx b/NAPS2.Lib/Lang/Resources/UiStrings.hu.resx index 7d3e45c701..eb578d8976 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.hu.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.hu.resx @@ -16,19 +16,19 @@ Névjegy - Copyright {0} NAPS2 Contributors + Copyright {0} NAPS2 Közreműködők Ikonok forrása: - Frissítések keresése + Frissítések ellenőrzése - Ok + OK - Támogatás + Adományozás Profilok @@ -40,13 +40,13 @@ Szerkesztés - Oldal törlése + Törlés - Képolvasás + Beolvasás - Set Default + Beállítás alapértelmezettként Másolás @@ -54,6 +54,18 @@ Beillesztés + + Visszavonás + + + Mégis + + + Visszavonás {0} + + + Mégis {0} + Kész @@ -76,22 +88,22 @@ Importálás - PDF mentése + PDF mentés - PDF beállításai + PDF beállítások Képek mentése - Kép beállításai + Kép beállítások - PDF küldése Email-ben + PDF küldése e-mailben - Email beállítások + E-mail beállítások Nyomtatás @@ -100,16 +112,16 @@ Kép - Nézet + Megtekintés Levágás - Fényerő / kontraszt + Fényerő / Kontraszt - Hue / Saturation + Színárnyalat / telítettség Fekete-fehér @@ -118,7 +130,7 @@ Élesítés - Document Correction + Dokumentum javítás Visszaállítás @@ -136,10 +148,10 @@ Átfordítás - Visszatorzítás + Kiegyenesítés - Custom Rotation + Egyéni forgatás Le @@ -148,32 +160,65 @@ Fel - Rendezés + Átrendezés - Átrendezés + Átlapolás Elhagyás - Alternate Interleave + Alternatív átlapolás - Alternatív képminőség javítás + Alternatív Deinterleave - Reverse + Visszafelé + + + Összes visszafordítása + + + Kiválasztott visszafordítása - Kép tisztítása + Összes törlése - Clear All + Összes törlése Nyelv + + Beállítások + + + Felület + + + A "Beolvasás" menü megváltoztatja az alapértelmezett profilt + + + "Profilok" eszköztár megjelenítése + + + Oldalszámok megjelenítése + + + "Beolvasás" gomb alapértelmezett művelete: + + + "Mentés" gomb alapértelmezett művelete: + + + Alkalmazás + + + Csak egyetlen NAPS2 példány engedélyezése + Névjegy @@ -186,59 +231,86 @@ Kicsinyítés + + Eredeti méret + + + Méretezés az ablakhoz + - Save + Mentés + + + Összes mentése + + + Kiválasztott mentése - Save All as PDF + Összes mentése PDF-ként - Save Selected as PDF + Kiválasztott mentése PDF-ként - Save All as Images + Összes mentése képekként - Save Selected as Images + Kiválasztottak mentése képekként + + + Összes küldése e-mailben + + + Kiválasztott küldése e-mailben - Email All as PDF + Összes elküldése e-mailben, PDF-ként - Email Selected as PDF + Kiválasztottak elküldése e-mailbenm PDF-ként - Profil-beálítások + Profil beállítások - Név: + Megjelenített név: - WIA meghajtó + WIA illesztőprogram - TWAIN meghajtó + TWAIN illesztőprogram + + + ESCL illesztőprogram + + + ESCL hálózati illesztőprogram + + + ESCL USB illesztőprogram - Apple Driver + Apple illesztőprogram - SANE Driver + SANE illesztőprogram Eszköz: - Képolvasó kiválasztása + Eszköz kiválasztása - A következő bállítások használata + Előre meghatározott beállítások használata - Use native UI + Natív felhasználói felület használata - Forrás: + Papírforrás: Oldalméret: @@ -267,6 +339,9 @@ Automatikus mentés beállításai + + Automatikus mentés beállításai + Haladó @@ -279,8 +354,11 @@ Forrás kiválasztása + + Eszköz kiválasztása + - Háttérbe + Futtatás a háttérben NAPS2 @@ -292,7 +370,7 @@ Not Another PDF Scanner - OCR Telepítés + Karakterfelismerés beállítás A PDF-ek kereshetővé tétele karakterfelismeréssel @@ -301,22 +379,28 @@ Karakterfelismerés nyelve: - OCR mode: + Karakterfelismerés mód: + + + Fehéregyensúly javítása és zaj eltávolítása - Automatically run OCR after scanning + Karakterfelismerés automatikus futtatása beolvasás után + + + Előzetes OCR futtatás a beolvasás után További nyelvek beszerzése - OCR Letöltés + Karakterfelismerés letöltés - Az karakterfelismerés használatához le kell tölteni az összes szkennelési nyelvet. + A karakterfelismerés használatához minden olyan nyelvet le kell töltenie, amelyet be szeretne olvasni. - Válasszon ki legalább egy nyelvet.: + Válasszon ki egy vagy több nyelvet: Becsült letöltési méret: {0} MB @@ -340,12 +424,465 @@ {0} / {1} - Revert + Alaphelyzet - Apply to all {0} selected images + Alkalmazás az összes kiválasztott {0} képre Összes kijelölése + + Helyreállítás + + + Most nem + + + Beolvasott képek helyreállítása + + + A {1} {2}-kor beolvasott {0} kép lehet, hogy nem került mentésre, és visszaállítható. Szeretné visszaállítani őket? + + + Helyettesítők + + + Mentési felszólítás kihagyása + + + Egyoldalas fájlok + + + PDF titkosítása + + + Megjelenítés + + + Alapértelmezések visszaállítása + + + PDF beállítások + + + Nyomtatás engedélyezése + + + Legjobb minőségű nyomtatás engedélyezése + + + Dkoumentum módosításának engedélyezése + + + Dokumentum összeállításának engedélyezése + + + Tartalom másolásának engedélyezése + + + Tartalom másolásának engedélyezése az akadálymentesítés érdekében + + + Jegyzet engedélyezése + + + Űrlap kitöltésének engedélyezése + + + Alapértelmezett fájl elérési útvonal: + + + Fájl elérési útvonal: + + + Cím: + + + Szerző: + + + Tárgy: + + + Kulcsszavak: + + + Tulajdonos jelszó: + + + Felhasználó jelszó: + + + Emlékezzen ezekre a beállításokra + + + Metaadatok + + + Titkosítás + + + Kompatibilitás + + + Helyettesítők + + + Év + + + Év (00-99) + + + Hónap (01-12) + + + Nap (01-31) + + + Óra (0-23) + + + Perc (00-59) + + + Másodperc (00-59) + + + Automatikusan növekvő szám (4 számjegy) + + + Automatikusan növekvő szám (3 számjegy) + + + Automatikusan növekvő szám (2 számjegy) + + + Automatikusan növekvő szám (1 számjegy) + + + Fájlnév + + + Előnézet: + + + Magas JPEG minőség (80+) elérése érdekében növelje a képminőséget a profilban a legjobb eredmény eléréséhez. + + + Jpeg minőség + + + Tiff beállítások + + + Tömörítés: + + + Kép beállítások + + + E-mail beállítások + + + Beállítások + + + Szolgáltató + + + Változtatás + + + Melléklet neve: + + + E-mail szolgáltató kiválasztása + + + Engedélyezés + + + Várakozás az engedélyezésre... + + + Hiba + + + Technikai részletek + + + Jelszó + + + A következő fájl titkosított, és a megnyitásához jelszó szükséges: + + + Kérdezzen rá a fájl elérési útvonalára + + + Egy fájl oldalanként + + + Egy fájl beolvasásonként + + + Különálló fájlok Patch-T szerint + + + További információ + + + Képek törlése mentés után + + + Képek megtartása a munkamenetek között + + + Egyéni oldalméret + + + Név (nem kötelező) + + + Méretek + + + Egyéni felbontás + + + Dpi + + + Várakozás a TWAIN-re a befejezéshez... + + + Haladó profilbeállítások + + + Képminőség + + + Legjobb minőség (nagy fájlok) + + + Üres oldalak + + + Üres oldalak kizárása + + + Fehér küszöbérték + + + Lefedettségi küszöbérték + + + Utófeldogozás + + + Beolvasott oldalak kiegyenesítése + + + Fényerő/kontraszt alkalmazása a beolvasás után + + + Igazításon alapuló eltolási szélesség (WIA) + + + Nyújtás az oldal méretére + + + Levágás oldalméretre + + + Kétoldalas oldalak megfordítása + + + Duplex (kétoldalas) oldalak hátoldalának megfordítása + + + Wia verzió: + + + Twain megvalósítás: + + + Kötegelt beolvasás + + + Ha kész, nyomja meg a Kezdés gombot. + + + Beolvasási konfiguráció + + + Kimenet + + + Profil: + + + Egyetlen beolvasás + + + Többszörös beolvasás ( kérdés a beolvasások között) + + + Többszörös beolvasás (rögzített késleltetés a beolvasások között) + + + A beolvasások száma: + + + Beolvasások közötti idő (másodperc): + + + Képek betöltése a NAPS2-be + + + Mentés egyetlen fájlba + + + Mentés több fájlba + + + Kezdés + + + Következő beolvasás + + + Készen áll {0} beolvasásra. + + + Mappa megnyitása + + + Eszközök + + + Hibakeresési naplózás engedélyezése + + + Megosztás + + + Lapolvasó megosztása + + + Lapolvasó megosztása + + + A megosztott lapolvasók a helyi hálózat más számítógépeiről is használhatók, ha a másik számítógép NAPS2 profilbeállításaiban kiválasztja az "ESCL illesztőprogramot". + + + Megosztott lapolvasó beállítások + + + Biztos benne, hogy nem szeretné többé megosztani a {0} lapolvasót? + + + Megosztás akkor is, ha a NAPS2 be van zárva + + + Több nyelv... + + + Több nyelv + + + A natív TWAIN folyamat megjelenítése + + + Felosztás + + + Egyesítés + + + Kézi IP + + + Kézi IP + + + IP/Gazdagép + + + Port + + + Csatlakozás + + + Csatlakozási hiba. + + + Mindig kérdezzen rá + + + Eszközök keresése... + + + {0} eszköz található. + + + 1 eszköz található. + + + Nem található eszköz. + + + Oldalsáv + + + Nem találja a lapolvasóját? Olvasson a NAPS2 Flatpak korlátairól. + + + Billentyűparancsok + + + Billentyűparancsok + + + Lapolvasás a {0} profillal + + + Beolvasás az alapértelmezett profillal + + + Lapolvasás új profillal + + + Hozzárendelés + + + Hozzárendelés megszüntetése + + + Művelet + + + Billentyűparancs + + + Téma: + + + Szerkesztés... + + + Szerkesztés a következővel: {0} + + + Szerkesztés... + + + Hiba az alkalmazás indításakor {0} + + + A lapolvasó megosztásának leállítása + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.id.resx b/NAPS2.Lib/Lang/Resources/UiStrings.id.resx new file mode 100644 index 0000000000..5391327caa --- /dev/null +++ b/NAPS2.Lib/Lang/Resources/UiStrings.id.resx @@ -0,0 +1,888 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Tentang + + + Copyright {0} , Kontributor NAPS2 + + + Ikon dari: + + + Periksa pembaharuan + + + OK + + + Donasi + + + Profiles + + + New + + + Edit + + + Hapus + + + Scan + + + Set Default + + + Salin + + + Paste + + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + + + Selesai + + + Default + + + New Profile + + + Pindai Tumpak + + + OCR + + + Profiles + + + Import + + + Save PDF + + + PDF Settings + + + Save Images + + + Image Settings + + + Emailkan PDF + + + Pengaturan Email + + + Print + + + Image + + + View + + + Crop + + + Kontras / Kecerahan + + + Hue / Saturation + + + Hitam Putih + + + Sharpen + + + Koreksi Dokumen + + + Reset + + + Rotate + + + Rotate Left + + + Rotate Right + + + Putar Balik + + + miring-mereng + + + Rotasi custom + + + Move Up + + + Move Down + + + Reorder + + + Interleave + + + Deinterleave + + + Alternatif Interleave + + + Alternatif Deinterleave + + + Reverse + + + Reverse All + + + Reverse Selected + + + Bersihkan + + + Bersihkan semua + + + Language + + + Settings + + + Antarmuka + + + Menu "Pindai" mengubah profil bawaan + + + Show "Profiles" toolbar + + + Show page numbers + + + Aksi bawaan tombol "Pindai": + + + Aksi bawaan tombol "Simpan": + + + Aplikasi + + + Only allow a single NAPS2 instance + + + Tentang + + + Zoom + + + Zoom In + + + Zoom Out + + + Zoom Actual + + + Scale With Window + + + Save + + + Save All + + + Save Selected + + + Save All as PDF + + + Save Selected as PDF + + + Save All as Images + + + Save Selected as Images + + + Emailkan Semua + + + Emailkan Pilihan + + + Emailkan Semua sebagai PDF + + + Emailkan Pilihan sebagai PDF + + + Profile Settings + + + Nama Tampilan: + + + WIA Driver + + + TWAIN Driver + + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + + + Driver utk Apple + + + SANE Driver + + + piranti: + + + Pilih perangkat + + + Use predefined settings + + + Use native UI + + + Paper source: + + + Page size: + + + Resolution: + + + Kecerahan: + + + Kedalaman bit: + + + Penyelarasan horizontal: + + + Scale: + + + Kontras: + + + Aktifkan Penyimpanan Otomatis + + + Pengaturan (Auto Save) + + + Pengaturan (Auto Save) + + + Tingkat Lanjutan + + + Batal + + + Select + + + Select Source + + + Select Device + + + Run in Background + + + NAPS2 + + + NAPS2 - {0} + + + Not Another PDF Scanner + + + OCR Setup + + + Make PDFs searchable using OCR + + + OCR language: + + + OCR mode: + + + Perbaiki white balance dan hilangkan noise + + + Auto Jalankan OCR Teks setelah pemindaian + + + Pre-emptively run OCR after scanning + + + Dapatkan bahasa lain + + + OCR Download + + + Using OCR requires you to download each language you want to scan. + + + Select one or more languages: + + + Estimasi ukuran unduhan: {0} MB + + + Unduh + + + Sedang mengunduh + + + Preview + + + Next + + + Previous + + + {0} of {1} + + + Revert + + + Terapkan ke semua {0} gambar yang dipilih + + + Select All + + + Recover + + + Not Now + + + Recover Scanned Images + + + {0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them? + + + Placeholders + + + Skip save prompt + + + Single page files + + + Enkripsi PDF + + + Show + + + Restore Defaults + + + PDF Settings + + + Ijinkan pencetakan + + + Ijinkan pencetakan berkualitas baik + + + Ijinkan perubahan terhadap dokument + + + Ijinkan perakitan dokumen dan penyisipan + + + Ijinkan penyalinan konten + + + Ijinkan penyalinan konten untuk aksesibilitas + + + Ijinkan pemberian annotasi + + + Ijinkan pengisian isian formulir + + + Path Default Berkas: + + + Lokasi berkas: + + + Title: + + + Author: + + + Subject: + + + Keywords: + + + Owner Password: + + + User Password: + + + Remember these settings + + + Metadata + + + Enkripsi + + + Kompatibilitas + + + Placeholders + + + Year + + + Year (00-99) + + + Month (01-12) + + + Tanggal (01-31) + + + Jam (0-23) + + + Minute (00-59) + + + Second (00-59) + + + penambahan nomor auto (4 digit) + + + penambahan nomor auto (3 digit) + + + penambahan nomor auto (2 digit) + + + penambahan nomor auto (1 digit) + + + Nama Berkas: + + + Preview: + + + Untuk JPEG kualitas tinggi, tingkatkan juga kualitas gambar di profilmu untuk hasil terbaik. + + + Jpeg Quality + + + Tiff Options + + + Kompresi: + + + Image Settings + + + Pengaturan Email + + + Settings + + + Provider + + + Ubah + + + Nama berkas dilampirkan: + + + Pilih penyedia email + + + Authorize + + + Waiting for authorization... + + + Kesalahan + + + Technical Details + + + Password + + + The following file is encrypted and requires a password to open: + + + Prompt for file path + + + One file per page + + + One file per scan + + + Separate files by Patch-T + + + More info + + + Bersihkan gambar setelah Menyimpan (Saving) + + + Keep images across sessions + + + Ukuran halaman Custom + + + Name (optional) + + + Dimensi + + + Custom Resolution + + + Dpi + + + Waiting for TWAIN to complete... + + + Pengaturan Profile Tingkat Lanjut + + + Image Quality + + + Maximum quality (large files) + + + Laman Kosong + + + Lewati halaman kosong + + + White Threshold + + + jangkauan Threshold + + + Post-processing + + + Halaman terpindai cek miring-mereng + + + Terapkan tingkat kontras/kecerahan setelah pindai selesai + + + Offset width based on alignment (WIA) + + + Stretch to page size + + + Crop ke ukuran halaman + + + Balik halaman bolak-balik + + + Balik sisi halaman bolak-balik + + + Wia Version: + + + Twain Implementation: + + + Pindai Tumpak + + + Press Start when ready. + + + Scan Configuration + + + Output + + + Profile: + + + Single scan + + + Multiple scans (prompt between scans) + + + Multiple scans (fixed delay between scans) + + + Number of scans: + + + Time between scans (seconds): + + + Load images into NAPS2 + + + Save to a single file + + + Save to multiple files + + + Start + + + Next Scan + + + Ready for scan {0}. + + + Open Folder + + + Tools + + + Aktifkan debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Anda yakin ingin berhenti membagikan {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Gabungkan + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Hubungkan + + + Kesalahan koneksi. + + + Selalu tanyakan + + + Searching for devices... + + + {0} devices found. + + + 1 perangkat ditemukan. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.it.resx b/NAPS2.Lib/Lang/Resources/UiStrings.it.resx index c1237911b7..e7dd540cdb 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.it.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.it.resx @@ -13,22 +13,22 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Informazioni + Info sul programma - Copyright {0} NAPS2 Contributors + Copyright {0} contributori NAPS2 - Trova icone su: + Libreria icone: - Ricerca degli aggiornamenti: + Cerca aggiornamenti OK - Fai una donazione. + Dona Profili @@ -54,6 +54,18 @@ Incolla + + Annulla operazione + + + Ripeti operazione + + + Annulla operazione {0} + + + Ripeti operazione {0} + Fatto @@ -61,7 +73,7 @@ Predefinito - Nuovo Profilo + Nuovo profilo Scansione in serie @@ -82,13 +94,13 @@ Impostazioni PDF - Salva Immagine + Salva immagini Impostazioni immagine - Email PDF + Invia PDF via email Impostazioni email @@ -103,7 +115,7 @@ Visualizza - Taglia + Ritaglia Luminosità/contrasto @@ -115,13 +127,13 @@ Bianco e nero - Focalizzare + Nitidezza - Document Correction + Correzione documento - Azzera + Ripristina Ruota @@ -136,10 +148,10 @@ Capovolgi - Ruota ed allinea + Ruota e allinea - Rotazione Personalizzata + Rotazione personalizzata Sposta su @@ -151,64 +163,115 @@ Riordina - Interlaccia + Interleave - Deinterlaccia + Deinterleave - Interleave alternativo + Interfoliazione alternativa - Deinterleave alternativo + Inverti interfoliazione alternativa - Inverso + Inverti + + + Inverti tutto + + + Inverti selezionati - Cancella + Azzera - Clear All + Azzera tutto Lingua + + Impostazioni + + + Interfaccia + + + Menu "Scansiona" modifica il profilo predefinito + + + Visualizza barra "Profili" + + + Visualizza numeri di pagina + + + Azione predefinita del tasto "Scansiona": + + + Azione predefinita del tasto "Salva": + + + Applicazione + + + Consenti una sola istanza di NAPS2 + - Informazioni + Info sul programma Zoom - Aumenta zoom + Zoom + - Riduci zoom + Zoom - + + + Zoom attuale + + + Adatta alla finestra - Save + Salva + + + Salva tutti + + + Salva selezionati - Save All as PDF + Salva tutti come PDF - Save Selected as PDF + Salva selezionati come PDF - Save All as Images + Salva tutti come immagini - Save Selected as Images + Salva selezionati come immagini + + + Invia email a tutti + + + Invia selezionati via email - Email All as PDF + Invia e-mail a tutti come PDF - Email Selected as PDF + Invia selezionati via email come PDF - Configurazione Profilo + Impostazioni profilo Nome visualizzato: @@ -219,8 +282,17 @@ Driver TWAIN + + Driver ESCL + + + Driver di rete ESCL + + + Driver USB ESCL + - Apple Driver + Driver Apple Driver SANE @@ -229,7 +301,7 @@ Dispositivo: - Scegli Dispositivo + Scegli dispositivo Usa impostazioni predefinite @@ -265,7 +337,10 @@ Abilita salvataggio automatico - Salva impostazioni automaticamente + Salva automaticamente impostazioni + + + Salva automaticamente impostazioni Avanzate @@ -279,8 +354,11 @@ Seleziona sorgente + + Seleziona dispositivo + - Esegui in Background + Esegui in background NAPS2 @@ -295,7 +373,7 @@ Installa OCR - Rendi i PDF ricercabili usando l'OCR + Rendi PDF ricercabili usando l'OCR Lingua OCR: @@ -303,8 +381,14 @@ Modalità OCR: + + Correggi bilanciamento del bianco e rimuovi il rumore + - Avvia automaticamente l'OCR dopo la scansione + Dopo la scansione avvia automaticamente l'OCR + + + Esegui preventivamente OCR dopo la scansione Installa altre lingue @@ -313,25 +397,25 @@ Scarica OCR - L'uso dell'OCR richiede che venga scaricata ogni lingua che vuoi far riconoscere. + L'uso dell'OCR richiede che venga scaricata ogni lingua che si vuole far riconoscere. Seleziona una o più lingue: - Dimensione stimata del download: {0} MB + Dimensione stimata download: {0} MB Scarica - Avanzamento download + Progresso download Anteprima - Avanti + Successiva Precedente @@ -348,4 +432,457 @@ Seleziona tutto + + Ripristina + + + Non ora + + + Ripristino immagini digitalizzate + + + {0} immagine/i digitalizzata/e il {1} alle ore {2} potrebbero non essere state salvate e sono recuperabili. Vuoi recuperarle? + + + Segnaposti + + + Salta richiesta salvataggio + + + File a pagina singola + + + Cifra il PDF + + + Visualizza + + + Ripristina predefiniti + + + Impostazioni PDF + + + Consenti stampa + + + Consenti stampa in alta qualità + + + Consenti modifica documento + + + Consenti composizione documento + + + Consenti copia contenuti + + + Consenti copia contenuti per accessibilità + + + Consenti annotazioni + + + Consenti compilazione modulo + + + Percorso file predefinito: + + + Percorso file: + + + Titolo: + + + Autore: + + + Oggetto: + + + Parole chiave: + + + Password proprietario: + + + Password utente: + + + Ricorda queste impostazioni + + + Metadati + + + Crifratura + + + Compatibilità + + + Segnaposti + + + Anno + + + Anno (00-99) + + + Mese (01-12) + + + Giorno (01-31) + + + Ora (0-23) + + + Minuti (00-59) + + + Secondi (00-59) + + + Incremento automatico numero (4 cifre) + + + Incremento automatico numero (3 cifre) + + + Incremento automatico numero (2 cifre) + + + Incremento automatico numero (1 cifra) + + + Nome file: + + + Anteprima: + + + Per ottenere i migliori risultati con JPEG di qualità elevata (80+), aumenta nel profilo il valore 'Qualità Immagine'. + + + Qualità Jpeg + + + Opzioni Tiff + + + Compressione: + + + Impostazioni immagine + + + Impostazioni email + + + Impostazioni + + + Fornitore + + + Modifica + + + Nome allegato: + + + Scegli provider email + + + Autorizza + + + In attesa di autorizzazione... + + + Errore + + + Dettagli tecnici + + + Password + + + Il seguente file è cifrato e richiede una password per essere aperto: + + + Richiesta percorso file + + + Un file per pagina + + + Un file per scansione + + + Separa file tramite Patch-T + + + Maggiori informazioni + + + Cancella immagini dopo il salvataggio + + + Mantieni le immagini tra una sezione e l'altra + + + Formato pagina personalizzato + + + Nome (opzionale) + + + Dimensioni + + + Risoluzione personalizzata + + + DPI + + + In attesa di TWAIN per completare... + + + Impostazioni profilo avanzate + + + Qualità immagine + + + Massima qualità (file di grandi dimensioni) + + + Pagine vuote + + + Escludi pagine vuote + + + Soglia bianco + + + Soglia copertura + + + Post-processo + + + Pagine allineate scansionate + + + Applica luminosità/contrasto dopo la scansione + + + Offset larghezza in base ad allineamento (WIA) + + + Adatta a dimensione pagina + + + Ritaglia alla dimensione pagina + + + Capovolgi pagine fronte retro + + + Capovolgi lati posteriori pagine fronte/retro + + + Versione Wia: + + + Implementazione Twain: + + + Scansione in serie + + + Seleziona 'Avvia' quando sei pronto. + + + Configurazione scansione + + + Destinazione + + + Profilo: + + + Scansione singola + + + Scansioni multiple (richiesta dopo ogni scansione) + + + Scansioni multiple (ritardo fisso tra le scansioni) + + + Numero scansioni: + + + Intervallo tra le scansioni (secondi): + + + Carica immagini in NAPS2 + + + Salva in un singolo file + + + Salva in file multipli + + + Avvia + + + Scansione successiva + + + Pronto per la scansione {0}. + + + Apri cartella + + + Strumenti + + + Abilita la registrazione dei log di debug + + + Condividi + + + Condivisione scanner + + + Condivisione scanner + + + Gli scanner condivisi possono essere usati da altri computer nella rete locale selezionando "Driver ESCL" nelle impostazioni del profilo NAPS2 dell'altro computer. + + + Impostazioni scanner condiviso + + + Sei sicuro di voler interrompere la condivisione di {0}? + + + Condividi anche quando NAPS2 è chiuso + + + Lingue multiple... + + + Lingue multiple + + + Visualizza avanzamento TWAIN nativo + + + Dividi + + + Combina + + + IP manuale + + + IP manuale + + + IP/host + + + Porta + + + Connetti + + + Errore connessione. + + + Chiedi sempre + + + Ricerca dispositivi... + + + {0} dispositivi trovati. + + + 1 dispositivo trovato. + + + Nessun dispositivo trovato. + + + Barra laterale + + + Non riesci a trovare lo scanner? Leggi le limitazioni del Flatpak NAPS2. + + + Scorciatoie tastiera + + + Scorciatoie tastiera + + + Scansiona con il profilo {0} + + + Scansiona con Profilo Predefinito + + + Scansiona con nuovo profilo + + + Assegna + + + Rimuovi assegnazione + + + Azione + + + Collegamento + + + Tema: + + + Modifica con... + + + Modifica con {0} + + + Modifica con... + + + Errore avvio applicazione {0} + + + Stop condivisione scanner + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.ja.resx b/NAPS2.Lib/Lang/Resources/UiStrings.ja.resx index d7d97ded6e..701474f772 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.ja.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.ja.resx @@ -54,6 +54,18 @@ 貼り付け + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + 完了しました @@ -165,6 +177,12 @@ 反転させる + + Reverse All + + + Reverse Selected + クリア @@ -174,6 +192,33 @@ 言語 + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + "Save" button default action: + + + Application + + + Only allow a single NAPS2 instance + NAPS2について @@ -186,9 +231,21 @@ ズームアウト + + 実寸サイズ + + + ウインドウに合わせる + Save + + Save All + + + Save Selected + Save All as PDF @@ -201,6 +258,12 @@ Save Selected as Images + + Email All + + + Email Selected + Email All as PDF @@ -219,6 +282,15 @@ TWAINドライバ + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -267,6 +339,9 @@ 自動保存に関する設定 + + 自動保存に関する設定 + 詳細設定 @@ -279,6 +354,9 @@ ソースの選択 + + Select Device + バックグラウンドで実行 @@ -303,9 +381,15 @@ OCRモード: + + Fix white balance and remove noise + スキャン後にOCRを自動適用する + + Pre-emptively run OCR after scanning + 他の言語を追加 @@ -348,4 +432,457 @@ すべて選択 + + 回復 + + + 後で + + + スキャンイメージを回復する + + + {2} に {1} でスキャンされた {0} 個の画像は保存されませんでした。回復可能ですが、回復しますか? + + + プレースホルダー + + + 保存確認をスキップ + + + 単一ページのファイル + + + 暗号化されたPDF + + + 見せる + + + 標準設定に戻す + + + PDFの設定 + + + 印刷を許可する + + + オリジナル画質の印刷を許可する + + + ドキュメントの変更を許可 + + + ドキュメントの結合を許可 + + + コンテンツのコピーを許可 + + + アクセシビリティのためにコンテンツのコピーを許可する + + + 注釈を許可 + + + フォームへの書き込みを許可 + + + デフォルトのパス: + + + ファイルのパス: + + + タイトル: + + + 作成者: + + + 件名: + + + キーワード: + + + オーナーのパスワード: + + + ユーザーのパスワード: + + + 設定を保存する + + + メタデータ + + + 暗号化 + + + 互換性 + + + プレースホルダー + + + + + + (00-99) 年 + + + (01-12)月 + + + (01-31)日 + + + (0-23)時間 + + + (00-59)分 + + + (00-59)秒 + + + 連番の自動付与 (4 ケタ) + + + 連番の自動付与 (3 ケタ) + + + 連番の自動付与 (2 ケタ) + + + Auto-incrementing number (1 digits) + + + ファイル名 + + + プレビュー: + + + 80以上の高画質JPEGを得るためには、プロファイル内の画質設定をあげてください。. + + + JPEG画像の品質 + + + TIFF形式の設定 + + + 圧縮: + + + 画像設定 + + + メールの設定 + + + Settings + + + プロバイダー + + + 変更 + + + 添付ファイルの名前: + + + メールプロバイダを選択 + + + 認証 + + + 認証待ち... + + + エラー + + + 技術的な詳細 + + + パスワード + + + The following file is encrypted and requires a password to open: + + + Prompt for file path + + + ページごとに1つファイルを作成 + + + スキャンごとに1つのファイルを作成 + + + Patch-Tでファイル分割 + + + 詳細情報 + + + 保存後にイメージをクリア + + + Keep images across sessions + + + ページサイズ設定 + + + 名前 (オプション) + + + 大きさ + + + Custom Resolution + + + Dpi + + + TWAINの完了を待機中です... + + + プロファイル設定(詳細) + + + 画質 + + + 最大画質(ファイルサイズ大) + + + 空白のページ + + + 余白ページを除去 + + + 白さのしきい値 + + + 閾値 + + + 後処理 + + + 歪み補正をする + + + スキャン後に明度とコントラストの処理を行う + + + Offset width based on alignment (WIA) + + + ページの大きさに合わせる + + + ページサイズに切り抜く + + + 両面のページを反転 + + + Flip back sides of duplex pages + + + Wia Version: + + + Twainの補助機能: + + + 一括スキャン + + + 準備ができたらスタートを押してください. + + + スキャン設定 + + + 出力 + + + プロファイル: + + + 1枚だけスキャン + + + 複数をスキャン(スキャン毎に通知する) + + + 複数をスキャン(スキャン間に遅延を設ける) + + + スキャンの数: + + + スキャン間の時間 (seconds): + + + NAPS2にイメージを読み込む + + + 1つのファイルに保存 + + + 複数のファイルに保存 + + + 開始 + + + 次のスキャン + + + {0}個の準備が完了しました. + + + フォルダーを開く + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + 1 device found. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.ko.resx b/NAPS2.Lib/Lang/Resources/UiStrings.ko.resx index 9aabd98b22..914ac7ad17 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.ko.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.ko.resx @@ -16,19 +16,19 @@ 정보 - Copyright {0} NAPS2 Contributors + Copyright {0} NAPS2 기여자들 아이콘 출처: - Check for updates + 업데이트 확인 확인 - Donate + 후원 프로파일 @@ -54,6 +54,18 @@ 붙여넣기 + + 실행취소 + + + 다시 실행 + + + {0} 실행취소 + + + {0} 다시 실행 + 완료 @@ -88,7 +100,7 @@ 이미지 설정 - 이메일 PDF + PDF로 이메일 보내기 이메일 설정 @@ -103,7 +115,7 @@ 보기 - 잘라내기 + 자르기 밝기 / 명암 @@ -118,7 +130,7 @@ 샤픈 - Document Correction + 문서 교정 초기화 @@ -165,15 +177,48 @@ 뒤집기 + + 모두 뒤집기 + + + 선택한 항목 뒤집기 + - 비우기 + 지우기 - Clear All + 모두 지우기 언어 + + 설정 + + + 인터페이스 + + + "스캔" 메뉴로 기본 프로파일 변경하기 + + + "프로파일" 도구모음 보이기 + + + 페이지 번호 보이기 + + + "스캔" 버튼 기본 작동: + + + "저장"버튼 기본 작동: + + + 어플리케이션 + + + 한 개의 NAPS2 창만 허용하기 + 정보 @@ -186,26 +231,44 @@ 축소 + + 실제 크기로 확대 + + + 창 크기와 함께 크기 변경 + - Save + 저장 + + + 모두 저장 + + + 선택된 항목 저장 - Save All as PDF + 모두 PDF로 저장 - Save Selected as PDF + 선택된 항목을 PDF로 저장 - Save All as Images + 모두 이미지로 저장 - Save Selected as Images + 선택된 항목을 이미지로 저장 + + + 모두 이메일 보내기 + + + 선택된 항목 이메일로 보내기 - Email All as PDF + 모두 PDF로 이메일로 보내기 - Email Selected as PDF + 선택된 항목을 PDF로 이메일 보내기 프로파일 설정 @@ -219,11 +282,20 @@ TWAIN 드라이버 + + ESCL 드라이버 + + + ESCL 네트워크 드라이버 + + + ESCL USB 드라이버 + - Apple Driver + Apple 드라이버 - SANE Driver + SANE 드라이버 장치: @@ -267,6 +339,9 @@ 자동 저장 설정 + + 자동 저장 설정 + 상세 @@ -279,8 +354,11 @@ 원본 선택 + + 장치 선택 + - Run in Background + 백그라운드에서 실행 NAPS2 @@ -301,10 +379,16 @@ OCR 언어: - OCR mode: + OCR 모드: + + + 화이트밸런스 조정 및 노이즈 제거 - Automatically run OCR after scanning + 스캔 후 자동으로 OCR 실행 + + + 스캔 후 자동으로 OCR 실행 더 많은 언어 설치하기 @@ -313,10 +397,10 @@ OCR 다운로드 - OCR 을 이용하여 스캔하고자 하는 각 언어를 선택 해 주세요.. + OCR을 이용하려면 스캔하고자 하는 언어를 다운로드 해야 합니다. - 한 개 또는 그 이상의 언어를 선택 해 주세요.: + 한 개 이상의 언어를 선택 해 주세요: 예상 다운로드 용량: {0} MB @@ -325,7 +409,7 @@ 다운로드 - 다운로드 진행상황 + 다운로드 진행 중 미리보기 @@ -340,7 +424,7 @@ {1} 의 {0} - 되돌림 + 되돌리기 선택된 {0} 개의 이미지에 적용 @@ -348,4 +432,457 @@ 전체 선택 + + 복구 + + + 나중에 + + + 스캔 된 이미지 복구 + + + {1} {2} 에 스캔된 {0} 개의 이미지를 복구 할 수 있습니다. 복구 하시겠습니까? + + + 파일명 형식 + + + 저장 확인창 건너뛰기 + + + 단일 페이지 파일 + + + PDF 암호화 + + + 보기 + + + 초기 설정으로 복원 + + + PDF 설정 + + + 인쇄 허용 + + + 고품질 인쇄 허용 + + + 문서 변경 허용 + + + 문서 조합 허용 + + + 내용 복사 허용 + + + 접근성을 위한 내용 복사 허용 + + + 주석 허용 + + + 서식 채우기 허용 + + + 기본 파일 경로: + + + 파일 경로: + + + 제목: + + + 작성자: + + + 제목: + + + 검색어: + + + 소유자 암호: + + + 사용자 암호: + + + 설정 기억하기 + + + 메타데이터 + + + 암호화 + + + 호환성 + + + 파일명 형식 + + + 년도 + + + 년도 (00-99) + + + 월 (01-12) + + + 일 (01-31) + + + 시간 (0-23) + + + 분 (00-59) + + + 초 (00-59) + + + 자동 증가 번호 (네자리) + + + 자동 증가 번호 (세자리) + + + 자동 증가 번호 (두자리) + + + 자동 증가 번호 (한자리) + + + 파일명 + + + 미리보기: + + + 높은 JPEG 품질(80 이상)을 선택한 경우 최상의 결과를 위해 프로파일에서 이미지 품질도 높히는 것이 좋습니다. + + + Jpeg 품질 + + + TIFF 옵션 + + + 압축 방법: + + + 이미지 설정 + + + 이메일 설정 + + + 설정 + + + 제공자 + + + 변경 + + + 첨부명: + + + 이메일 제공자 선택 + + + 인증 + + + 인증을 기다리는 중... + + + 오류 + + + 기술 상세 + + + 암호 + + + 다음 파일은 암호화 되어 있으며 열기 위해서는 암호가 필요합니다: + + + 파일 경로창 + + + 페이지 당 한 개 파일 + + + 스캔 당 한 개 파일 + + + Patch-T 를 통한 파일 분할 + + + 상세 정보 + + + 자동 저장 후 이미지 지우기 + + + 세션간 이미지 유지하기 + + + 페이지 크기 사용자 설정 + + + 이름 (선택사항) + + + 크기 + + + Custom Resolution + + + Dpi + + + TWAIN 완료 대기중... + + + 상세 프로필 설정 + + + 이미지 품질 + + + 최대 품질 (용량 큼) + + + 빈 페이지 + + + 빈 페이지 제외 + + + 백색 임계값 + + + 적용범위 임계값 + + + 후처리 + + + 스캔 된 페이지 기울기 보정 + + + 스캔 후 밝기/대비 적용 + + + 정렬을 기반으로 한 오프셋 폭 (WIA) + + + 용지 사이즈에 맞춰 늘리기 + + + 용지 사이즈에 맞춰 자르기 + + + 양면 인쇄 된 페이지 뒤집기 + + + 양면인쇄의 뒷면 회전하기 + + + WIA 버전: + + + TWAIN 구현: + + + 일괄 스캔 + + + 준비가 되면 시작을 눌러주세요.. + + + 스캔 설정 + + + 출력 + + + 프로파일: + + + 한 장 스캔 + + + 여러장 스캔하기 (매 장마다 팝업 표시) + + + 여러장 스캔하기 (매 장마다 정해진 시간 만큼 지연) + + + 스캔 장 수: + + + 스캔 사이의 지연 시간 (초): + + + NAPS2 에 이미지 불러오기 + + + 한 개의 파일로 저장하기 + + + 여러 파일로 저장하기 + + + 시작 + + + 다음 스캔 + + + 스캔 {0} 준비됨. + + + 폴더 열기 + + + 도구 + + + 디버그 로깅 활성화 + + + 공유 + + + 스캐너 공유 + + + 스캐너 공유 + + + 공유된 스캐너는 로컬 네트워크에 있는 다른 컴퓨터의 NAPS2 프로필 설정에서 "ESCL 드라이버"를 선택해 사용할 수 있습니다. + + + 공유된 스캐너 설정 + + + {0} 의 공유를 중단하시겠습니까? + + + NAPS2를 닫아도 공 + + + 복수의 언어... + + + 복수의 언어 + + + 기본 TWAIN 진행과정 보이기 + + + 나누기 + + + 합치기 + + + IP 수동 설정 + + + IP 수동 설정 + + + IP/호스트 + + + 포트 + + + 연결 + + + 연결 오류. + + + 항상 묻기 + + + 장치를 찾는 중... + + + {0} 장치 발견됨. + + + 1 장치 발견됨. + + + 발견된 장치 없음. + + + 사이드바 + + + 스캐너를 찾을 수 없습니까? NAPS2 Flatpak의 한계에 대해서 확인해보세요. + + + 키보드 단축키 + + + 키보드 단축키 + + + {0} 프로파일을 사용하여 스캔하기 + + + 기본 프로파일을 사용하여 스캔하기 + + + 새로운 프로파일을 사용하여 스캔하기 + + + 할당 + + + 할당 취소 + + + 동작 + + + 단축키 + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.lt.resx b/NAPS2.Lib/Lang/Resources/UiStrings.lt.resx index 8321eb3698..b76490b63c 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.lt.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.lt.resx @@ -54,6 +54,18 @@ Įterpti + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + Atlikta @@ -165,6 +177,12 @@ Atvirkščiai + + Reverse All + + + Reverse Selected + Išvalyti @@ -174,6 +192,33 @@ Kalba + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + "Save" button default action: + + + Application + + + Only allow a single NAPS2 instance + Apie programą @@ -186,9 +231,21 @@ Sumažinti + + Tikrasis mastelis + + + Pagal langą + Save + + Save All + + + Save Selected + Save All as PDF @@ -201,6 +258,12 @@ Save Selected as Images + + Email All + + + Email Selected + Email All as PDF @@ -219,6 +282,15 @@ TWAIN tvarkyklė + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -267,6 +339,9 @@ Automatinio išsaugojimo nustatymai + + Automatinio išsaugojimo nustatymai + Papildomi @@ -279,6 +354,9 @@ Pasirinkti šaltinį + + Select Device + Run in Background @@ -303,9 +381,15 @@ OCR mode: + + Fix white balance and remove noise + Automatically run OCR after scanning + + Pre-emptively run OCR after scanning + Įdiegti daugiau kalbų @@ -348,4 +432,457 @@ Pažymėti viską + + Atkurti + + + Ne dabar + + + Atkurti nuskenuotus vaizdus + + + {0} vaizdas(-ai) nuskenuotas(-i) nuo {1} iki {2} nebuvo išsaugoti ir gali būti atstatyti. Ar norite juos atstatyti? + + + Vietaženkliai + + + Skip save prompt + + + Single page files + + + Užšifruoti PDF + + + Rodyti + + + Atstatyti numatytas parinktis + + + PDF nustatymai + + + Leisti spausdinti + + + Leisti spausdinti visiškos kokybės + + + Leisti keisti dokumentą + + + Leisti surinkti dokumentą + + + Leisti kopijuoti turinį + + + Leisti kopijuoti turinį prieinamumui + + + Leisti anotacijas + + + Lesti pildyti formą + + + Default File Path: + + + Kelias iki bylos: + + + Antraštė: + + + Autorius: + + + Tema: + + + Raktažodžiai: + + + Savininko slaptažodis: + + + Naudotojo slaptažodis: + + + Atsiminti šiuos nustatymus + + + Meta-duomenys + + + Šifravimas + + + Suderinamumas + + + Vietaženkliai + + + Metai + + + Metai (00-99) + + + Mėnuo (01-12) + + + Diena (01-31) + + + Valanda (0-23) + + + Minutė (00-59) + + + Sekundė (00-59) + + + Automatinis numerio didinimas (4 skaičiai) + + + Automatinis numerio didinimas (3 skaičiai) + + + Automatinis numerio didinimas (2 skaičiai) + + + Auto-incrementing number (1 digits) + + + Bylos pavadinimas + + + Peržiūra: + + + For high JPEG qualities (80+), also increase Image Quality in your profile for best results. + + + JPEG kokybė + + + Tiff Options + + + Compression: + + + Vaizdo parametrai + + + El. pašto nustatymai + + + Settings + + + Provider + + + Change + + + Priedo pavadinimas: + + + Choose Email Provider + + + Authorize + + + Waiting for authorization... + + + Klaida + + + Technical Details + + + Slaptažodis + + + The following file is encrypted and requires a password to open: + + + Prompt for file path + + + Viena byla puslapiui + + + Viena byla skenavimui + + + Separate files by Patch-T + + + Daugiau informacijos + + + Išvalyti vaizdus po išsaugojimo + + + Keep images across sessions + + + Pasirinktinis puslapio dydis + + + Name (optional) + + + Dimensions + + + Custom Resolution + + + Dpi + + + Laukiama TWAIN užbaigimo... + + + Išplėstiniai profilio nustatymai + + + Vaizdo kokybė + + + Maksimali kokybė (didelės bylos) + + + Tušti puslapiai + + + Pašalinti tuščius puslapius + + + Baltumo slenkstis + + + Padengimo slenkstis + + + Post-processing + + + Deskew scanned pages + + + Pritaikyti ryškumą/kontrastą po skenavimo + + + Offset width based on alignment (WIA) + + + Stretch to page size + + + Crop to page size + + + Flip duplexed pages + + + Flip back sides of duplex pages + + + Wia Version: + + + TWAIN realizacija: + + + Paketinis skenavimas + + + Paspauskite Pradėti, kai būsite pasiruošę. + + + Skenavimo nustatymai + + + Išvestis + + + Profilis: + + + Viengubas skenavimas + + + Kelių puslapių skenavimas (pranešimas tarp skenavimų) + + + Kelių puslapių skenavimas (fiksuotas uždelsimas tarp skenavimų) + + + Skenavimų kiekis: + + + Laikas tarp skenavimų (sekundės): + + + Įkrauti vaizdus į NAPS2 + + + Išsaugoti į vieną bylą + + + Išsaugoti į kelias bylas + + + Pradėti + + + Kitas skenavimas + + + Paruoštas skenavimui {0}. + + + Open Folder + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + 1 device found. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.lv.resx b/NAPS2.Lib/Lang/Resources/UiStrings.lv.resx index 1fc07b62ee..4b6c582adc 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.lv.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.lv.resx @@ -16,7 +16,7 @@ Par - Copyright {0} NAPS2 Contributors + {0} NAPS2 Contributors autortiesības Ikonas no: @@ -25,7 +25,7 @@ Pārbaudīt atjauninājumus - OK + Apstiprināt Ziedot @@ -54,6 +54,18 @@ Ielīmēt + + Atcelt + + + Atkārtot + + + Atcelt {0} + + + Atkārtot {0} + Pabeigts @@ -118,7 +130,7 @@ Asināt - Document Correction + Koriģēt dokumentu Atiestatīt @@ -165,15 +177,48 @@ Apvērst + + Apriezt visu + + + Apgriezt izvēlēto + Notīrīt - Clear All + Notīrīt visu Valoda + + Uzstādījumi + + + Saskarne + + + "Skenēt" komandkarte maina noklusējuma profilu + + + Show "Profiles" toolbar + + + Show page numbers + + + Pogas "Skenēt" noklusējuma darbība: + + + Pogas "Saglabāt" noklusējuma darbība: + + + Lietotne + + + Atļaut tikai vienu NAPS2 eksemplāru + Par @@ -186,26 +231,44 @@ Samazināt + + Oriģinālais izmērs + + + Mērogot ar logu + - Save + Saglabāt + + + Saglabāt visu + + + Saglabāt izvēlēto - Save All as PDF + Saglabāt visu kā PDF Save Selected as PDF - Save All as Images + Saglabāt visu kā attēlus Save Selected as Images + + Visu nosūtīt uz e-pastu + + + Nosūtīt uz e-pastu izvēlēto + - Email All as PDF + Visu nosūtīt uz e-pastu kā PDF - Email Selected as PDF + Nosūtīt uz e-pastu izvēlēto kā PDF Profila iestatījumi @@ -219,11 +282,20 @@ TWAIN draiveris + + ESCL draiveris + + + ESCL tīkla draiveris + + + ESCL USB draiveris + - Apple Driver + Apple draiveris - SANE Driver + SANE draiveris Ierīce: @@ -267,6 +339,9 @@ Automātiski saglabāt iestatījumus + + Automātiski saglabāt iestatījumus + Papildu @@ -279,6 +354,9 @@ Izvēlēties avotu + + Izvēlieties ierīci + Darbojas fonā @@ -301,11 +379,17 @@ OCR valoda: - OCR mode: + OCR režīms: + + + Fiksēt baltā balansu un dzēst trokšņus Automātiski palaist OCR (teksta atpazīšanu) pēc skenēšanas + + Pēc skanēšanas sākt teksta atpazīšanu + Iegūt vairāk valodu @@ -348,4 +432,457 @@ Izvēlēties visu + + Atgūt + + + Ne tagad + + + Atgūt skenētos attēlus + + + {0} attēls(i), kas skenēti {1} {2}, iespējams nav saglabāti un ir atgūstami. Vai vēlaties tos atgūt? + + + Vietrāži + + + Izlaist saglabāšanas dialogu + + + Vienas lappuses datnes + + + Šifrēt PDF + + + Rādīt + + + Atiestatīt uz noklusēto + + + PDF iestatījumi + + + Atļaut drukāšanu + + + Atļaut augstākās kvalitātes drukāšanu + + + Atļaut dokumenta modificēšanu + + + Atļaut dokumenta montāžu + + + Atļaut satura kopēšanu + + + Atļaut pieejamības satura kopēšanu + + + Atļaut anotācijas + + + Atļaut veidlapas aizpildīšanu + + + Noklusētais datnes ceļš: + + + Datnes ceļš: + + + Virsraksts: + + + Autors: + + + Temats: + + + Atslēgvārdi: + + + Īpašnieka parole: + + + Lietotāja parole: + + + Atcerēties šos iestatījumus + + + Metadati + + + Šifrēšana + + + Savietojamība + + + Vietrāži + + + Gads + + + Gads (00-99) + + + Mēnesis (01-12) + + + Diena (01-31) + + + Stunda (0-23) + + + Minūte (00-59) + + + Sekunde (00-59) + + + Automātiski palielināt skaitli (4 cipari) + + + Automātiski palielināt skaitli (3 cipari) + + + Automātiski palielināt skaitli (2 cipari) + + + Automātiski palielināt skaitli (1 numurs) + + + Datnes nosaukums + + + Priekšskatījums: + + + Ja iestatīta augsta JPEG kvalitāte (80+), paaugstini iestatījumu Attēla kvalitāte arī profilā, lai iegūtu labāko rezultātu. + + + JPEG kvalitāte + + + Tiff iestatījumi + + + Kompresija: + + + Attēla iestatījumi + + + E-pasta iestatījumi + + + Uzstādījumi + + + Provaideris + + + Mainīt + + + Pielikuma nosaukums: + + + Izvēlēties e-pasta provaideri + + + Autorizēt + + + Gaida autorizāciju... + + + Kļūda + + + Tehniskās detaļas + + + Parole + + + The following file is encrypted and requires a password to open: + + + Datnes ceļa dialogs + + + Lappuse vienā datnē + + + Skenējums vienā datnē + + + Sadalīt datnēs pēc Patch-T + + + Vairāk informācijas + + + Notīrīt attēlus pēc saglabāšanas + + + Saglabāt attēlus starp seansiem + + + Pielāgots lapas izmērs + + + Nosaukums (neobligāts) + + + Izmēri + + + Custom Resolution + + + Dpi + + + Gaida TWAIN, lai pabeigtu... + + + Papildu profila iestatījumi + + + Attēla kvalitāte + + + Maksimālā kvalitāte (lielas datnes) + + + Tukšās lapas + + + Izlaist tukšās lapas + + + Baltā slieksnis + + + Pārklājuma slieksnis + + + Pēcapstrāde + + + Izlīdzināt skenētās lapas + + + Pēc skenēšanas koriģēt spilgtumu/kontrastu + + + Platuma nobīde pēc pielīdzināšanas (WIA) + + + Izstiept līdz lappuses izmēram + + + Apgriezt līdz lappuses izmēram + + + Apvērst dupleksās lapas + + + Apgriezt puses divpusējām lapām + + + Wia versija: + + + TWAIN realizācija: + + + Partijas skenēšana + + + Nospied Sākt, kad gatavs. + + + Skenēšanas konfigurācija + + + Izvade + + + Profils: + + + Viens skenējums + + + Vairākas skenēšanas (jautāt starp skenējumiem) + + + Vairākas skenēšanas (fiksēta aizture starp skenējumiem) + + + Skenējumu skaits: + + + Laiks starp skenēšanām (sekundes): + + + Ielādēt attēlus NAPS2 + + + Saglabāt vienā datnē + + + Saglabāt vairākās datnēs + + + Sākt + + + Nākamais skenējums + + + Gatavs skenēšanai {0}. + + + Atvērt mapi + + + Rīki + + + Ieslēgt atkļūdošanu + + + Kopīgot + + + Skenera kopīgošana + + + Skenera kopīgošana + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Vai tiešām vēlaties atcelt {0} kopīgošanu? + + + Share even when NAPS2 is closed + + + Vairākas valodas... + + + Vairākas valodas + + + Show native TWAIN progress + + + Split + + + Apvienot + + + Manuālā IP adrese + + + Manuālā IP adrese + + + IP/Host adrese + + + Ports + + + Pieslēgties + + + Pieslēguma kļūda. + + + Vienmēr prasīt + + + Tiek meklētas ierīces... + + + Atrasta(-s) {0} ierīce(-s). + + + Atrasta 1 ierīce. + + + Neviena ierīce nav atrasta. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Skenēt ar noklusējuma profilu + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.nb.resx b/NAPS2.Lib/Lang/Resources/UiStrings.nb.resx index 32bce2d70d..f7911144ec 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.nb.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.nb.resx @@ -54,6 +54,18 @@ Paste + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + Ferdig @@ -165,6 +177,12 @@ Omvendt + + Reverse All + + + Reverse Selected + Tøm @@ -174,6 +192,33 @@ Språk + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + "Save" button default action: + + + Application + + + Only allow a single NAPS2 instance + Om @@ -186,9 +231,21 @@ Zoom Out + + Zoom Actual + + + Skaler med Vindu + Save + + Save All + + + Save Selected + Save All as PDF @@ -201,6 +258,12 @@ Save Selected as Images + + Email All + + + Email Selected + Email All as PDF @@ -219,6 +282,15 @@ TWAIN Driver + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -267,6 +339,9 @@ Innstillinger for Automatisk Lagring + + Innstillinger for Automatisk Lagring + Avansert @@ -279,6 +354,9 @@ Velg kilde + + Select Device + Run in Background @@ -303,9 +381,15 @@ OCR mode: + + Fix white balance and remove noise + Kjør OCR automatisk etter skanning + + Pre-emptively run OCR after scanning + Installer flere språk @@ -348,4 +432,457 @@ Velg alle + + Gjenopprett + + + Ikke nå + + + Gjenopprett slettede bilder + + + {0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them? + + + Placeholders + + + Skip save prompt + + + Single page files + + + Krypter PDF + + + Vis + + + Restore Defaults + + + PDF Settings + + + Tillat utskrift + + + Tillat fullkvalitets utskrift + + + Tillat dokumentmodifikasjon + + + Tillat sammenstilling av dokument + + + Tillat innholdskopiering + + + Tillat innholdskopiering for tilgjengelighet + + + Tillat merknader + + + Tillat skjemautfylling + + + Standard filsti: + + + Filsti: + + + Title: + + + Forfatter: + + + Subject: + + + Keywords: + + + Owner Password: + + + User Password: + + + Remember these settings + + + Metadata + + + Kryptering + + + Kompatibilitet + + + Placeholders + + + Year + + + Year (00-99) + + + Month (01-12) + + + Dag (01-31) + + + Time (0-23) + + + Minute (00-59) + + + Sekund (00-59) + + + Auto-økningstall (4 siffer) + + + Auto-økningstall (3 siffer) + + + Auto-økningstall (2 siffer) + + + Auto-incrementing number (1 digits) + + + Filnavn + + + Forhåndsvisning: + + + For high JPEG qualities (80+), also increase Image Quality in your profile for best results. + + + Jpeg Quality + + + Tiff Options + + + Komprimering: + + + Innstillinger for bilde + + + E-postinnstillinger + + + Settings + + + Provider + + + Change + + + Vedleggsnavn: + + + Velg eposttilbyder + + + Autoriser + + + Waiting for authorization... + + + Feil + + + Technical Details + + + Password + + + The following file is encrypted and requires a password to open: + + + Prompt for file path + + + One file per page + + + One file per scan + + + Separate files by Patch-T + + + More info + + + Tøm bilder etter lagring + + + Keep images across sessions + + + Egendefinert Sidestørrelse + + + Name (optional) + + + Størrelser + + + Custom Resolution + + + Dpi + + + Waiting for TWAIN to complete... + + + Avanserte profilinnstillinger + + + Bildekvalitet + + + Høyeste kvalitet (store filer) + + + Blanke sider + + + Utelat blanke sider + + + White Threshold + + + Dekningsterskel + + + Post-processing + + + Deskew scanned pages + + + Tilfør lyshet/kontrast etter skanning + + + Offset width based on alignment (WIA) + + + Stretch to page size + + + Beskjær til sidestørrelse + + + Speilvend dupleks-sider + + + Flip back sides of duplex pages + + + Wia Version: + + + Twain Implementation: + + + Serieskanning + + + Press Start when ready. + + + Skanneoppsett + + + Output + + + Profil: + + + Enkeltskanning + + + Multiple scans (prompt between scans) + + + Multiple scans (fixed delay between scans) + + + Number of scans: + + + Time between scans (seconds): + + + Load images into NAPS2 + + + Lagre til en enkelt fil + + + Lagre til flere filer + + + Start + + + Next Scan + + + Ready for scan {0}. + + + Open Folder + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + 1 device found. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.nl.resx b/NAPS2.Lib/Lang/Resources/UiStrings.nl.resx index 6d516abcca..caec66ff0c 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.nl.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.nl.resx @@ -16,7 +16,7 @@ Over - Copyright {0} NAPS2 Contributors + Copyright {0} NAPS2 bijdragers Pictogrammen van: @@ -28,7 +28,7 @@ OK - Doneer + Doneren Profielen @@ -54,6 +54,18 @@ Plakken + + Ongedaan maken + + + Opnieuw + + + {0} ongedaan maken + + + Opnieuw {0} + Gereed @@ -79,16 +91,16 @@ PDF opslaan - PDF instellingen + PDF-instellingen Afbeeldingen opslaan - Instellingen afbeeldingen + Afbeeldingsinstellingen - PDF mailen + PDF e-mailen E-mailinstellingen @@ -118,7 +130,7 @@ Verscherpen - Document Correction + Documentcorrectie Reset @@ -139,7 +151,7 @@ Rechtzetten - Aangepast roteren + Aangepast draaien Omhoog @@ -165,15 +177,48 @@ Omdraaien + + Alles ongedaan maken + + + Selectie omkeren + Wissen - Clear All + Alles wissen Taal + + Instellingen + + + Uiterlijk + + + Menu "Scan" wijzigt het standaardprofiel + + + Werkbalk "Profielen" weergeven + + + Paginanummers weergeven + + + Standaardactie van de knop "Scannen": + + + Standaardactie van de knop "Opslaan": + + + Toepassing + + + Slechts één NAPS2-instantie toestaan + Over @@ -186,26 +231,44 @@ Uitzoomen + + Zoom 100% + + + Schalen + - Save + Opslaan + + + Alles opslaan + + + Selectie bewaren - Save All as PDF + Alles opslaan als PDF - Save Selected as PDF + Selectie opslaan als PDF - Save All as Images + Alles opslaan als afbeeldingen - Save Selected as Images + Selectie opslaan als afbeeldingen + + + Alles e-mailen + + + Selectie e-mailen - Email All as PDF + Alles als PDF e-mailen - Email Selected as PDF + Selectie als PDF e-mailen Profielinstellingen @@ -219,8 +282,17 @@ TWAIN driver + + ESCL Driver + + + ESCL Netwerk Driver + + + ESCL USB Driver + - Apple Driver + Apple driver SANE driver @@ -267,6 +339,9 @@ Instellingen voor automatisch opslaan + + Instellingen voor automatisch opslaan + Geavanceerd @@ -274,13 +349,16 @@ Annuleren - Selecteer + Selecteren Bron selecteren + + Apparaat selecteren + - In de achtergrond draaien + Op de achtergrond uitvoeren NAPS2 @@ -292,20 +370,26 @@ Not Another PDF Scanner - OCR instellingen + OCR-instellingen - PDFs doorzoekbaar maken middels OCR + Maak PDF's doorzoekbaar met OCR - OCR taal: + OCR-taal: - OCR modus: + OCR-modus: + + + Witbalans verbeteren en ruis verminderen Start OCR automatisch na het scannen + + Start OCR automatisch na het scannen + Meer talen installeren @@ -316,7 +400,7 @@ Voor OCR moet u elke taal downloaden die u wilt laten herkennen. - Kies een of meer talen: + Kies één of meer talen: Geschatte omvang: {0} MB @@ -325,7 +409,7 @@ Downloaden - Downloadvoortgang + Voortgang downloaden Voorbeeld @@ -340,7 +424,7 @@ {0} van {1} - Herstel + Herstellen Toepassen op {0} geselecteerde afbeeldingen @@ -348,4 +432,457 @@ Alles selecteren + + Herstellen + + + Niet nu + + + Herstel gescande afbeeldingen + + + {0} afbeelding(en) gescand op {1} om {2} zijn mogelijk niet opgeslagen en zijn te herstellen. Wilt u herstellen? + + + Placeholders + + + Opslaanvenster overslaan + + + Enkele-pagina bestanden + + + PDF versleutelen + + + Tonen + + + Standaardinstellingen herstellen + + + PDF-instellingen + + + Afdrukken toestaan + + + Afdrukken met volledige kwaliteit toestaan + + + Document bewerken toestaan + + + Documentassemblage toestaan + + + Inhoud kopiëren toestaan + + + Inhoud kopiëren voor toegankelijkheid toestaan + + + Annotaties toestaan + + + Formulier invullen toestaan + + + Standaard bestandspad: + + + Bestandspad: + + + Titel: + + + Auteur: + + + Onderwerp: + + + Sleutelwoorden: + + + Wachtwoord eigenaar: + + + Wachtwoord gebruiker: + + + Onthoud deze instellingen + + + Metadata + + + Versleuteling + + + Compatibiliteit + + + Placeholders + + + Jaar + + + Jaar (00-99) + + + Maand (01-12) + + + Dag (01-31) + + + Uur (0-23) + + + Minuut (00-59) + + + Seconde (00-59) + + + Automatisch ophogend nummer (4 cijfers) + + + Automatisch ophogend nummer (3 cijfers) + + + Automatisch ophogend nummer (2 cijfers) + + + Automatisch toenemend nummer (1 cijfer) + + + Bestandsnaam + + + Voorbeeld: + + + Voor hoge-kwaliteit JPEG (80+), verhoog ook de afbeeldingskwaliteit in uw profiel voor het beste resultaat. + + + JPEG kwaliteit + + + Tiff opties + + + Compressie: + + + Afbeeldingsinstellingen + + + E-mailinstellingen + + + Instellingen + + + Provider + + + Wijzig + + + Naam bijlage: + + + Kies e-mail provider + + + Autoriseren + + + Wacht op autorisatie... + + + Fout + + + Technische details + + + Wachtwoord + + + Het volgende bestand is versleuteld en heeft een wachtwoord nodig om te openen: + + + Vraag om bestandspad + + + Eén bestand per pagina + + + Eén bestand per scan + + + Bestanden scheiden met Patch-T + + + Meer informatie + + + Afbeeldingen verwijderen na opslaan + + + Afbeeldingen behouden tussen sessies + + + Aangepaste paginagrootte + + + Naam (optioneel) + + + Afmetingen + + + Aangepaste resolutie + + + Dpi + + + Wachten op TWAIN scan... + + + Geavanceerde profielinstellingen + + + Kwaliteit afbeelding + + + Maximale kwaliteit (grote bestanden) + + + Lege pagina's + + + Lege pagina's uitsluiten + + + Witdrempel + + + Dekkingsdrempel + + + Nabewerking + + + Gescande pagina's rechtzetten + + + Helderheid/contrast toepassen na scannen + + + Breedte aanpassen op basis van uitlijning (WIA) + + + Uitrekken naar paginagrootte + + + Bijsnijden naar paginagrootte + + + Dubbelzijdige pagina's omdraaien + + + Achterzijde van dubbelzijdige pagina's omdraaien + + + Wia versie: + + + Twain-implementatie: + + + Automatisch scannen + + + Klik 'Beginnen' om te starten. + + + Scaninstellingen + + + Uitvoer + + + Profiel: + + + Enkele scan + + + Meerdere scans (vraag tussen 2 scans) + + + Meerdere scans (vaste pauze tussen scans) + + + Aantal scans: + + + Tijd tussen scans (seconden): + + + Afbeeldingen laden in NAPS2 + + + Opslaan naar één bestand + + + Opslaan naar meerdere bestanden + + + Beginnen + + + Volgende scan + + + Klaar voor scannen {0}. + + + Map openen + + + Hulpmiddelen + + + Debug logging toestaan + + + Delen + + + Scanner delen + + + Scanner delen + + + Gedeelde scanners kunnen worden gebruikt vanaf andere computers op het lokale netwerk door "ESCL Driver" te selecteren in de NAPS2-profielinstellingen van de andere computer. + + + Instellingen gedeelde scanner + + + Weet u zeker dat u wilt stoppen met het delen van {0}? + + + Delen zelfs als NAPS2 gesloten is + + + Meerdere talen... + + + Meerdere talen + + + TWAIN-voortgang weergegeven + + + Splitsen + + + Samenvoegen + + + Handmatig IP-adres + + + Handmatig IP-adres + + + IP/Host + + + Poort + + + Verbinden + + + Verbindingsfout. + + + Altijd vragen + + + Zoeken naar apparaten... + + + {0} apparaten gevonden. + + + 1 apparaat gevonden. + + + Geen apparaten gevonden. + + + Zijbalk + + + Kun je je scanner niet vinden? Lees dan over de beperkingen van NAPS2 Flatpak. + + + Sneltoetsen + + + Sneltoetsen + + + Scannen met Profiel {0} + + + Scannen met standaardprofiel + + + Scannen met nieuw profiel + + + Toewijzen + + + Toewijzing verwijderen + + + Actie + + + Sneltoets + + + Thema: + + + Bewerken met... + + + Bewerken met {0} + + + Bewerken met... + + + Fout bij starten van toepassing {0} + + + Scanner delen stoppen + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.nn.resx b/NAPS2.Lib/Lang/Resources/UiStrings.nn.resx index 49066a1b95..d2c567f071 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.nn.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.nn.resx @@ -16,7 +16,7 @@ Om - Copyright {0} NAPS2 Contributors + Opphavsrett {0} NAPS2 bidragsytarar Ikonar frå: @@ -54,6 +54,18 @@ Lim inn + + Angre + + + Gjenta + + + Angre {0} + + + Gjenta {0} + Utført @@ -73,7 +85,7 @@ Profilar - Import + Importer Lagre PDF @@ -118,7 +130,7 @@ Gjer skarpare - Document Correction + Dokumentretting Resett @@ -165,20 +177,53 @@ Reverser + + Reverse All + + + Reverse Selected + Fjern - Clear All + Fjern alle Språk + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + "Save" button default action: + + + Application + + + Only allow a single NAPS2 instance + Om - Zoom + Forstørre Zoom inn @@ -186,26 +231,44 @@ Zoom ut + + Zoom faktisk + + + Skaler med vindu + - Save + Lagre + + + Save All + + + Save Selected - Save All as PDF + Lagre alle som PDF - Save Selected as PDF + Lagre valde som PDF - Save All as Images + Lagre alle som bilder - Save Selected as Images + Lagre valde som bilde + + + Email All + + + Email Selected - Email All as PDF + Send alle som PDF - Email Selected as PDF + Send valde som PDF i e-post Profilsettingar @@ -219,11 +282,20 @@ TWAIN driver + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + - Apple Driver + Apple driver - SANE Driver + SANE driver Skannar: @@ -267,6 +339,9 @@ Settingar for autolagring + + Settingar for autolagring + Avansert @@ -279,6 +354,9 @@ Vel kjelde + + Vel eining + Kjør i bakgrunnen @@ -286,7 +364,7 @@ NAPS2 - NAPS2 - {0} + NAPS2 – {0} Not Another PDF Scanner @@ -303,9 +381,15 @@ OCR modus: + + Fiks kvitbalanse og fjern støy + Kjør OCR automatisk etter skanning + + Kjør OCR automatisk etter skanning + Hent fleire språk @@ -348,4 +432,457 @@ Vel alle + + Hent tilbake + + + Ikkje nå + + + Hent tilbake skanna bilde + + + {0} bilde skanna på {1} ved {2} er kanskje ikkje lagra, men kan gjenopprettast. Vil du gjenoppretta dei? + + + Plassholdar + + + Hopp over lagre-spørsmål + + + Enkelside filer + + + Krypter PDF + + + Vis + + + Hent standard + + + PDF settingar + + + Tillat utskrift + + + Tillat full kvalitet utskrift + + + Tillat dokumentmodifikasjon + + + Tillat dokumentsamansetting + + + Tillat innhaldskopiering + + + Tillat innhaldskopiering for tilgjengelighet + + + Tillat merknader + + + Tillat formfylling + + + Standard fil-sti: + + + Fil-sti: + + + Tittel: + + + Forfattar: + + + Emne: + + + Stikkord: + + + Eigarpassord: + + + Brukarpassord: + + + Hugs desse settingane + + + Metadata + + + Kryptering + + + Kompatibilitet + + + Plassholdar + + + År + + + År (00-99) + + + Månad (01-12) + + + Dag (01-31) + + + Time (0-23) + + + Minutt (00-59) + + + Sekund (00-59) + + + Auto-inkrementer nummer (4 siffer) + + + Auto-inkrementer nummer (3 siffer) + + + Auto-inkrementer nummer (2 siffer) + + + Auto-incrementing number (1 digits) + + + Filnamn + + + Førehandsvising: + + + For høg JPEG kvalitet (80+), auk også bildekvalitet i profilen for å få best resultat. + + + Jpeg-kvalitet + + + Innstillingar for Tiff + + + Komprimering: + + + Bildesettingar + + + Epost-settingar + + + Settings + + + Leverandør + + + Endre + + + Vedleggsnamn: + + + Velg epost-leverandør + + + Autoriser + + + Venter på autorisasjon... + + + Feil + + + Tekniske detaljar + + + Passord + + + The following file is encrypted and requires a password to open: + + + Spør etter fil-sti + + + Ei fil per side + + + Ei fil per skann + + + Separate filer av Patch-T + + + Meir informasjon + + + Fjern foto etter lagring + + + Keep images across sessions + + + Eigendefinert sidestorleik + + + Namn (valfri) + + + Dimensjonar + + + Custom Resolution + + + Dpi + + + Venter på at TWAIN blir ferdig... + + + Avanserte profilsettingar + + + Bildekvalitet + + + Maksimum kvalitet (blir stor fil) + + + Tome sider + + + Ikkje bruk tome sider + + + Kvit-terskel + + + Dekningsterskel + + + Post-prosessering + + + Rett opp skanna sider + + + Juster lysstyrke/kontrast etter skanning + + + Offsett bredde basert på justering (WIA) + + + Strekk til sidestorleik + + + Beskjær til sidestorleik + + + Vend tosidige sider + + + Vend baksida av dupleks-sider + + + Wia versjon: + + + Twain implementering: + + + Multiskann + + + Trykk start når du er klar. + + + Skann-konfigurasjon + + + Data frå skannar + + + Profil: + + + Enkelt skann + + + Fleire skann (spør mellom skann) + + + Fleire skann (fast opphald mellom skann) + + + Tal på skann: + + + Tid mellom skann (sekund): + + + Last inn bilde i NAPS2 + + + Lagre til ei enkel fil + + + Lagre til fleire filer + + + Start + + + Neste skann + + + Klar for skann {0}. + + + Opne folder + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Fleire språk... + + + Fleire språk + + + Vis innebygd TWAIN framgang + + + Splitt + + + Slå saman + + + Manuell IP + + + Manuell IP + + + IP/Vert + + + Port + + + Kopla til + + + Tilkoplingsfeil. + + + Spør alltid + + + Søker etter einingar... + + + {0} einingar var funnen. + + + 1 eining funnen. + + + Ingen eining funnen. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Tastatursnarvegar + + + Tastatursnarvegar + + + Skann med profilen {0} + + + Scan With Default Profile + + + Skann med ny profil + + + Tilknytt + + + Opphev tilordning + + + Handling + + + Snarveg + + + Tema: + + + Endre med... + + + Endre med {0} + + + Endre med... + + + Feil under oppstart av programmet {0} + + + Stopp skannerdeling + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.pl.resx b/NAPS2.Lib/Lang/Resources/UiStrings.pl.resx index c0d0e009e3..34ca577ef2 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.pl.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.pl.resx @@ -54,6 +54,18 @@ Wklej + + Cofnij + + + Ponów + + + Cofnij {0} + + + Ponów {0} + Gotowe @@ -165,6 +177,12 @@ Оdwróć + + Odwróć wszystkie + + + Odwróć zaznaczone + Wyczyść @@ -174,6 +192,33 @@ Wybór języka + + Ustawienia + + + Interfejs + + + Menu „Skanuj” zmienia profil domyślny + + + Pokaż pasek narzędzi „Profile” + + + Pokaż numery stron + + + Domyślna czynność przycisku „Skanuj”: + + + Domyślna czynność przycisku „Zapisz”: + + + Aplikacja + + + Zezwól tylko na pojedynczą instancję NAPS2 + O programie @@ -186,9 +231,21 @@ Oddal + + Powiększenie rzeczywiste + + + Przeskaluj do okna + Zapisz + + Zapisz wszystkie + + + Zapisz zaznaczone + Zapisz wszystkie jako PDF @@ -201,6 +258,12 @@ Zapisz zaznaczone jako obrazy + + Wyślij wszystkie + + + Wyślij zaznaczone + Wyślij wszystkie jako PDF @@ -219,6 +282,15 @@ Sterownik TWAIN + + Sterownik ESCL + + + Sterownik sieciowy ESCL + + + Sterownik USB ESCL + Sterownik Apple @@ -267,6 +339,9 @@ Ustawienia automatycznego zapisywania + + Ustawienia automatycznego zapisywania + Zaawansowane @@ -279,6 +354,9 @@ Wybierz źródło + + Wybierz urządzenie + Uruchom w tle @@ -303,9 +381,15 @@ Tryb OCR: + + Napraw balans bieli i usuń szum + Automatycznie uruchom OCR po skanowaniu + + Zapobiegawczo uruchom OCR po skanowaniu + Uzyskaj więcej języków @@ -348,4 +432,457 @@ Zaznacz wszystko + + Odzyskaj + + + Nie teraz + + + Odzyskaj zeskanowane obrazy + + + {0} plik(i) przeskanowane dnia {1} o {2} mogły nie zostać zapisane, ale są do odzyskania. Czy chcesz je odzyskać? + + + Symbole zastępcze + + + Pomiń monit zapisu + + + Pliki jednostronicowe + + + Zaszyfruj PDF + + + Wyświetl + + + Przywróć domyślne + + + Ustawienia PDF + + + Zezwalaj na drukowanie + + + Zezwalaj na pełną jakość wydruku + + + Zezwalaj na modyfikowanie dokumentu + + + Zezwalaj na składanie dokumentu + + + Zezwalaj na kopiowanie zawartości + + + Zezwalaj na kopiowanie zawartości w celu ułatwienia dostępu + + + Zezwalaj na adnotacje + + + Zezwalaj na wypełnianie formularzy + + + Domyślna ścieżka pliku: + + + Ścieżka pliku: + + + Tytuł: + + + Autor: + + + Temat: + + + Słowa kluczowe: + + + Hasło właściciela: + + + Hasło użytkownika: + + + Zapamiętaj te ustawienia + + + Metadane + + + Szyfrowanie + + + Zgodność + + + Symbole zastępcze + + + Rok + + + Rok (00-99) + + + Miesiąc (01-12) + + + Dzień (01-31) + + + Godzina (0-23) + + + Minuta (00-59) + + + Sekunda (00-59) + + + Numer autoinkrementacji (4 cyfry) + + + Numer autoinkrementacji (3 cyfry) + + + Numer autoinkrementacji (2 cyfry) + + + Numer autoinkrementacji (1 cyfra) + + + Nazwa pliku + + + Podgląd: + + + Aby uzyskać wysoką jakość JPEG (80+) i najlepsze wyniki, również zwiększ w swoim profilu jakość obrazu. + + + Jakość JPEG + + + Opcje TIFF + + + Kompresja: + + + Ustawienia obrazu + + + Ustawienia poczty + + + Ustawienia + + + Dostawca + + + Zmień + + + Nazwa załącznika: + + + Wybierz dostawcę poczty e-mail + + + Autoryzuj + + + Oczekiwanie na autoryzację... + + + Błąd + + + Szczegóły techniczne + + + Hasło + + + Następujący plik jest zaszyfrowany i wymaga hasła do otwarcia: + + + Pytaj o ścieżkę pliku + + + Jeden plik na stronę + + + Jeden plik na skanowanie + + + Oddzielne pliki według Patch-T + + + Więcej informacji + + + Wyczyść obrazy po zapisaniu + + + Zachowaj obrazy między sesjami + + + Niestandardowy rozmiar strony + + + Nazwa (opcjonalnie) + + + Wymiary + + + Rozdzielczość niestandardowa + + + dpi + + + Oczekiwanie na zakończenie zadania TWAIN... + + + Zaawansowane ustawienia profilu + + + Jakość obrazu + + + Maksymalna jakość (duże pliki) + + + Puste strony + + + Wyklucz puste strony + + + Próg bieli + + + Próg pokrycia + + + Przetwarzanie końcowe + + + Prostuj zeskanowane strony + + + Zastosuj jasność/kontrast po skanowaniu + + + Szerokość przesunięcia na podstawie wyrównania (WIA) + + + Rozciągnij do rozmiaru strony + + + Przytnij do rozmiaru strony + + + Odwróć strony dwustronne + + + Odwróć tyły stron dwustronnych + + + Wersja WIA: + + + Implementacja TWAIN: + + + Skanowanie wsadowe + + + Kliknij Rozpocznij, gdy gotowe. + + + Konfiguracja skanowania + + + Wyjście + + + Profil: + + + Pojedyncze skanowanie + + + Wielokrotne skanowanie (monit pomiędzy skanowaniami) + + + Wielokrotne skanowanie (stały odstęp pomiędzy skanowaniami) + + + Liczba skanowań: + + + Czas pomiędzy skanowaniami (sekundy): + + + Załaduj obrazy do NAPS2 + + + Zapisz do pojedynczego pliku + + + Zapisz do wielu plików + + + Rozpocznij + + + Następne skanowanie + + + Gotowy na skanowanie {0}. + + + Otwórz folder + + + Narzędzia + + + Włącz rejestrowanie debugowania + + + Udostępnij + + + Udostępnianie skanera + + + Udostępnianie skanera + + + Udostępnionych skanerów można używać z innych komputerów w sieci lokalnej, wybierając opcję „Sterownik ESCL” w ustawieniach profilu NAPS2 drugiego komputera. + + + Ustawienia udostępnionego skanera + + + Czy na pewno chcesz zatrzymać udostępnianie {0}? + + + Udostępnij, nawet jeśli NAPS2 jest zamknięty + + + Wiele języków... + + + Wiele języków + + + Pokaż postęp natywnego TWAIN + + + Podziel + + + Połącz + + + Ręczny adres IP + + + Ręczny adres IP + + + Adres IP / Host + + + Port + + + Połącz + + + Błąd połączenia. + + + Zawsze pytaj + + + Wyszukiwanie urządzeń... + + + Znalezione urządzenia: {0}. + + + Znaleziono 1 urządzenie. + + + Nie znaleziono urządzeń. + + + Pasek boczny + + + Nie możesz znaleźć swojego skanera? Przeczytaj o ograniczeniach Flatpaka NAPS2. + + + Skróty klawiszowe + + + Skróty klawiszowe + + + Skanuj za pomocą profilu {0} + + + Skanuj za pomocą domyślnego profilu + + + Skanuj za pomocą nowego profilu + + + Przypisz + + + Usuń przypisanie + + + Czynność + + + Skrót + + + Motyw: + + + Edytuj w... + + + Edytuj w {0} + + + Edytuj w... + + + Błąd podczas uruchamiania aplikacji {0} + + + Zatrzymaj udostępnianie skanera + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.pt-BR.resx b/NAPS2.Lib/Lang/Resources/UiStrings.pt-BR.resx index 7e533bba1a..ee335f3afd 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.pt-BR.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.pt-BR.resx @@ -54,6 +54,18 @@ Colar + + Desfazer + + + Refazer + + + Desfazer {0} + + + Refazer {0} + Concluído @@ -118,7 +130,7 @@ Nitidez - Document Correction + Correção de Documento Restaurar @@ -165,15 +177,48 @@ Inverter + + Inverter tudo + + + Inverter selecionados + Limpar - Clear All + Limpar Tudo Idioma + + Configurações + + + Interface + + + O menu do botão "Digitalizar" altera o perfil padrão + + + Exibir barra de ferramentas "Perfis" + + + Mostrar números de páginas + + + Ação padrão do botão "Digitalizar": + + + Ação padrão do botão "Salvar": + + + Aplicativo + + + Permitir apenas uma instância do NAPS2 + Sobre @@ -186,26 +231,44 @@ Diminuir + + Tamanho Original + + + Ajustar à Janela + - Save + Salvar + + + Salvar Tudo + + + Salvar Selecionados - Save All as PDF + Salvar Tudo como PDF - Save Selected as PDF + Salvar seleção como PDF - Save All as Images + Salvar tudo como Imagens - Save Selected as Images + Salvar seleção como Imagens + + + Enviar tudo + + + Enviar Selecionados - Email All as PDF + Enviar tudo como PDF - Email Selected as PDF + E-mail Selecionado como PDF Configurar Perfil @@ -219,6 +282,15 @@ Driver TWAIN + + Driver ESCL + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -267,6 +339,9 @@ Configurações de salvamento automático + + Configurações de salvamento automático + Avançado @@ -277,7 +352,10 @@ Selecionar - Selecionar origem + Selecionar Origem + + + Selecionar dispositivo Executar em segundo plano @@ -303,9 +381,15 @@ Modo do OCR: + + Corrigir equilíbrio branco e remover ruído + Executar automaticamente o OCR após a digitalização + + Antecipar execução do OCR durante a digitalização + Instalar mais idiomas @@ -346,6 +430,459 @@ Aplicar para todas as {0} imagens selecionadas - Seleciona tudo + Selecionar tudo + + + Recuperar + + + Não agora + + + Recuperar imagens digitalizadas + + + {0} imagem(s) digitalizadas em {1} a {2} pode(m) não ter sido salva(s) mas é(são) recuperável(is). Deseja recuperá-las? + + + Substituições + + + Pular pedido de salvar + + + Arquivos de página única + + + Proteger PDF + + + Mostrar + + + Restaurar padrões + + + Configurações de PDF + + + Permitir Impressão + + + Permitir Qualidade Máxima da Impressão + + + Permitir Modificação do Documento + + + Permitir União do Documento + + + Permitir Cópia do Conteúdo + + + Permitir Cópia de Conteúdo para Acessibilidade + + + Permitir anotações + + + Permitir Preenchimento de Formulário + + + Caminho padrão do arquivo: + + + Caminho do arquivo: + + + Título: + + + Autor: + + + Assunto: + + + Palavras-chave: + + + Senha do Proprietário: + + + Senha do Usuário: + + + Lembrar essas configurações + + + Metadados + + + Criptografia + + + Compatibilidade + + + Substituições + + + Ano + + + Ano (00-99) + + + Mês (01-12) + + + Dia (01-31) + + + Hora (0-23) + + + Minutos (00-59) + + + Segundos (00-59) + + + Auto-incrementar número (4 dígitos) + + + Auto-incrementar número (3 dígitos) + + + Auto-incrementar número (2 dígitos) + + + Auto-incrementar número (1 dígito) + + + Nome do Arquivo + + + Visualizar: + + + Para altas qualidades de JPEG (80+), aumente também a Qualidade da Imagem no seu Perfil para obter melhores resultados. + + + Qualidade JPEG + + + Opções Tiff + + + Compressão: + + + Configurações de imagem + + + Configurações de e-mail + + + Configurações + + + Provedor + + + Alterar + + + Nome do anexo: + + + Escolha o provedor de e-mail + + + Autorizar + + + Aguardando autorização... + + + Erro + + + Detalhes Técnicos + + + Senha + + + O seguinte arquivo é criptografado e requer uma senha para abrir: + + + Solicitar o caminho do arquivo + + + Um arquivo por página + + + Um arquivo por digitalização + + + Separar Arquivos por Patch-T + + + Mais informações + + + Limpar imagens após salvar + + + Manter imagens entre as sessões + + + Tamanho de página personalizada + + + Nome (opcional) + + + Dimensões + + + Resolução Personalizada + + + Dpi + + + Aguardando driver TWAIN... + + + Configurações Avançadas do Perfil + + + Qualidade da imagem + + + Alta qualidade (arquivos grandes) + + + Páginas em Branco + + + Excluir páginas em branco + + + Limiar de branco + + + Limiar de Cobertura + + + Pós-processamento + + + Endireitar páginas digitalizadas + + + Aplicar Brilho/Contraste após digitalizar + + + Compensar largura com base no alinhamento (WIA) + + + Estender para o tamanho da página + + + Cortar para o tamanho da página + + + Virar páginas frente e verso + + + Virar o verso das páginas em duplex + + + Versão Wia: + + + Implementação Twain: + + + Digitalização em lote + + + Pressione Iniciar quando estiver pronto. + + + Configuração da digitalização + + + Saída + + + Perfil: + + + Única digitalização + + + Múltiplas digitalizações (Solicitar confirmação entre cada uma) + + + Múltiplas digitalizações (Fixar tempo entre cada uma) + + + Número de digitalizações: + + + Tempo entre digitalizações (segundos): + + + Carregar imagens em NAPS2 + + + Salve em um único arquivo + + + Salvar em múltiplos arquivos + + + Iniciar + + + Próxima Digitalização + + + Pronto para digitalizar {0}. + + + Abrir pasta + + + Ferramentas + + + Habilitar log de depuração + + + Compartilhar + + + Compartilhar Scanner + + + Compartilhar Scanner + + + Scanners compartilhados podem ser usados a partir de outros computadores na rede local selecionando "Driver ESCL" nas configurações de perfil NAPS2 do outro computador. + + + Configurações do Scanner Compartilhado + + + Tem certeza que deseja interromper o compartilhamento {0}? + + + Compartilhar mesmo quando o NAPS2 estiver fechado + + + Múltiplos idiomas... + + + Múltiplos idiomas + + + Mostrar progresso nativo do TWAIN + + + Separar + + + Combinar + + + IP manual + + + IP manual + + + IP/Host + + + Porta + + + Conectar + + + Erro de conexão. + + + Sempre Perguntar + + + Procurando dispositivos... + + + {0} dispositivos encontrados. + + + 1 dispositivo encontrado. + + + Nenhum dispositivo encontrado. + + + Barra lateral + + + Não consegue encontrar seu scanner? Leia sobre as limitações do NAPS2 Flatpak. + + + Atalhos de Teclado + + + Atalhos de Teclado + + + Digitalizar com o Perfil {0} + + + Digitalizar com o perfil padrão + + + Digitalizar com novo perfil + + + Associar + + + Desassociar + + + Ação + + + Atalho + + + Tema: + + + Editar com... + + + Editar com {0} + + + Editar com... + + + Erro ao iniciar o aplicativo {0} + + + Parar Compartilhamento de Scanner \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.pt-PT.resx b/NAPS2.Lib/Lang/Resources/UiStrings.pt-PT.resx index 2075fd0a0a..f9025b85d1 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.pt-PT.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.pt-PT.resx @@ -13,22 +13,22 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Sobre + Acerca - Copyright {0} NAPS2 Contributors + Direitos de autor {0}, Colaboradores NAPS2 Ícones de: - Check for updates + Procurar atualizações OK - Donate + Doar Perfis @@ -46,25 +46,37 @@ Digitalizar - Set Default + Definir como padrão Copiar - Paste + Colar + + + Desfazer + + + Refazer + + + Desfazer {0} + + + Refazer {0} Terminado - Default + Padrão - Novo Perfil + Novo perfil - Batch Scan + Digitalização em lote OCR @@ -76,22 +88,22 @@ Importar - Salvar PDF + Guardar PDF Definições PDF - Salvar imagens + Guardar imagens - Definições da Imagem + Definições de imagem - Enviar PDF por Email + Enviar por e-mail - Definições de Email + Definições de e-mail Imprimir @@ -100,43 +112,43 @@ Imagem - View + Ver Recortar - Brightness / Contrast + Brilho/Contraste - Hue / Saturation + Tom/Saturação - Preto e Branco + Preto e branco - Sharpen + Nitidez - Document Correction + Correção do documento - Reset + Repor - Rotate + Rodar - Rodar para a esquerda + Rodar à esquerda - Rodar para a direita + Rodar à direita - Rodar + Inverter - Deskew + Endireitar Rotação personalizada @@ -148,82 +160,142 @@ Mover para baixo - Reorder + Reordenar - Intercalamento + Intercalar Desintercalar - Alternate Interleave + Intercalar alternado - desintercalar alternativo + Desintercalar alternado - Reverse + Inverter + + + Inverter tudo + + + Inverter seleção Limpar - Clear All + Limpar tudo Idioma + + Definições + + + Interface + + + O menu "Digitalizar" altera o perfil padrão + + + Mostrar barra de ferramentas "Perfis" + + + Mostrar número de páginas + + + Ação padrão do botão "Digitalizar": + + + Ação padrão do botão "Guardar": + + + Aplicação + + + Permitir apenas uma instância NAPS2 + - Sobre + Acerca Ampliar - Zoom In + Ampliar - Zoom Out + Reduzir + + + Tamanho real + + + Ajustar à janela - Save + Guardar + + + Guardar tudo + + + Guardar seleção - Save All as PDF + Guardar tudo como PDF - Save Selected as PDF + Guardar seleção como PDF - Save All as Images + Guardar tudo como imagens - Save Selected as Images + Guardar seleção como imagem + + + Enviar tudo por e-mail + + + E-mail selecionado - Email All as PDF + Enviar tudo por e-mail como PDF - Email Selected as PDF + Enviar seleção por e-mail como PDF - Configurar Perfil + Definições de perfil Nome a exibir: - Driver WIA + Controlador WIA - Driver TWAIN + Controlador TWAIN + + + Controlador ESCL + + + Controlador de rede ESCL + + + Controlador ESCL USB - Apple Driver + Controlador Apple - SANE Driver + Controlador SANE Dispositivo: @@ -232,16 +304,16 @@ Escolha o dispositivo - Usar configuração pré-definida + Usar definições predefinidas - Use native UI + Usar interface nativa Origem do papel: - Tamanho da folha: + Tamanho da página: Resolução: @@ -250,7 +322,7 @@ Brilho: - Qualidade: + Profundidade de cor: Alinhamento horizontal: @@ -262,10 +334,13 @@ Contraste: - Enable Auto Save + Ativar guardar automático - Auto Save Settings + Definições de gravação automática + + + Definições de gravação automática Avançado @@ -274,13 +349,16 @@ Cancelar - Select + Selecionar - Select Source + Selecionar origem + + + Selecionar dispositivo - Run in Background + Enviar para segundo plano NAPS2 @@ -295,37 +373,43 @@ Configurar OCR - Make PDFs searchable using OCR + Tornar PDF pesquisáveis via OCR Idioma OCR: - OCR mode: + Modo OCR: + + + Corrigir balanço de brancos e remover ruído - Automatically run OCR after scanning + Executar OCR após a digitalização + + + Antecipar execução OCR após a digitalização - Instalar mais idiomas + Obter mais idiomas - Transferir OCR + Descarregar OCR - Using OCR requires you to download each language you want to scan. + Para usar OCR, é necessário descarregar os idiomas que pretende digitalizar. - Select one or more languages: + Escolha um ou mais idiomas: Tamanho estimado da transferência: {0} MB - Transferir + Descarregar - Evolução da Transferência + Progresso da descarga Pré-visualizar @@ -340,12 +424,465 @@ {0} de {1} - Revert + Reverter - Apply to all {0} selected images + Aplicar às {0} imagens selecionadas - Select All + Selecionar tudo + + + Recuperar + + + Agora não + + + Recuperar imagens digitalizadas + + + {0} digitalizações realizadas em {1} às {2} podem não ter sido guardadas e são recuperáveis. Recuperar agora? + + + Marcadores + + + Ignorar questão para guardar + + + Ficheiros de uma página + + + Cifrar PDF + + + Mostrar + + + Restaurar padrões + + + Definições PDF + + + Permitir impressão + + + Permitir impressão de alta qualidade + + + Permitir alteração do documento + + + Permitir organização do documento + + + Permitir cópia de conteúdo + + + Permitir cópia de conteúdo para acessibilidade + + + Permitir anotações + + + Permitir preenchimento de formulários + + + Caminho padrão do ficheiro: + + + Caminho do ficheiro: + + + Título: + + + Autor: + + + Assunto: + + + Palavras-chave: + + + Palavra-passe de proprietário: + + + Palavra-passe do utilizador: + + + Memorizar definições + + + Metadados + + + Cifra + + + Compatibilidade + + + Marcadores + + + Ano + + + Anos (00-99) + + + Mês (01-12) + + + Dias (01-31) + + + Horas (0-23) + + + Minutos (00-59) + + + Segundos (00-59) + + + Incrementar número automaticamente (4 dígitos) + + + Incrementar número automaticamente (3 dígitos) + + + Incrementar número automaticamente (2 dígitos) + + + Incrementar número automaticamente (1 dígito) + + + Nome do ficheiro: + + + Pré-visualizar: + + + Para qualidade JPEG alta (80+), aumente a qualidade da imagem no seu perfil para obter melhores resultados. + + + Qualidade JPEG + + + Opções TIFF + + + Compressão: + + + Definições de imagem + + + Definições de e-mail + + + Definições + + + Serviço + + + Alterar + + + Nome do anexo: + + + Escolha um serviço de e-mail + + + Autorizar + + + A aguardar autorização... + + + Erro + + + Detalhes técnicos + + + Palavra-passe + + + O seguinte ficheiro está cifrado e requer uma palavra-passe para ser aberto: + + + Pedir caminho do ficheiro + + + Um ficheiro por página + + + Um ficheiro por digitalização + + + Separar ficheiros com Patch-T + + + Mais informação + + + Limpar imagens após guardar + + + Manter imagens entre sessões + + + Tamanho de página personalizado + + + Nome (opcional) + + + Dimensões + + + Resolução personalizada + + + PPP + + + Aguardar que o TWAIN termine... + + + Definições avançadas do perfil + + + Qualidade da imagem + + + Alta qualidade (ficheiros grandes) + + + Páginas vazias + + + Excluir páginas vazias + + + Limite de brancos + + + Limite de cobertura + + + Pós-processamento + + + Endireitar páginas digitalizadas + + + Aplicar brilho/contraste após digitalização + + + Compensar largura em função do alinhamento (WIA) + + + Ajustar ao tamanho da página + + + Recortar ao tamanho da página + + + Inverter páginas digitalizadas em frente e verso + + + Inverter lados das páginas duplas + + + Versão WIA: + + + Implementação TWAIN: + + + Digitalização em lote + + + Prima Iniciar quando quiser. + + + Configurações de digitalização + + + Resultado + + + Perfil: + + + Digitalização individual + + + Múltiplas digitalizações (perguntar entre digitalizações) + + + Múltiplas digitalizações (intervalo fixo entre digitalizações) + + + Número de digitalizações: + + + Tempo entre digitalizações (segundos): + + + Carregar imagens em NAPS2 + + + Guardar para um ficheiro + + + Guardar para vários ficheiros + + + Iniciar + + + Digitalização seguinte + + + Pronto para a {0} digitalização. + + + Abrir pasta + + + Ferramentas + + + Ativar registos de depuração + + + Partilhar + + + Partilha de digitalizador + + + Partilha de digitalizador + + + Os digitalizadores partilhados podem ser usados a partir de outros computadores na rede local, selecionando "Controlador ESCL" nas definições do perfil NAPS2 do outro computador. + + + Definições do digitalizador partilhado + + + Tem a certeza que pretende parar de partilhar {0}? + + + Partilhar mesmo se NAPS2 estiver fechado + + + Vários idiomas... + + + Vários idiomas + + + Mostrar progresso TWAIN nativo + + + Separar + + + Combinar + + + IP manual + + + IP manual + + + IP/Anfitrião + + + Porta + + + Conectar + + + Erro de ligação. + + + Perguntar sempre + + + A procurar dispositivos... + + + {0} dispositivo(s) encontrado(s). + + + 1 dispositivo encontrado. + + + Não foram encontrados dispositivos. + + + Barra lateral + + + Não consegue encontrar o seu digitalizador? Leia sobre as limitações do NAPS2 Flatpak. + + + Teclas de atalho + + + Teclas de atalho + + + Digitalizar com o perfil {0} + + + Digitalizar com o perfil padrão + + + Digitalizar com o novo perfil + + + Atribuir + + + Remover atribuição + + + Ação + + + Atalho + + + Tema: + + + Editar com... + + + Editar com {0} + + + Editar com... + + + Erro ao iniciar aplicação {0} + + + Parar partilha de digitalizador \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.resx b/NAPS2.Lib/Lang/Resources/UiStrings.resx index cdccf299f5..ee2f198c70 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.resx @@ -159,6 +159,18 @@ Paste + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + Done @@ -270,6 +282,12 @@ Reverse + + Reverse All + + + Reverse Selected + Clear @@ -279,6 +297,33 @@ Language + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + "Save" button default action: + + + Application + + + Only allow a single NAPS2 instance + About @@ -291,6 +336,12 @@ Zoom Out + + Zoom Actual + + + Scale With Window + Save @@ -336,6 +387,15 @@ TWAIN Driver + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -399,6 +459,9 @@ Select Source + + Select Device + Run in Background @@ -423,9 +486,15 @@ OCR mode: + + Fix white balance and remove noise + Automatically run OCR after scanning + + Pre-emptively run OCR after scanning + Get more languages @@ -624,6 +693,9 @@ Email Settings + + Settings + Provider @@ -672,6 +744,9 @@ Clear images after saving + + Keep images across sessions + Custom Page Size @@ -681,6 +756,12 @@ Dimensions + + Custom Resolution + + + Dpi + Waiting for TWAIN to complete... @@ -726,6 +807,9 @@ Flip duplexed pages + + Flip back sides of duplex pages + Wia Version: @@ -783,4 +867,130 @@ Open Folder + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + 1 device found. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.ro.resx b/NAPS2.Lib/Lang/Resources/UiStrings.ro.resx index 0e6f46702f..2a836e1b8b 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.ro.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.ro.resx @@ -16,7 +16,7 @@ Despre - Copyright {0} NAPS2 Contributors + Drepturi de autor {0} Colaboratori NAPS2 Icoane din: @@ -28,7 +28,7 @@ Ok - Donate + Donează Profiluri @@ -54,6 +54,18 @@ Lipire + + Anulare + + + Refacere + + + Anulează {0} + + + Refacere {0} + Finalizat @@ -118,10 +130,10 @@ Claritate - Document Correction + Corecție Document - Reset + Resetare Rotește @@ -157,7 +169,7 @@ Deîntrețesere - Alternate Interleave + Intercalare alternativă Deintercalare alternativă @@ -165,15 +177,48 @@ Inversează + + Inversează Tot + + + Inversează ce este selectat + Şterge - Clear All + Ștergere totală Limbă + + Setări + + + Interfață + + + Meniul "Scanare" schimbă profilul implicit + + + Afișează bara de instrumente "Profiluri" + + + Arată numerele paginii + + + Acțiune implicit buton "Scanează": + + + Acțiune implicită buton "Salvează: + + + Aplicație + + + Permite doar o singură instanță NAPS2 + Despre @@ -186,26 +231,44 @@ Micșorează + + Zoom actual + + + Scalează odată cu fereastra + - Save + Salvează + + + Salvează tot + + + Salvează selecția - Save All as PDF + Salvează toate ca PDF - Save Selected as PDF + Salvați cele selectate ca PDF - Save All as Images + Salvează toate ca Imagini - Save Selected as Images + Salvați cele selectate ca imagini + + + Trimiteți prin e-mail tuturor + + + Trimiteți prin e-mail partea selectată - Email All as PDF + Trimiteți prin e-mail tuturor ca PDF - Email Selected as PDF + Trimiteți prin e-mail partea selectată ca PDF Configurări profil @@ -219,8 +282,17 @@ Driver TWAIN + + Driver-ul ESCL + + + Driver de rețea ESCL + + + Driver USB ESCL + - Apple Driver + Driver Apple Driver SANE @@ -265,7 +337,10 @@ Activează salvarea automată - Setări de salvare automată + Setări salvare automată + + + Setări salvare automată Avansat @@ -279,6 +354,9 @@ Alegeți sursa + + Selectați dispozitivul + Rulează în fundal @@ -289,7 +367,7 @@ NAPS2 - {0} - Not Another PDF Scanner + Nici alt scaner PDF Configurează OCR @@ -301,11 +379,17 @@ Limba OCR: - OCR mode: + Modul OCR: + + + Repară balanța de alb și elimină zgomotul Rulează automat modulul OCR după scanare + + Rulează automat modulul OCR după scanare + Instalează mai multe limbi @@ -348,4 +432,457 @@ Selectează tot + + Recuperează + + + Nu acum + + + Recuperează imaginile scanate + + + {0} imagini scanate în {1} la {2} nu au fost salvate și pot fi recuperate. Doriți să le recuperăm? + + + Înlocuitori + + + Omite notificarea de salvare + + + Fișier cu o singură pagină. + + + Criptează PDF + + + Arată + + + Restaurare valori implicite + + + Setări PDF + + + Permite imprimarea + + + Permite printarea la calitate maxima + + + Permite modificarea documentului + + + Permite asamblarea documentului + + + Permite copierea conținutului + + + Permite copierea conținutului pentru Accesibilitate + + + Permite note explicative + + + Permite completarea formularului + + + Cale implicită fișier: + + + Cale fișier: + + + Titlu: + + + Autor: + + + Subiect: + + + Cuvinte cheie: + + + Parolă Autor: + + + Parolă utilizator: + + + Păstrează aceste setări + + + Metadate + + + Criptare + + + Compatibilitate + + + Înlocuitori + + + Anul + + + Anul (00-99) + + + Luna (01-12) + + + Ziua (01-31) + + + Ora (0-23) + + + Minute (00-59) + + + Secunde (00-59) + + + Incrementează automat numărul (patru cifre) + + + Incrementează automat numarul (trei cifre) + + + Incrementează automat numărul (două cifre) + + + Incrementează automat numărul (o cifră) + + + Nume fişier + + + Previzualizare: + + + Pentru calități JPEG înalte (80+), creșteți calitatea imaginii în profilul dvs. pentru cele mai bune rezultate. + + + Calitate JPEG + + + Opțiuni Tiff + + + Comprimare: + + + Preferințe imagine + + + Configurări email + + + Setări + + + Furnizor + + + Schimbă + + + Numele ataşamentului: + + + Alege serviciul de email + + + Autorizează + + + Se așteaptă autorizarea.... + + + Eroare + + + Detalii tehnice + + + Parolă + + + Următorul fișier este criptat și necesită o parolă pentru a se deschide: + + + Cere calea fișierului + + + Un fișier pe pagină + + + Fișiere individuale per scanare + + + Separarea fișierelor pe baza codurilor Patch-T + + + Mai multe informații + + + Șterge imaginile după salvare + + + Păstrează imaginile pe durata sesiunilor + + + Mărime personalizată a paginii + + + Nume (opțional) + + + Dimensiuni + + + Custom Resolution + + + Dpi + + + Se așteaptă ca scanerul TWAIN să termine... + + + Setări avansate profil + + + Calitatea Imaginii + + + Calitate maximă (fișiere mari) + + + Pagini goale + + + Elimină paginile goale + + + Limita albă + + + Pragul de acoperire + + + Postprocesare + + + Îndreaptă paginile scanate + + + Aplică luminozitate/contrast după scanare + + + Offset bazat pe aliniere (WIA) + + + Redimensionează la mărimea paginii + + + Decupează la mărimea paginii + + + Întoarce paginile de pe verso + + + Întoarcere laturi anterioare ale paginilor duplex + + + Versiunea Wia: + + + Implementare Twain: + + + Scanare multiplă + + + Apasă Start când ești pregătit. + + + Configurare scanare + + + Rezultat + + + Profil: + + + O singură scanare + + + Scanare multiplă (cu confirmare între scanări) + + + Scanare multiplă (cu o pauză între scanări) + + + Numarul de scanări: + + + Durată între scanări (secunde): + + + Încarcă imagini în NAPS2 + + + Salvează într-un singur fișier + + + Salvează în fișiere separate + + + Începe + + + Următoarea scanare + + + Gata de scanare {0}. + + + Deschide folder-ul + + + Unelte + + + Activează jurnalul de depanare + + + Distribuie + + + Distribuire scaner + + + Distribuire scaner + + + Scanerele partajate pot fi utilizate de alte calculatoare din rețeaua locală selectând "ESCL Driver" din setările de profil ale celorlalte calculatoare NAPS2. + + + Setări scaner partajat + + + Ești sigur că vrei să oprești distribuirea {0}? + + + Distribuie chiar și atunci când NAPS2 este închis + + + Limbi multiple... + + + Limbi multiple + + + Arată progresul nativ TWAIN + + + Împarte + + + Combină + + + IP manual + + + IP manual + + + IP/Host + + + Port + + + Conectare + + + Eroare de conexiune. + + + Întreabă întotdeauna + + + Căutare dispozitive... + + + {0} dispozitive găsite. + + + Un dispozitiv găsit. + + + Niciun dispozitiv găsit. + + + Bară laterală + + + Nu îți găsești scanerul? Citește despre limitările NAPS2 Flatpak. + + + Comenzi rapide tastatură + + + Comenzi rapide tastatură + + + Scanează cu profilul {0} + + + Scanare cu profilul implicit + + + Scanare cu profil nou + + + Asociază + + + Anulează atribuirea + + + Acţiune + + + Comandă rapidă + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.ru.resx b/NAPS2.Lib/Lang/Resources/UiStrings.ru.resx index 99c0c55865..f2719841f9 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.ru.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.ru.resx @@ -16,16 +16,16 @@ О программе - Copyright {0} NAPS2 Contributors + Авторские права защищены {0} Контрибьюторы NAPS2 Значки из: - Проверить наличие обновлений + Проверять наличие обновлений - OK + ОК Пожертвовать @@ -54,6 +54,18 @@ Вставить + + Отменить + + + Повторить + + + Отменить {0} + + + Повторить {0} + Готово @@ -118,7 +130,7 @@ Резкость - Document Correction + Коррекция документа Сброс @@ -142,10 +154,10 @@ Другой поворот - Выше + Переместить выше - Ниже + Переместить ниже Изменить порядок @@ -163,17 +175,50 @@ Альтернативное устранение чередования - Обратить + В обратном порядке + + + Обратить всё + + + Обратить выделенное Очистить - Clear All + Очистить всё Язык + + Настройки + + + Интерфейс + + + Меню "Сканировать" меняет профиль по умолчанию + + + Показывать панель "Профили" + + + Показывать номера страниц + + + Действие кнопки "Сканировать" по умолчанию: + + + Действие кнопки «Сохранить»: + + + Приложение + + + Разрешать только один экземпляр NAPS2 + О программе @@ -186,26 +231,44 @@ Уменьшить + + Исходный размер + + + Масштабировать с окном + - Save + Сохранить + + + Сохранить всё + + + Сохранить выбранные - Save All as PDF + Сохранить все как PDF - Save Selected as PDF + Сохранить выбранные как PDF - Save All as Images + Сохранить все как изображения - Save Selected as Images + Сохранить выбранные как изображения + + + Отправить всё + + + Отправить отмеченные - Email All as PDF + Отправить всё как PDF - Email Selected as PDF + Отправить отмеченные как PDF Настройки профиля @@ -219,8 +282,17 @@ Драйвер TWAIN + + Драйвер ESCL + + + Сетевой драйвер ESCL + + + USB-драйвер ESCL + - Apple Driver + Драйвер Apple Драйвер SANE @@ -267,6 +339,9 @@ Параметры автосохранения + + Параметры автосохранения + Расширенные @@ -279,6 +354,9 @@ Выбрать источник + + Выбор устройства + Запустить в фоновом режиме @@ -303,9 +381,15 @@ Режим распознавания текста (OCR): + + Исправить баланс белого и убрать шум + Автоматически запустить распознавание текста (OCR) после сканирования + + Превентивно запускать распознавание после сканирования + Установить другие языки @@ -334,7 +418,7 @@ Далее - Назад + Предыдущие {0} из {1} @@ -348,4 +432,457 @@ Выбрать все + + Восстановить + + + Не сейчас + + + Восстановить отсканированные изображения + + + Изображения ({0} шт.), сканированные {1} в {2}, возможно, не были сохранены и могут быть восстановлены. Хотите восстановить их? + + + Поля для заполнения + + + Пропустить запрос на сохранение + + + Одностраничные файлы + + + Зашифровать PDF + + + Показать + + + Восстановить по умолчанию + + + Настройки PDF + + + Разрешить печать + + + Разрешить печать в полном качестве + + + Разрешить изменение документа + + + Разрешить сборку документа + + + Разрешить копирование содержимого + + + Разрешить копирование содержимого средствами для людей с ограниченными возможностями + + + Разрешить примечания + + + Разрешить заполнение форм + + + Путь к файлу: + + + Путь к файлу: + + + Заголовок: + + + Автор: + + + Тема: + + + Ключевые слова: + + + Пароль владельца: + + + Пароль пользователя: + + + Запомнить эти настройки + + + Метаданные + + + Шифрование + + + Совместимость + + + Поля для заполнения + + + Год + + + Год (00-99) + + + Месяц (01-12) + + + День (0-31) + + + Час (0-23) + + + Минута (00-59) + + + Секунда (00-59) + + + Автоувеличение номера (4 цифры) + + + Автоувеличение номера (3 цифры) + + + Автоувеличение номера (2 цифры) + + + Автоувеличение номера (1 цифра) + + + Имя файла: + + + Предпросмотр: + + + При высоком качестве JPEG (80+) рекомендуется увеличить параметр "Максимальное качество" в настройках профиля для получения наилучших результатов сканирования.. + + + Качество JPEG + + + Настройки TIFF + + + Сжатие: + + + Настройки изображения + + + Параметры эл. почты + + + Настройки + + + Провайдер + + + Изменить + + + Имя вложения: + + + Выбрать поставщика эл. почты + + + Авторизация + + + Ожидание авторизации... + + + Ошибка + + + Технические сведения + + + Пароль + + + Этот файл зашифрован, и для открытия требуется пароль: + + + Запрашивать путь сохранения + + + Один файл на страницу + + + Один файл на сканирование + + + Разделить файлы по Patch-T + + + Дополнительно + + + Очистить картинки после сохранения + + + Сохранять изображения между сеансами + + + Другой размер страницы + + + Имя (опционально) + + + Размеры + + + Пользовательское разрешение + + + Dpi + + + Ожидание завершения работы TWAIN... + + + Расширенные настройки профиля + + + Качество изображения + + + Максимальное качество (большой размер) + + + Чистые страницы + + + Исключить пустые страницы + + + Порог белого + + + Порог покрытия + + + Постобработка + + + Выровнять отсканированные страницы + + + Применить яркость/контраст после сканирования + + + Смещение ширины при выравнивании (WIA) + + + Растянуть до размера страницы + + + Обрезать до размера страницы + + + Развернуть дуплексные страницы + + + Перевернуть обратные стороны дуплексных листов + + + Версия WIA: + + + Реализация TWAIN: + + + Пакетное сканирование + + + Нажмите «Запустить», чтобы начать. + + + Настройка сканирования + + + Вывод + + + Профили: + + + Одиночное сканирование + + + Многостраничное сканирование (запрос перед каждым сканированием) + + + Многостраничное сканирование (с задержкой по времени) + + + Количество страниц: + + + Время между сканированием (в секундах): + + + Загрузить изображения в NAPS2 + + + Сохранить в один файл + + + Сохранить как множество файлов + + + Запустить + + + Сканировать + + + Готов сканировать {0}. + + + Открыть папку + + + Инструменты + + + Включить ведение журнала отладки + + + Поделиться + + + Общий доступ к сканеру + + + Общий доступ к сканеру + + + Общие сканеры можно использовать с других компьютеров в локальной сети, выбрав "ESCL Driver" в настройках профиля NAPS2 на другом компьютере. + + + Настройки общего сканера + + + Вы уверены, что хотите отключить общий доступ к {0}? + + + Сохранять общий доступ даже при закрытом NAPS2 + + + Несколько языков... + + + Несколько языков + + + Показать "нативный" прогресс TWAIN + + + Разделить + + + Объединить + + + Ручной IP + + + Ручной IP + + + IP/Хост + + + Порт + + + Подключиться + + + Ошибка подключения. + + + Всегда спрашивать + + + Поиск устройств... + + + Найдено устройств: {0}. + + + 1 устройство найдено. + + + Устройств не найдено. + + + Боковая панель + + + Не удается найти сканер? Читайте об ограничениях NAPS2 Flatpak. + + + Горячие клавиши + + + Горячие клавиши + + + Сканировать с профилем {0} + + + Сканировать с профилем по умолчанию + + + Сканировать с новым профилем + + + Назначить + + + Сбросить + + + Действие + + + Горячая клавиша + + + Тема: + + + Редактировать с помощью... + + + Редактировать с помощью {0} + + + Редактировать с помощью... + + + Ошибка при запуске приложения {0} + + + Отключить общий доступ к сканеру + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.si.resx b/NAPS2.Lib/Lang/Resources/UiStrings.si.resx index 1b32a18b91..3ad2c09d6e 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.si.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.si.resx @@ -54,6 +54,18 @@ Paste + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + Done @@ -112,7 +124,7 @@ Hue / Saturation - Black & White + Black and White Sharpen @@ -165,6 +177,12 @@ Reverse + + Reverse All + + + Reverse Selected + Clear @@ -174,6 +192,33 @@ Language + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + "Save" button default action: + + + Application + + + Only allow a single NAPS2 instance + මෘදුකාංගය පිලිබඳ තොරතුරු @@ -186,9 +231,21 @@ Zoom Out + + Zoom Actual + + + Scale With Window + Save + + Save All + + + Save Selected + Save All as PDF @@ -201,6 +258,12 @@ Save Selected as Images + + Email All + + + Email Selected + Email All as PDF @@ -219,6 +282,15 @@ TWAIN Driver + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -229,7 +301,7 @@ Device: - Choose device + උපාංගය තෝරන්න Use predefined settings @@ -267,11 +339,14 @@ Auto Save Settings + + Auto Save Settings + ප්‍රගත - Cancel + අවලංගු කරන්න Select @@ -279,6 +354,9 @@ Select Source + + Select Device + Run in Background @@ -303,9 +381,15 @@ OCR mode: + + Fix white balance and remove noise + Automatically run OCR after scanning + + Pre-emptively run OCR after scanning + Get more languages @@ -348,4 +432,457 @@ Select All + + Recover + + + Not Now + + + Recover Scanned Images + + + {0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them? + + + Placeholders + + + Skip save prompt + + + Single page files + + + Encrypt PDF + + + Show + + + Restore Defaults + + + PDF Settings + + + මුද්‍රණයට ඉඩදේ + + + පුර්ණ ගුණාත්මයෙන් යුතුව මුද්‍රණය අනුමත කරයි + + + ලේඛන සංශෝධනය කිරීමට ඉඩ ලබාදේ + + + ලේඛන එකලස් කිරීමට ඉඩ ලබාදේ + + + අන්තර්ගතය පිටපත් කිරීමට ඉඩ ලබාදේ + + + ප්‍රවේශය සඳහා අන්තර්ගතය පිටපත් කිරීමට ඉඩ ලබාදේ + + + අනුසටහන් සඳහා ඉඩ ලබාදේ + + + ෆෝරම පිරවීමට ඉඩ ලබාදේ + + + Default File Path: + + + File Path: + + + Title: + + + රචකයා : + + + Subject: + + + Keywords: + + + Owner Password: + + + User Password: + + + Remember these settings + + + Metadata + + + Encryption + + + Compatibility + + + Placeholders + + + Year + + + Year (00-99) + + + Month (01-12) + + + Day (01-31) + + + Hour (0-23) + + + Minute (00-59) + + + Second (00-59) + + + Auto-incrementing number (4 digits) + + + Auto-incrementing number (3 digits) + + + Auto-incrementing number (2 digits) + + + Auto-incrementing number (1 digits) + + + File Name: + + + Preview: + + + For high JPEG qualities (80+), also increase Image Quality in your profile for best results. + + + Jpeg Quality + + + Tiff Options + + + Compression: + + + Image Settings + + + Email Settings + + + Settings + + + Provider + + + Change + + + ඇමුණුමෙහි නම : + + + Choose Email Provider + + + Authorize + + + Waiting for authorization... + + + Error + + + Technical Details + + + Password + + + The following file is encrypted and requires a password to open: + + + Prompt for file path + + + One file per page + + + One file per scan + + + Separate files by Patch-T + + + More info + + + Clear images after saving + + + Keep images across sessions + + + Custom Page Size + + + Name (optional) + + + Dimensions + + + Custom Resolution + + + Dpi + + + Waiting for TWAIN to complete... + + + ප්‍රගත පැතිකඩ සැකසුම් + + + Image Quality + + + Maximum quality (large files) + + + Blank Pages + + + Exclude blank pages + + + White Threshold + + + Coverage Threshold + + + Post-processing + + + Deskew scanned pages + + + Apply brightness/contrast after scan + + + Offset width based on alignment (WIA) + + + Stretch to page size + + + Crop to page size + + + Flip duplexed pages + + + Flip back sides of duplex pages + + + Wia Version: + + + Twain Implementation: + + + Batch Scan + + + Press Start when ready. + + + Scan Configuration + + + Output + + + Profile: + + + Single scan + + + Multiple scans (prompt between scans) + + + Multiple scans (fixed delay between scans) + + + Number of scans: + + + Time between scans (seconds): + + + Load images into NAPS2 + + + Save to a single file + + + Save to multiple files + + + Start + + + Next Scan + + + Ready for scan {0}. + + + Open Folder + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + 1 device found. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.sk.resx b/NAPS2.Lib/Lang/Resources/UiStrings.sk.resx index 5b606f8c5c..38d85daeed 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.sk.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.sk.resx @@ -16,7 +16,7 @@ O programe - Copyright {0} NAPS2 Contributors + Copyright {0} prispievatelia NAPS2 Ikony z: @@ -28,7 +28,7 @@ OK - Obdarovať + Darovať Profily @@ -54,6 +54,18 @@ Prilepiť + + Späť + + + Vpred + + + Späť {0} + + + Vpred {0} + Hotovo @@ -79,7 +91,7 @@ Uložiť PDF - Nastavenie PDF + Nastavenia PDF Uložiť obrázky @@ -118,7 +130,7 @@ Zaostriť - Document Correction + Oprava dokumentu Vynulovať @@ -165,15 +177,48 @@ Opačné poradie + + Obrátiť všetko + + + Obrátiť vybrané + Zmazať - Clear All + Zmazať všetko Jazyk + + Nastavenia + + + Rozhranie + + + Ponuka "Skenovať" zmení predvolený profil + + + Zobraziť panel nástrojov "Profily" + + + Zobraziť čísla stránok + + + Predvolená akcia tlačidla "Skenovať": + + + Predvolená akcia tlačidla "Uložiť": + + + Aplikácia + + + Povoliť len jednu inštanciu NAPS2 + O programe @@ -186,26 +231,44 @@ Oddialenie + + Aktuálne priblíženie + + + Mierka podľa okna + - Save + Uložiť + + + Uložiť všetko + + + Uložiť vybrané - Save All as PDF + Uložiť všetko ako PDF - Save Selected as PDF + Uložiť vybrané ako PDF - Save All as Images + Uložiť všetko ako obrázky - Save Selected as Images + Uložiť vybrané ako obrázky + + + Poslať e-mail všetkým + + + Poslať e-mail vybraným - Email All as PDF + Poslať e-mail všetkým ako PDF - Email Selected as PDF + Poslať e-mail vybraným ako PDF Nastavenia profilu @@ -219,8 +282,17 @@ Ovládač TWAIN + + Ovládač ESCL + + + Sieťový ovládač ESCL + + + Ovládač ESCL USB + - Apple Driver + Ovládač Apple Ovládač SANE @@ -267,6 +339,9 @@ Nastavenia automatického ukladania + + Nastavenia automatického ukladania + Rozšírené @@ -279,6 +354,9 @@ Vybrať zdroj + + Výber zariadenia + Spustiť na pozadí @@ -289,7 +367,7 @@ NAPS2 - {0} - Not Another PDF Scanner + Nie je to ďalší skener PDF Nastavenie OCR @@ -303,9 +381,15 @@ Režim OCR: + + Oprava vyváženia bielej a odstránenie šumu + Po skenovaní automaticky spustiť OCR + + Predbežné spustenie OCR po skenovaní + Získať viac jazykov @@ -348,4 +432,457 @@ Vybrať všetko + + Obnoviť + + + Teraz nie + + + Obnoviť naskenované obrázky + + + {0} skenovaných obrázkov na {1} v {2} zostalo neuložených a sú obnoviteľné. Chcete ich obnoviť? + + + Zástupné symboly + + + Preskočiť výzvu na uloženie + + + Jednostránkové súbory + + + Zašifrovať PDF + + + Ukázať + + + Obnoviť pôvodné + + + Nastavenia PDF + + + Povoliť tlač + + + Povoliť tlač v plnej kvalite + + + Povoliť úpravy dokumentu + + + Povoliť zostavovanie dokumentu + + + Povoliť kopírovanie obsahu + + + Povoliť dostupnosť kopírovania obsahu + + + Povoliť vysvetlivky + + + Povoliť vyplňovanie formulárov + + + Pôvodná cesta k súborom: + + + Cesta súboru: + + + Titulok: + + + Autor: + + + Predmet: + + + Kľúčové slová: + + + Heslo vlastníka: + + + Heslo užívateľa: + + + Zapamätať si tieto nastavenia + + + Metaúdaje + + + Šifrovanie + + + Kompatibilta + + + Zástupné symboly + + + Rok + + + Rok (00-99) + + + Mesiac (01-12) + + + Deň (01-31) + + + Hodina (0-23) + + + Minúta (00-59) + + + Sekunda (00-59) + + + Automatické zvyšovanie čísla (4 číslice) + + + Automatické zvyšovanie čísla (3 číslice) + + + Automatické zvyšovanie čísla (2 číslice) + + + Automaticky sa zvyšujúce číslo (1 číslica) + + + Názov súboru: + + + Náhľad: + + + Pre najlepšie výsledky vysokej kvality obrázkov JPEG (80+), zvýšte tiež Kvalitu obrázku vo svojom profile. + + + Kvalita JPEG + + + Možnosti TIFF + + + Kompresia: + + + Nastavenia obrázkov + + + Nastavenie e-mailu + + + Nastavenia + + + Poskytovateľ + + + Zmeniť + + + Názov prílohy: + + + Vyberte poskytovateľa e-mailu + + + Autorizovať + + + Čaká sa na autorizáciu... + + + Chyba + + + Technické detaily + + + Heslo + + + Nasledujúci súbor je zašifrovaný a na jeho otvorenie je potrebné heslo: + + + Výzva na zadanie cesty k súboru + + + Jeden súbor na stránku + + + Jeden súbor na skenovanie + + + Oddeľte súbory podľa Patch-T + + + Viac informácií + + + Po uložení obrázky vymažte + + + Zachovať obrázky naprieč reláciami + + + Vlastná veľkosť stránky + + + Názov (nepovinné) + + + Rozmery + + + Vlastné rozlíšenie + + + Dpi + + + Čaká sa na dokončenie TWAIN... + + + Rozšírené nastavenia profilu + + + Kvalita obrázku + + + Maximálna kvalita (veľké súbory) + + + Prázdne stránky + + + Vylúčiť prázdne stránky + + + Prah bielej + + + Prahová hodnota pokrytia + + + Následné spracovanie + + + Vyrovnanie sklonu skenovaných stránok + + + Po skenovaní použite jas/kontrast + + + Šírka posunu na základe zarovnania (WIA) + + + Roztiahnuť na veľkosť stránky + + + Orezať na veľkosť strany + + + Preklopiť obojstranný scan + + + Preklopenie zadných strán obojstranných strán + + + Verzia WIA: + + + Implementácia TWAIN: + + + Dávkové skenovanie + + + Ak budete pripravení, stlačte Štart. + + + Konfigurácia skenovania + + + Výstup + + + Profil: + + + Jedno skenovanie + + + Viacnásobné skenovanie (výzva medzi skenovaniami) + + + Viacnásobné skenovanie (s pevným oneskorením medzi skenovaniami) + + + Počet skenov: + + + Čas medzi skenovaniami (v sekundách): + + + Načítanie obrázkov do NAPS2 + + + Uložiť do jedného súboru + + + Uložiť do viacerých súborov + + + Štart + + + Ďalšie skenovanie + + + Pripravené na skenovanie {0}. + + + Otvoriť priečinok + + + Nástroje + + + Povoliť záznam ladenia + + + Zdieľať + + + Zdieľanie skenera + + + Zdieľanie skenera + + + Zdieľané skenery môžu byť použité z iných počítačov v miestnej sieti zvolením možnosti "Ovládač ESCL" v nastaveniach profilu NAPS2 iného počítača. + + + Nastavenia zdieľaného skeneru + + + Naozaj chcete prestať zdieľať {0}? + + + Zdieľať aj keď je NAPS2 zavretý + + + Viacero jazykov... + + + Viacero jazykov + + + Zobraziť prirodzený priebeh TWAIN + + + Rozdeliť + + + Kombinovať + + + Manuálne IP + + + Manuálne IP + + + IP/hostiteľ + + + Port + + + Pripojiť + + + Chyba pripojenia. + + + Vždy sa pýtať + + + Vyhľadávanie zariadení... + + + Nájdených {0} zariadení. + + + Nájdené 1 zariadenie. + + + Nenájdené žiadne zariadenia. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Skenovať pomocou predvoleného profilu + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.sl.resx b/NAPS2.Lib/Lang/Resources/UiStrings.sl.resx index c0a2fbd6f3..b882dea62d 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.sl.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.sl.resx @@ -46,7 +46,7 @@ Skeniraj - Nastavi Privzeto + Nastavi privzeto Kopiraj @@ -54,6 +54,18 @@ Prilepi + + Razveljavi + + + Uveljavi + + + Razveljevi {0} + + + Uveljjavi {0} + Končaj @@ -118,7 +130,7 @@ Izostritev - Document Correction + Popravek dokumenta Ponastavi @@ -157,23 +169,56 @@ Razpletanje - Drugačno Prepletanje + Drugačno prepletanje - Drugačno Razpletanje + Drugačno razpletanje Obrni Vrstni Red + + Obrnite vse + + + Obratno izbrano + Počisti - Clear All + Počisti vse Jezik + + Nastavitve + + + Vmesnik + + + Meni "Skeniraj" spremeni privzeti profil + + + Prikaži vrstico "Profili" + + + Prikaži številko strani + + + Privzeto dejanje gumba "Skeniraj": + + + Privzeto dejanje gumba "Shrani": + + + Aplikacija + + + Dovoli samo eno aktivno okno NAPS2 + Vizitka @@ -186,26 +231,44 @@ Oddalji + + Naravna Velikost + + + Povečava Glede Na Okno + - Save + Shrani + + + Shrani vse + + + Shrani izbrano - Save All as PDF + Shrani vse kot PDF - Save Selected as PDF + Shrani izbrano kot PDF - Save All as Images + Shrani vse kot sliko - Save Selected as Images + Shrani izbrano kot slike + + + Pošlji vse + + + Shrani izbrano - Email All as PDF + Pošlji vse kot PDF - Email Selected as PDF + E-pošta izbrana kot PDF Nastavitve Profila @@ -219,8 +282,17 @@ TWAIN Gonilnik + + ESCL gonilnik + + + ESCL omrežni gonilnik + + + ESCL USB gonilnik + - Apple Driver + Gonilnik Apple SANE gonilnik @@ -267,6 +339,9 @@ Nastavitve Samodejnega Shranjevanja + + Nastavitve Samodejnega Shranjevanja + Napredno @@ -279,8 +354,11 @@ Izberi Izvor + + Izberi napravo + - Poženi v ozadju + Izvajaj v ozadju NAPS2 @@ -303,9 +381,15 @@ Način OCR: + + Popravi ravnovesje beline in odstrani šum + Avtomatsko zaženi OCR po skeniranju + + Izvedi OCR po skeniranju + Pridobi več jezikov @@ -343,9 +427,462 @@ Povrni - Uveljavi na vseh {0} izbranih slikah + Uveljavite na vseh {0} izbranih slikah Izberi Vse + + Reši + + + Ne Sedaj + + + Reši Skenirane Slike + + + {0} slik skeniranih na {1} ob {2} morda ni bilo shranjenih in jih je mogoče povrniti. Ali jih želite povrniti? + + + Mesta + + + Preskoči okno shranjevanja + + + Datoteke z eno stranjo + + + Šifriraj PDF + + + Pokaži + + + Povrni privzete nastavitve + + + PDF Nastavitve + + + Dovolite tiskanje + + + Dovolite tiskanje v najboljši kvaliteti + + + Dovolite spreminjanje dokumenta + + + Dovolite sestavo dokumenta + + + Dovolite kopiranje vsebine + + + Dovolite kopiranje vsebine za dostopnost + + + Dovolite opombe + + + Dovolite izpolnjevanje obrazca + + + Privzeta pot datotek: + + + Pot datoteke: + + + Naslov: + + + Avtor: + + + Zadeva: + + + Ključne besede: + + + Lastniško Geslo: + + + Uporabniško Geslo: + + + Zapomni si te nastavitve + + + Metapodatki + + + Šifriranje + + + Združljivost + + + Mesta + + + Leto + + + Leto (00-99) + + + Mesec (01-12) + + + Dan (01-31) + + + Ura (0-23) + + + Minuta (00-59) + + + Sekunda (00-59) + + + Naraščajoče število (4 cifre) + + + Naraščajoče število (3 cifre) + + + Naraščajoče število (2 cifri) + + + Naraščajoče število (1 cifra) + + + Ime Datoteke + + + Predogled: + + + Za najboljši rezultat pri visoki JPEG kakovosti (80+), povišajte tudi kakovost slike v vašem profilu. + + + Jpeg Kvaliteta + + + Tiff možnosti + + + Stiskanje: + + + Nastavitve Slike + + + Nastavitve e-pošte + + + Nastavitve + + + Ponudnik + + + Spremeni + + + Ime Priponke: + + + Izberite e-poštnega ponudnika + + + Odobri + + + Čakanje na odobritev... + + + Napaka + + + Tehnične Podrobnosti + + + Geslo + + + Naslednja datoteka je šifrirana in za odpiranje zahteva geslo: + + + Zahtevaj vnos poti datoteke + + + Ena datoteka na stran + + + Ena datoteka na sken + + + Loči datoteke po Patch-T + + + Več informacij + + + Po shranjevanju počisti slike + + + Ohrani slike med sejami + + + Velikost strani po meri + + + Ime (neobvezno) + + + Velikosti + + + Custom Resolution + + + Dpi + + + Čakam na TWAIN, da dokonča... + + + Napredne nastavitve profila + + + Kvaliteta Slike + + + Maximum quality (velike datoteke) + + + Prazne Strani + + + Izpusti prazne strani + + + Meja Beline + + + Meja pokritja + + + Post-procesiranje + + + Poravnaj skenirane strani + + + Uveljavite svetlost/kontrast po skeniranju + + + Zamik glede na poravnavo (WIA) + + + Raztegni na velikost strani + + + Obreži na velikost strani + + + Preslikaj obojestranske strani + + + Obrni zadnje strani dvostranskih listov + + + Različica Wia: + + + Twain Implementacija: + + + Zaporedno Skeniranje + + + Pritisnite Začni, ko boste pripravljeni. + + + Nastavitev Skeniranja + + + Izhod + + + Profil: + + + Skeniraj eno + + + Več strani (poziv med posameznimi branji) + + + Več strani (časovni zamik med posameznimi branji) + + + Število strani: + + + Čas med posameznimi skeni (v sekundah): + + + Naloži slike v NAPS2 + + + Shrani v eno datoteko + + + Shrani v več datotek + + + Začetek + + + Naslednje Skeniranje + + + Pripravljen na skeniranje {0}. + + + Odpri Mapo + + + Orodja + + + Omogoči beleženje iskanja in odstranjevanja napak (debug) + + + Souporaba + + + Skupna raba skenerja + + + Skupna raba skenerja + + + Skenerje v skupni rabi lahko uporabljajo drugi računalniki v lokalnem omrežju z izbiro "ESCL gonilnik" v nastavitvah profila NAPS2 na drugem računalniku. + + + Skupna raba nastavitev skenerja + + + Ali si prepričan, da želiš ustaviti deljenje {0}? + + + Skupna raba, četudi je NAPS2 zaprt + + + Večjezično... + + + Večjezično + + + Prikaži nativni TWAIN napredek + + + Razdeli + + + Združi + + + Ročni IP naslov + + + Ročni IP naslov + + + IP/gostitelj + + + Vrata + + + Poveži + + + Napaka v povezavi. + + + Vedno vprašaj + + + Iščem naprave... + + + {0} naprav najdenih. + + + Najdena je bila 1 naprava. + + + Ne najdem naprav. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Skeniraj s privzetim profilom + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.sq.resx b/NAPS2.Lib/Lang/Resources/UiStrings.sq.resx index 773293028a..7a7e0f8eab 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.sq.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.sq.resx @@ -16,7 +16,7 @@ Rreth - Copyright {0} NAPS2 Contributors + Të drejtat e autorit {0} Kontribuesit e NAPS2 Ikonat nga: @@ -54,6 +54,18 @@ Ngjit + + Zhbëje + + + Ribëje + + + Zhbëje {0} + + + Ribëje {0} + U krye @@ -118,7 +130,7 @@ Thekso - Document Correction + Korrigjimi i dokumentit Rivendos @@ -165,15 +177,48 @@ Kthe mbrapsht + + Ktheji të gjitha së prapthi + + + Kthe së prapthi pjesën e zgjedhur + Pastro - Clear All + Pastro të gjitha Gjuha + + Cilësimet + + + Ndërfaqja + + + Меню "Скан" меняет профиль по умолчанию + + + Shfaqe rreshtin e mjeteve "Profilet" + + + Shfaqi numrat e faqeve + + + Veprimi i paracaktuar i butonit "Skano": + + + Veprimi i paracaktuar i butonit "Ruaj": + + + Приложение + + + Lejo vetëm një instancë të NAPS2 + Rreth @@ -186,26 +231,44 @@ Zvogëlo + + Zmadho aktualen + + + Shkallëzo me dritaren + - Save + Ruaj + + + Ruaji të gjitha + + + Ruaj pjesën e zgjedhur - Save All as PDF + Ruaji të gjitha si PDF - Save Selected as PDF + Ruaj pjesën e zgjedhur si Imazhe PDF - Save All as Images + Ruaji të gjitha si Imazhe - Save Selected as Images + Ruaj pjesën e zgjedhur si Imazhe + + + Dërgoje të gjithë me e-mail + + + Dërgo me e-mail pjesën e zgjedhur - Email All as PDF + Dërgoje të gjithë me e-mail si PDF - Email Selected as PDF + Dërgo me e-mail pjesë e zgjedhur si PDF Cilësimet e profilit @@ -219,8 +282,17 @@ Drejtuesi TWAIN + + Drejtuesi ESCL + + + Drejtuesi i rrjetit të ESCL + + + Drejtuesi USB i ESCL + - Apple Driver + Drejtuesi i Apple Drejtuesi SANE @@ -267,6 +339,9 @@ Cilësimet e ruajtjes automatike + + Cilësimet e ruajtjes automatike + I avancuar @@ -279,6 +354,9 @@ Zgjidh burimin + + Zgjidh pajisjen + Ekzekuto në sfond @@ -303,9 +381,15 @@ Regjimi OCR: + + Rregullo balancimin e ngjyrës së bardhë dhe hiq zhurmën + Ekzekuto OCR automatikisht pas skanimit + + Ekzekuto në mënyrë paraprake OCR mbas skanimit + Merr më shumë gjuhë @@ -348,4 +432,457 @@ Zgjidhi të gjitha + + Rikupero + + + Jo tani + + + Rikupero imazhet e skanuara + + + {0} imazh(e) i(të) skanuar në {1} në {2} mund të mos jenë ruajtur, dhe janë të rikuperueshme. A dëshironi t'i rikuperoni ato? + + + Vendmbajtëset + + + Kapërce shenjën e ruajtjes + + + Skedarët me një faqe + + + Shifro PDF + + + Shfaq + + + Rikthe të paracaktuarat + + + Cilësimet e PDF + + + Lejo printimin + + + Lejo printimin me cilësi të plotë + + + Lejo modifikimin e dokumentit + + + Lejo montimin e dokumentit + + + Lejo kopjimin e përmbajtjes + + + Lejo kopjimin e përmbajtjes për mundësi hyrjeje + + + Lejo Shënimet + + + Lejo plotësimin e formularit + + + Shtegu i paracaktuar i skedarit: + + + Shtegu i skedarit: + + + Titulli: + + + Autori: + + + Subjekti: + + + Fjalët kyçe: + + + Fjalëkalimi i pronarit: + + + Fjalëkalimi i përdoruesit: + + + Mbaj mend këto cilësime + + + Të dhëna mbi të dhënat + + + Shifrimi + + + Pajtueshmëria + + + Vendmbajtëset + + + Viti + + + Viti (00-99) + + + Muaji (01-12) + + + Dita (01-31) + + + Ora (0-23) + + + Minuta (00-59) + + + Sekonda (00-59) + + + Po rrit automatikisht numrin (4 shifra) + + + Po rrit automatikisht numrin (3 shifra) + + + Po rrit automatikisht numrin (2 shifra) + + + Duke rritur automatikisht numrin (1 shifër) + + + Emri i skedarit + + + Shiko paraprakisht: + + + Për cilësi të larta të JPEG (80+), gjithashtu rrisni Cilësinë e imazhit te profili juaj për rezultatet më të mira. + + + Cilësi Jpeg + + + Opsionet Tiff + + + Ngjeshja: + + + Cilësimet e imazhit + + + Cilësimet e emailit + + + Cilësimet + + + Siguruesi i shërbimit + + + Ndrysho + + + Emri i bashkëlidhjes: + + + Zgjidh siguriuesin e shërbimit të emailit + + + Autorizo + + + Po pret për autorizim... + + + Gabim + + + Detajet teknike + + + Fjalëkalimi + + + Skedari i mëposhtëm është i shifruar dhe kërkon një fjalëkaim për t'u hapur: + + + Pyet për shtegun e skedarit + + + Një skedar për faqe + + + Një skedar për skanim + + + Ndaj skedarët me Patch-T + + + Më shumë informacion + + + Pastro imazhet pas ruajtjes + + + Mbaji imazhet ndërmjet sesioneve + + + Madhësi e personalizuar e faqes + + + Emri (jo i detyrueshëm) + + + Dimensionet + + + Rezolucion i personalizuar + + + Dpi + + + Po pret që të përfundojë TWAIN... + + + Cilësimet e avancuara të profilit + + + Cilësia e imazhit + + + Cilësi maksimale (skedarë të mëdhenj) + + + Faqet bosh + + + Përjashto faqet bosh + + + Kufiri i bardhë + + + Limiti i mbulimit + + + Pas përpunimit + + + Drejto faqet e skanuara + + + Zbato shkëlqimin/kontrastin pas skanimit + + + Gjerësia e zhvendosjes bazuar në drejtimin (WIA) + + + Zgjero te madhësia e faqes + + + Prit te madhësia e faqes + + + Kthe përmbys faqet duplekse + + + Kalo mbrapa anët e faqeve duplekse + + + Versioni Wia: + + + Implementimi i Twain: + + + Skanim në grup + + + Shtyp Fillo kur të jetë gati. + + + Konfigurimi i skanimit + + + Dalja + + + Profili: + + + Skanim i vetëm + + + Shumë skanime (pyet ndërmjet skanimeve) + + + Shumë skanime (vonesë fikse ndërmjet skanimeve) + + + Numri i skanimeve: + + + Koha ndërmjet skanimeve (sekonda): + + + Ngarko imazhet në NAPS2 + + + Ruaj në skedar të vetëm + + + Ruaj në shumë skedarë + + + Fillo + + + Skanimi tjetër + + + Gati për skanim {0}. + + + Hap Dosjen + + + Mjetet + + + Aktivizo ruajtjen e logeve të debug + + + Bëje të disponueshme + + + Disponueshmëria e skanerit + + + Disponueshmëria e skanerit + + + Skanerat e disponueshëm në rrjet mund të përdoren nga kompjuterat e tjerë në rrjetin lokal duke zgjedhur "Drajveri i ESCL" në cilësimet e profilit të NAPS2. + + + Cilësimet e skanerit të disponueshëm + + + Je i sigurt se dëshiron të ndalosh disponueshmërinë e {0}? + + + Bëje të disponueshme edhe kur NAPS2 është i mbyllur + + + Shumë gjuhë... + + + Shumë gjuhë + + + Shfaqe progresin nativ të TWAIN + + + Ndaje + + + Kombinoje + + + IP statike + + + IP statike + + + IP/Makina + + + Porta + + + Lidhu + + + Gabim në lidhje. + + + Pyet gjithmonë + + + Po kërkon pajisjet... + + + U gjetën {0} pajisje. + + + U gjet 1 pajisje. + + + Nuk u gjet asnjë pajisje. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Shkurtimet nga tastiera + + + Shkurtimet nga tastiera + + + Skano me profilin {0} + + + Skano me profilin e paracaktuar + + + Skano me profilin e ri + + + Cakto + + + Mos cakto + + + Action + + + Shortcut + + + Motivi: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Gabim në hapjen e aplikacionit {0} + + + Ndalo ndarjen e skanerit + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.sr-CS.resx b/NAPS2.Lib/Lang/Resources/UiStrings.sr-CS.resx index 50219fd97b..5d3c7f1889 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.sr-CS.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.sr-CS.resx @@ -54,6 +54,18 @@ Nalepi + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + Gotovo @@ -165,6 +177,12 @@ Unazad + + Reverse All + + + Reverse Selected + Očisti @@ -174,6 +192,33 @@ Jezik (language) + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + "Save" button default action: + + + Application + + + Only allow a single NAPS2 instance + O programu @@ -186,9 +231,21 @@ Zoom Out + + Uvećaj na pravu veličinu + + + Skaliraj sa veličinom prozora + Save + + Save All + + + Save Selected + Save All as PDF @@ -201,6 +258,12 @@ Save Selected as Images + + Email All + + + Email Selected + Email All as PDF @@ -219,6 +282,15 @@ TWAIN Driver + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -267,6 +339,9 @@ Podešavanja automatskog snimanja + + Podešavanja automatskog snimanja + Napredne opcije @@ -279,6 +354,9 @@ Odaberi izvor + + Select Device + Run in Background @@ -303,9 +381,15 @@ OCR mode: + + Fix white balance and remove noise + Automatically run OCR after scanning + + Pre-emptively run OCR after scanning + Instaliraj više jezika @@ -348,4 +432,457 @@ Izaberi sve sve + + Oporavi + + + Ne sada + + + Oporavi skenirane slike + + + {0} slika skeniranih na {1} na {2} možda nije sačuvano, i mogu ponovo da se kreiraju. Da li želite da ih kreirate ponovo? + + + Placeholders + + + Skip save prompt + + + Single page files + + + Enkriptuj PDF + + + Pokaži + + + Vrati na podrazumevana podešavanja + + + PDF podešavanja + + + Dozvoli štampu + + + Dozvoli štampu najvišeg kvaliteta + + + Dozvoli izmene u dokumentu + + + Dozvoli sastavljanje dokumenta (Assembly) + + + Dozvoli kopiranje sadržaja + + + Dozvoli kopiranje sadržaja radi dostupnosti + + + Dozvoli anotacije + + + Dozvoli popunjavanje formi + + + Podrazumevana putanja: + + + Putanja datoteke: + + + Naslov: + + + Autor: + + + Tema: + + + Ključne reči: + + + Lozinka vlasnika: + + + Korisnička lozinka: + + + Zapamti ova podešavanja + + + Metapodaci + + + Enkripcija + + + Kompatibilnost + + + Placeholders + + + Godina + + + Godina (00-99) + + + Mesec (01-12) + + + Dan (01-31) + + + Sat (0-23) + + + Minut (00-59) + + + Sekunde (00-59) + + + Automatsko uvećavanje broja (4 cifre) + + + Automatsko uvećavanje broja (3 cifre) + + + Automatsko uvećavanje broja (2 cifre) + + + Auto-incrementing number (1 digits) + + + Naziv datoteke + + + Pregled: + + + For high JPEG qualities (80+), also increase Image Quality in your profile for best results. + + + Jpeg kvalitet + + + Tiff Options + + + Kompresija: + + + Podešavanje slike + + + E-mail podešavanja + + + Settings + + + Provider + + + Change + + + Naziv priloga: + + + Choose Email Provider + + + Authorize + + + Waiting for authorization... + + + Greška + + + Technical Details + + + Lozinka + + + The following file is encrypted and requires a password to open: + + + Prompt for file path + + + Svaka strana je zasebna datoteka + + + Svaki sken je zasebna datoteka + + + Separate files by Patch-T + + + Više informacija + + + Očisti slike nakon snimanja + + + Keep images across sessions + + + Proizvoljna veličina strane + + + Name (optional) + + + Dimensions + + + Custom Resolution + + + Dpi + + + Čekam da TWAIN završi... + + + Napredne opcije profila + + + Kvalitet slike + + + Maksimalni kvalitet (velike datoteke) + + + Prazne strane + + + Izostavi prazne strane + + + Granična vrednost belog + + + Granična vrednost pokrivenosti + + + Post-processing + + + Deskew scanned pages + + + Primeni osvetljenje/kontrast nakon skeniranja + + + Offset width based on alignment (WIA) + + + Stretch to page size + + + Smanjiti do veličine stranice + + + Flip duplexed pages + + + Flip back sides of duplex pages + + + Wia Version: + + + Uvođenje TWAIN-a: + + + Paketno skeniranje + + + Pritisni Start kada si spreman. + + + Konfiguracija skeniranja + + + Izlazni rezultat + + + Profile: + + + Single scan + + + Višestruko skeniranje (pitaj između skeniranja) + + + Višestruko skeniranje (fiksno kašnjenje između skeniranja) + + + Broj skenova: + + + Vreme između skenova (sekunde): + + + Učitaj slike u aplikaciju + + + Snimi kao jednu datoteku + + + Snimi kao više datoteka + + + Start + + + Sledeći sken + + + Spreman za skeniranje {0}. + + + Open Folder + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + 1 device found. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.sr.resx b/NAPS2.Lib/Lang/Resources/UiStrings.sr.resx index f5ccb4c25e..2c0b2391a2 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.sr.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.sr.resx @@ -54,6 +54,18 @@ Налепи + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + Готово @@ -165,6 +177,12 @@ Уназад + + Reverse All + + + Reverse Selected + Очисти @@ -174,6 +192,33 @@ Језик (language) + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + "Save" button default action: + + + Application + + + Only allow a single NAPS2 instance + О програму @@ -186,9 +231,21 @@ Смањи + + Увећај на праву величину + + + Скалирај са величином прозора + Save + + Save All + + + Save Selected + Save All as PDF @@ -201,6 +258,12 @@ Save Selected as Images + + Email All + + + Email Selected + Email All as PDF @@ -219,6 +282,15 @@ ТWАИН Драјвер + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -267,6 +339,9 @@ Подешавања аутоматског снимања + + Подешавања аутоматског снимања + Напредне опције @@ -279,6 +354,9 @@ Одабери извор + + Select Device + Run in Background @@ -303,9 +381,15 @@ OCR mode: + + Fix white balance and remove noise + Automatically run OCR after scanning + + Pre-emptively run OCR after scanning + Инсталирај више језика @@ -348,4 +432,457 @@ Изабери све + + Опорави + + + Не сада + + + Опорави скениране слике + + + {0} слика скенираних на {1} на {2} можда није сачувано, и могу поново да се креирају. Да ли желите да их креирате поново? + + + Алокатор дужине и формата датотеке + + + Skip save prompt + + + Single page files + + + Енкриптуј PDF + + + Покажи + + + Врати на подразумевана подешавања + + + PDF подешавања + + + Дозволи штампу + + + Дозволи штампу највишег квалитета + + + Дозволи измене у документу + + + Дозволи састављање документа (Assembly) + + + Дозволи копирање садржаја + + + Дозволи копирање садржаја ради доступности + + + Дозволи анотације + + + Дозволи попуњавање форми + + + подразумевани фајл: + + + Путања датотеке: + + + Наслов: + + + Аутор: + + + Тема: + + + Кључне речи: + + + Лозинка власника: + + + Корисничка лозинка: + + + Запамти ова подешавања + + + Метаподаци + + + Енкрипција + + + Компатибилност + + + Алокатор дужине и формата датотеке + + + Година + + + Година (00-99) + + + Месец (01-12) + + + Дан (01-31) + + + Сат (0-23) + + + Минут (00-59) + + + Секунде (00-59) + + + Аутоматско увећавање броја (4 цифре) + + + Аутоматско увећавање броја (3 цифре) + + + Аутоматско увећавање броја (2 цифре) + + + Auto-incrementing number (1 digits) + + + Назив датотеке + + + Преглед: + + + For high JPEG qualities (80+), also increase Image Quality in your profile for best results. + + + Jpeg квалитет + + + Tiff Options + + + Compression: + + + Подешавање слике + + + Е-маил подешавања + + + Settings + + + Provider + + + Change + + + Назив прилога: + + + Choose Email Provider + + + Authorize + + + Waiting for authorization... + + + Грешка + + + технички детаљи + + + Лозинка + + + The following file is encrypted and requires a password to open: + + + Prompt for file path + + + Свака страна је засебна датотека + + + Сваки скен је засебна датотека + + + Separate files by Patch-T + + + Више информација + + + Очисти слике након снимања + + + Keep images across sessions + + + Произвољна величина стране + + + Име (опционално) + + + димензије + + + Custom Resolution + + + Dpi + + + Чекам да TWAIN заврши... + + + Напредне опције профила + + + Квалитет слике + + + Максимални квалитет (велике датотеке) + + + Празне стране + + + Изостави празне стране + + + Гранична вредност белог + + + Гранична вредност покривености + + + Post-processing + + + Deskew scanned pages + + + Примени осветљење/контраст након скенирања + + + Offset width based on alignment (WIA) + + + Stretch to page size + + + Crop to page size + + + окрени дуплирану страну + + + Flip back sides of duplex pages + + + Wia Version: + + + Увођење TWAIN-a: + + + Пакетно скенирање + + + Притисни Старт када си спреман. + + + Конфигурација скенирања + + + Излазни резултат + + + Profile: + + + Појединачни скен + + + Вишеструко скенирање (питај између скенирања) + + + Вишеструко скенирање (фиксно кашњење између скенирања) + + + Број скенова: + + + Време између скенова (секунде): + + + Учитај слике у апликацију + + + Сними као једну датотеку + + + Сними као више датотека + + + Старт + + + Следећи скен + + + Спреман за скенирање {0}. + + + Отвори фолдер + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Увек питај + + + Searching for devices... + + + {0} devices found. + + + 1 уређај је пронађен + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.sv.resx b/NAPS2.Lib/Lang/Resources/UiStrings.sv.resx index 169c3760ca..085734563a 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.sv.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.sv.resx @@ -16,13 +16,13 @@ Om - Copyright {0} NAPS2 Contributors + Copyright {0} bidragsgivare till NAPS2 Ikoner från: - Kontrollera för uppdatering + Sök efter uppdateringar OK @@ -54,6 +54,18 @@ Klistra in + + Ångra + + + Gör om + + + Ångra {0} + + + Gör om {0} + Klart @@ -118,7 +130,7 @@ Skarpare - Document Correction + Dokumentkorrektion Återställ @@ -163,22 +175,55 @@ Alternativ avinterfoliering - Omvänt + Vänd om + + + Vänd om alla + + + Vänd om markerade Rensa - Clear All + Rensa alla Språk + + Inställningar + + + Gränssnitt + + + Menyn "Skanna" ändrar standardprofil + + + Visa verktygsfältet "Profiler" + + + Visa sidnummer + + + Standardåtgärd för knappen "Skanna": + + + Standardåtgärd för knappen "Spara": + + + Applikation + + + Tillåt endast en NAPS2-instans + Om - Zoom + Zooma Zooma in @@ -186,26 +231,44 @@ Zooma ut + + Zooma faktisk storlek + + + Anpassa till fönsterstorlek + - Save + Spara + + + Spara alla + + + Spara markerade - Save All as PDF + Spara alla som PDF - Save Selected as PDF + Spara markerade som PDF - Save All as Images + Spara alla som bilder - Save Selected as Images + Spara markerade som bilder + + + E-posta alla + + + E-posta markerade - Email All as PDF + E-posta alla som PDF - Email Selected as PDF + E-posta markerade som PDF Profilinställningar @@ -219,11 +282,20 @@ TWAIN-drivrutin + + ESCL-drivrutin + + + ESCL-nätverksdrivrutin + + + ESCL-USB-drivrutin + - Apple Driver + Apple-drivrutin - SANE Driver + SANE-drivrutin Enhet: @@ -267,6 +339,9 @@ Spara inställningar automatiskt + + Spara inställningar automatiskt + Avancerat @@ -279,6 +354,9 @@ Välj källa + + Välj enhet + Körs i bakgrunden @@ -286,7 +364,7 @@ NAPS2 - NAPS2 - {0} + NAPS2 − {0} Not Another PDF Scanner @@ -301,10 +379,16 @@ OCR-språk: - OCR läge: + OCR-läge: + + + Åtgärda vitbalans och ta bort brus - Automatiskt köra ORC scanning + Kör automatiskt OCR efter skanning + + + Kör OCR efter skanning i förebyggande syfte Hämta fler språk @@ -348,4 +432,457 @@ Markera alla + + Återställ + + + Inte nu + + + Återställ skannade bilder + + + {0} bild(er) skannad(e) på {1} i {2} kanske inte har sparats, och är återställningsbara. Vill du återställa dem? + + + Platshållare + + + Fråga inte vid spara + + + Enstaka sid filer + + + Kryptera PDF + + + Visa + + + Återställ standard + + + PDF-inställningar + + + Tillåt utskrift + + + Tillåt full utskriftskvalitet + + + Tillåt dokumentändring + + + Tillåt dokumentsammanslagning + + + Tillåt innehållskopiering + + + Tillåt innehållskopiering för tillgänglighet + + + Tillåt annoteringar + + + Tillåt formulärifyllning + + + Standard filsökväg: + + + Filsökväg: + + + Titel: + + + Upphovsman: + + + Ämne: + + + Nyckelord: + + + Ägarlösenord: + + + Användarlösenord: + + + Kom ihåg dessa inställningar + + + Metadata + + + Kryptering + + + Kompatibilitet + + + Platshållare + + + År + + + År (00-99) + + + Månad (01-12) + + + Dag (01-31) + + + Timma (0-23) + + + Minut (00-59) + + + Sekund (00-59) + + + Automatiskt uppräkningnummer (4 siffror) + + + Automatiskt uppräkningnummer (3 siffror) + + + Automatiskt uppräkningnummer (2 siffror) + + + Auto-uppräknande nummer (1 siffra) + + + Filnamn + + + Förhandsgranskning: + + + För hög JPEG kvalitet (80+), öka bild kvaliteten i din profil för bästa resultat. + + + JPEG-kvalitet + + + Tiff Val + + + Komprimera: + + + Bildinställningar + + + E-postinställningar + + + Inställningar + + + Leverantör + + + Ändra + + + Bilagenamn: + + + Välj e-post leverantör + + + Autentisera + + + Väntar på auktorisering... + + + Fel + + + Tekniska detaljer + + + Lösenord + + + Följande fil är krypterad och kräver ett lösenord för att öppnas: + + + Ange fil sökväg + + + En fil per sida + + + En fil per skanning + + + Delade filer av Patch-T + + + Mer info + + + Rensa bort bilder efter att de sparats + + + Behåll bilder mellan sessioner + + + Anpassad sidstorlek + + + Namn (tillägg) + + + Dimensioner + + + Anpassad upplösning + + + DPI + + + Väntar på att TWAIN skall slutföras... + + + Avancerade profilinställningar + + + Bildkvalitet + + + Maximal kvalitet (stora filer) + + + Tomma sidor + + + Utelämna tomma sidor + + + Vittröskelvärde: + + + Tröskelvärde + + + Efterbehandling + + + Räta upp skannade sidor + + + Tillämpa ljus/kontrast efter skanning + + + Förskjutningsbredd baserad på justering (WIA) + + + Anpassa till sidstorlek + + + Beskär till sidstorlek + + + Vänd duplexsidor + + + Vänd duplexsidors baksidor + + + WIA-version: + + + TWAIN-implementering: + + + Mängdskanning + + + Tryck start när det är klart. + + + Skanningskonfiguration + + + Utdata + + + Profil: + + + Enkelskanning + + + Flera skanningar (fråga mellan varje skanning) + + + Flera skanningar (fast fördröjning mellan skanningar) + + + Antal skanningar: + + + Tid mellan skanningar (sekunder): + + + Läs in bilder i NAPS2 + + + Spara som en fil + + + Spara som flera filer + + + Starta + + + Nästa skanning + + + Klar att skanna {0}. + + + Öppna mapp + + + Verktyg + + + Aktivera felsökningsregistrering + + + Dela + + + Skannerdelning + + + Skannerdelning + + + Det går att använda delade skannrar från andra datorer på det lokala nätverket genom att välja "ESCL-drivrutin" i NAPS2:s profilinställningar på den andra datorn. + + + Inställningar för delad skanner + + + Är du säker på att du vill sluta dela {0}? + + + Dela också när NAPS2 är stängt + + + Flera språk... + + + Flera språk + + + Visa inbyggt TWAIN-förlopp + + + Dela + + + Sammanfoga + + + Manuell IP + + + Manuell IP + + + IP/Värd + + + Port + + + Anslut + + + Anslutningsfel. + + + Fråga alltid + + + Söker efter enheter ... + + + {0} enheter hittades. + + + 1 enhet hittades. + + + Inga enheter hittades. + + + Sidofält + + + Hittar du inte din skanner? Läs om begränsningar i NAPS2 som Flatpak. + + + Kortkommandon + + + Kortkommandon + + + Skanna med profilen {0} + + + Skanna med standardprofil + + + Skanna med ny profil + + + Tilldela + + + Ta bort tilldelning + + + Åtgärd + + + Kortkommando + + + Tema: + + + Redigera med... + + + Redigera med {0} + + + Redigera med... + + + Fel vid applikationsstart {0} + + + Stäng av skannerdelning + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.th.resx b/NAPS2.Lib/Lang/Resources/UiStrings.th.resx new file mode 100644 index 0000000000..c143d13368 --- /dev/null +++ b/NAPS2.Lib/Lang/Resources/UiStrings.th.resx @@ -0,0 +1,888 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + เกี่ยวกับ + + + Copyright {0} NAPS2 Contributors + + + Icons from: + + + ตรวจสอบเวอร์ชันใหม่ + + + ตกลง + + + บริจาค + + + โปรไฟล์ + + + ใหม่ + + + แก้ไข + + + ลบ + + + Scan + + + Set Default + + + คัดลอก + + + วาง + + + Undo + + + ทำซ้ำ + + + Undo {0} + + + Redo {0} + + + เสร็จ + + + ค่าเริ่มต้น + + + New Profile + + + Batch Scan + + + OCR + + + โปรไฟล์ + + + นำเข้า + + + บันทึก PDF + + + การตั้งค่า PDF + + + บันทึกภาพ + + + Image Settings + + + ส่งอีเมล PDF + + + ตั้งค่าอีเมล + + + พิมพ์ + + + รูปภาพ + + + เปิดดูภาพ + + + ครอบตัด + + + ความสว่าง / ความเปรียบต่าง + + + เฉดสี / ความอิ่มตัว + + + ขาวดำ + + + ความ​คม​ชัด + + + ปรับแก้เอกสาร + + + Reset + + + หมุน + + + หมุนซ้าย + + + หมุนขวา + + + กลับด้าน + + + ปรับให้ตรง + + + Custom Rotation + + + เลื่อนขึ้น + + + เลื่อนลง + + + จัดเรียงหน้าใหม่ + + + แทรกสลับหน้า + + + แยกหน้าแบบสลับ + + + แทรกสลับหน้าแบบสลับทิศทาง + + + แยกหน้าแบบสลับทิศทาง + + + Reverse + + + Reverse All + + + Reverse Selected + + + ล้าง + + + ล้างทั้งหมด + + + ภาษา + + + ตั้งค่า + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + ค่าเริ่มต้นของปุ่ม "สแกน" + + + ค่าเริ่มต้นของปุ่ม "บันทึก" + + + Application + + + Only allow a single NAPS2 instance + + + เกี่ยวกับ + + + Zoom + + + Zoom In + + + Zoom Out + + + Zoom Actual + + + Scale With Window + + + บันทึก + + + บันทึกทั้งหมด + + + Save Selected + + + บันทึกทั้งหมดเป็น PDF + + + บันทึกรายการที่เลือกเป็น PDF + + + บันทึกทั้งหมดเป็นภาพ + + + Save Selected as Images + + + Email All + + + Email Selected + + + ส่งอีเมลทั้งหมดเป็น PDF + + + Email Selected as PDF + + + Profile Settings + + + Display name: + + + WIA Driver + + + TWAIN Driver + + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + + + Apple Driver + + + SANE Driver + + + อุปกรณ์: + + + เลือกอุปกรณ์ + + + Use predefined settings + + + Use native UI + + + แหล่งกระดาษ: + + + ขนาดหน้า: + + + ความละเอียด: + + + ความสว่าง: + + + ความลึกบิต: + + + Horizontal align: + + + Scale: + + + Contrast: + + + บันทึกอัตโนมัติ + + + การตั้งค่าบันทึกอัตโนมัติ + + + การตั้งค่าบันทึกอัตโนมัติ + + + ขั้นสูง + + + ยกเลิก + + + เลือก + + + Select Source + + + Select Device + + + Run in Background + + + NAPS2 + + + NAPS2 - {0} + + + Not Another PDF Scanner + + + OCR Setup + + + Make PDFs searchable using OCR + + + OCR language: + + + OCR mode: + + + Fix white balance and remove noise + + + Automatically run OCR after scanning + + + Pre-emptively run OCR after scanning + + + Get more languages + + + OCR Download + + + Using OCR requires you to download each language you want to scan. + + + Select one or more languages: + + + Estimated download size: {0} MB + + + ดาวน์โหลด + + + ความคืบหน้าการดาวน์โหลด + + + ดูตัวอย่าง + + + ถัดไป + + + Previous + + + {0} of {1} + + + Revert + + + Apply to all {0} selected images + + + เลือกทั้งหมด + + + Recover + + + Not Now + + + Recover Scanned Images + + + {0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them? + + + Placeholders + + + Skip save prompt + + + Single page files + + + เข้ารหัส PDF + + + Show + + + Restore Defaults + + + การตั้งค่า PDF + + + Allow Printing + + + Allow Full Quality Printing + + + Allow Document Modification + + + Allow Document Assembly + + + Allow Content Copying + + + Allow Content Copying for Accessibility + + + Allow Annotations + + + Allow Form Filling + + + ที่อยู่ไฟล์เริ่มต้น: + + + File Path: + + + Title: + + + Author: + + + Subject: + + + Keywords: + + + Owner Password: + + + รหัสผ่านผู้ใช้: + + + Remember these settings + + + Metadata + + + การเข้ารหัส + + + Compatibility + + + Placeholders + + + ปี + + + ปี (00-99) + + + เดือน (01-12) + + + วัน (01-31) + + + ชั่วโมง (0-23) + + + นาที (00-59) + + + Second (00-59) + + + Auto-incrementing number (4 digits) + + + Auto-incrementing number (3 digits) + + + การเพิ่มจำนวนอัตโนมัติ (2 หลัก) + + + การเพิ่มจำนวนอัตโนมัติ (1 หลัก) + + + ชื่อไฟล์: + + + Preview: + + + For high JPEG qualities (80+), also increase Image Quality in your profile for best results. + + + Jpeg Quality + + + Tiff Options + + + การบีบอัด: + + + Image Settings + + + ตั้งค่าอีเมล + + + ตั้งค่า + + + Provider + + + เปลี่ยนแปลง + + + Attachment Name: + + + เลือกผู้ให้บริการอีเมล + + + อนุญาต + + + Waiting for authorization... + + + ผิดพลาด + + + Technical Details + + + รหัสผ่าน + + + The following file is encrypted and requires a password to open: + + + Prompt for file path + + + One file per page + + + One file per scan + + + Separate files by Patch-T + + + ข้อมูลเพิ่มเติม + + + ล้างภาพหลังจากบันทึก + + + Keep images across sessions + + + Custom Page Size + + + ชื่อ (ถ้ามี) + + + Dimensions + + + Custom Resolution + + + Dpi + + + Waiting for TWAIN to complete... + + + การตั้งค่าโปรไฟล์ขั้นสูง + + + Image Quality + + + Maximum quality (large files) + + + หน้าว่าง + + + Exclude blank pages + + + White Threshold + + + Coverage Threshold + + + Post-processing + + + Deskew scanned pages + + + Apply brightness/contrast after scan + + + Offset width based on alignment (WIA) + + + Stretch to page size + + + Crop to page size + + + Flip duplexed pages + + + Flip back sides of duplex pages + + + Wia Version: + + + Twain Implementation: + + + Batch Scan + + + Press Start when ready. + + + Scan Configuration + + + Output + + + โปรไฟล์: + + + Single scan + + + Multiple scans (prompt between scans) + + + Multiple scans (fixed delay between scans) + + + Number of scans: + + + Time between scans (seconds): + + + Load images into NAPS2 + + + Save to a single file + + + Save to multiple files + + + เริ่ม + + + Next Scan + + + Ready for scan {0}. + + + Open Folder + + + เครื่องมือ + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + รวมเอกสาร + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + เชื่อมต่อ + + + การเชื่อมต่อผิดพลาด + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + พบ 1 อุปกรณ์ + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.tr.resx b/NAPS2.Lib/Lang/Resources/UiStrings.tr.resx index 331adcc1a6..e569c3e505 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.tr.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.tr.resx @@ -16,7 +16,7 @@ Hakkında - Copyright {0} NAPS2 Contributors + Telif Hakkı {0} NAPS2 Katkıcıları Simgeler: @@ -54,6 +54,18 @@ Yapıştır + + Geri al + + + Yinele + + + Geri al: {0} + + + Geri al: {0} + Tamam @@ -67,7 +79,7 @@ Yığın Tarama - OCR + OKT Profiller @@ -118,7 +130,7 @@ Keskinleştir - Document Correction + Belge Düzeltimi Sıfırla @@ -165,15 +177,48 @@ Ters Çevir + + Tümünü Geri Al + + + Seçimi Ters Çevir + Temizle - Clear All + Tümünü Temizle Dil + + Ayarlar + + + Arayüz + + + "Tara" menüsü öntanımlı profili değiştirir + + + "Profiller" araç çubuğunu göster + + + Sayfa numaralarını göster + + + "Tara" düğmesi öntanımlı eylemi: + + + "Kaydet" düğmesi öntanımlı eylemi: + + + Uygulama + + + Yalnızca tek NAPS2 örneğine izin ver + Hakkında @@ -186,26 +231,44 @@ Uzaklaştır + + Olağan Yakınlaşma + + + Pencereye Göre Ölçeklendir + - Save + Kaydet + + + Tümünü Kaydet + + + Seçilenleri Kaydet - Save All as PDF + Tümünü PDF Olarak Kaydet - Save Selected as PDF + Seçileni PDF Olarak Kaydet - Save All as Images + Tümünü Resim Olarak Kaydet - Save Selected as Images + Seçileni Resim Olarak Kaydet + + + Tümünü E-postala + + + Seçilenleri E-postala - Email All as PDF + Tümünü PDF Olarak E-Postala - Email Selected as PDF + Seçileni PDF Olarak E-Postala Profil Ayarları @@ -219,8 +282,17 @@ TWAIN Sürücüsü + + ESCL Sürücüsü + + + ESCL Ağ Sürücüsü + + + ESCL USB Sürücüsü + - Apple Driver + Apple Sürücüsü SANE Sürücüsü @@ -229,7 +301,7 @@ Aygıt: - Aygıtı seç + Aygıt seç Öntanımlı ayarları kullan @@ -267,6 +339,9 @@ Kendiliğinden Kaydetme Ayarları + + Kendiliğinden Kaydetme Ayarları + Gelişmiş @@ -279,6 +354,9 @@ Kaynağı Seç + + Aygıt Seç + Arka Planda Çalıştır @@ -292,34 +370,40 @@ Not Another PDF Scanner - OCR Kurulumu + OKT Kurulumu - OCR kullanarak aranabilir PDF oluştur + OKT kullanarak aranabilir PDF oluştur - OCR dili: + OKT dili: - OCR kipi: + OKT kipi: + + + Beyaz dengesini düzelt ve gürültüyü kaldır - Taradıktan sonra OCR'yi kendiliğinden çalıştır + Taradıktan sonra OKT'yi kendiliğinden çalıştır + + + Taramadan sonra OKT'yi kendiliğinden çalıştır Daha çok dil yükle - OCR Dili Yükle + OKT İndir - OCR kullanmak için taramak istediğiniz dili indirmeniz gerekir. + OKT kullanmak için taramak istediğiniz dili indirmeniz gerekir. - Bir veya daha çok dil seçin: + Bir ya da daha çok dil seç: - Tahmini indirme boyutu: {0} MB + Öngörülen indirme boyutu: {0} MB İndir @@ -328,7 +412,7 @@ İndirme Süreci - Önizleme + Ön İzle İleri @@ -348,4 +432,457 @@ Tümünü Seç + + Kurtar + + + Şimdi Değil + + + Taranmış Resimleri Kurtar + + + {1} {2} tarihinde taranan {0} resim kaydedilmedi ve kurtarılabilir. Kurtarmak ister msiniz? + + + Yer Tutucular + + + Kaydetme istemini atla + + + Tekli sayfa dosyaları + + + PDF'yi Şifrele + + + Göster + + + Öntanımlıları Geri Yükle + + + PDF Ayarları + + + Baskıya İzin Ver + + + Tam Kalite Baskıya İzin Ver + + + Belge Düzenlemeye İzin Ver + + + Belge Toparlamaya İzin Ver + + + İçerik Kopyalamaya İzin Ver + + + Erişilebilirlik İçin İçerik Kopyalamaya İzin Ver + + + Açıklamalara İzin Ver + + + Form Doldurmaya İzin Ver + + + Öntanımlı Dosya Yolu: + + + Dosya Yolu: + + + Başlık: + + + Yazar: + + + Konu: + + + Anahtar Sözcükler: + + + Sahip Parolası: + + + Kullanıcı Parolası: + + + Bu ayarları anımsa + + + Üst Veri + + + Şifreleme + + + Uyumluluk + + + Yer Tutucular + + + Yıl + + + Yıl (00-99) + + + Ay (01-12) + + + Gün (01-31) + + + Saat (0-23) + + + Dakika (00-59) + + + Saniye (00-59) + + + Kendiliğinden artan sayı (4 hane) + + + Kendiliğinden artan sayı (3 hane) + + + Kendiliğinden artan sayı (2 hane) + + + Kendiliğinden artan sayı (1 hane) + + + Dosya Adı + + + Ön İzleme: + + + Yüksek JPEG kalitesi için (80+), ayrıca en iyi sonuçlar için profilinizdeki Resim Kalitesi'ni yükseltin. + + + Jpeg Kalitesi + + + Tiff Seçenekleri + + + Sıkıştırma: + + + Resim Ayarları + + + E-Posta Ayarları + + + Ayarlar + + + Sağlayıcı + + + Değiştir + + + Ek Adı: + + + E-Posta Sağlayıcı Seç + + + Yetkilendir + + + Yetkilendirme bekleniyor... + + + Hata + + + Teknik Ayrıntılar + + + Parola + + + Bu dosya şifrelenmiş ve açmak için bir şifre gerekiyor: + + + Dosya yolu için komut iste + + + Sayfa başı tek dosya + + + Tarama başı tek dosya + + + Dosyaları Patch-T'ye göre ayır + + + Daha çok bilgi + + + Kaydettikten sonra resimleri temizle + + + Görüntüleri oturumlar arasında sakla + + + Özel Sayfa Boyutu + + + Ad (isteğe bağlı) + + + Boyutlar + + + Özel Çözünürlük + + + Dpi + + + Tamamlamak için TWAIN bekleniyor... + + + Gelişmiş Profil Ayarları + + + Resim Kalitesi + + + En yüksek kalite (büyük dosyalar) + + + Boş Sayfalar + + + Boş sayfaları dışarıda bırak + + + Beyaz Eşiği + + + Kapsam Eşiği + + + İleri İşleme + + + Taranan sayfaların eğriliğini düzelt + + + Taramadan sonra parlaklık/karşıtlık uygula + + + Hizalamayı taban alan ofset genişliği (WIA) + + + Sayfa boyutuna ger + + + Sayfa boyutuna kırp + + + Çift taraflı sayfaları ters yüz et + + + Çift taraflı sayfaların arka yüzlerini çevirin + + + Wia Sürümü: + + + Twain Uyarlaması: + + + Yığın Tarama + + + Hazır olduğunda Başlat'a basın. + + + Tarama Yapılandırması + + + Çıktı + + + Profil: + + + Tekli tarama + + + Çoklu tarama (taramalar arası komut istemi) + + + Çoklu tarama (taramalar arası sabit gecikme) + + + Tarama sayısı: + + + Taramalar arası zaman (saniye): + + + Resimleri NAPS2'ye yükle + + + Tek dosyaya kaydet + + + Çoklu dosyalara kaydet + + + Başlat + + + Sonraki Tarama + + + {0}. tarama için hazır. + + + Klasörü Aç + + + Araçlar + + + Hata ayıklama günlüğünü etkinleştir + + + Paylaş + + + Tarayıcı Paylaşımı + + + Tarayıcı Paylaşımı + + + Paylaşılan tarayıcılar, yerel ağdaki diğer bilgisayarların NAPS2 profil ayarlarında "ESCL Sürücüsü" seçilerek kullanılabilir. + + + Paylaşılan Tarayıcı Ayarları + + + Paylaşımı durdurmak istediğinize emin misiniz {0}? + + + NAPS2 kapalıyken de paylaş + + + Çoklu Dil... + + + Çoklu Dil + + + Özgün TWAIN ilerlemesini göster + + + Böl + + + Birleştir + + + Elle IP + + + Elle IP + + + IP/Ana Makine + + + Port + + + Bağlan + + + Bağlantı hatası. + + + Her Zaman Sor + + + Aygıtlar aranıyor... + + + {0} aygıt bulundu. + + + 1 aygıt bulundu. + + + Hiçbir aygıt bulunamadı. + + + Kenar çubuğu + + + Tarayıcınızı bulamıyor musunuz? NAPS2 Flatpak'ın kısıtlamalarını okuyun. + + + Klavye Kısayolları + + + Klavye Kısayolları + + + {0} Profiliyle Tara + + + Öntanımlı Profille Tara + + + Yeni Profille Tara + + + Ata + + + Atamayı Kaldır + + + Eylem + + + Kısayol + + + Tema: + + + Şununla düzenle... + + + {0} ile düzenle + + + Şununla düzenle... + + + Uygulama başlatılırken hata {0} + + + Tarayıcı Paylaşımını Durdur + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.uk.resx b/NAPS2.Lib/Lang/Resources/UiStrings.uk.resx index 0fded0ce7d..84b8d041c9 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.uk.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.uk.resx @@ -16,19 +16,19 @@ Про програму - Copyright {0} NAPS2 Contributors + Всі права застережено {0} Розробники NAPS2 - Піктограми взяті з: + Використання значків: - Check for updates + Перевірка оновлень - OK + Гаразд - Donate + Ваш внесок Профілі @@ -40,7 +40,7 @@ Редагувати - Видалити + Вилучити Сканувати @@ -52,22 +52,34 @@ Копіювати - Paste + Вставити + + + Повернути + + + Повторити + + + Повернути {0} + + + Повторити {0} Завершити - Default + Типово Створити профіль - Batch Scan + Пакетне сканування - Розпізнавання + Розпізнати Профілі @@ -76,7 +88,7 @@ Імпортувати - Зберегти PDF + Зберегти у PDF Налаштування PDF @@ -106,19 +118,19 @@ Обрізати - Brightness / Contrast + Яскравість / контрастність - Hue / Saturation + Відтінок / насиченість Чорно-білий - Sharpen + Різкість - Document Correction + Зміни до документу Перезавантажити зображення @@ -133,19 +145,19 @@ Обернути праворуч - Відзеркалити + Віддзеркалити - Deskew + Вирівняти перекіс Ротація користувачем - Пересунути вище + Перемістити вище - Пересунути нижче + Перемістити нижче Змінити порядок @@ -157,23 +169,56 @@ Позбавити чергування - Alternate Interleave + Альтернативна міжчерговість - Alternate Deinterleave + Альтенативна віддаленість - Зворотній + Зворотній порядок + + + Всі у зворотньому порядку + + + Вибрані у зворотньому порядку Очистити - Clear All + Очистити все Мова + + Налаштування + + + Інтерфейс + + + Вибір в меню "Сканувати" змінює типовий профіль + + + Показувати панель "Профілі" + + + Показувати нумерацію сторінок + + + Типова поведінка, коли натиснуто кнопку "Сканувати": + + + Типова поведінка, коли натиснуто кнопку "Зберегти": + + + Застосунок + + + Дозволити лише один примірник NAPS2 + Про програму @@ -186,26 +231,44 @@ Зменшити + + Справжній розмір + + + Масштабувати у вікні + - Save + Зберегти + + + Зберегти все + + + Зберегти вибране - Save All as PDF + Зберегти все у PDF - Save Selected as PDF + Зберегти вибране у PDF - Save All as Images + Зберегти все як зображення - Save Selected as Images + Зберегти вибране як зображення + + + Надіслати все + + + Вибрано ел. пошту - Email All as PDF + Надіслати все як PDF - Email Selected as PDF + Вибрано надіслати PDF ел. поштою Налаштовування профілю @@ -214,16 +277,25 @@ Назва: - WIA драйвер + Драйвер WIA - TWAIN драйвер + Драйвер TWAIN + + + Драйвер ESCL + + + Мережевий драйвер ESCL + + + Драйвер USB для ESCL - Apple Driver + Драйвер Apple - SANE Driver + Драйвер SANE Пристрій: @@ -262,10 +334,13 @@ Контрастність: - Enable Auto Save + Увімкнути автоматичне збереження - Auto Save Settings + Автоматично зберігати налаштування + + + Автоматично зберігати налаштування Додатково @@ -274,13 +349,16 @@ Скасувати - Select + Вибрати - Select Source + Вибрати джерело + + + Виберіть пристрій - Run in Background + Виконувати на задньому фоні NAPS2 @@ -301,25 +379,31 @@ Мова розпізнавання: - OCR mode: + Режим розпізнавання: + + + Виправляти баланс білого кольору та прибирати шум - Automatically run OCR after scanning + Автоматично розпізнавати після сканування + + + Автоматично розпізнавати після сканування Встановити інші мови - Ззавантаження файлів для розпізнавання + Звантаження файлів для розпізнавання - Для розпізнавання потрібно завантажити відповідні мовні файли. + Для розпізнавання потрібно звантажити відповідні мовні файли. Виберіть одну або кілька мов:: - Приблизний об’єм звантаження: {0} МБ + Приблизний розмір звантаження: {0} МБ Звантаження @@ -337,15 +421,468 @@ Попередня - {0} з {1} + {0} із {1} Повернути - Apply to all {0} selected images + Застосувати до всіх {0} вибраних зображень Вибрати все + + Відновити + + + Не зараз + + + Відновити відскановані зображення + + + Зображення ({0} шт.), відскановані на {1} в {2}, можливо не були збережені та можуть бути відновлені. Бажаєте їх відновити? + + + Заповнювачі + + + Пропускати запит на збереження + + + Файли з одинарною сторінкою + + + Шифрувати PDF + + + Показати + + + Скинути до типових + + + Налаштування PDF + + + Дозволити друк + + + Дозволити високоякісний друк + + + Дозволити модифікацію документу + + + Дозволити складання документу + + + Дозволити копіювати вміст + + + Дозволити копіювати вміст для доступу + + + Дозволити аннотації + + + Дозволити заповнення форм + + + Типовий шлях до файлу: + + + Шлях файлу: + + + Заголовок: + + + Автор: + + + Тема: + + + Ключові слова: + + + Пароль власника: + + + Пароль користувача: + + + Зберегти налаштуваання + + + Метадані + + + Шифрування + + + Сумісність + + + Заповнювачі + + + Рік + + + Рік (00-99) + + + Місяць (1-12) + + + День (1-31) + + + Година (0-23) + + + Хвилини (00-59) + + + Секунда (1-59) + + + Автоматичне збільшення номера (4 цифри) + + + Автоматичне збільшення номера (3 цифри) + + + Автоматичне збільшення номера (2 цифри) + + + Автоматичне збільшення номера (1 цифра) + + + Назва файлу + + + Перегляд: + + + Для отримання кращої якості зображень JPEG (80+) рекомендується збільшити якість зображення у вашому профілі сканування. + + + Якість JPEG + + + Параметри Tiff + + + Стиснення: + + + Налаштування зображення + + + Параметри електронної пошти + + + Налаштування + + + Постачальник + + + Змінити + + + Ім'я вкладення: + + + Виберіть постачальника послуг ел. пошти + + + Авторизація + + + Очікування авторизації... + + + Помилка + + + Технічні деталі + + + Пароль + + + Цей файл зашифровано. Щоби його відкрити, потрібно зазначити пароль: + + + Запит на шлях до файлу + + + Кожна сторінка в окремий файл + + + Все відскановане в одному файлі + + + Розділити файли за допомогою Patch-T + + + Докладно + + + Прибирати зображення після збереження + + + Зберігати зображення між сесіями + + + Розмір сторінки користувача + + + Ім'я (необов'язково) + + + Розміри + + + Власна роздільна здатність + + + точок на дюйм + + + Очікування завершення від TWAIN... + + + Розширені параметри профілю + + + Якість зображення + + + Максимальна якість (великий розмір) + + + Порожні сторінки + + + Прибрати порожні сторінки + + + Поріг білого кольору + + + Поріг покриття + + + Післяобробка + + + Вирівняти перекіс відсканованих сторінок + + + Визначити яскравість/контрастність після сканування + + + Ширина зсуву на основі вирівнювання (WIA) + + + Розтягнути до розміру сторінки + + + Обрізати за розмірами стоірнки + + + Віддзеркалити двосторонні сторінки + + + Віддзеркалити задній бік двосторонніх сторінок + + + Версія Wia: + + + Реалізація Twain: + + + Пакетне сканування + + + Натисніть Старт, коли будете готові. + + + Налаштування сканування + + + Результат + + + Профіль: + + + Одноразове сканування + + + Сканування багатьох сторінок (запит на продовження між скануваннями) + + + Сканування багатьох сторінок (фіксована затримка між скануваннями) + + + Кількість сторінок: + + + Затримка між скануванням (у секундах) + + + Завантажити зображення до NAPS2 + + + Зберегти все в один файл + + + Зберегти все в різні файли + + + Старт + + + Наступна сторінка + + + Готовий сканувати {0). + + + Відкрити каталог + + + Інструменти + + + Увімкнути журнал зневадження + + + Спільний доступ + + + Спільний доступ до сканера + + + Спільний доступ до сканера + + + Сканери у спільному доступі можна використовувати на віддалених комп'ютерах локальної мережі. Для цього потрібно вибрати "Драйвер ESCL" в налаштуваннях профіля віддаленого комп'ютерау. + + + Налаштування сканера у спільному доступі + + + Дійсно прибрати спільний доступ {0}? + + + Надавати доступ навіть тоді, коли NAPS2 буде закрито + + + Декілька мов... + + + Декілька мов + + + Показувати нативне сканування TWAIN + + + Розділити + + + Об'єднати + + + Вручну IP + + + Вручну IP + + + IP-адреса/назва хосту + + + Порт + + + З'єднатися + + + Помилка під час з'єднання. + + + Завжди питати + + + Пошук пристроїв... + + + Знайдено {0} пристроїв. + + + Знайдено 1 пристрій. + + + Не знайдено пристрої. + + + Бічна панель + + + Не знаходите ваш сканер? Перегляньте обмеження збірки Flatpak NAPS2. + + + Клавіатурні скорочення + + + Клавіатурні скорочення + + + Сканувати з профілем {0} + + + Сканувати з типовим профілем + + + Сканувати з новим профілем + + + Призначити + + + Скинути + + + Дія + + + Скорочення + + + Тема: + + + Редагувати у... + + + Редагувати у {0} + + + Редагувати у... + + + Помилка під час запуску застосунку {0} + + + Зупинити спільний доступ до сканера + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.vi.resx b/NAPS2.Lib/Lang/Resources/UiStrings.vi.resx index 1255fde6b5..84983ed611 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.vi.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.vi.resx @@ -54,6 +54,18 @@ Dán + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + Xong @@ -165,6 +177,12 @@ Đảo ngược + + Reverse All + + + Reverse Selected + Xóa @@ -174,6 +192,33 @@ Ngôn ngữ + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + "Save" button default action: + + + Application + + + Only allow a single NAPS2 instance + Thông Tin Phần Mềm @@ -186,9 +231,21 @@ Thu Nhỏ + + Phóng thực tế + + + Tỉ lệ với cửa sổ + Save + + Save All + + + Save Selected + Save All as PDF @@ -201,6 +258,12 @@ Save Selected as Images + + Email All + + + Email Selected + Email All as PDF @@ -219,6 +282,15 @@ TWAIN Driver + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -267,6 +339,9 @@ Cài đặt Tự động Lưu + + Cài đặt Tự động Lưu + Nâng cao @@ -279,6 +354,9 @@ Chọn nguồn + + Select Device + Chạy ngầm @@ -303,9 +381,15 @@ Chế độ OCR: + + Fix white balance and remove noise + Tự động thực thi OCR (nhận diện chữ trên bản scan) sau khi scan + + Pre-emptively run OCR after scanning + Thêm Ngôn ngữ @@ -348,4 +432,457 @@ Chọn hết + + Phục hồi + + + bây giờ không thấy + + + Khôi phục hình ảnh scan + + + {0} hình ảnh) quét trên {1} tại {2} có thể không được lưu lại, và có thể phục hồi được. Bạn có muốn khôi phục chúng? + + + Placeholders + + + Bỏ qua Lưu nhanh + + + Single page files + + + Mã hóa PDF + + + Xem + + + Khôi phục mặc định + + + Cài đặt PDF + + + Cho phép in + + + Cho phép cải thiện bản In + + + Cho phép thay đổi tài liệu + + + Cho phép tài liệu Assembly + + + Cho phép sao chép nội dung + + + Cho phép sao chép chỉnh sửa nội dung + + + Cho phép ghi chú + + + Cho phép Điền Form + + + Đường dẫn mặc định: + + + Đường dẫn: + + + Tiêu đề: + + + Tác giả: + + + Tiêu đề: + + + Từ khóa: + + + Owner Password: + + + Dùng Mật khẩu: + + + Ghi các thiết lập + + + Metadata + + + Encryption + + + Khả năng tương thích + + + Placeholders + + + Năm + + + Năm (00-99) + + + Tháng (01-12) + + + Ngày(01-31) + + + Giờ (0-23) + + + Phút (00-59) + + + Giây (00-59) + + + Tự động tăng số (4 chữ số) + + + Tự động tăng số (3 chữ số) + + + Tự động tăng số (2 chữ số) + + + Auto-incrementing number (1 digits) + + + Tên tệp + + + Xem trước: + + + Để JPEG được chất lượng cao, nhớ tăng chất lượng hình trong thiết lập mẫu có sẵn của bạn.. + + + Chất lượng hình ảnh + + + Thông số Tiff + + + Nén: + + + Cài đặt ảnh + + + Cài đặt Email + + + Settings + + + Nhà cung cấp + + + Thay đổi + + + Tên tập tin đính kèm: + + + Chọn nhà cung cấp Email + + + Cho phép + + + Đang đợi cho phép... + + + Lỗi + + + Chi tiết kỹ thuật + + + Mật Khẩu + + + The following file is encrypted and requires a password to open: + + + Hỏi nơi lưu. + + + Một tập tin cho mỗi trang + + + Một tập tin cho mỗi quét + + + Tập tin riêng biệt của Patch-T + + + Thêm thông tin + + + Xóa ảnh sau khi lưu + + + Keep images across sessions + + + Tùy chỉnh kiểu giấy + + + Tên (bắt buộc) + + + Kích thước + + + Custom Resolution + + + Dpi + + + Đang chờ TWAIN để hoàn thành... + + + Cài đặt chi tiết + + + Chất lượng ảnh + + + Chất lượng tối đa (tập tin lớn) + + + Trang Trắng + + + Loại trừ các trang trống + + + Ngưỡng trắng + + + Bảo hiểm Threshold + + + Post-processing + + + Deskew scanned pages + + + Áp dụng độ sáng / tương phản sau khi quét + + + Offset width based on alignment (WIA) + + + Kéo dãn ra vừa kích thước trang + + + Cắt về kích thước trang + + + Lật 2 mặt trang + + + Flip back sides of duplex pages + + + Wia Version: + + + Twain thực hiện: + + + Scan hàng loạt + + + Nhấn Start khi đã sẵn sàng. + + + Cấu hình quét + + + Đầu ra + + + Hồ sơ: + + + quét đơn + + + Quét nhiều (nhắc giữa quét) + + + Quét nhiều (trễ cố định giữa quét) + + + Số trang quét: + + + Thời gian giữa quét (giây): + + + Tải hình ảnh vào NAPS2 + + + Lưu vào một tập tin duy nhất + + + Lưu vào nhiều file + + + Bắt Đầu + + + Quét Trang tiếp + + + Sẵn sàng cho quét {0}. + + + Mở Thư mục + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + 1 device found. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.zh-CN.resx b/NAPS2.Lib/Lang/Resources/UiStrings.zh-CN.resx index 224a6ec478..e088a15e92 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.zh-CN.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.zh-CN.resx @@ -16,7 +16,7 @@ 关于 - Copyright {0} NAPS2 Contributors + 版权所有 {0} NAPS2 及其参与贡献者 图标来源: @@ -54,6 +54,18 @@ 粘贴 + + 撤销 + + + 重做 + + + 撤消 {0} + + + 重做 {0} + 完成 @@ -118,7 +130,7 @@ 锐化 - Document Correction + 文档校正 重置 @@ -165,15 +177,48 @@ 反转 + + 反转全部 + + + 反转所选项 + 清除 - Clear All + 清除全部 语言 + + 设置 + + + 界面 + + + "扫描"菜单可以更改默认配置文件 + + + 显示"配置文件"工具栏 + + + 显示页码 + + + "扫描"按钮默认操作: + + + "保存"按钮默认操作: + + + 应用 + + + 仅允许单个 NAPS2 实例 + 关于 @@ -186,26 +231,44 @@ 缩小 + + 缩放至实际大小 + + + 使用窗口比例 + - Save + 保存 + + + 保存所有 + + + 保存所选 - Save All as PDF + 保存所有到 PDF 文件 - Save Selected as PDF + 保存所选为PDF - Save All as Images + 保存保存所有到图像 - Save Selected as Images + 保存所选为图片 + + + 发送所有页面 + + + 发送所选页 - Email All as PDF + 发送所有页面(PDF) - Email Selected as PDF + 发送所选页(PDF) 配置文件设置 @@ -219,8 +282,17 @@ TWAIN 驱动 + + ESCL 驱动程序 + + + ESCL 网络驱动程序 + + + ESCL USB 驱动程序 + - Apple Driver + Apple 驱动程序 SANE 驱动 @@ -267,6 +339,9 @@ 自动保存设置 + + 自动保存设置 + 高级 @@ -279,6 +354,9 @@ 选择来源 + + 选择设备 + 在后台运行 @@ -303,9 +381,15 @@ OCR 模式: + + 修复白平衡并移除噪点 + 扫描后自动进行 OCR 识别 + + 扫描后自动运行 OCR + 获取更多的语言 @@ -348,4 +432,457 @@ 全选 + + 修复 + + + 现在不需要 + + + 修复已扫描的图像 + + + 使用 {2} 在 {1} 上扫描的 {0} 张图像可能还未保存,现在可以恢复。您需要恢复这些图像吗? + + + 格式符 + + + 跳过保存提示 + + + 单页文件 + + + 加密 PDF + + + 显示 + + + 还原默认值 + + + PDF 设置 + + + 允许打印 + + + 允许完全质量打印 + + + 允许修改文件 + + + 允许组合文件 + + + 允许复制内容 + + + 允许复制内容以实现辅助功能 + + + 允许注释 + + + 允许填写表单 + + + 默认文件路径: + + + 文件路径: + + + 标题: + + + 作者: + + + 主题: + + + 关键词: + + + 文件所有者密码: + + + 用户密码: + + + 记住这些设定 + + + 元数据 + + + 加密 + + + 兼容性 + + + 格式符 + + + 年份 + + + 年份 (00-99) + + + 月份 (01-12) + + + 日 (01-31) + + + 小时 (0-23) + + + 分钟 (00-59) + + + 秒 (00-59) + + + 自动递增(4位数字) + + + 自动递增(3位数字) + + + 自动递增(2位数字) + + + 自动递增(1位数字) + + + 文件名 + + + 预览: + + + 对于 JPEG 高质量 (80+),还可以在配置文件中增加图像质量,以获得最佳效果. + + + JPEG 品质 + + + TIFF 格式设置 + + + 压缩: + + + 图像设定 + + + 邮件设置 + + + 设置 + + + 服务供应商 + + + 改变 + + + 附件名称: + + + 选择电子邮件服务商 + + + 授权 + + + 等待授权... + + + 错误 + + + 技术细节 + + + 密码 + + + 以下文件已加密,请输入密码打开: + + + 文件路径提示 + + + 每页一个文件 + + + 每次扫描一个文件 + + + 通过 Patch-T 拆分文件 + + + 更多信息 + + + 保存后清除图片 + + + 在整个会话期间保留图像 + + + 自定义纸张大小 + + + 名称(可选) + + + 尺寸 + + + 自定义分辨率 + + + DPI + + + 正在等待 TWAIN 完成... + + + 高级配置文件设置 + + + 图像质量 + + + 最佳质量(大文件) + + + 空白页 + + + 排除空白页 + + + 白色阀值 + + + 阈值范围 + + + 后处理 + + + 对已扫描页面进行倾斜校正 + + + 扫描后调整亮度/对比度 + + + 基于对齐的偏移宽度 (WIA) + + + 扩展至纸张大小 + + + 裁剪到页面大小 + + + 翻转偶数页面 + + + 翻转双面打印的背面 + + + Wia 版本: + + + Twain 实现: + + + 批量扫描 + + + 准备好后点击开始. + + + 扫描设置 + + + 输出 + + + 配置文件: + + + 单次扫描 + + + 多次扫描(每次扫描间提示) + + + 多次扫描(每次扫描间固定延时) + + + 扫描次数: + + + 扫描之间的间隔(秒): + + + 载入图片至 NAPS2 + + + 保存为单个文件 + + + 保存为多个文件 + + + 启动 + + + 下一个扫描 + + + 已准备扫描 {0}. + + + 打开文件夹 + + + 工具 + + + 启用调试日志 + + + 共享 + + + 扫描仪共享 + + + 扫描仪共享 + + + 共享的扫描仪可以通过在其他计算机的 NAPS2 配置中选择 “ESCL 驱动程序” 从本地网络上的其他计算机中使用。 + + + 共享扫描仪设置 + + + 您确定想要停止分享 {0} 吗? + + + 即使在NAPS2关闭时也共享 + + + 多语言... + + + 多语言 + + + 显示本地 TWAIN 进程 + + + 拆分 + + + 合并 + + + 手动IP地址 + + + 手动IP地址 + + + IP/Host + + + 端口 + + + 连接 + + + 连接出错 + + + 总是询问 + + + 正在搜索设备… + + + 找到 {0} 台设备 + + + 找到 1 台设备 + + + 未找到设备 + + + 侧边栏 + + + 找不到您的扫描仪吗?了解关于 NAPS Flatpak 的限制。 + + + 键盘快捷键 + + + 键盘快捷键 + + + 使用配置文件{0}扫描 + + + 使用默认配置文件扫描 + + + 使用新配置文件扫描 + + + 分配 + + + 取消分配 + + + 操作 + + + 快捷键 + + + 主题: + + + 编辑方式... + + + 使用 {0} 编辑 + + + 编辑方式... + + + 启动应用程序 {0} 出错 + + + 停止扫描仪共享 + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.zh-TW.resx b/NAPS2.Lib/Lang/Resources/UiStrings.zh-TW.resx index 90abf335d7..af5ba3589c 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.zh-TW.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.zh-TW.resx @@ -54,6 +54,18 @@ Paste + + Undo + + + Redo + + + Undo {0} + + + Redo {0} + 完成 @@ -165,6 +177,12 @@ 反轉 + + Reverse All + + + Reverse Selected + 清除 @@ -174,6 +192,33 @@ 語言 + + Settings + + + Interface + + + "Scan" menu changes default profile + + + Show "Profiles" toolbar + + + Show page numbers + + + "Scan" button default action: + + + "Save" button default action: + + + Application + + + Only allow a single NAPS2 instance + 關於 @@ -186,9 +231,21 @@ Zoom Out + + 縮放實際大小 + + + 使用視窗比例 + Save + + Save All + + + Save Selected + Save All as PDF @@ -201,6 +258,12 @@ Save Selected as Images + + Email All + + + Email Selected + Email All as PDF @@ -219,6 +282,15 @@ TWAIN 驅動程式 + + ESCL Driver + + + ESCL Network Driver + + + ESCL USB Driver + Apple Driver @@ -267,6 +339,9 @@ Auto Save Settings + + Auto Save Settings + 高级 @@ -279,6 +354,9 @@ Select Source + + Select Device + Run in Background @@ -303,9 +381,15 @@ OCR mode: + + Fix white balance and remove noise + Automatically run OCR after scanning + + Pre-emptively run OCR after scanning + 獲取更多的語言 @@ -348,4 +432,457 @@ 全選 + + 修復 + + + 現在不要 + + + 復原掃瞄過的圖像 + + + 在 {1} 上已掃瞄 {0} 個圖像,在 {2} 可能沒有被儲存,且可復原。您要將它們復原嗎? + + + 版面配置區 + + + Skip save prompt + + + Single page files + + + 加密 PDF + + + 顯示 + + + 還原預設值 + + + PDF 設定 + + + 允許列印 + + + 允許完整品質列印 + + + 允許文件修改 + + + 允許文件組件 + + + 允許內容複製 + + + 允許對協助工具內容複製 + + + 允許註釋 + + + 允許表單填寫 + + + Default File Path: + + + File Path: + + + 標題: + + + 作者: + + + 主旨: + + + 關鍵字: + + + 擁有者密碼: + + + 使用者密碼: + + + 記住這些設定 + + + 中繼資料 + + + 加密 + + + Compatibility + + + 版面配置區 + + + 年份 + + + 年份 (00-99) + + + 月份 (01-12) + + + 天 (01-31) + + + 小時 (0-23) + + + 分鐘 (00-59) + + + 秒鐘 (00-59) + + + 自動遞增數(4 個數字) + + + 自動遞增數(3 個數字) + + + 自動遞增數(2 個數字) + + + Auto-incrementing number (1 digits) + + + 檔案名稱 + + + 預覽: + + + For high JPEG qualities (80+), also increase Image Quality in your profile for best results. + + + JPEG 品質 + + + Tiff Options + + + Compression: + + + 圖像設定 + + + 電子郵件設定 + + + Settings + + + Provider + + + Change + + + 附件名稱: + + + Choose Email Provider + + + Authorize + + + Waiting for authorization... + + + 錯誤 + + + Technical Details + + + 密碼 + + + The following file is encrypted and requires a password to open: + + + Prompt for file path + + + One file per page + + + One file per scan + + + Separate files by Patch-T + + + More info + + + Clear images after saving + + + Keep images across sessions + + + 自訂頁面大小 + + + Name (optional) + + + Dimensions + + + Custom Resolution + + + Dpi + + + 等待 TWAIN 完成... + + + 高级配置文件设置 + + + Image Quality + + + 最高品質 (大檔) + + + Blank Pages + + + Exclude blank pages + + + White Threshold + + + Coverage Threshold + + + Post-processing + + + Deskew scanned pages + + + Apply brightness/contrast after scan + + + Offset width based on alignment (WIA) + + + Stretch to page size + + + Crop to page size + + + Flip duplexed pages + + + Flip back sides of duplex pages + + + Wia Version: + + + Twain Implementation: + + + Batch Scan + + + Press Start when ready. + + + Scan Configuration + + + Output + + + Profile: + + + Single scan + + + Multiple scans (prompt between scans) + + + Multiple scans (fixed delay between scans) + + + Number of scans: + + + Time between scans (seconds): + + + Load images into NAPS2 + + + Save to a single file + + + Save to multiple files + + + Start + + + Next Scan + + + Ready for scan {0}. + + + Open Folder + + + Tools + + + Enable debug logging + + + Share + + + Scanner Sharing + + + Scanner Sharing + + + Shared scanners can be used from other computers on the local network by selecting "ESCL Driver" in the other computer's NAPS2 profile settings. + + + Shared Scanner Settings + + + Are you sure you want to stop sharing {0}? + + + Share even when NAPS2 is closed + + + Multiple Languages... + + + Multiple Languages + + + Show native TWAIN progress + + + Split + + + Combine + + + Manual IP + + + Manual IP + + + IP/Host + + + Port + + + Connect + + + Connection error. + + + Always Ask + + + Searching for devices... + + + {0} devices found. + + + 1 device found. + + + No devices found. + + + Sidebar + + + Can't find your scanner? Read about limitations of the NAPS2 Flatpak. + + + Keyboard Shortcuts + + + Keyboard Shortcuts + + + Scan With Profile {0} + + + Scan With Default Profile + + + Scan With New Profile + + + Assign + + + Unassign + + + Action + + + Shortcut + + + Theme: + + + Edit with... + + + Edit with {0} + + + Edit with... + + + Error starting application {0} + + + Stop Scanner Sharing + \ No newline at end of file diff --git a/NAPS2.Lib/Lang/TranslationMigrator.cs b/NAPS2.Lib/Lang/TranslationMigrator.cs new file mode 100644 index 0000000000..cdefc52b2b --- /dev/null +++ b/NAPS2.Lib/Lang/TranslationMigrator.cs @@ -0,0 +1,24 @@ +using System.Globalization; +using System.Resources; + +namespace NAPS2.Lang; + +public static class TranslationMigrator +{ + public static string PickTranslated(ResourceManager resourceManager, string originalName, string replacementName) + { + string englishOriginal = resourceManager.GetString(originalName, CultureInfo.GetCultureInfo("en-US"))!; + string englishReplacement = resourceManager.GetString(replacementName, CultureInfo.GetCultureInfo("en-US"))!; + string translatedOriginal = resourceManager.GetString(originalName)!; + string translatedReplacement = resourceManager.GetString(replacementName)!; + if (translatedReplacement != englishReplacement) + { + return translatedReplacement; + } + if (translatedOriginal != englishOriginal) + { + return translatedOriginal; + } + return englishReplacement; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Lang/po/af.po b/NAPS2.Lib/Lang/po/af.po index 78d9b5630c..dc759a293c 100644 --- a/NAPS2.Lib/Lang/po/af.po +++ b/NAPS2.Lib/Lang/po/af.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Afrikaans\n" "Language: af\n" @@ -19,276 +19,286 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "\"Stoor\"knoppie verstek aksie:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "\"Skandeeer\" knoppie verstek aksie:" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "\"Skandeer\" keuselys verander verstek profiel" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 Toestel gevind." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" -msgstr " " - -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" +msgstr "24-bis Kleur" #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message msgid "About" -msgstr "Thông Tin Phần Mềm" +msgstr "Meer omtrent NAPS2" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." -msgstr "Thu thập dữ liệu..." +msgstr "Data word ingetrek..." + +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" -msgstr "Nâng cao" +msgstr "Gevorderd" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" -msgstr "Cài đặt chi tiết" +msgstr "Gevorderde Profiel Instellings" #: MiscResources.resx$AllCount$Message msgid "All ({0})" -msgstr "Tất cả ({0})" +msgstr "Alles ({0})" #: MiscResources.resx$FileTypeAllFiles$Message msgid "All Files" -msgstr "Tất cả các tập tin" +msgstr "Alle Lêers" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" -msgstr "Cho phép ghi chú" +msgstr "Magtig Notas" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" -msgstr "Cho phép sao chép nội dung" +msgstr "Magtig Inhoud-kopieer" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" -msgstr "Cho phép sao chép chỉnh sửa nội dung" +msgstr "Magtig Inhoud-kopieer vir Toeganklikheid" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" -msgstr "Cho phép tài liệu Assembly" +msgstr "Magtig Dokument Saamstelling" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" -msgstr "Cho phép tài liệu Modification" +msgstr "Magtig Dokument Veranderings" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" -msgstr "Cho phép Điền Form" +msgstr "Magtig Vorm Invul" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" -msgstr "Cho phép cải thiện bản In" +msgstr "Magtig Voll Kwaliteit Druk" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" -msgstr "Cho phép in" +msgstr "Magtig Druk" #: UiStrings.resx$AltDeinterleave$Message msgid "Alternate Deinterleave" -msgstr "" +msgstr "Hersorteer Dalende Interblad" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" -msgstr "" +msgstr "Dalende Interblad" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Vra Altyd" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Vra Altyd" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" -msgstr "" +msgstr "Addisionele komponent benodig om hierdie PDF in te trek. Wil jy dit nou aflaai?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "'n Nuwe weergawe is beskikbaar." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "OCR fout het voorgekom." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "Magtigings-fout het voorgekom." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." -msgstr "Có lỗi xảy ra khi cố gắng tự động Lưu." +msgstr "Daar was 'n fout tydens outo-stoor." + +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Daar was 'n fout met installasie van die nuwe weergawe." #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." -msgstr "Có lỗi xảy ra khi cố gắng để lưu các tập tin." +msgstr "Daar was 'n fout tydens leêr-stoor." #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "Daar was a 'n fout tydens die e-pos versending." +msgstr "Daar was 'n fout tydens die e-pos versending." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." -msgstr "Có lỗi xảy ra trong khi đang cố gắng để gửi một email." +msgstr "Daar was 'n fout tydens die e-pos versending." #: MiscResources.resx$UnknownDriverError$Message #: SdkResources.resx$UnknownDriverError$Message msgid "An error occurred with the scanning driver." -msgstr "Có lỗi xảy ra với trình điều khiển máy quét." +msgstr "Daar was n fout met die skandeer drywer." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" msgstr "Die program is tans besig met 'n taak. Is jy seker dat jy die program wil verlaat en die taak kanselleer?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "xảy ra một lỗi không rõ trong đợt quét." +msgid "An unknown error occurred during the batch scan." +msgstr "Obekende fout tydens bondel skandeer." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "" +msgstr "Nuwe weergawe is beskikbaar." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "Một bản cập nhật để OCR có sẵn." +msgstr "Nuwe OCR weergawe is beskikbaar." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple Drywer" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Pos" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Toepassing" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" -msgstr "Áp dụng độ sáng / tương phản sau khi quét" +msgstr "Pas helderheid / kontras aan na skandering" #: UiStrings.resx$ApplyToSelected$Message msgid "Apply to all {0} selected images" -msgstr "Áp dụng cho tất cả {0} hình ảnh được chọn" +msgstr "Pas toe op al {0} gekiesde beelde" #: MiscResources.resx$ConfirmCancelBatch$Message msgid "Are you sure you want to cancel the batch scan?" -msgstr "" +msgstr "Bevestig bondel-skandeer kanselasie?" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" -msgstr "Bạn có chắc chắn bạn muốn xóa {0} sản phẩm (s)?" +msgstr "Verseker dat jy {0} item(s) wil verwyder?" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "Bạn có chắc chắn muốn xóa \"{0}\"?" +msgstr "Verseker dat jy \"{0}\" wil uitvee?" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" -msgstr "Bạn có chắc chắn bạn muốn xóa hồ sơ {0}?" +msgstr "Verseker dat jy profiel {0} wil uitvee?" #: MiscResources.resx$ConfirmDeleteItems$Message msgid "Are you sure you want to delete {0} item(s)?" -msgstr "Bạn có chắc chắn muốn xóa {0} sản phẩm (s)?" +msgstr "Verseker dat jy {0} item(s) wil uitvee?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "Bạn có chắc chắn muốn xóa {0} hồ sơ?" +msgstr "Verseker dat jy {0} profiele wil uitvee?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Bevestig stop van deel {0}?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Bạn có chắc chắn bạn muốn hoàn tác các thay đổi đến {0} hình ảnh (s)?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" -msgstr "Tên tập tin đính kèm:" +msgstr "Aanhangsel Naam:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" -msgstr "Tác giả:" +msgstr "Skrywer:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "Magtig" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "Outo" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" -msgstr "Cài đặt Tự động Lưu" +msgstr "Outo-stoor" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "số tự động-incrementing (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Outo-inkrement nommer (1 syfer)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" -msgstr "số tự động-incrementing (2 digits)" +msgstr "Outo-inkrement nommer (2 syfers)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" -msgstr "số tự động-incrementing (3 digits)" +msgstr "Outo-inkrement nommer (3 syfers)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" -msgstr "" +msgstr "Outo-inkrement nommer (4 syfers)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "" +msgstr "Outo-OCR na skandering" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" -msgstr "Scan hàng loạt" +msgstr "Bondel Skandeer" #: MiscResources.resx$BatchStatusCancelled$Message msgid "Batch cancelled." -msgstr "Hủy Scan hàng loạt." +msgstr "Bondel gekanseleer." #: MiscResources.resx$BatchStatusComplete$Message msgid "Batch completed successfully." -msgstr "Scan hàng loạt thành công." +msgstr "Bondel suksesvol afgehandel." #: MiscResources.resx$BatchStatusError$Message msgid "Batch scan stopped due to error." @@ -296,414 +306,463 @@ msgstr "Scan hàng loạt dừng lại do lỗi." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Beste" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" -msgstr "Chiều sâu:" +msgstr "Bisdiepte:" #: MiscResources.resx$FileTypeBmp$Message msgid "Bitmap Files (*.bmp)" -msgstr "" +msgstr "Bitmap Leêrs (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" -msgstr "Trắng & Đen" +msgid "Black and White" +msgstr "Swart en Wit" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" -msgstr "Trang Trắng" +msgstr "Leë bladsye" #: UiStrings.resx$BrightnessContrast$Message msgid "Brightness / Contrast" -msgstr "" +msgstr "Helderheid / Kontras" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Độ sáng:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" -msgstr "Hủy" +msgstr "Kanselleer" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" -msgstr "" +msgstr "Kanseleer Bondel" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." -msgstr "Đang dừng...." +msgstr "Besig om te kanseleer...." #: SettingsResources.resx$HorizontalAlign_Center$Message msgid "Center" -msgstr "Canh giữa" +msgstr "Senter" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" -msgstr "" +msgstr "Verander" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "" +msgstr "Kyk vir nuwe weergawes" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "" +msgstr "Besig om te kyk..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "" +msgstr "Kies e-pos Verskaffer" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" -msgstr "Chọn thuộc tính" +msgstr "Kies Profiel" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" -msgstr "Chọn Ổ Đĩa" +msgstr "Kies Toestel" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message msgid "Clear" -msgstr "Xóa" +msgstr "Maak skoon" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Verwyder Alles" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" -msgstr "Xóa ảnh sau khi lưu" +msgstr "Verwyder Beelde na stoor" #: MiscResources.resx$Close$Message msgid "Close" -msgstr "Đóng" +msgstr "Maak toe" + +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Kombineer" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Kommunikasie met die skandeertoestel is onderbreek." -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" -msgstr "Khả năng tương thích" +msgstr "Aanpasbaarheid" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" -msgstr "" +msgstr "Samepersing:" + +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Koppel" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Verbindingsfout." -#: FEditProfile.resx$label7.Text$Message #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" -msgstr "Tương phản:" +msgstr "Kontras:" #: UiStrings.resx$Copy$Message msgid "Copy" -msgstr "Sao chép" +msgstr "Kopieer" #: MiscResources.resx$CopyProgress$Message msgid "Copy Progress" -msgstr "Sao chép Tiến trình" +msgstr "Kopieëringvordering" #: MiscResources.resx$Copying$Message msgid "Copying..." -msgstr "Đang sao chép..." +msgstr "Besig om te kopieer..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Kopiereg {0} NAPS2-bydraers" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" -msgstr "Bảo hiểm Threshold" +msgstr "Dekkingsdrempel" #: UiStrings.resx$Crop$Message msgid "Crop" -msgstr "Chỉnh sửa" +msgstr "Knip" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" -msgstr "Krimp to bladsygrootte" +msgstr "Knip na bladsygrootte" #: MiscResources.resx$CustomPageSizeFormat$Message msgid "Custom ({0}x{1} {2})" -msgstr "Tùy chỉnh ({0}x{1} {2})" +msgstr "Eie Groote ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" -msgstr "Tùy chỉnh kiểu giấy" +msgstr "Pasgemaakte Bladsy Grootte" + +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" -msgstr "Tùy chỉnh xoay" +msgstr "Pasgemaakte Rotasie" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" -msgstr "" +msgstr "Pasgemaakte SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Tùy chỉnh..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" -msgstr "Ngày(01-31)" +msgstr "Dag(01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" -msgstr "Mặc định" +msgstr "Verstek" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" -msgstr "Đường dẫn mặc định:" +msgstr "Verstek lêeradres:" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" -msgstr "" +msgstr "Dalende Interblad" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" -msgstr "Xóa" +msgstr "Vee uit" #: UiStrings.resx$Deskew$Message msgid "Deskew" -msgstr "" +msgstr "Reguit-trek" #: MiscResources.resx$AutoDeskewProgress$Message msgid "Deskew Progress" -msgstr "" +msgstr "Reguit-trek Vordering" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" -msgstr "" +msgstr "Trek geskandeerde blaaie reguit" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." -msgstr "" +msgstr "Besig om reguit te trek..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" -msgstr "Thiết bị:" +msgstr "Toestel:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" -msgstr "Kích thước" +msgstr "Dimensies" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" -msgstr "Hiển thị tên:" +msgstr "Vertoon Naam:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Dokument Regstelling" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "" +msgstr "Skenk" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" -msgstr "Xong" +msgstr "Klaar" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" -msgstr "Tải Về" +msgstr "Aflaai" #: MiscResources.resx$DownloadError$Message msgid "Download Error" -msgstr "Tải Về lỗi" +msgstr "Aflaai Misluk" #: MiscResources.resx$DownloadNeeded$Message msgid "Download Needed" -msgstr "" +msgstr "Aflaai Benodig" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" -msgstr "Tiến độ tải về" +msgstr "Aflaai Vordering" + +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" -msgstr "Hai mặt" +msgstr "Duplex" + +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL Drywer" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL Netwerk Drywer" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB Drywer" #: UiStrings.resx$Edit$Message msgid "Edit" -msgstr "Sửa" +msgstr "Wysig" + +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "ePos Alles" #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "ePos Alles as PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "" +msgstr "ePosPDF" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" -msgstr "" +msgstr "ePos PDF Vordering" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "ePos Gekies" #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "ePos Gekose as PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" -msgstr "Cài đặt Email" +msgstr "E-posinstellings" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" -msgstr "Kích hoạt Lưu tự động" +msgstr "Aktiveer Outo-stoor" + +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Aktiveer ontfout-aantekening" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" -msgstr "Mã hóa PDF" +msgstr "Enkripteer PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" -msgstr "" +msgstr "Enkripsie" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "" +msgstr "Verbeterde Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" -msgstr "Lỗi" +msgstr "Fout" + +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" -msgstr "Ước tính kích thước download: {0} MB" +msgstr "Beraamde Aflaai grootte: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Uitruilbare beeldlêer (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" -msgstr "Loại trừ các trang trống" +msgstr "Sluit leë bladsye uit" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Vinnig" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" -msgstr "" +msgstr "Voerder" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Tên tệp" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Đường dẫn:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Maak witbalans reg en verwyder geraas" + #: UiStrings.resx$Flip$Message msgid "Flip" -msgstr "Lật" +msgstr "Flip" + +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Blaai agterkante van dupleksbladsye om" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" -msgstr "Lật 2 mặt trang" +msgstr "Flip duplex bladsye" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "" +msgstr "Vir hoë JPEG-eienskappe (80+), verhoog ook beeldkwaliteit in jou profiel vir die beste resultate." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" -msgstr "" +msgstr "GIF Lêer (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" -msgstr "Thêm Ngôn ngữ" +msgstr "Kry meer tale" #: SettingsResources.resx$Source_Glass$Message msgid "Glass" -msgstr "Kính" +msgstr "Glas" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" -msgstr "" +msgstr "Grys" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" -msgstr "Chỉnh chiều ngang:" +msgstr "Horisontale plasing:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" -msgstr "Giờ (0-23)" +msgstr "Uur (0-23)" #: UiStrings.resx$HueSaturation$Message msgid "Hue / Saturation" -msgstr "" +msgstr "Tint / Versadiging" + +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Gasheer" #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" -msgstr "" +msgstr "Ikone van:" #: UiStrings.resx$Image$Message msgid "Image" -msgstr "Ảnh" +msgstr "Beeld" #: MiscResources.resx$FileTypeImageFiles$Message msgid "Image Files" -msgstr "TỆP ẢNH" +msgstr "Beeld Lêers" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" -msgstr "Chất lượng ảnh" +msgstr "Beeld Kwaliteit" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" -msgstr "Cài đặt ảnh" +msgstr "Beeld Instellings" #: MiscResources.resx$ImageSaved$Message msgid "Image saved." @@ -711,119 +770,159 @@ msgstr "Lưu ảnh." #: UiStrings.resx$Import$Message msgid "Import" -msgstr "Chèn" +msgstr "Intrek" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" -msgstr "Chèn xong" +msgstr "Intrek Vordering" #: MiscResources.resx$ImportingFormat$Message msgid "Importing {0}..." -msgstr "Đang chèn {0}..." +msgstr "Intrek Besig {0}..." #: MiscResources.resx$Importing$Message msgid "Importing..." -msgstr "Đang chèn..." +msgstr "Intrek Besig..." #: MiscResources.resx$Install$Message msgid "Install {0}" -msgstr "" +msgstr "Installeer {0}" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" -msgstr "Cài đặt thành công" +msgstr "Installasie Afgehandel" #: MiscResources.resx$InstallFailedTitle$Message msgid "Installation Failed" -msgstr "Cài đặt bị lỗi" +msgstr "Installasie het misluk" #: MiscResources.resx$InstallCompletePromptRestart$Message msgid "Installation complete. Do you want to restart NAPS2 now?" -msgstr "Cài đặt hoàn tất. Bạn có muốn khởi động lại NAPS2 bây giờ?" +msgstr "Installasie Afgehandel. Herlaai NAPS2?" #: MiscResources.resx$InstallFailed$Message msgid "Installation failed." -msgstr "Cài đặt thất bại." +msgstr "Installasie het misluk." + +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Koppelvlak" #: UiStrings.resx$Interleave$Message msgid "Interleave" -msgstr "" +msgstr "Stygende Interblad" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" -msgstr "" +msgstr "JPEG Lêer (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000 Lêer (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" +msgstr "Jpeg Kwaliteit" + +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Behoue beelde oor sessies" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" msgstr "" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" -msgstr "Từ khóa:" +msgstr "Sleutelwoorde:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" -msgstr "Ngôn ngữ" +msgstr "Taal" + +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" -msgstr "Trái" +msgstr "Links" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" -msgstr "Legacy (UI bản địa chỉ)" +msgstr "Tradisioneel (Slegs inheemse UI)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" -msgstr "Lưu ảnh into NAPS2" +msgstr "Laai Beelde in tot NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" -msgstr "Hãy tìm kiếm các file PDF sử dụng OCR" +msgstr "Maak PDF's soekbaar met OCR" + +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Hand-ingevoerde IP" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" -msgstr "Tập tin quá lớn" +msgstr "Maksimum Kwaliteit (Groot Lêers)" + +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Geheue Oordrag" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: UiStrings.resx$Metadata$Message msgid "Metadata" -msgstr "" +msgstr "Metadata" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minuut" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Maand" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Meer inligting" #: UiStrings.resx$MoveDown$Message msgid "Move Down" -msgstr "Af Beweeg" +msgstr "Skuif af" #: UiStrings.resx$MoveUp$Message msgid "Move Up" msgstr "Skuif Op" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Veelvuldige Tale" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Veelvuldige Tale..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Veelvuldige skanderings (vaste vertraging tussen skanderings)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Veelvuldige skanderings (vinnige tussen skanderings)" @@ -831,17 +930,17 @@ msgstr "Veelvuldige skanderings (vinnige tussen skanderings)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "" +msgstr "NAPS2 is heeltemal gratis. Oorweeg dit om 'n skenking te maak." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Naam (opsioneel)" @@ -849,6 +948,10 @@ msgstr "Naam (opsioneel)" msgid "Name missing." msgstr "Noem ontbreek." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Inheemse Oordrag" + #: UiStrings.resx$New$Message msgid "New" msgstr "Nuwe" @@ -861,7 +964,7 @@ msgstr "Nuwe Profiel" msgid "Next" msgstr "Volgende" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Volgende Skandering" @@ -870,15 +973,18 @@ msgstr "Volgende Skandering" msgid "No device selected." msgstr "Geen toestel gekies." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Geen toestel gevind nie." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Geen bladsye is in die voerder." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "" +msgstr "Geen verskaffer gkies nie." #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message @@ -887,68 +993,53 @@ msgstr "Geen skandering toestel gevind." #: MiscResources.resx$NoUpdates$Message msgid "No updates available." -msgstr "" +msgstr "Geen nuuter weergawes beskikbaar nie." #: SettingsResources.resx$TiffComp_None$Message msgid "None" -msgstr "" +msgstr "Geen" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Netta A+ PDF Skandeerder" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Nie Nou Nie" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Aantal skanderings:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR Aflaai" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "" +msgstr "OCR-vordering" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCR Opstel" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR taal:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "" +msgstr "OCR-modus:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "Goed" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Afstand breedte gebaseer op belyning (WIA)" @@ -956,88 +1047,92 @@ msgstr "Afstand breedte gebaseer op belyning (WIA)" msgid "Old DSM" msgstr "Ou DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" -msgstr "Een per bladsy" +msgstr "Een lêer per bladsy" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" -msgstr "Een per skandering" +msgstr "Een lêer per skandering" #: MiscResources.resx$FilesCouldNotBeDownloaded$Message msgid "One or more files could not be downloaded." msgstr "Een of meer lêers kon nie afgelaai word." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Laat slegs 'n enkele NAPS2-instansie toe" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" -msgstr "Oop Vouer" +msgstr "Maak Vouer oop" #: MiscResources.resx$ActiveOperations$Message msgid "Operation in Progress" +msgstr "Besig met handeling" + +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" msgstr "" #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Web Toegang" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Uitset" #: MiscResources.resx$OverwriteFile$Message msgid "Overwrite File" -msgstr "Oorskryf Lêer" +msgstr "Skryf Oor Lêer" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Eienaar Wagwoord:" #: MiscResources.resx$FileTypePdf$Message msgid "PDF Document (*.pdf)" -msgstr "PDF dokument (* .pdf)" +msgstr "PDF dokument (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF Stellings" #: MiscResources.resx$PdfSaved$Message msgid "PDF saved." -msgstr "PDF gered." +msgstr "PDF gestoor." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "PNG-lêer (* png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Bladsy grootte:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Bladsy bron:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Wagwoord" @@ -1045,21 +1140,24 @@ msgstr "Wagwoord" msgid "Paste" msgstr "Plak" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Plekhouers" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Poort" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" -msgstr "" +msgstr "Agterna-verwerking" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Doen voorlopige OCR na skandering" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Druk Begin wanneer gereed." @@ -1067,7 +1165,7 @@ msgstr "Druk Begin wanneer gereed." msgid "Preview" msgstr "Voorskou" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Voorskou:" @@ -1080,12 +1178,11 @@ msgstr "Vorige" msgid "Print" msgstr "Afdruk" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Profiel Stellings" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profiel:" @@ -1094,263 +1191,352 @@ msgstr "Profiel:" msgid "Profiles" msgstr "Profiele" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Vra indien gekies" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" -msgstr "" +msgstr "Vra vir lêeradres" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "Verskaffer" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Gereed vir skandering {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Herstel" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Herstel Geskandeerde Beelde" #: MiscResources.resx$Recovering$Message msgid "Recovering..." -msgstr "Herstel..." +msgstr "Besig om te Herstel..." #: MiscResources.resx$RecoveryProgress$Message msgid "Recovery Progress" -msgstr "" +msgstr "Herwinningsvordering" + +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Doen oor" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Doen {0} oor" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" -msgstr "" +msgstr "Onthou hierdie instellings" #: UiStrings.resx$Reorder$Message msgid "Reorder" -msgstr "" +msgstr "Herrangskik" #: UiStrings.resx$Reset$Message msgid "Reset" -msgstr "" +msgstr "Stel terug" #: MiscResources.resx$ResetImage$Message msgid "Reset Image" -msgstr "" +msgstr "Stel Beeld terug" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" -msgstr "" +msgstr "Resolusie:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" -msgstr "" +msgstr "Herstel verstekwaardes" #: UiStrings.resx$Reverse$Message msgid "Reverse" -msgstr "" +msgstr "Keer om" + +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Keer Alles om" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Keer gekiesde om" #: UiStrings.resx$Revert$Message msgid "Revert" -msgstr "" +msgstr "Keer terug" #: SettingsResources.resx$HorizontalAlign_Right$Message msgid "Right" -msgstr "" +msgstr "Regs" #: UiStrings.resx$Rotate$Message msgid "Rotate" -msgstr "" +msgstr "Roteer" #: UiStrings.resx$RotateLeft$Message msgid "Rotate Left" -msgstr "" +msgstr "Roteer Links" #: UiStrings.resx$RotateRight$Message msgid "Rotate Right" -msgstr "" +msgstr "Roteer Regs" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "" +msgstr "Hardloop in agtergrond" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "" +msgstr "Besig om OCR uit te voer..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "SANE Drywer" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Stoor" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Stoor Alles" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Stoor Alles as Beelde" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Stoor Alles as PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message msgid "Save Images" -msgstr "" +msgstr "Stoor Beelde" #: MiscResources.resx$SaveImagesProgress$Message msgid "Save Images Progress" -msgstr "" +msgstr "Beeldstoor Vordering" #: MiscResources.resx$SavePdf$Message #: UiStrings.resx$SavePdf$Message msgid "Save PDF" -msgstr "" +msgstr "Stoor PDF" #: MiscResources.resx$SavePdfProgress$Message msgid "Save PDF Progress" -msgstr "" +msgstr "PDF-Stoor Vordering" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Stoor gekiesde" #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Stoor Gekiesde as Beelde" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Stoor Gekiesde as PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" -msgstr "" +msgstr "Stoor na 'n enkele lêer" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" -msgstr "" +msgstr "Stoor na veelvuldige lêers" #: MiscResources.resx$BatchStatusSaving$Message msgid "Saving batch results..." -msgstr "" +msgstr "Stoor tans bondelresultate..." #: MiscResources.resx$SavingFormat$Message msgid "Saving {0}..." -msgstr "" +msgstr "Stoor tans {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" -msgstr "" +msgstr "Skaal Met Venster" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" -msgstr "" +msgstr "Skaal:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" -msgstr "" +msgstr "Skandeer" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" +msgstr "Skandeer opstelling" + +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Skandeer met verstekprofiel" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" msgstr "" #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" -msgstr "" +msgstr "Geskandeerde Beeld" + +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Skandeerder Deel" #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" -msgstr "" +msgstr "Skandeer bladsy {0}" #: MiscResources.resx$BatchStatusScanPage$Message msgid "Scanning page {0} (scan {1})..." -msgstr "" +msgstr "Skandeer tans bladsy {0} (skandeer {1})..." #: MiscResources.resx$BatchStatusPage$Message #: MiscResources.resx$ScanProgressPage$Message msgid "Scanning page {0}..." -msgstr "" +msgstr "Skandeer tans bladsy {0}..." + +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Soek tans vir toestelle..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" -msgstr "" +msgstr "Sekonde (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" -msgstr "" +msgstr "Kies" #: UiStrings.resx$SelectAll$Message msgid "Select All" msgstr "Kies alles" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Kies Toestel" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" -msgstr "" +msgstr "Kies Bron" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." -msgstr "" +msgstr "Kies 'n profiel voordat u Skandeer klik." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" -msgstr "" +msgstr "Kies een of meer tale:" #: MiscResources.resx$SelectedCount$Message msgid "Selected ({0})" -msgstr "" +msgstr "Gekies ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" -msgstr "" +msgstr "Skei lêers met Patch-T" #: UiStrings.resx$SetDefault$Message msgid "Set Default" -msgstr "" +msgstr "Stel verstek" + +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Stellings" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Deel" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Deel selfs wanneer NAPS2 toe is" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Gedeelde skandeer-instellings" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Gedeelde skandeerders kan vanaf ander rekenaars op die plaaslike netwerk gebruik word deur \"ESCL Driver\" in die ander rekenaar se NAPS2-profielinstellings te kies." #: UiStrings.resx$Sharpen$Message msgid "Sharpen" +msgstr "Verskerp" + +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Show$Message msgid "Show" +msgstr "Wys" + +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Wys \"Profiele\" nutsbalk" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Wys inheemse TWAIN-vordering" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Wys bladsy nommers" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" msgstr "" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" -msgstr "" +msgstr "Enkel bladsy lêers" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" -msgstr "" +msgstr "Enkel skandeering" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" -msgstr "" +msgstr "Slaan stoor-boodskap oor" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Split" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "" @@ -1358,12 +1544,11 @@ msgstr "" msgid "TIFF File (*.tiff, *.tif)" msgstr "" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "" @@ -1372,6 +1557,10 @@ msgstr "" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,8 +1583,8 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" msgstr "" #: MiscResources.resx$DevicePaperJam$Message @@ -1443,192 +1632,226 @@ msgstr "" msgid "The selected scanner is offline." msgstr "" -#: FImageSettings.resx$groupTiff.Text$Message -msgid "Tiff Options" +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message +msgid "Tiff Options" +msgstr "Tiff keuses" + +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" +msgstr "Titel:" + +#: UiStrings.resx$Tools$Message +msgid "Tools" msgstr "" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 in)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" +msgstr "US Legal (8.5x14 in)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" msgstr "" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" -msgstr "" +msgstr "Ongebergde Veranderings" #: MiscResources.resx$UpdateProgress$Message msgid "Update Progress" -msgstr "" +msgstr "Opdateer-vordering" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Nuwe weergawe soek is afgeskakel." #: MiscResources.resx$Updating$Message msgid "Updating..." -msgstr "" +msgstr "Besig om nuwe weergawe te instaleeri..." #: MiscResources.resx$UploadingEmail$Message msgid "Uploading email..." -msgstr "" +msgstr "Besig om ePos op te laai..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" -msgstr "" +msgstr "Gebruik inheemse UI" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" -msgstr "" +msgstr "Gebruik vooraf gedefinieerde instellings" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" -msgstr "" +msgstr "Gebruiker Wagwoord:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." -msgstr "" +msgstr "Die gebruik van OCR vereis dat jy elke taal wat jy wil skandeer, aflaai." #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message msgid "Version {0}" -msgstr "" +msgstr "Weergawe {0}" #: UiStrings.resx$View$Message msgid "View" -msgstr "" +msgstr "Aansig" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" -msgstr "" +msgstr "WIA Drywer" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." -msgstr "" +msgstr "Wag tans vir TWAIN om te voltooi..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." -msgstr "" +msgstr "Wag vir magtiging..." #: MiscResources.resx$BatchStatusWaitingForScan$Message msgid "Waiting for scan {0}..." -msgstr "" +msgstr "Wag tans vir skandering {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" -msgstr "" +msgstr "Wit Drumpel" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "WIA Weergawe:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" -msgstr "" +msgstr "Jaar" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" -msgstr "" +msgstr "Jaar (00-99)" #: MiscResources.resx$PdfNoPermissionToExtractContent$Message #: SdkResources.resx$PdfNoPermissionToExtractContent$Message msgid "You do not have permission to copy content from the file '{0}'." -msgstr "" +msgstr "Jy het nie toestemming om inhoud vanaf die lêer '{0}' te kopieer nie." #: MiscResources.resx$DontHavePermission$Message msgid "You don't have permission to save files at this location." -msgstr "" +msgstr "Jy het nie toestemming om lêers hier te stoor nie." #: MiscResources.resx$ExitWithUnsavedChanges$Message msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" -msgstr "" +msgstr "Jy het ongebergde veranderinge. Is jy seker jy wil die program verlaat en veranderinge verloor?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" -msgstr "" +msgstr "Zoom" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" -msgstr "" +msgstr "Werklike grootte" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" -msgstr "" +msgstr "Vergroot" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" -msgstr "" +msgstr "Verklein" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "duim" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" -msgstr "" +msgstr "van {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" -msgstr "" +msgstr "{0} / {1} leêrs" + +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} Toestelle gevind." -#: FRecover.resx$lblPrompt.Text$Message +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" -msgstr "" +msgstr "{0} beeld(e) geskandeer op {1} teen {2} mag dalk nie gestoor het nie, en is herwinbaar. Wiljy dit herwin?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." -msgstr "" +msgstr "{0} Beelde gestoor." #: MiscResources.resx$PdfStatus$Message #: UiStrings.resx$XOfY$Message msgid "{0} of {1}" -msgstr "" +msgstr "{0} van {1}" diff --git a/NAPS2.Lib/Lang/po/ar.po b/NAPS2.Lib/Lang/po/ar.po index 97fc3f3236..b3d7c8a605 100644 --- a/NAPS2.Lib/Lang/po/ar.po +++ b/NAPS2.Lib/Lang/po/ar.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Arabic\n" "Language: ar\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "100 نقطة لكل بوصة" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "إجراء \"حفظ\" الافتراضي:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "1200 نقطة لكل بوصة" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "الإجراء الافتراضي لزر \"مسح\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "150 نقطة لكل بوصة" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "القائمة \"المسح\" تغير الملف الشخصي الافتراضي" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "200 نقطة لكل بوصة" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "تم العثور على جهاز 1." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "لون 24-بت" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "300 نقطة لكل بوصة" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "400 نقطة لكل بوصة" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "600 نقطة لكل بوصة" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "800 نقطة لكل بوصة" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "أ3 297 × 420 مم" @@ -76,12 +60,15 @@ msgstr "حول" msgid "Acquiring data..." msgstr "الحصول على البيانات..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "متقدم" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "إعدادات الملف الشخصي المتقدمة" @@ -93,35 +80,35 @@ msgstr "كل ({0})" msgid "All Files" msgstr "كل الملفات" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "اسمح بالتعليقات التوضيحية Annotations" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "اسمح بنسخ المحتوى" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "السماح بنسخ المحتوى لسهولة الوصول" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "السماح بتجميع المستندات" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "السماح بتعديل الملف" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "السماح بملء الحقول" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "السماح بالطباعة بالجودة الكاملة" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "السماح بالطباعة" @@ -133,17 +120,22 @@ msgstr "Deinterleave البديل" msgid "Alternate Interleave" msgstr "Interleave البديل" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "التحويل البديل" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "اسأل دائما" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" -msgstr "قوائم الأرشفة هى نتاج ماتم إنشاءه سابقا فى الإعدادات العامة (التراميز) والمجموعات بشقيها الرئيسي والفرعى والقوالب وهيكلها بعد أن تم إنشاءها لتظهرلنا على الشكل المراد إظهارها عليه :" +msgstr "قوائم الأرشفة هى نتاج ماتم إنشاءه سابقا فى الإعدادات العامة (التراميز) والمجموعات بشقيها الرئيسي والفرعى والقوالب وهيكلها بعد أن تم إنشاءها لتظهرلنا على الشكل المراد إظهارها عليه؟" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "حدث خطأ ما أثناء تثبيت التحديثات‏." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "" #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "حدث خطأ أثناء محاولة التوثيق‏." msgid "An error occurred when trying to auto save." msgstr "حدث خطأ عند محاولة الحفظ الآلي." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "حدث خطأ ما أثناء تثبيت التحديثات‏." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "حدث خطأ أثناء محاولة حفظ الملف." @@ -168,15 +164,15 @@ msgstr "حدث خطأ أثناء محاولة إرسال بريد إلكترون #: MiscResources.resx$UnknownDriverError$Message #: SdkResources.resx$UnknownDriverError$Message msgid "An error occurred with the scanning driver." -msgstr "حدث خطأ مع تعريف المسح الضوئي.." +msgstr "حدث خطأ مع تعريف المسح الضوئي." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "هناك عملية قيد التنفيذ الآن. هل أنت متأكد أنك تريد إلغاء العملية والخروج؟‏" +msgstr "هناك عملية قيد التنفيذ الآن. هل أنت متأكد أنك تريد إلغاء العملية والخروج؟" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "حدث خطأ غير معروف أثناء المسح الضوئي على دفعات." +msgid "An unknown error occurred during the batch scan." +msgstr "حدث خطأ غير معروف أثناء مسح الدفعة." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -190,7 +186,15 @@ msgstr "هناك تحديث متاح للـ OCR." msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "تطبيق السطوع/التباين بعد المسح الضوئي" @@ -208,7 +212,7 @@ msgstr "أمتأكد من أنك تريد مسح {0} عنصر؟" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "هل أنت متأكد أنك تريد حذف \"{0}\"?" +msgstr "هل أنت متأكد أنك تريد حذف \"{0}\"؟" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" @@ -222,19 +226,27 @@ msgstr "أمتأكد أنك ترغب في حذف هذا/هذه {0} العنصر/ msgid "Are you sure you want to delete {0} profiles?" msgstr "هل أنت متأكد من أنك تريد حذف الملفات الشخصية {0}؟" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "هل أنت متأكد من أنك تريد التراجع عن التغييرات التي أجريتها على {0} صورة؟" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "اسم المرفق:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "المؤلف:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "رَخّص" @@ -242,29 +254,27 @@ msgstr "رَخّص" msgid "Auto" msgstr "" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "إعدادات الحفظ التلقائي" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "رقم متزايد تلقائياً (عدد واحد)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "رقم متزايد تلقائياً (عددين)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "رقم متزايد تلقائياً (3 أعداد)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "رقم متزايد تلقائياً (4 أعداد)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "مباشرة اقلاع قارئ الحروف بعد الفحص" @@ -277,8 +287,8 @@ msgstr "B4 (250 × 353 مم)" msgid "B5 (176x250 mm)" msgstr "B5 (176 × 250 مم)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "مسح ضوئي على دفعات" @@ -298,7 +308,6 @@ msgstr "المسح الضوئي على دفعات توقف بسبب خطأ." msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "عمق البت:" @@ -309,10 +318,10 @@ msgstr "ملفات صور نقطية (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "أبيض و أسود" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "صفحات فارغة" @@ -320,7 +329,6 @@ msgstr "صفحات فارغة" msgid "Brightness / Contrast" msgstr "السطوع-التباين" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "السطوع:" @@ -329,24 +337,11 @@ msgstr "السطوع:" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "إلغاء" @@ -363,19 +358,19 @@ msgstr "جارِ الإلغاء...." msgid "Center" msgstr "منتصف" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "تغيير" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "التحقق من وجود تحديثات:" +msgstr "التحقق من وجود تحديثات" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "يجري الفحص......" +msgstr "يجري الفحص..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "اختر مقدم خدمة الاميل" @@ -383,7 +378,6 @@ msgstr "اختر مقدم خدمة الاميل" msgid "Choose Profile" msgstr "اختيار ملف الشخصي" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "اختيار جهاز" @@ -397,7 +391,7 @@ msgstr "محو" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "محو الصور بعد الحفظ" @@ -405,16 +399,30 @@ msgstr "محو الصور بعد الحفظ" msgid "Close" msgstr "إغلاق" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "التوافق" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "الضغط:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "التباين:" @@ -435,7 +443,7 @@ msgstr "ينسخ..." msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "تغطية العتبة" @@ -443,7 +451,7 @@ msgstr "تغطية العتبة" msgid "Crop" msgstr "قطع" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "اقتصاص لحجم الصفحة" @@ -451,10 +459,14 @@ msgstr "اقتصاص لحجم الصفحة" msgid "Custom ({0}x{1} {2})" msgstr "مخصص ({0} × {1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "حجم صفحة مخصص" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "استدارة مخصصة" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "بروتوكول نقل البريد البسيط المخصص" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "مخصص..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "اليوم (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "الإفتراضي" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "مسار الملف الإفتراضي:" @@ -487,7 +504,6 @@ msgstr "مسار الملف الإفتراضي:" msgid "Deinterleave" msgstr "" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "" msgid "Deskew Progress" msgstr "تقدم Deskew" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" @@ -509,16 +525,14 @@ msgstr "" msgid "Deskewing..." msgstr "مسح ضوئي..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "الجهاز:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "الأبعاد" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "اسم العرض:" @@ -532,12 +546,10 @@ msgstr "" msgid "Donate" msgstr "تبرع" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "تم" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "تنزيل" @@ -550,19 +562,47 @@ msgstr "خطأ في التنزيل" msgid "Download Needed" msgstr "تحميل اللوازم" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "التقدم التنزيل" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "تحرير" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +616,32 @@ msgstr "إرسال PDF بالبريد" msgid "Email PDF Progress" msgstr "تقدم إرسال PDF بالبريد" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "إعدادات البريد الإلكتروني" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "تمكين الحفظ التلقائي" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "تشفير الـ PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "التشفير" @@ -602,12 +649,15 @@ msgstr "التشفير" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Windows ملف التعريف المحسن (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "خطأ" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "حجم التنزيل المقدّر: {0} ميغابايت" msgid "Exchangeable Image File (*.exif)" msgstr "ملف صورة قابلة لإستبدال (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "استبعاد الصفحات الفارغة" @@ -629,24 +679,31 @@ msgstr "" msgid "Feeder" msgstr "المغذِّي" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "اسم الملف" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "مسار الملف:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "قلب" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "قلب الصفحات المزدوجة" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" @@ -654,7 +711,6 @@ msgstr "" msgid "GIF File (*.gif)" msgstr "ملف GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "الحصول على المزيد من اللغات" @@ -671,12 +727,11 @@ msgstr "" msgid "Grayscale" msgstr "تدرج الرمادي" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "محاذاة أفقية:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "ساعة (0-23)" @@ -684,6 +739,10 @@ msgstr "ساعة (0-23)" msgid "Hue / Saturation" msgstr "التدرج والتشبع" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "الأيقونات من:" @@ -696,12 +755,12 @@ msgstr "صورة" msgid "Image Files" msgstr "ملفات صور" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "جودة الصورة" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "إعدادات الصورة" @@ -731,7 +790,7 @@ msgstr "تنصيب {0}" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" -msgstr "اكتمل التثبيت." +msgstr "اكتمل التثبيت" #: MiscResources.resx$InstallFailedTitle$Message msgid "Installation Failed" @@ -745,6 +804,10 @@ msgstr "اكتمل التثبيت. هل تريد إعادة تشغيل NAPS2 ا msgid "Installation failed." msgstr "فشل التثبيت." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "" @@ -757,11 +820,20 @@ msgstr "ملف JPEG (*.jpg, *.jpeg)" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "جودة Jpeg" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "الكلمات المفتاحية:" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "اللغة" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "يسار" @@ -781,33 +857,48 @@ msgstr "يسار" msgid "Legacy (native UI only)" msgstr "وراثي (واجهة المستخدم الأصلية فقط)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "تحميل صور إلى NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "جعل ملفات PDF قابلة للبحث باستخدام التعرف الضوئي على الحروف OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "الجودة القصوى (ملفات كبيرة)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "بيانات وصفية" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "دقيقة (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "شهر (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "المزيد من المعلومات" @@ -819,11 +910,19 @@ msgstr "تحريك لأسفل" msgid "Move Up" msgstr "تحريك لأعلي" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "مسوح ضوئية متعددة (توقيت تأخير ثابت بين عمليات المسح)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "مسوح ضوئية متعددة (مطالبة بين عمليات المسح)" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "الاسم (اختياري)" @@ -849,6 +948,10 @@ msgstr "الاسم (اختياري)" msgid "Name missing." msgstr "الاسم مفقود." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "جديد" @@ -861,7 +964,7 @@ msgstr "ملف شخصي جديد" msgid "Next" msgstr "التالي" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "المسح الضوئي التالي" @@ -870,12 +973,15 @@ msgstr "المسح الضوئي التالي" msgid "No device selected." msgstr "لم يتم تحديد أي جهاز." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "لم يتم العثور على أي أجهزة." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "لا توجد صفحات في المغذي." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "" @@ -897,11 +1003,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "ليس الآن" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "عدد عمليات المسح الضوئي:" @@ -909,7 +1015,6 @@ msgstr "عدد عمليات المسح الضوئي:" msgid "OCR" msgstr "التعرف الضوئي على الحروف OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "تحميل التعرف الضوئي على الحروف OCR" @@ -918,37 +1023,23 @@ msgstr "تحميل التعرف الضوئي على الحروف OCR" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "إعداد التعرف الضوئي على الحروف OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "لغة التعرف الضوئي على الحروف OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "حسناً" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "إزاحة العرض استناداً إلى المحاذاة (WIA)" @@ -956,13 +1047,11 @@ msgstr "إزاحة العرض استناداً إلى المحاذاة (WIA)" msgid "Old DSM" msgstr "DSM القديم" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "ملف واحد لكل صفحة" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "ملف واحد لكل عملية مسح ضوئي" @@ -970,7 +1059,11 @@ msgstr "ملف واحد لكل عملية مسح ضوئي" msgid "One or more files could not be downloaded." msgstr "تعذر تنزيل ملف واحد أو أكثر." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "فتح المجلد" @@ -978,11 +1071,15 @@ msgstr "فتح المجلد" msgid "Operation in Progress" msgstr "العملية قيد التقدم" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "الإخراج" @@ -990,7 +1087,7 @@ msgstr "الإخراج" msgid "Overwrite File" msgstr "الكتابة فوق الملف" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "كلمة مرور المالك:" @@ -998,8 +1095,8 @@ msgstr "كلمة مرور المالك:" msgid "PDF Document (*.pdf)" msgstr "مستند PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "إعدادات PDF" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "ملف PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "حجم الصفحة:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "مصدر الورق:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "كلمة المرور" @@ -1045,21 +1140,24 @@ msgstr "كلمة المرور" msgid "Paste" msgstr "لصق" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "عناصر نائبة" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "اضغط بدأ عندما تكون جاهزاً." @@ -1067,7 +1165,7 @@ msgstr "اضغط بدأ عندما تكون جاهزاً." msgid "Preview" msgstr "معاينة" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "معاينة:" @@ -1080,12 +1178,11 @@ msgstr "السابق" msgid "Print" msgstr "طباعة" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "إعدادات الملف الشخصي" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "الملف الشخصي:" @@ -1094,23 +1191,27 @@ msgstr "الملف الشخصي:" msgid "Profiles" msgstr "الملفات الشخصية" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "مزود" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "جاهز للمسح الضوئي {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "استرداد" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "استرداد الصور الممسوحة ضوئياً" @@ -1122,9 +1223,15 @@ msgstr "يستردّ..." msgid "Recovery Progress" msgstr "تقدّم الاسترداد" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "تذكر هذه الإعدادات" @@ -1140,15 +1247,11 @@ msgstr "إعادة تعيين" msgid "Reset Image" msgstr "إعادة تعيين الصورة" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "الدقة:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "استعادة الإعدادات الافتراضية" @@ -1156,6 +1259,14 @@ msgstr "استعادة الإعدادات الافتراضية" msgid "Reverse" msgstr "عكس" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "عودة" @@ -1176,7 +1287,6 @@ msgstr "استدارة لليسار" msgid "Rotate Right" msgstr "استدارة لليمين" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1185,7 +1295,6 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1303,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "حفظ PDF" msgid "Save PDF Progress" msgstr "تقدم حفظ PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "حفظ إلى ملف واحد" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "حفظ إلى ملفات متعددة" @@ -1244,29 +1363,45 @@ msgstr "حفظ نتائج الدفعات..." msgid "Saving {0}..." msgstr "يحفظ {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "تحجيم مع النافذة" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "المقياس:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "مسح ضوئي" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "تكوين المسح الضوئي" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "صورة ممسوحة ضوئياً" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "يمسح ضوئياً الصفحة {0}" @@ -1280,11 +1415,14 @@ msgstr "يمسح ضوئياً الصفحة {0} (مسح ضوئي) {1}..." msgid "Scanning page {0}..." msgstr "يمسح ضوئياً الصفحة {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "ثانية (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "اختيار" @@ -1293,7 +1431,10 @@ msgstr "اختيار" msgid "Select All" msgstr "اختيار الكل" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "اختيار مصدر" @@ -1302,7 +1443,6 @@ msgstr "اختيار مصدر" msgid "Select a profile before clicking Scan." msgstr "حدد ملف شخصي قبل النقر فوق مسح ضوئي." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "اختيار لغة واحدة أو أكثر:" @@ -1311,8 +1451,7 @@ msgstr "اختيار لغة واحدة أو أكثر:" msgid "Selected ({0})" msgstr "مختار ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,37 +1459,84 @@ msgstr "" msgid "Set Default" msgstr "تعين كافتراضي" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "إظهار" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "صفحه واحده" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "مسح ضوئي فردي" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "تخطي مطالبة الحفظ" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "بدء" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "الموضوع:" @@ -1358,12 +1544,11 @@ msgstr "الموضوع:" msgid "TIFF File (*.tiff, *.tif)" msgstr "ملف TIFF (*.tiff، *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "تعريف TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "تفاصيل تقنيه" @@ -1372,6 +1557,10 @@ msgstr "تفاصيل تقنيه" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "الملف {0} موجود بالفعل. هل تريد الكتابة فوقه؟" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "تم تشفير الملف التالي ويتطلب كلمة مرور للفتح: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "الماسح الضوئي المحدد مشغول." msgid "The selected scanner is offline." msgstr "الماسح الضوئي المحدد غير متصل." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "خيارات Tiff" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "الوقت بين عمليات المسح الضوئي (ثواني):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "العنوان:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "" @@ -1467,6 +1676,22 @@ msgstr "US قانوني (8.5x14 بوصة)" msgid "US Letter (8.5x11 in)" msgstr "US رسالة (8.5x11 بوصة)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "التغييرات التي لم يتم حفظها" @@ -1487,21 +1712,18 @@ msgstr "تحديث..." msgid "Uploading email..." msgstr "تحميل الرساله..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "استخدام واجهة المستخدم الأصلية" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "استخدام إعدادات معرفة مسبقاً" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "كلمة مرور المستخدم:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "استخدام التعرف الضوئي على الحروف OCR يتطلب منك تحميل كل لغة تريد مسحها ضوئيا." @@ -1515,16 +1737,15 @@ msgstr "الإصدارة {0}" msgid "View" msgstr "عرض" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "تعريف WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "في انتظار TWAIN للإكتمال..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "انتظر التفويض..." @@ -1532,19 +1753,19 @@ msgstr "انتظر التفويض..." msgid "Waiting for scan {0}..." msgstr "في انتظار المسح الضوئي {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "سنة" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "سنة (00-99)" @@ -1561,21 +1782,18 @@ msgstr "ليس لديك إذن لحفظ الملفات في هذا الموقع. msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "لديك تغييرات غير محفوظة. هل أنت متأكد من أنك تريد الخروج وتجاهل هذه التغييرات؟" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "تكبير" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "التكبير الفعلي" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "تكبير" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "تصغير" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "{0} ({1} × {2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "{0}/{1} ميغابايت" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0}/{1} ملفات" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} نقطة لكل بوصة" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} صورة/صور ممسوحة ضوئياً على {1} في {2} قد لم يتم حفظها، وهي قابلة للاسترداد. هل تريد استعادتها؟" diff --git a/NAPS2.Lib/Lang/po/bg.po b/NAPS2.Lib/Lang/po/bg.po index 0e130ef668..48e0759c50 100644 --- a/NAPS2.Lib/Lang/po/bg.po +++ b/NAPS2.Lib/Lang/po/bg.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Bulgarian\n" "Language: bg\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Запазване:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Сканиране:" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "100 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Сканиране" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "Намерено е 1 устройство." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24 битов цвят" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "A3 (297x420 мм)" @@ -76,12 +60,15 @@ msgstr "Относно" msgid "Acquiring data..." msgstr "Получаване на данни..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Допълнителни настройки" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Допълнителни настройки на профила" @@ -93,73 +80,82 @@ msgstr "Всички ({0})" msgid "All Files" msgstr "Всички файлове" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Разрешаване на анотации" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Разрешаване копиране на съдържанието" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Разреши копиране на съдържание за достъпност" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Разреши конструиране на документ" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Разрешаване на промяна на документа" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Разрешаване на попълване на формуляри" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Разрешаване на принтиране с високо качество" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Разреши принтиране" #: UiStrings.resx$AltDeinterleave$Message msgid "Alternate Deinterleave" -msgstr "" +msgstr "Редуващо разпръскване" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" -msgstr "" +msgstr "Заместващо редуване" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Винаги питай" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Постоянно напомняне" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" -msgstr "" +msgstr "Необходим е допълнителен компонент за импортиране на този PDF файл. Искате ли да го изтеглите сега?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "" +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Възникна грешка при OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "Възникна грешка при опит за упълномощаване." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." msgstr "Грешка при опит за автоматичен запис на файла." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Грешка при опит за инталация на обновление." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Възникна грешка при опит за запис на файла." #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "" +msgstr "Възникна грешка при опит за изпращане на имейла." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." @@ -172,11 +168,11 @@ msgstr "Проблем с драйвъра на скенера." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "" +msgstr "Операцията е в развитие. Сигурни ли сте, че искате да излезете и да отмените операцията?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Възникна неизвестна грешка при групово сканиране." +msgid "An unknown error occurred during the batch scan." +msgstr "Възникна неизвестна грешка по време на груповото сканиране." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -184,13 +180,21 @@ msgstr "Налично обновяване." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "Наличен е ъпдейт за OCR модула." +msgstr "Наличен е актуализация за OCR модула." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple драйвер" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple имейл" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Приложение" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Приложи яркост/контраст след сканиране" @@ -222,19 +226,27 @@ msgstr "Сигурни ли сте, че искате да изтриете {0} msgid "Are you sure you want to delete {0} profiles?" msgstr "Сигурни ли сте, че искате да изтриете профила {0}?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Сигурни ли сте, че искате да спрете споделянето на {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Сигурни ли сте, че искате да отмените редакцията на {0} документ/а ?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Име на прикачения файл:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Автор:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Упълномощи" @@ -242,29 +254,27 @@ msgstr "Упълномощи" msgid "Auto" msgstr "Автоматично" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Автоматично запазване на настройките" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Автоматично отброяване (1 цифра)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Автоматично нарастване (с 1 цифра)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Автоматично отброяване (2 цифри)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Автоматично отброяване (3 цифри)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Автоматично отброяване (4 цифри)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Автоматично стартиране на Оптично разпознаване след сканиране" @@ -277,8 +287,8 @@ msgstr "B4 (250x353 мм)" msgid "B5 (176x250 mm)" msgstr "B5 (176x250 мм)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Групово сканиране" @@ -296,9 +306,8 @@ msgstr "Груповото сканиране спря поради грешка #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Най-добър" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Дълбочина на цвета:" @@ -309,10 +318,10 @@ msgstr "Bitmap файл (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Черно и бяло" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Празни страници" @@ -320,33 +329,19 @@ msgstr "Празни страници" msgid "Brightness / Contrast" msgstr "Яркост / Контраст" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Яркост:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Отказ" @@ -363,7 +358,7 @@ msgstr "Анулиране...." msgid "Center" msgstr "Център" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Промяна" @@ -375,7 +370,7 @@ msgstr "Проверка за обновяване" msgid "Checking..." msgstr "Проверяване..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Избери е-майл доставчик" @@ -383,7 +378,6 @@ msgstr "Избери е-майл доставчик" msgid "Choose Profile" msgstr "Изберете профил" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Изберете устройство" @@ -395,9 +389,9 @@ msgstr "Изчистване" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Изчистване на всичко" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Изчисти изображенията след запис" @@ -405,16 +399,30 @@ msgstr "Изчисти изображенията след запис" msgid "Close" msgstr "Затвори" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Комбиниране" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Комуникацията със сканиращото устройство беше прекъсната." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Съвместимост" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Компресиране:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Свързване" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Грешка при свързване." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Контраст:" @@ -433,61 +441,69 @@ msgstr "Копиране..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Авторско право {0} на сътрудници на NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" -msgstr "" +msgstr "Праг на покритие" #: UiStrings.resx$Crop$Message msgid "Crop" msgstr "Изрязване" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" -msgstr "" +msgstr "Изрязване до размера на страницата" #: MiscResources.resx$CustomPageSizeFormat$Message msgid "Custom ({0}x{1} {2})" msgstr "По избор ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Нестандартен размер на страница" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Завъртане по избор" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" -msgstr "" +msgstr "Персонализиран SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "По избор..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Ден (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "По подразбиране" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Директория по подразбиране:" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" -msgstr "" +msgstr "Смесване" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -495,49 +511,45 @@ msgstr "Изтриване" #: UiStrings.resx$Deskew$Message msgid "Deskew" -msgstr "" +msgstr "Изправяне" #: MiscResources.resx$AutoDeskewProgress$Message msgid "Deskew Progress" -msgstr "" +msgstr "Напредък на изправянето" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" -msgstr "" +msgstr "Изправяне на сканираните страници" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." -msgstr "" +msgstr "Изправяне..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Устройство:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Размери" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Име за показване:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Корекция на документа" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "" +msgstr "Даряване" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Готово" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Сваляне" @@ -548,24 +560,52 @@ msgstr "Грешка при сваляне" #: MiscResources.resx$DownloadNeeded$Message msgid "Download Needed" -msgstr "" +msgstr "Необходимо изтегляне" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Напредък на свалянето" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Двустранно" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL драйвер" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL мрежов драйвер" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB драйвер" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Редактиране" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Изпращане на имейла на всички" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Изпращане на имейла към всички като PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,25 +616,32 @@ msgstr "Изпрати PDF по имейл" msgid "Email PDF Progress" msgstr "Напредък на изпращане на PDF по имейл" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Имейлът е избран" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Имейлът е избран като PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Настройки за имейл" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Автоматично запазване" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Активиране на регистрирането на грешки" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Шифроване на PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Шифроване" @@ -602,12 +649,15 @@ msgstr "Шифроване" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Подобрен Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Грешка" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,46 +665,52 @@ msgstr "Приблизителен размер за сваляне: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "стандарт за метаданни в графични и звукови файлове (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Пропусни празните страници" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Бърз" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Захранваща тава за листи" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Име на файл" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Местоположение на файла:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Коригиране баланса на бялото и премахване на шума" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Обръщане" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Обърнете обратните страни на страниците" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Обърни двустранно сканираните страници" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "" +msgstr "За високо качество на JPEG (80+), също така увеличете и качеството на изображението във вашия профил за най-добри резултати." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" msgstr "GIF файл (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Още езици" @@ -665,24 +721,27 @@ msgstr "Стъкло" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Степени на сивото" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Хоризонтално подравняване:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Час (0-23)" #: UiStrings.resx$HueSaturation$Message msgid "Hue / Saturation" -msgstr "" +msgstr "Нюанс / Насищане" + +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Host" #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" @@ -696,12 +755,12 @@ msgstr "Изображение" msgid "Image Files" msgstr "Изображения" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Качество на изображението" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Настройки на изображението" @@ -727,7 +786,7 @@ msgstr "Импортиране..." #: MiscResources.resx$Install$Message msgid "Install {0}" -msgstr "" +msgstr "Инсталиране {0}" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" @@ -745,9 +804,13 @@ msgstr "Инсталацията е завършена. Искате ли да msgid "Installation failed." msgstr "Неуспешна инсталация." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Интерфейс" + #: UiStrings.resx$Interleave$Message msgid "Interleave" -msgstr "" +msgstr "Редуване" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" @@ -755,59 +818,87 @@ msgstr "JPEG файл (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG-2000 файл (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Jpeg качество" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Съхраняване на изображенията по време на сесиите" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Ключови думи:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW - алгоритъм за компресиране на данни без загуби" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Език" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Ляво" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" +msgstr "Остаряло (само с присъщия потребителски интерфейс)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Внасяне на изображения в NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Създай PDF с възможност за търсене посредством OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Ръчно IP" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Максимално качество (по-големи файлове)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Прехвърляне на паметта" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Допълнителни данни" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Минути (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Месец (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Повече информация" @@ -819,11 +910,19 @@ msgstr "Преместване надолу" msgid "Move Up" msgstr "Преместване нагоре" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Множество езици" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Множество езици..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Многократно сканиране (фиксирано забавяне между сканиранията)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Многократно сканиране (запитване между сканиранията)" @@ -831,17 +930,17 @@ msgstr "Многократно сканиране (запитване между #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "" +msgstr "NAPS2 е напълно безплатен. Обмислете дарение." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Име (незадължително)" @@ -849,6 +948,10 @@ msgstr "Име (незадължително)" msgid "Name missing." msgstr "Липсва име." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Местен трансфер" + #: UiStrings.resx$New$Message msgid "New" msgstr "Нов" @@ -861,7 +964,7 @@ msgstr "Нов профил" msgid "Next" msgstr "Следващ" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Следващо сканиране" @@ -870,15 +973,18 @@ msgstr "Следващо сканиране" msgid "No device selected." msgstr "Не е избрано устройство." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Няма намерени устройства." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Няма листoве в устройството за подаване." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "" +msgstr "Няма избран доставчик." #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message @@ -887,68 +993,53 @@ msgstr "Не е открито сканиращо устройство." #: MiscResources.resx$NoUpdates$Message msgid "No updates available." -msgstr "" +msgstr "Няма налични актуализации." #: SettingsResources.resx$TiffComp_None$Message msgid "None" -msgstr "" +msgstr "Нито един" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Не е като други PDF скенери" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Не сега" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Брой сканирания:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "Оптично разпознаване на думи" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Сваляне на OCR" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "" +msgstr "Напредък на оптичното разпознаване" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Настройки на OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Език за OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "" +msgstr "OCR режим:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "Да" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Компенсирай широчината на база подравняване (WIA)" @@ -956,13 +1047,11 @@ msgstr "Компенсирай широчината на база подравн msgid "Old DSM" msgstr "Стар DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Всяка страница в отделен файл" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Всяко сканиране в отделен файл" @@ -970,19 +1059,27 @@ msgstr "Всяко сканиране в отделен файл" msgid "One or more files could not be downloaded." msgstr "Един или няколко файла не могат да бъдат свалени." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Позволяване на само един екземпляр на NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Отвори папка" #: MiscResources.resx$ActiveOperations$Message msgid "Operation in Progress" +msgstr "Операцията е в ход" + +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" msgstr "" #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Web Access" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Резултат" @@ -990,7 +1087,7 @@ msgstr "Резултат" msgid "Overwrite File" msgstr "Презаписване на файла" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Парола на собственика:" @@ -998,8 +1095,8 @@ msgstr "Парола на собственика:" msgid "PDF Document (*.pdf)" msgstr "PDF документ (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Настройки на PDF" @@ -1009,35 +1106,33 @@ msgstr "PDF записан." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "PNG файл (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Размер на листовете:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Източник на хартия:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Парола" @@ -1045,21 +1140,24 @@ msgstr "Парола" msgid "Paste" msgstr "Постави" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Заместители" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Порт" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" -msgstr "" +msgstr "Последваща обработка" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Стартирайте превантивно OCR след сканиране" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Натиснете Старт, когато сте готови." @@ -1067,7 +1165,7 @@ msgstr "Натиснете Старт, когато сте готови." msgid "Preview" msgstr "Преглед" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Преглед:" @@ -1080,12 +1178,11 @@ msgstr "Предишен" msgid "Print" msgstr "Печат" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Настройки на профила" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Профили:" @@ -1094,23 +1191,27 @@ msgstr "Профили:" msgid "Profiles" msgstr "Профили" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Ако е избрано напомняне" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" -msgstr "" +msgstr "Напомняне за път на файла" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "Доставчик" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Готовност за сканиране {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Възстановяване" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Възстановяване на сканираните изображения" @@ -1122,9 +1223,15 @@ msgstr "Възстановяване..." msgid "Recovery Progress" msgstr "Напредък на възстановяването" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Възстановяване на отменено действие" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Възстановяване на отменено действие {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Запомни тези настройки" @@ -1140,15 +1247,11 @@ msgstr "Изчистване" msgid "Reset Image" msgstr "Връщане изображението в начално състояние" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Резолюция:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Възстановяване на настройки по подразбиране" @@ -1156,6 +1259,14 @@ msgstr "Възстановяване на настройки по подразб msgid "Reverse" msgstr "Обръщане" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Обръщане на всички" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Обратно на избраното" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Връщане в начално състояние" @@ -1176,31 +1287,34 @@ msgstr "Завъртане наляво" msgid "Rotate Right" msgstr "Завъртане надясно" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "" +msgstr "Изпълнение във фонов режим" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "" +msgstr "Започва оптичното разпознаване..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "SANE драйвер" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Запазване" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Запазване на всички" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Запазване на всички като изображения" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Запазване на всички като PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Запис в PDF" msgid "Save PDF Progress" msgstr "Напредък на запазване на PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Запазване на избраното" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Запазване на избраното като изображения" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Запазване на избраното като PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Запис в един файл" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Запис в отделни файлове" @@ -1244,29 +1363,45 @@ msgstr "Запазване на груповата задача..." msgid "Saving {0}..." msgstr "Запазване {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Мащабиране според прозореца" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Мащаб:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Сканиране" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Настройки на сканирането" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Сканиране с профил по подразбиране" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Сканирано изображение" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Споделяне на скенер" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Сканиране на страница {0}" @@ -1280,11 +1415,14 @@ msgstr "Сканиране на страница {0} (сканиране {1})... msgid "Scanning page {0}..." msgstr "Сканиране на страница {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Търсят се устройства..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Секунди (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Избери" @@ -1293,7 +1431,10 @@ msgstr "Избери" msgid "Select All" msgstr "Избери всички" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Избиране на Устройство" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Избери източник" @@ -1302,7 +1443,6 @@ msgstr "Избери източник" msgid "Select a profile before clicking Scan." msgstr "Изберете профил преди да натиснете \"Сканиране\"." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Избери един или повече езици:" @@ -1311,46 +1451,92 @@ msgstr "Избери един или повече езици:" msgid "Selected ({0})" msgstr "Избрани ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" -msgstr "" +msgstr "Разделяне на файловете чрез Patch-T" #: UiStrings.resx$SetDefault$Message msgid "Set Default" msgstr "Задаване като настройки по подразбиране" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Настройки" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Споделяне" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Споделяне дори когато NAPS2 е затворен" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Споделени настройки на скенера" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Споделените скенери могат да се използват от други компютри в локалната мрежа, като изберете \"ESCL драйвер\" в настройките на NAPS2 профила на другия компютър." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" +msgstr "Изостряне" + +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Показване" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message -msgid "Single page files" +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Показване на лентата с инструменти \"Профили\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Показване на напредъка на TWAIN" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Показване на номерата на страниците" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SinglePageFiles$Message +msgid "Single page files" +msgstr "Файлове с една страница" + +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Единично сканиране" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Пропусни запитването за запис" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Разделяне" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Старт" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message -msgid "Stretch to page size" +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$StretchToPageSize$Message +msgid "Stretch to page size" +msgstr "Разтягане до размера на страницата" + +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Тема:" @@ -1358,24 +1544,27 @@ msgstr "Тема:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF файл (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN драйвър" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Технически данни" #: MiscResources.resx$TesseractNotAvailable$Message #: SdkResources.resx$TesseractNotAvailable$Message msgid "The OCR engine is not available. Make sure to install the required package:" -msgstr "" +msgstr "OCR двигател не е наличен. Уверете се, че сте инсталирали необходимия пакет:" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Операцията за OCR изтече." #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" -msgstr "" +msgstr "Драйверът SANE не е наличен. Уверете се, че сте инсталирали необходимите пакети:" #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message @@ -1394,9 +1583,9 @@ msgstr "Файлът не може да бъде презаписан, защо msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Вече има файл {0} . Искате ли да бъде презаписан?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Този файл е шифрован и е нужна парола за отварянето му: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Следният файл е шифрован и изисква парола за отваряне:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1416,7 +1605,7 @@ msgstr "Капакът на скенера не е затворен." #: MiscResources.resx$DriverNotSupported$Message #: SdkResources.resx$DriverNotSupported$Message msgid "The selected driver is not supported on this system." -msgstr "" +msgstr "Избраният драйвер не се поддържа от тази система." #: MiscResources.resx$DeviceNotFound$Message #: SdkResources.resx$DeviceNotFound$Message @@ -1443,19 +1632,39 @@ msgstr "Избраният скенер е недостъпен." msgid "The selected scanner is offline." msgstr "Избраният скенер е недостъпен." -#: FImageSettings.resx$groupTiff.Text$Message -msgid "Tiff Options" +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Работният процес се срина." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Работният процес се срина. Проверете програмата за преглед на събитията на Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message +msgid "Tiff Options" +msgstr "Опции на Tiff" + +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Интервал между сканиранията (в секунди):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Заглавие:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Инструменти" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Twain реализация:" @@ -1467,41 +1676,54 @@ msgstr "US Legal (8.5x14 инча)" msgid "US Letter (8.5x11 in)" msgstr "US писмо (8.5x11 инч)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Отмяна на извършено действие" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Отмяна на извършено действие {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Незаписани промени" #: MiscResources.resx$UpdateProgress$Message msgid "Update Progress" -msgstr "" +msgstr "Напредък на актуализацията" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Проверката за актуализация е деактивирана." #: MiscResources.resx$Updating$Message msgid "Updating..." -msgstr "" +msgstr "Актуализиране..." #: MiscResources.resx$UploadingEmail$Message msgid "Uploading email..." -msgstr "" +msgstr "Качване на имейл..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Използвай вградения потребителски интерфейс" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Използвай готови настройки" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Потребителска парола:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "За използването на OCR е нужно да свалите всеки от езиците, които искате да сканирате ." @@ -1515,36 +1737,35 @@ msgstr "Версия {0}" msgid "View" msgstr "Изглед" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA драйвър" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Изчакване TWAIN да приключи задачата..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." -msgstr "" +msgstr "Чака се позволение..." #: MiscResources.resx$BatchStatusWaitingForScan$Message msgid "Waiting for scan {0}..." msgstr "Изчакване на сканиране {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Праг на бялото" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Wia версия:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Година" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Година (00-99)" @@ -1561,21 +1782,18 @@ msgstr "Нямате права за създаването на файлове msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Има незаписани промени. Сигурни ли сте, че искате да затворите програмата и да отхвърлите тези промени?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Увеличаване/Намаляване" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Действително увеличение" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Увеличаване" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Намаляване" @@ -1598,28 +1816,33 @@ msgstr "от {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} файлове" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} намерени устройства." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} изображение/я, сканирани от {1} на {2} вероятно не са записани, но могат да бъдат възстановени. Искате ли да ги възстановите?" diff --git a/NAPS2.Lib/Lang/po/bn.po b/NAPS2.Lib/Lang/po/bn.po index bc9ccaf4bd..2484804982 100644 --- a/NAPS2.Lib/Lang/po/bn.po +++ b/NAPS2.Lib/Lang/po/bn.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Bengali\n" "Language: bn\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" -msgstr "" - -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" +msgstr "২৪-বিট রঙ" #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "এ৩ (২৯৭x৪২০ এমএম)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "এ৪ (২১০x২৯৭ এমএম)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "এ৫ (১৪৮x২১০ এমএম)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -74,14 +58,17 @@ msgstr "" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." +msgstr "তথ্য সংগ্রহ করা হচ্ছে..." + +#: UiStrings.resx$Action$Message +msgid "Action" msgstr "" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "" @@ -93,35 +80,35 @@ msgstr "" msgid "All Files" msgstr "" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "" @@ -133,16 +120,21 @@ msgstr "" msgid "Alternate Interleave" msgstr "" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." msgstr "" #: MiscResources.resx$AuthError$Message @@ -153,6 +145,10 @@ msgstr "" msgid "An error occurred when trying to auto save." msgstr "" +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "" + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "" @@ -168,14 +164,14 @@ msgstr "" #: MiscResources.resx$UnknownDriverError$Message #: SdkResources.resx$UnknownDriverError$Message msgid "An error occurred with the scanning driver." -msgstr "" +msgstr "স্ক্যানিং ড্রাইভারের সাথে একটি ত্রুটি ঘটেছে৷" #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" msgstr "" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "" #: MiscResources.resx$UpdateAvailable$Message @@ -190,7 +186,15 @@ msgstr "" msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "" @@ -208,33 +212,41 @@ msgstr "" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "" +msgstr "আপনি কি নিশ্চিত যে আপনি \"{0}\" মুছে ফেলতে চান?" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" -msgstr "" +msgstr "আপনি কি নিশ্চিত \"{0}\" প্রোফাইলটি মুছে ফেলতে চান?" #: MiscResources.resx$ConfirmDeleteItems$Message msgid "Are you sure you want to delete {0} item(s)?" -msgstr "" +msgstr "আপনি কি নিশ্চিত যে আপনি \"{0}\"টি জিনিসকে মুছে ফেলতে চান?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" +msgstr "আপনি কি নিশ্চিত যে আপনি \"{0}\"টি প্রোফাইলকে মুছে ফেলতে চান?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" msgstr "" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "" @@ -242,29 +254,27 @@ msgstr "" msgid "Auto" msgstr "" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "" @@ -277,8 +287,8 @@ msgstr "" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "" @@ -298,7 +308,6 @@ msgstr "" msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "" @@ -309,10 +318,10 @@ msgstr "" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "" @@ -320,7 +329,6 @@ msgstr "" msgid "Brightness / Contrast" msgstr "" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "" @@ -329,24 +337,11 @@ msgstr "" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "" @@ -363,7 +358,7 @@ msgstr "" msgid "Center" msgstr "" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "" @@ -375,7 +370,7 @@ msgstr "" msgid "Checking..." msgstr "" -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "" @@ -383,7 +378,6 @@ msgstr "" msgid "Choose Profile" msgstr "" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "" @@ -397,7 +391,7 @@ msgstr "" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "" @@ -405,16 +399,30 @@ msgstr "" msgid "Close" msgstr "" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "" @@ -435,7 +443,7 @@ msgstr "" msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "" @@ -443,7 +451,7 @@ msgstr "" msgid "Crop" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "" @@ -451,10 +459,14 @@ msgstr "" msgid "Custom ({0}x{1} {2})" msgstr "" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "" -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "" @@ -487,7 +504,6 @@ msgstr "" msgid "Deinterleave" msgstr "" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "" msgid "Deskew Progress" msgstr "" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" @@ -509,16 +525,14 @@ msgstr "" msgid "Deskewing..." msgstr "" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "" @@ -532,12 +546,10 @@ msgstr "" msgid "Donate" msgstr "" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "" @@ -550,19 +562,47 @@ msgstr "" msgid "Download Needed" msgstr "" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +616,32 @@ msgstr "" msgid "Email PDF Progress" msgstr "" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "" @@ -602,12 +649,15 @@ msgstr "" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "" msgid "Exchangeable Image File (*.exif)" msgstr "" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "" @@ -629,24 +679,31 @@ msgstr "" msgid "Feeder" msgstr "" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "" + +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" msgstr "" #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" @@ -654,7 +711,6 @@ msgstr "" msgid "GIF File (*.gif)" msgstr "" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "" @@ -671,12 +727,11 @@ msgstr "" msgid "Grayscale" msgstr "" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "" @@ -684,6 +739,10 @@ msgstr "" msgid "Hue / Saturation" msgstr "" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "" @@ -696,12 +755,12 @@ msgstr "" msgid "Image Files" msgstr "" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "" @@ -745,6 +804,10 @@ msgstr "" msgid "Installation failed." msgstr "" +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "" @@ -757,11 +820,20 @@ msgstr "" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "" @@ -781,33 +857,48 @@ msgstr "" msgid "Legacy (native UI only)" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "" @@ -819,11 +910,19 @@ msgstr "" msgid "Move Up" msgstr "" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "" @@ -849,6 +948,10 @@ msgstr "" msgid "Name missing." msgstr "" +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "" @@ -861,7 +964,7 @@ msgstr "" msgid "Next" msgstr "" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "" @@ -870,12 +973,15 @@ msgstr "" msgid "No device selected." msgstr "" +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "" -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "" @@ -897,11 +1003,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "" @@ -909,7 +1015,6 @@ msgstr "" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "" @@ -918,37 +1023,23 @@ msgstr "" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "" @@ -970,7 +1059,11 @@ msgstr "" msgid "One or more files could not be downloaded." msgstr "" -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "" @@ -978,11 +1071,15 @@ msgstr "" msgid "Operation in Progress" msgstr "" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "" @@ -990,7 +1087,7 @@ msgstr "" msgid "Overwrite File" msgstr "" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "" @@ -998,8 +1095,8 @@ msgstr "" msgid "PDF Document (*.pdf)" msgstr "" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "" @@ -1045,21 +1140,24 @@ msgstr "" msgid "Paste" msgstr "" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "" @@ -1067,7 +1165,7 @@ msgstr "" msgid "Preview" msgstr "" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "" @@ -1080,12 +1178,11 @@ msgstr "" msgid "Print" msgstr "" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "" @@ -1094,23 +1191,27 @@ msgstr "" msgid "Profiles" msgstr "" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "" -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "" @@ -1122,9 +1223,15 @@ msgstr "" msgid "Recovery Progress" msgstr "" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "" @@ -1140,15 +1247,11 @@ msgstr "" msgid "Reset Image" msgstr "" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "" @@ -1156,6 +1259,14 @@ msgstr "" msgid "Reverse" msgstr "" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "" @@ -1176,7 +1287,6 @@ msgstr "" msgid "Rotate Right" msgstr "" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1185,7 +1295,6 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1303,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "" msgid "Save PDF Progress" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "" @@ -1244,29 +1363,45 @@ msgstr "" msgid "Saving {0}..." msgstr "" -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "" @@ -1280,11 +1415,14 @@ msgstr "" msgid "Scanning page {0}..." msgstr "" -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "" @@ -1293,7 +1431,10 @@ msgstr "" msgid "Select All" msgstr "সব নির্বাচন করুন" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "" @@ -1302,7 +1443,6 @@ msgstr "" msgid "Select a profile before clicking Scan." msgstr "" -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "" @@ -1311,8 +1451,7 @@ msgstr "" msgid "Selected ({0})" msgstr "" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,37 +1459,84 @@ msgstr "" msgid "Set Default" msgstr "" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "" @@ -1358,12 +1544,11 @@ msgstr "" msgid "TIFF File (*.tiff, *.tif)" msgstr "" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "" @@ -1372,6 +1557,10 @@ msgstr "" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,8 +1583,8 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" msgstr "" #: MiscResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "" msgid "The selected scanner is offline." msgstr "" -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "" @@ -1487,21 +1712,18 @@ msgstr "" msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "" @@ -1515,16 +1737,15 @@ msgstr "" msgid "View" msgstr "" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "" -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1532,19 +1753,19 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "" -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "" @@ -1561,21 +1782,18 @@ msgstr "" msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} ডিপিআই" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "" diff --git a/NAPS2.Lib/Lang/po/bs.po b/NAPS2.Lib/Lang/po/bs.po new file mode 100644 index 0000000000..2b26d91421 --- /dev/null +++ b/NAPS2.Lib/Lang/po/bs.po @@ -0,0 +1,1857 @@ +msgid "" +msgstr "" +"Project-Id-Version: naps2\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-04-19 21:51+0000\n" +"PO-Revision-Date: 2025-08-30 22:28\n" +"Last-Translator: \n" +"Language-Team: Bosnian\n" +"Language: bs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Translate Toolkit 1.13.0\n" +"X-Poedit-SourceCharset: iso-8859-1\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Crowdin-Project: naps2\n" +"X-Crowdin-Project-ID: 531762\n" +"X-Crowdin-Language: bs\n" +"X-Crowdin-File: templates.pot\n" +"X-Crowdin-File-ID: 75\n" + +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Zadana funckija dugmeta \"Saćuvaj\":" + +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Zadana funckija dugmeta \"Skeniraj\":" + +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Meni za \"Skeniranje\" mijenja zadani profil" + +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "Pronađen 1 uređaj." + +#: SettingsResources.resx$BitDepth_24Color$Message +msgid "24-bit Color" +msgstr "24-bitne boje" + +#: SettingsResources.resx$PageSize_A3$Message +msgid "A3 (297x420 mm)" +msgstr "A3 (297x420 mm)" + +#: SettingsResources.resx$PageSize_A4$Message +msgid "A4 (210x297 mm)" +msgstr "A4 (210x297 mm)" + +#: SettingsResources.resx$PageSize_A5$Message +msgid "A5 (148x210 mm)" +msgstr "A5 (148x210 mm)" + +#: UiStrings.resx$About$Message +#: UiStrings.resx$AboutFormTitle$Message +msgid "About" +msgstr "O programu" + +#: MiscResources.resx$AcquiringData$Message +msgid "Acquiring data..." +msgstr "Pribavljanje podataka..." + +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Radnja" + +#: UiStrings.resx$Advanced$Message +msgid "Advanced" +msgstr "Napredno" + +#: UiStrings.resx$AdvancedProfileFormTitle$Message +msgid "Advanced Profile Settings" +msgstr "Postavke naprednog profila" + +#: MiscResources.resx$AllCount$Message +msgid "All ({0})" +msgstr "Sve ({0})" + +#: MiscResources.resx$FileTypeAllFiles$Message +msgid "All Files" +msgstr "Sve datoteke" + +#: UiStrings.resx$AllowAnnotations$Message +msgid "Allow Annotations" +msgstr "Dozvoli anotacije" + +#: UiStrings.resx$AllowContentCopying$Message +msgid "Allow Content Copying" +msgstr "Dozvoli kopiranje sadržaja" + +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message +msgid "Allow Content Copying for Accessibility" +msgstr "Dozvoli kopiranje sadržaja radi pristupačnosti" + +#: UiStrings.resx$AllowDocumentAssembly$Message +msgid "Allow Document Assembly" +msgstr "Dozvoli sastavljanje dokumenata" + +#: UiStrings.resx$AllowDocumentModification$Message +msgid "Allow Document Modification" +msgstr "Dozvoli izmjene na dokumentu" + +#: UiStrings.resx$AllowFormFilling$Message +msgid "Allow Form Filling" +msgstr "Dozvoli ispunjavanje formulara" + +#: UiStrings.resx$AllowFullQualityPrinting$Message +msgid "Allow Full Quality Printing" +msgstr "Dozvoli štampanje u punom kvalitetu" + +#: UiStrings.resx$AllowPrinting$Message +msgid "Allow Printing" +msgstr "Dozvoli štampanje" + +#: UiStrings.resx$AltDeinterleave$Message +msgid "Alternate Deinterleave" +msgstr "Zamjeni izmjenično" + +#: UiStrings.resx$AltInterleave$Message +msgid "Alternate Interleave" +msgstr "Zamjeni naizmjenično" + +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Uvijek pitaj" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Uvijek upitaj" + +#: MiscResources.resx$PdfImportComponentNeeded$Message +msgid "An additional component is needed to import this PDF file. Would you like to download it now?" +msgstr "Dodatna komponenta je potrebna za uvoz ove PDF datoteke. Da li je želite preuzeti?" + +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Pojavila se greška pri pokretanju OPZ." + +#: MiscResources.resx$AuthError$Message +msgid "An error occurred when trying to authorize." +msgstr "Greška se pojavila pri pokušaju autorizacije." + +#: MiscResources.resx$AutoSaveError$Message +msgid "An error occurred when trying to auto save." +msgstr "Greška se pojavila pri pokušaju automatskog snimanja." + +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Greška se pojavila pri pokušaju instaliranja ažuriranja." + +#: MiscResources.resx$ErrorSaving$Message +msgid "An error occurred when trying to save the file." +msgstr "Greška se pojavila pri pokušaju snimanja datoteke." + +#: MiscResources.resx$ErrorEmailing$Message +msgid "An error occurred when trying to send the email." +msgstr "Greška se pojavila pri pokušaju slanja e-maila." + +#: MiscResources.resx$EmailError$Message +msgid "An error occurred while trying to send an email." +msgstr "Greška se pojavila pri pokušaju slanja nekog e-maila." + +#: MiscResources.resx$UnknownDriverError$Message +#: SdkResources.resx$UnknownDriverError$Message +msgid "An error occurred with the scanning driver." +msgstr "Greška se pojavila kod drivera za skeniranje." + +#: MiscResources.resx$ExitWithActiveOperations$Message +msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" +msgstr "Radnja je u toku. Da li sigurno želite otkazati radnju i izaći?" + +#: MiscResources.resx$BatchError$Message +msgid "An unknown error occurred during the batch scan." +msgstr "Nepoznata greška se pojavila tokom serijskog skeniranja." + +#: MiscResources.resx$UpdateAvailable$Message +msgid "An update is available." +msgstr "Dostupno je ažuriranje." + +#: MiscResources.resx$OcrUpdateAvailable$Message +msgid "An update to OCR is available." +msgstr "Dostupno je ažuriranje za OPZ." + +#: UiStrings.resx$AppleDriver$Message +msgid "Apple Driver" +msgstr "Apple Driver" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Aplikacija" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message +msgid "Apply brightness/contrast after scan" +msgstr "Primijeni osvjetljenje/kontrast nakon skeniranja" + +#: UiStrings.resx$ApplyToSelected$Message +msgid "Apply to all {0} selected images" +msgstr "Primijeni na sve(-ih) {0} izabrane(-ih) slike(-a)" + +#: MiscResources.resx$ConfirmCancelBatch$Message +msgid "Are you sure you want to cancel the batch scan?" +msgstr "Sigurni ste da želite otkazati serijsko skeniranje?" + +#: MiscResources.resx$ConfirmClearItems$Message +msgid "Are you sure you want to clear {0} item(s)?" +msgstr "Sigurni ste da želite očistiti {0} stavke(i)?" + +#: MiscResources.resx$ConfirmDelete$Message +msgid "Are you sure you want to delete \"{0}\"?" +msgstr "Sigurni ste da želite izbrisati \"{0}\"?" + +#: MiscResources.resx$ConfirmDeleteSingleProfile$Message +msgid "Are you sure you want to delete the profile {0}?" +msgstr "Sigurni ste da želite izbrisati profil {0}?" + +#: MiscResources.resx$ConfirmDeleteItems$Message +msgid "Are you sure you want to delete {0} item(s)?" +msgstr "Sigurni ste da želite izbrisati {0} stavke(i)?" + +#: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message +msgid "Are you sure you want to delete {0} profiles?" +msgstr "Sigurni ste da želite izbrisati {0} profil(a)?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Da li želite zaustaviti dijeljenje {0}?" + +#: MiscResources.resx$ConfirmResetImages$Message +msgid "Are you sure you want undo your changes to {0} image(s)?" +msgstr "Sigurni ste da želite vratiti promjene na {0} slike(a)?" + +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Dodijeli" + +#: UiStrings.resx$AttachmentNameLabel$Message +msgid "Attachment Name:" +msgstr "Naziv priloga:" + +#: UiStrings.resx$AuthorLabel$Message +msgid "Author:" +msgstr "Autor:" + +#: UiStrings.resx$AuthorizeFormTitle$Message +msgid "Authorize" +msgstr "Autoriziraj" + +#: SettingsResources.resx$TiffComp_Auto$Message +msgid "Auto" +msgstr "Automatska" + +#: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message +msgid "Auto Save Settings" +msgstr "Postavke automatskog snimanja" + +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Autorastući broj (1 cifra)" + +#: UiStrings.resx$AutoIncrementing2Digit$Message +msgid "Auto-incrementing number (2 digits)" +msgstr "Autorastući broj (2 cifre)" + +#: UiStrings.resx$AutoIncrementing3Digit$Message +msgid "Auto-incrementing number (3 digits)" +msgstr "Autorastući broj (3 cifre)" + +#: UiStrings.resx$AutoIncrementing4Digit$Message +msgid "Auto-incrementing number (4 digits)" +msgstr "Autorastući broj (4 cifre)" + +#: UiStrings.resx$RunOcrAfterScanning$Message +msgid "Automatically run OCR after scanning" +msgstr "Automatski pokreni OPZ nakon skeniranja" + +#: SettingsResources.resx$PageSize_B4$Message +msgid "B4 (250x353 mm)" +msgstr "B4 (250x353 mm)" + +#: SettingsResources.resx$PageSize_B5$Message +msgid "B5 (176x250 mm)" +msgstr "B5 (176x250 mm)" + +#: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message +msgid "Batch Scan" +msgstr "Serijsko skeniranje" + +#: MiscResources.resx$BatchStatusCancelled$Message +msgid "Batch cancelled." +msgstr "Serija otkazana." + +#: MiscResources.resx$BatchStatusComplete$Message +msgid "Batch completed successfully." +msgstr "Serija kompletirana uspješno." + +#: MiscResources.resx$BatchStatusError$Message +msgid "Batch scan stopped due to error." +msgstr "Serijsko skeniranje zaustavljeno zbog greške." + +#: SettingsResources.resx$OcrMode_Best$Message +msgid "Best" +msgstr "Najbolji" + +#: UiStrings.resx$BitDepthLabel$Message +msgid "Bit depth:" +msgstr "Bitni kvalitet:" + +#: MiscResources.resx$FileTypeBmp$Message +msgid "Bitmap Files (*.bmp)" +msgstr "Bitmap datoteke (*.bmp)" + +#: SettingsResources.resx$BitDepth_1BlackAndWhite$Message +#: UiStrings.resx$BlackAndWhite$Message +msgid "Black and White" +msgstr "Crna i bijela boja" + +#: UiStrings.resx$BlankPages$Message +msgid "Blank Pages" +msgstr "Prazne stranice" + +#: UiStrings.resx$BrightnessContrast$Message +msgid "Brightness / Contrast" +msgstr "Osvjetljenost / Kontrast" + +#: UiStrings.resx$BrightnessLabel$Message +msgid "Brightness:" +msgstr "Osvjetljenost:" + +#: SettingsResources.resx$TiffComp_Ccitt4$Message +msgid "CCITT4" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Ne može se pronaći vaš skener? Pročitajte o ograničenjima NAPS2 flatpak-a." + +#: MiscResources.resx$Cancel$Message +#: UiStrings.resx$Cancel$Message +msgid "Cancel" +msgstr "Otkaži" + +#: MiscResources.resx$CancelBatch$Message +msgid "Cancel Batch" +msgstr "Otkaži seriju" + +#: MiscResources.resx$BatchStatusCancelling$Message +msgid "Cancelling...." +msgstr "Otkazivanje...." + +#: SettingsResources.resx$HorizontalAlign_Center$Message +msgid "Center" +msgstr "centralno" + +#: UiStrings.resx$Change$Message +msgid "Change" +msgstr "Promijeni" + +#: UiStrings.resx$CheckForUpdates$Message +msgid "Check for updates" +msgstr "Provjeri ažuriranja" + +#: MiscResources.resx$CheckingForUpdates$Message +msgid "Checking..." +msgstr "Provjeravanje..." + +#: UiStrings.resx$EmailProviderFormTitle$Message +msgid "Choose Email Provider" +msgstr "Izaberi e-mail provajdera" + +#: MiscResources.resx$ChooseProfile$Message +msgid "Choose Profile" +msgstr "Izaberite profil" + +#: UiStrings.resx$ChooseDevice$Message +msgid "Choose device" +msgstr "Izaberite uređaj" + +#: MiscResources.resx$Clear$Message +#: UiStrings.resx$Clear$Message +msgid "Clear" +msgstr "Očisti" + +#: UiStrings.resx$ClearAll$Message +msgid "Clear All" +msgstr "Očisti sve" + +#: UiStrings.resx$ClearAfterSaving$Message +msgid "Clear images after saving" +msgstr "Očisti slike nakon snimanja" + +#: MiscResources.resx$Close$Message +msgid "Close" +msgstr "Zatvori" + +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Kombinuj" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Prekinuta je komunikacija sa uređajem za skeniranje." + +#: UiStrings.resx$Compatibility$Message +msgid "Compatibility" +msgstr "Kompatibilnost" + +#: UiStrings.resx$CompressionLabel$Message +msgid "Compression:" +msgstr "Kompresija:" + +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Poveži se" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Greška u povezivanju." + +#: UiStrings.resx$ContrastLabel$Message +msgid "Contrast:" +msgstr "Kontrast:" + +#: UiStrings.resx$Copy$Message +msgid "Copy" +msgstr "Kopiraj" + +#: MiscResources.resx$CopyProgress$Message +msgid "Copy Progress" +msgstr "Napredak kopiranja" + +#: MiscResources.resx$Copying$Message +msgid "Copying..." +msgstr "Kopiranje..." + +#: UiStrings.resx$CopyrightFormat$Message +msgid "Copyright {0} NAPS2 Contributors" +msgstr "Autorska prava zaštićena {0} NAPS2 saradnici" + +#: UiStrings.resx$CoverageThreshold$Message +msgid "Coverage Threshold" +msgstr "Prag pokrivenosti" + +#: UiStrings.resx$Crop$Message +msgid "Crop" +msgstr "Izreži" + +#: UiStrings.resx$CropToPageSize$Message +msgid "Crop to page size" +msgstr "Izreži na dimenzije stranice" + +#: MiscResources.resx$CustomPageSizeFormat$Message +msgid "Custom ({0}x{1} {2})" +msgstr "Prilagođeno ({0}x{1} {2})" + +#: UiStrings.resx$PageSizeFormTitle$Message +msgid "Custom Page Size" +msgstr "Prilagođene dimenzije stranice" + +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Prilagođena rezolucija" + +#: UiStrings.resx$CustomRotation$Message +msgid "Custom Rotation" +msgstr "Prilagođena rotacija" + +#: SettingsResources.resx$EmailProviderType_CustomSmtp$Message +msgid "Custom SMTP" +msgstr "Prilagođeni SMTP" + +#: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message +msgid "Custom..." +msgstr "Prilagođeno..." + +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Tamna" + +#: UiStrings.resx$Day2Digit$Message +msgid "Day (01-31)" +msgstr "Dan (01-31)" + +#: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message +#: SettingsResources.resx$TwainImpl_Default$Message +#: SettingsResources.resx$WiaVersion_Default$Message +#: UiStrings.resx$Default$Message +msgid "Default" +msgstr "Zadana" + +#: UiStrings.resx$DefaultFilePathLabel$Message +msgid "Default File Path:" +msgstr "Zadana putanja do datoteke:" + +#: UiStrings.resx$Deinterleave$Message +msgid "Deinterleave" +msgstr "Izmjenično" + +#: MiscResources.resx$Delete$Message +#: UiStrings.resx$Delete$Message +msgid "Delete" +msgstr "Izbriši" + +#: UiStrings.resx$Deskew$Message +msgid "Deskew" +msgstr "Ravnanje" + +#: MiscResources.resx$AutoDeskewProgress$Message +msgid "Deskew Progress" +msgstr "Napredak ravnanja" + +#: UiStrings.resx$DeskewScannedPages$Message +msgid "Deskew scanned pages" +msgstr "Izravnaj skenirane stranice" + +#: MiscResources.resx$AutoDeskewing$Message +msgid "Deskewing..." +msgstr "Ravnanje..." + +#: UiStrings.resx$DeviceLabel$Message +msgid "Device:" +msgstr "Uređaj:" + +#: UiStrings.resx$Dimensions$Message +msgid "Dimensions" +msgstr "Dimenzije" + +#: UiStrings.resx$DisplayNameLabel$Message +msgid "Display name:" +msgstr "Naziv za prikaz:" + +#: UiStrings.resx$DocumentCorrection$Message +msgid "Document Correction" +msgstr "Ispravka dokumenta" + +#: MiscResources.resx$Donate$Message +#: UiStrings.resx$Donate$Message +msgid "Donate" +msgstr "Donirajte" + +#: UiStrings.resx$Done$Message +msgid "Done" +msgstr "Gotovo" + +#: UiStrings.resx$Download$Message +msgid "Download" +msgstr "Preuzmi" + +#: MiscResources.resx$DownloadError$Message +msgid "Download Error" +msgstr "Greška pri preuzimanju" + +#: MiscResources.resx$DownloadNeeded$Message +msgid "Download Needed" +msgstr "Preuzimanje je potrebno" + +#: UiStrings.resx$DownloadProgressFormTitle$Message +msgid "Download Progress" +msgstr "Preuzimanje u toku" + +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "TPI" + +#: SettingsResources.resx$Source_Duplex$Message +msgid "Duplex" +msgstr "Dvostran" + +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL drajver" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL mrežni drajver" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB drajver" + +#: UiStrings.resx$Edit$Message +msgid "Edit" +msgstr "Uredi" + +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Uredi pomoću {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Uredi pomoću..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Pošalji sve e-mailom" + +#: UiStrings.resx$EmailAllAsPdf$Message +msgid "Email All as PDF" +msgstr "Pošalji e-mailom sve kao PDF" + +#: MiscResources.resx$EmailPdf$Message +#: UiStrings.resx$EmailPdf$Message +msgid "Email PDF" +msgstr "Pošalji PDF e-mailom" + +#: MiscResources.resx$EmailPdfProgress$Message +msgid "Email PDF Progress" +msgstr "Pošalji napredak PDF-ova" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Pošalji označeno e-mailom" + +#: UiStrings.resx$EmailSelectedAsPdf$Message +msgid "Email Selected as PDF" +msgstr "Pošalji označeno kao PDF e-mailom" + +#: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message +msgid "Email Settings" +msgstr "Postavke e-maila" + +#: UiStrings.resx$EnableAutoSave$Message +msgid "Enable Auto Save" +msgstr "Omogući auto. čuvanje " + +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Omogući zapise otklanjanja grešaka" + +#: UiStrings.resx$EncryptPdf$Message +msgid "Encrypt PDF" +msgstr "Šifriraj PDF" + +#: UiStrings.resx$Encryption$Message +msgid "Encryption" +msgstr "Šifriranje" + +#: MiscResources.resx$FileTypeEmf$Message +msgid "Enhanced Windows MetaFile (*.emf)" +msgstr "Poboljšana Windows metadatoteka (*.emf)" + +#: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message +msgid "Error" +msgstr "Greška" + +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Greška pri pokretanju aplikacije {0}" + +#: MiscResources.resx$EstimatedDownloadSize$Message +#: UiStrings.resx$EstimatedDownloadSize$Message +msgid "Estimated download size: {0} MB" +msgstr "Procjenjena veličina preuzimanja: {0} MB" + +#: MiscResources.resx$FileTypeExif$Message +msgid "Exchangeable Image File (*.exif)" +msgstr "Zamjenjiva slikovna datoteka (*.exif)" + +#: UiStrings.resx$ExcludeBlankPages$Message +msgid "Exclude blank pages" +msgstr "Izuzmi prazne stranice" + +#: SettingsResources.resx$OcrMode_Fast$Message +msgid "Fast" +msgstr "Brzi" + +#: SettingsResources.resx$Source_Feeder$Message +msgid "Feeder" +msgstr "Ulagač papira" + +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Naziv datoteke:" + +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" +msgstr "Putanja do datoteke:" + +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Popravi balans bijele i ukloni šum" + +#: UiStrings.resx$Flip$Message +msgid "Flip" +msgstr "Obrni" + +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Obrnite pozadinske stranice obostranih listova" + +#: UiStrings.resx$FlipDuplexedPages$Message +msgid "Flip duplexed pages" +msgstr "Obrni obostrane listove" + +#: UiStrings.resx$JpegQualityHelp$Message +msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." +msgstr "Za visoki JPEG kvalitet (80+), za najbolje rezultate potrebno je povećati i kvalitet slika na vašem profilu." + +#: MiscResources.resx$FileTypeGif$Message +msgid "GIF File (*.gif)" +msgstr "GIF datoteka (*.gif)" + +#: UiStrings.resx$GetMoreLanguages$Message +msgid "Get more languages" +msgstr "Preuzmi više jezika" + +#: SettingsResources.resx$Source_Glass$Message +msgid "Glass" +msgstr "Skenersko staklo" + +#: SettingsResources.resx$EmailProviderType_Gmail$Message +msgid "Gmail" +msgstr "Gmail" + +#: SettingsResources.resx$BitDepth_8Grayscale$Message +msgid "Grayscale" +msgstr "Nijanse sive boje" + +#: UiStrings.resx$HorizontalAlignLabel$Message +msgid "Horizontal align:" +msgstr "Horizontalno poravnanje:" + +#: UiStrings.resx$Hour2Digit$Message +msgid "Hour (0-23)" +msgstr "Sat (0-23)" + +#: UiStrings.resx$HueSaturation$Message +msgid "Hue / Saturation" +msgstr "Nijanse / Zasićenost" + +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Host" + +#: UiStrings.resx$IconsFrom$Message +msgid "Icons from:" +msgstr "Ikonice sa:" + +#: UiStrings.resx$Image$Message +msgid "Image" +msgstr "Slika" + +#: MiscResources.resx$FileTypeImageFiles$Message +msgid "Image Files" +msgstr "Slikovne datoteke" + +#: UiStrings.resx$ImageQuality$Message +msgid "Image Quality" +msgstr "Kvalitet slike" + +#: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message +msgid "Image Settings" +msgstr "Postavke slike" + +#: MiscResources.resx$ImageSaved$Message +msgid "Image saved." +msgstr "Slika je sačuvana." + +#: UiStrings.resx$Import$Message +msgid "Import" +msgstr "Uvoz" + +#: MiscResources.resx$ImportProgress$Message +msgid "Import Progress" +msgstr "Napredak uvoza" + +#: MiscResources.resx$ImportingFormat$Message +msgid "Importing {0}..." +msgstr "Uvoženje {0}..." + +#: MiscResources.resx$Importing$Message +msgid "Importing..." +msgstr "Uvoženje..." + +#: MiscResources.resx$Install$Message +msgid "Install {0}" +msgstr "Instaliraj {0}" + +#: MiscResources.resx$InstallComplete$Message +msgid "Installation Complete" +msgstr "Instalacija završena" + +#: MiscResources.resx$InstallFailedTitle$Message +msgid "Installation Failed" +msgstr "Instalacija nije uspjela" + +#: MiscResources.resx$InstallCompletePromptRestart$Message +msgid "Installation complete. Do you want to restart NAPS2 now?" +msgstr "Instalacija završena. Da li želite sada restartovati NAPS2?" + +#: MiscResources.resx$InstallFailed$Message +msgid "Installation failed." +msgstr "Instalacija nije uspjela." + +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Interfejs" + +#: UiStrings.resx$Interleave$Message +msgid "Interleave" +msgstr "Naizmjenično" + +#: MiscResources.resx$FileTypeJpeg$Message +msgid "JPEG File (*.jpg, *.jpeg)" +msgstr "JPEG datoteka (*.jpg, *.jpeg)" + +#: MiscResources.resx$FileTypeJp2$Message +msgid "JPEG2000 File (*.jp2, *.jpx)" +msgstr "JPEG2000 datoteka (*.jp2, *.jpx)" + +#: UiStrings.resx$JpegQuality$Message +msgid "Jpeg Quality" +msgstr "Jpeg kvalitet" + +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Zadrži slike kroz sesije" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Prečaci tastature" + +#: UiStrings.resx$KeywordsLabel$Message +msgid "Keywords:" +msgstr "Ključne riječi:" + +#: SettingsResources.resx$TiffComp_Lzw$Message +msgid "LZW" +msgstr "LZW" + +#: UiStrings.resx$Language$Message +msgid "Language" +msgstr "Jezik" + +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Napišite recenziju" + +#: SettingsResources.resx$HorizontalAlign_Left$Message +msgid "Left" +msgstr "Lijevo" + +#: SettingsResources.resx$TwainImpl_Legacy$Message +msgid "Legacy (native UI only)" +msgstr "Legat (samo nativni KI)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Svijetla" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Sviđa Vam se NAPS2?" + +#: UiStrings.resx$LoadIn$Message +msgid "Load images into NAPS2" +msgstr "Učitaj slike u NAPS2" + +#: UiStrings.resx$MakePdfsSearchable$Message +msgid "Make PDFs searchable using OCR" +msgstr "Omogući pretraživanje u PDF-ovima pomoću OPZ-a" + +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "IP adresa" + +#: UiStrings.resx$MaximumQuality$Message +msgid "Maximum quality (large files)" +msgstr "Maksimalan kvalitet (velike datoteke)" + +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Memorijski transfer" + +#: UiStrings.resx$Metadata$Message +msgid "Metadata" +msgstr "Metapodaci" + +#: UiStrings.resx$Minute2Digit$Message +msgid "Minute (00-59)" +msgstr "Minuta (00-59)" + +#: UiStrings.resx$Month2Digit$Message +msgid "Month (01-12)" +msgstr "Mjesec (01-12)" + +#: UiStrings.resx$MoreInfo$Message +msgid "More info" +msgstr "Više informacija" + +#: UiStrings.resx$MoveDown$Message +msgid "Move Down" +msgstr "Pomjeri dolje" + +#: UiStrings.resx$MoveUp$Message +msgid "Move Up" +msgstr "Pomjeri gore" + +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Više jezika" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Više jezika..." + +#: UiStrings.resx$MultipleScansDelay$Message +msgid "Multiple scans (fixed delay between scans)" +msgstr "Višestruko skeniranje (fiksni razmak između skeniranja)" + +#: UiStrings.resx$MultipleScansPrompt$Message +msgid "Multiple scans (prompt between scans)" +msgstr "Višestruko skeniranje (prompt između skeniranja)" + +#: MiscResources.resx$NAPS2$Message +#: SdkResources.resx$NAPS2$Message +#: UiStrings.resx$Naps2$Message +msgid "NAPS2" +msgstr "NAPS2" + +#: UiStrings.resx$Naps2TitleFormat$Message +msgid "NAPS2 - {0}" +msgstr "NAPS2 - {0}" + +#: MiscResources.resx$DonatePrompt$Message +msgid "NAPS2 is completely free. Consider making a donation." +msgstr "NAPS2 je potpuno besplatan. Razmotrite donaciju kao podršku." + +#: UiStrings.resx$NameOptional$Message +msgid "Name (optional)" +msgstr "Naziv (neobavezno)" + +#: MiscResources.resx$NameMissing$Message +msgid "Name missing." +msgstr "Nedostaje naziv." + +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Nativni transfer" + +#: UiStrings.resx$New$Message +msgid "New" +msgstr "Novi" + +#: UiStrings.resx$NewProfile$Message +msgid "New Profile" +msgstr "Novi profil" + +#: UiStrings.resx$Next$Message +msgid "Next" +msgstr "Slijedeći" + +#: UiStrings.resx$BatchPromptFormTitle$Message +msgid "Next Scan" +msgstr "Slijedeće skeniranje" + +#: MiscResources.resx$NoDeviceSelected$Message +#: SdkResources.resx$NoDeviceSelected$Message +msgid "No device selected." +msgstr "Nije izabran uređaj." + +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Nema pronađenih uređaja." + +#: MiscResources.resx$NoPagesInFeeder$Message +#: SdkResources.resx$NoPagesInFeeder$Message +msgid "No pages are in the feeder." +msgstr "Nema papira u ulagaču." + +#: SettingsResources.resx$EmailProvider_NotSelected$Message +msgid "No provider selected." +msgstr "Nije izabran provajder." + +#: MiscResources.resx$NoDevicesFound$Message +#: SdkResources.resx$NoDevicesFound$Message +msgid "No scanning device was found." +msgstr "Nije pronađen uređaj za skeniranje." + +#: MiscResources.resx$NoUpdates$Message +msgid "No updates available." +msgstr "Nema dostupnih ažuriranja." + +#: SettingsResources.resx$TiffComp_None$Message +msgid "None" +msgstr "Nijedan" + +#: UiStrings.resx$Naps2FullName$Message +msgid "Not Another PDF Scanner" +msgstr "Not Another PDF Scanner" + +#: UiStrings.resx$NotNow$Message +msgid "Not Now" +msgstr "Ne sada" + +#: UiStrings.resx$NumberOfScansLabel$Message +msgid "Number of scans:" +msgstr "Broj skeniranja:" + +#: UiStrings.resx$Ocr$Message +msgid "OCR" +msgstr "OPZ (OCR)" + +#: UiStrings.resx$OcrDownloadFormTitle$Message +msgid "OCR Download" +msgstr "Preuzmi jezike za OPZ" + +#: MiscResources.resx$OcrProgress$Message +msgid "OCR Progress" +msgstr "Napredak OPZ-a" + +#: UiStrings.resx$OcrSetupFormTitle$Message +msgid "OCR Setup" +msgstr "Postavke za OPZ" + +#: UiStrings.resx$OcrLanguageLabel$Message +msgid "OCR language:" +msgstr "Jezik OPZ-a:" + +#: UiStrings.resx$OcrModeLabel$Message +msgid "OCR mode:" +msgstr "Način OPZ-a:" + +#: UiStrings.resx$OK$Message +msgid "OK" +msgstr "U redu" + +#: UiStrings.resx$OffsetWidth$Message +msgid "Offset width based on alignment (WIA)" +msgstr "Širina ofseta (pomaka) na osnovu poravnanja (WIA)" + +#: SettingsResources.resx$TwainImpl_OldDsm$Message +msgid "Old DSM" +msgstr "Stari DSM" + +#: UiStrings.resx$OneFilePerPage$Message +msgid "One file per page" +msgstr "Jedna datoteka po stranici" + +#: UiStrings.resx$OneFilePerScan$Message +msgid "One file per scan" +msgstr "Jedna datoteka po skeniranju" + +#: MiscResources.resx$FilesCouldNotBeDownloaded$Message +msgid "One or more files could not be downloaded." +msgstr "Jedna ili više datoteka se ne može preuzeti." + +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Dozvoli samo jednu programsku instancu NAPS2" + +#: UiStrings.resx$OpenFolder$Message +msgid "Open Folder" +msgstr "Otvori folder" + +#: MiscResources.resx$ActiveOperations$Message +msgid "Operation in Progress" +msgstr "Radnja u toku" + +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (novi)" + +#: SettingsResources.resx$EmailProviderType_OutlookWeb$Message +msgid "Outlook Web Access" +msgstr "Outlook web pristup" + +#: UiStrings.resx$Output$Message +msgid "Output" +msgstr "Izlaz" + +#: MiscResources.resx$OverwriteFile$Message +msgid "Overwrite File" +msgstr "Prepisati preko datoteke" + +#: UiStrings.resx$OwnerPasswordLabel$Message +msgid "Owner Password:" +msgstr "Vlasnička lozinka:" + +#: MiscResources.resx$FileTypePdf$Message +msgid "PDF Document (*.pdf)" +msgstr "PDF dokument (*.pdf)" + +#: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message +msgid "PDF Settings" +msgstr "PDF postavke" + +#: MiscResources.resx$PdfSaved$Message +msgid "PDF saved." +msgstr "PDF sačuvan." + +#: SettingsResources.resx$PdfCompat_PdfA1B$Message +msgid "PDF/A-1b" +msgstr "PDF/A-1b" + +#: SettingsResources.resx$PdfCompat_PdfA2B$Message +msgid "PDF/A-2b" +msgstr "PDF/A-2b" + +#: SettingsResources.resx$PdfCompat_PdfA3B$Message +msgid "PDF/A-3b" +msgstr "PDF/A-3b" + +#: SettingsResources.resx$PdfCompat_PdfA3U$Message +msgid "PDF/A-3u" +msgstr "PDF/A-3u" + +#: MiscResources.resx$FileTypePng$Message +msgid "PNG File (*.png)" +msgstr "PNG daoteka (*.png)" + +#: UiStrings.resx$PageSizeLabel$Message +msgid "Page size:" +msgstr "Dimenzija stranice:" + +#: UiStrings.resx$PaperSourceLabel$Message +msgid "Paper source:" +msgstr "Izvor dokumenta:" + +#: UiStrings.resx$PdfPasswordFormTitle$Message +msgid "Password" +msgstr "Lozinka" + +#: UiStrings.resx$Paste$Message +msgid "Paste" +msgstr "Zalijepi" + +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message +msgid "Placeholders" +msgstr "Šablonski nazivi" + +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" + +#: UiStrings.resx$PostProcessing$Message +msgid "Post-processing" +msgstr "Naknadna obrada" + +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Preventivno pokreni OPZ nakon skeniranja" + +#: UiStrings.resx$PressStartWhenReady$Message +msgid "Press Start when ready." +msgstr "Pritisnite \"Start\" kada ste spremni." + +#: UiStrings.resx$PreviewFormTitle$Message +msgid "Preview" +msgstr "Pregled" + +#: UiStrings.resx$PreviewLabel$Message +msgid "Preview:" +msgstr "Pregled:" + +#: UiStrings.resx$Previous$Message +msgid "Previous" +msgstr "Prethodno" + +#: MiscResources.resx$Print$Message +#: UiStrings.resx$Print$Message +msgid "Print" +msgstr "Štampanje" + +#: UiStrings.resx$EditProfileFormTitle$Message +msgid "Profile Settings" +msgstr "Postavke profila" + +#: UiStrings.resx$ProfileLabel$Message +msgid "Profile:" +msgstr "Profil:" + +#: UiStrings.resx$Profiles$Message +#: UiStrings.resx$ProfilesFormTitle$Message +msgid "Profiles" +msgstr "Profili" + +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Upitaj ako je izabrano" + +#: UiStrings.resx$PromptForFilePath$Message +msgid "Prompt for file path" +msgstr "Pitaj za putanju do datoteke" + +#: UiStrings.resx$Provider$Message +msgid "Provider" +msgstr "Provajder" + +#: UiStrings.resx$ReadyForScan$Message +msgid "Ready for scan {0}." +msgstr "Spremno za skeniranje {0}." + +#: UiStrings.resx$Recover$Message +msgid "Recover" +msgstr "Oporavak" + +#: UiStrings.resx$RecoverFormTitle$Message +msgid "Recover Scanned Images" +msgstr "Oporavi skenirane slike" + +#: MiscResources.resx$Recovering$Message +msgid "Recovering..." +msgstr "Oporavljanje..." + +#: MiscResources.resx$RecoveryProgress$Message +msgid "Recovery Progress" +msgstr "Napredak oporavka" + +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Ponovi radnju" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Ponovi {0}" + +#: UiStrings.resx$RememberTheseSettings$Message +msgid "Remember these settings" +msgstr "Zapamti ove postavke" + +#: UiStrings.resx$Reorder$Message +msgid "Reorder" +msgstr "Preuredi" + +#: UiStrings.resx$Reset$Message +msgid "Reset" +msgstr "Resetuj" + +#: MiscResources.resx$ResetImage$Message +msgid "Reset Image" +msgstr "Resetuj sliku" + +#: UiStrings.resx$ResolutionLabel$Message +msgid "Resolution:" +msgstr "Rezolucija:" + +#: UiStrings.resx$RestoreDefaults$Message +msgid "Restore Defaults" +msgstr "Vrati na zadano" + +#: UiStrings.resx$Reverse$Message +msgid "Reverse" +msgstr "Preokreni" + +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Preokreni sve" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Preokreni izabrano" + +#: UiStrings.resx$Revert$Message +msgid "Revert" +msgstr "Vrati" + +#: SettingsResources.resx$HorizontalAlign_Right$Message +msgid "Right" +msgstr "Desno" + +#: UiStrings.resx$Rotate$Message +msgid "Rotate" +msgstr "Rotiraj" + +#: UiStrings.resx$RotateLeft$Message +msgid "Rotate Left" +msgstr "Rotiraj lijevo" + +#: UiStrings.resx$RotateRight$Message +msgid "Rotate Right" +msgstr "Rotiraj desno" + +#: UiStrings.resx$RunInBackground$Message +msgid "Run in Background" +msgstr "Radi u pozadini" + +#: MiscResources.resx$RunningOcr$Message +msgid "Running OCR..." +msgstr "OPZ u toku..." + +#: UiStrings.resx$SaneDriver$Message +msgid "SANE Driver" +msgstr "SANE drajver" + +#: UiStrings.resx$Save$Message +msgid "Save" +msgstr "Saćuvaj" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Sačuvaj sve" + +#: UiStrings.resx$SaveAllAsImages$Message +msgid "Save All as Images" +msgstr "Sačuvaj sve kao slike" + +#: UiStrings.resx$SaveAllAsPdf$Message +msgid "Save All as PDF" +msgstr "Sačuvaj sve kao PDF" + +#: MiscResources.resx$SaveImages$Message +#: UiStrings.resx$SaveImages$Message +msgid "Save Images" +msgstr "Saćuvaj slike" + +#: MiscResources.resx$SaveImagesProgress$Message +msgid "Save Images Progress" +msgstr "Saćuvaj napredak slika" + +#: MiscResources.resx$SavePdf$Message +#: UiStrings.resx$SavePdf$Message +msgid "Save PDF" +msgstr "Sačuvaj PDF" + +#: MiscResources.resx$SavePdfProgress$Message +msgid "Save PDF Progress" +msgstr "Sačuvaj napredak PDF-ova" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Sačuvaj izabrano" + +#: UiStrings.resx$SaveSelectedAsImages$Message +msgid "Save Selected as Images" +msgstr "Sačuvaj izabrano kao slike" + +#: UiStrings.resx$SaveSelectedAsPdf$Message +msgid "Save Selected as PDF" +msgstr "Sačuvaj izabrano kao PDF" + +#: UiStrings.resx$SaveToSingleFile$Message +msgid "Save to a single file" +msgstr "Sačuvaj u jednu datoteku" + +#: UiStrings.resx$SaveToMultipleFiles$Message +msgid "Save to multiple files" +msgstr "Sačuvaj u više datoteka" + +#: MiscResources.resx$BatchStatusSaving$Message +msgid "Saving batch results..." +msgstr "Čuvanje serijskih rezultata..." + +#: MiscResources.resx$SavingFormat$Message +msgid "Saving {0}..." +msgstr "Spašavam {0}..." + +#: UiStrings.resx$ScaleWithWindow$Message +msgid "Scale With Window" +msgstr "Skaliraj s prozorom" + +#: UiStrings.resx$ScaleLabel$Message +msgid "Scale:" +msgstr "Skaliraj:" + +#: MiscResources.resx$Scan$Message +#: UiStrings.resx$Scan$Message +msgid "Scan" +msgstr "Skeniraj" + +#: UiStrings.resx$ScanConfig$Message +msgid "Scan Configuration" +msgstr "Podešavanje skeniranja" + +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Skeniraj sa zadanim profilom" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Skeniraj s novim profilom" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Skeniraj s profilom {0}" + +#: MiscResources.resx$ScannedImage$Message +msgid "Scanned Image" +msgstr "Skenirana slika" + +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Dijeljenje skenera" + +#: MiscResources.resx$ScanPageProgress$Message +msgid "Scanning page {0}" +msgstr "Skeniranje stranice {0}" + +#: MiscResources.resx$BatchStatusScanPage$Message +msgid "Scanning page {0} (scan {1})..." +msgstr "Skeniranje stranice {0} (skeniraj {1})..." + +#: MiscResources.resx$BatchStatusPage$Message +#: MiscResources.resx$ScanProgressPage$Message +msgid "Scanning page {0}..." +msgstr "Skeniranje stranice {0}..." + +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Traženje uređaja..." + +#: UiStrings.resx$Second2Digit$Message +msgid "Second (00-59)" +msgstr "Sekunda (00-59)" + +#: UiStrings.resx$Select$Message +msgid "Select" +msgstr "Izaberi" + +#: UiStrings.resx$SelectAll$Message +msgid "Select All" +msgstr "Izaberi sve" + +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Izaberite uređaj" + +#: UiStrings.resx$SelectSource$Message +msgid "Select Source" +msgstr "Izaberi izvor" + +#: MiscResources.resx$SelectProfileBeforeScan$Message +msgid "Select a profile before clicking Scan." +msgstr "Izaberite profil prije klika na \"Skeniraj\"." + +#: UiStrings.resx$OcrSelectLanguageLabel$Message +msgid "Select one or more languages:" +msgstr "Izaberite jedan ili više jezika:" + +#: MiscResources.resx$SelectedCount$Message +msgid "Selected ({0})" +msgstr "Izabrano ({0})" + +#: UiStrings.resx$SeparateByPatchT$Message +msgid "Separate files by Patch-T" +msgstr "Razdvoji datoteke pomoću Patch-T" + +#: UiStrings.resx$SetDefault$Message +msgid "Set Default" +msgstr "Postavi na zadano" + +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Postavke" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Dijeli" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Dijeli i kada je NAPS2 zatvoren" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Postavke dijeljenog skenera" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Dijeljeni skeneri mogu se koristiti s drugih računara na lokalnoj mreži tako što se izabere \"ESCL\" drajver u postavkama NAPS2 profila na drugim računarima." + +#: UiStrings.resx$Sharpen$Message +msgid "Sharpen" +msgstr "Izoštri" + +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Prečac" + +#: UiStrings.resx$Show$Message +msgid "Show" +msgstr "Prikaži" + +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Prikaži alatnu traku \"Profili\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Prikaži napredak nativnog TWAIN-a" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Prikaži brojeve stranica" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Bočna traka" + +#: UiStrings.resx$SinglePageFiles$Message +msgid "Single page files" +msgstr "Jednostrane datoteke" + +#: UiStrings.resx$SingleScan$Message +msgid "Single scan" +msgstr "Jednostruko skeniranje" + +#: UiStrings.resx$SkipSavePrompt$Message +msgid "Skip save prompt" +msgstr "Preskoči pitanja za čuvanje" + +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Podijeli" + +#: UiStrings.resx$Start$Message +msgid "Start" +msgstr "Start" + +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Zaustavi dijeljenje skenera" + +#: UiStrings.resx$StretchToPageSize$Message +msgid "Stretch to page size" +msgstr "Raširi na veličinu stranice" + +#: UiStrings.resx$SubjectLabel$Message +msgid "Subject:" +msgstr "Predmet:" + +#: MiscResources.resx$FileTypeTiff$Message +msgid "TIFF File (*.tiff, *.tif)" +msgstr "TIFF datoteka (*.tiff, *.tif)" + +#: UiStrings.resx$TwainDriver$Message +msgid "TWAIN Driver" +msgstr "TWAIN drajver" + +#: UiStrings.resx$TechnicalDetails$Message +msgid "Technical Details" +msgstr "Tehnički detalji" + +#: MiscResources.resx$TesseractNotAvailable$Message +#: SdkResources.resx$TesseractNotAvailable$Message +msgid "The OCR engine is not available. Make sure to install the required package:" +msgstr "Mehanizam OPZ-a nije dostupan. Pobrinite se da instalirate potreban paket:" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Operacija OPZ-a je istekla." + +#: MiscResources.resx$SaneNotAvailable$Message +#: SdkResources.resx$SaneNotAvailable$Message +msgid "The SANE driver is not available. Make sure to install the required packages:" +msgstr "SANE drajver nije dostupan. Pobrinite se da instalirate potrebne pakete:" + +#: MiscResources.resx$ImportErrorCouldNot$Message +#: SdkResources.resx$ImportErrorCouldNot$Message +msgid "The file '{0}' could not be imported." +msgstr "Datoteka '{0}' se ne može uvesti." + +#: MiscResources.resx$ImportErrorNAPS2Pdf$Message +msgid "The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported." +msgstr "Datoteka '{0}' se ne može uvesti. Samo PDF datoteke nastale u NAPS2 se mogu uvesti." + +#: MiscResources.resx$FileInUse$Message +msgid "The file could not be overwritten because it is currently in use." +msgstr "Ne može se pisati preko datoteke, jer se ona trenutno koristi." + +#: MiscResources.resx$ConfirmOverwriteFile$Message +msgid "The file {0} already exists. Do you want to overwrite it?" +msgstr "Datoteka {0} već postoji. Da li želite da pišete preko iste?" + +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Slijedeća datoteka je šifrirana i zathijeva lozinku za otvaranje:" + +#: MiscResources.resx$DevicePaperJam$Message +#: SdkResources.resx$DevicePaperJam$Message +msgid "The scanner has a paper jam." +msgstr "Papir je zaglavio u skeneru." + +#: MiscResources.resx$DeviceWarmingUp$Message +#: SdkResources.resx$DeviceWarmingUp$Message +msgid "The scanner is warming up." +msgstr "Skener se zagrijava." + +#: MiscResources.resx$DeviceCoverOpen$Message +#: SdkResources.resx$DeviceCoverOpen$Message +msgid "The scanner's cover is open." +msgstr "Poklopac skenera je otvoren." + +#: MiscResources.resx$DriverNotSupported$Message +#: SdkResources.resx$DriverNotSupported$Message +msgid "The selected driver is not supported on this system." +msgstr "Izabrani drajver nije podržan na ovom sistemu." + +#: MiscResources.resx$DeviceNotFound$Message +#: SdkResources.resx$DeviceNotFound$Message +msgid "The selected scanner could not be found." +msgstr "Izabrani skener se ne može pronaći." + +#: MiscResources.resx$NoFeederSupport$Message +#: SdkResources.resx$NoFeederSupport$Message +msgid "The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver." +msgstr "Izabrani skener nema podršku za korištenje ulagača papira. Ako skener ima ulagač papira, pokušajte s korištenjem drugog drajvera." + +#: MiscResources.resx$NoDuplexSupport$Message +#: SdkResources.resx$NoDuplexSupport$Message +msgid "The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver." +msgstr "Izabrani skener nema podršku za korištenje dupleksa - dvostranog skeniranja. Ako skener treba da podržava dupleks, pokušajte s korištenjem drugog drajvera." + +#: MiscResources.resx$DeviceBusy$Message +#: SdkResources.resx$DeviceBusy$Message +msgid "The selected scanner is busy." +msgstr "Izabrani skener je zauzet." + +#: MiscResources.resx$DeviceOffline$Message +#: SdkResources.resx$DeviceOffline$Message +msgid "The selected scanner is offline." +msgstr "Izabrani skener je isključen." + +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Radni proces je prekinut." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Radni proces je prekinut. Provjerite preglednik događaja u Windowsu." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Tema:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message +msgid "Tiff Options" +msgstr "TIFF opcije" + +#: UiStrings.resx$TimeBetweenScansLabel$Message +msgid "Time between scans (seconds):" +msgstr "Vrijeme između skeniranja (sekunde):" + +#: UiStrings.resx$TitleLabel$Message +msgid "Title:" +msgstr "Naslov:" + +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Alati" + +#: UiStrings.resx$TwainImplLabel$Message +msgid "Twain Implementation:" +msgstr "TWAIN implementacija:" + +#: SettingsResources.resx$PageSize_Legal$Message +msgid "US Legal (8.5x14 in)" +msgstr "Američki legal (8,5x14 inča)" + +#: SettingsResources.resx$PageSize_Letter$Message +msgid "US Letter (8.5x11 in)" +msgstr "Američko pismo (8,5x11 inča)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Oslobodi" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Poništi radnju" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Poništi {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Nepoznat skener" + +#: MiscResources.resx$UnsavedChanges$Message +msgid "Unsaved Changes" +msgstr "Nesačuvane promjene" + +#: MiscResources.resx$UpdateProgress$Message +msgid "Update Progress" +msgstr "Napredak ažuriranja" + +#: MiscResources.resx$UpdateCheckDisabled$Message +msgid "Update checking is disabled." +msgstr "Provjera ažuriranja je isključena." + +#: MiscResources.resx$Updating$Message +msgid "Updating..." +msgstr "Ažuriranje..." + +#: MiscResources.resx$UploadingEmail$Message +msgid "Uploading email..." +msgstr "Prijenos e-pošte..." + +#: UiStrings.resx$UseNativeUi$Message +msgid "Use native UI" +msgstr "Koristi nativni KI" + +#: UiStrings.resx$UsePredefinedSettings$Message +msgid "Use predefined settings" +msgstr "Koristi predefinisane postavke" + +#: UiStrings.resx$UserPasswordLabel$Message +msgid "User Password:" +msgstr "Korisnička loznika:" + +#: UiStrings.resx$OcrDownloadSummaryText$Message +msgid "Using OCR requires you to download each language you want to scan." +msgstr "Korištenje OCR (OPZ) zahtjeva preuzimanje pojedinačnih jezika koje želite skenirati." + +#: MiscResources.resx$Version$Message +#: SdkResources.resx$Version$Message +msgid "Version {0}" +msgstr "Verzija {0}" + +#: UiStrings.resx$View$Message +msgid "View" +msgstr "Pogled" + +#: UiStrings.resx$WiaDriver$Message +msgid "WIA Driver" +msgstr "WIA drajver" + +#: UiStrings.resx$WaitingForTwain$Message +msgid "Waiting for TWAIN to complete..." +msgstr "Čekanje TWAIN-a da završi..." + +#: UiStrings.resx$WaitingForAuthorization$Message +msgid "Waiting for authorization..." +msgstr "Čekanje na autorizaciju..." + +#: MiscResources.resx$BatchStatusWaitingForScan$Message +msgid "Waiting for scan {0}..." +msgstr "Čekanje na skeniranje {0}..." + +#: UiStrings.resx$WhiteThreshold$Message +msgid "White Threshold" +msgstr "Prag bijeline" + +#: UiStrings.resx$WiaVersionLabel$Message +msgid "Wia Version:" +msgstr "Verzija WIA:" + +#: UiStrings.resx$Year4Digit$Message +msgid "Year" +msgstr "Godina" + +#: UiStrings.resx$Year2Digit$Message +msgid "Year (00-99)" +msgstr "Godina (00-99)" + +#: MiscResources.resx$PdfNoPermissionToExtractContent$Message +#: SdkResources.resx$PdfNoPermissionToExtractContent$Message +msgid "You do not have permission to copy content from the file '{0}'." +msgstr "Nemate dozvolu za kopiranje sadržaja iz datoteke '{0}'." + +#: MiscResources.resx$DontHavePermission$Message +msgid "You don't have permission to save files at this location." +msgstr "Nemate dozvolu za čuvanje datoteka na ovoj lokaciji." + +#: MiscResources.resx$ExitWithUnsavedChanges$Message +msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" +msgstr "Niste sačuvali promjene. Da li želite izaći i odbaciti ove promjene?" + +#: UiStrings.resx$Zoom$Message +msgid "Zoom" +msgstr "Zumiranje" + +#: UiStrings.resx$ZoomActual$Message +msgid "Zoom Actual" +msgstr "Aktualno zumiranje" + +#: UiStrings.resx$ZoomIn$Message +msgid "Zoom In" +msgstr "Uvećaj" + +#: UiStrings.resx$ZoomOut$Message +msgid "Zoom Out" +msgstr "Smanji" + +#: SettingsResources.resx$PageSizeUnit_Centimetre$Message +msgid "cm" +msgstr "cm" + +#: SettingsResources.resx$PageSizeUnit_Inch$Message +msgid "in" +msgstr "in" + +#: SettingsResources.resx$PageSizeUnit_Millimetre$Message +msgid "mm" +msgstr "mm" + +#: MiscResources.resx$OfN$Message +msgid "of {0}" +msgstr "od {0}" + +#: SettingsResources.resx$TwainImpl_X64$Message +msgid "x64" +msgstr "x64" + +#: MiscResources.resx$NamedPageSizeFormat$Message +msgid "{0} ({1}x{2} {3})" +msgstr "{0} ({1}x{2} {3})" + +#: MiscResources.resx$ProgressFormat$Message +msgid "{0} / {1}" +msgstr "{0} / {1}" + +#: MiscResources.resx$SizeProgress$Message +msgid "{0} / {1} MB" +msgstr "{0} / {1} MB" + +#: MiscResources.resx$FilesProgressFormat$Message +msgid "{0} / {1} files" +msgstr "{0} / {1} datoteke-a" + +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} pronađen(a) uređaj(a)." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} TPI" + +#: UiStrings.resx$RecoverPrompt$Message +msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" +msgstr "{0} slika(e) skenirane {1} u {2} možda nije(-su) snimljeno(e), i moguće ih je oporaviti. Da li ih želite oporaviti?" + +#: MiscResources.resx$ImagesSaved$Message +msgid "{0} images saved." +msgstr "{0} slike(a) snimljene(o)." + +#: MiscResources.resx$PdfStatus$Message +#: UiStrings.resx$XOfY$Message +msgid "{0} of {1}" +msgstr "{0} od {1}" + diff --git a/NAPS2.Lib/Lang/po/ca.po b/NAPS2.Lib/Lang/po/ca.po index bed020b8f3..8f959b97ed 100644 --- a/NAPS2.Lib/Lang/po/ca.po +++ b/NAPS2.Lib/Lang/po/ca.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Catalan\n" "Language: ca\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "100 ppp" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Acció predeterminada del botó \"Desa\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "1200 ppp" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Acció predeterminada del botó \"Escaneja\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "150 ppp" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "El menú \"Escaneja\" canvia el perfil per defecte" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "1200 ppp" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "S'ha trobat 1 dispositiu." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "Color de 24-bits" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "300 ppp" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "400 ppp" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "600 ppp" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "800 ppp" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -74,14 +58,17 @@ msgstr "Quant a" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." -msgstr "S'estan obtenint les dades......" +msgstr "S'estan obtenint les dades..." + +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Acció" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Avançat" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Configuració avançada del perfil" @@ -93,35 +80,35 @@ msgstr "Tot ({0})" msgid "All Files" msgstr "Tots els fitxers" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Permet anotacions" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Permet la còpia del contingut" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Permet l'accés al contingut per part d'eines d'accessibilitat" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Permet el muntatge de documents" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Permet la modificació del document" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Permet l'emplenament de camps de formulari" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" -msgstr "Permet la impressió en resolució alta" +msgstr "Permet la impressió en alta resolució" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Permet la impressió" @@ -133,17 +120,22 @@ msgstr "Desintercalat alternatiu" msgid "Alternate Interleave" msgstr "Intercalat alternatiu" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Baixada opcional" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Demana sempre" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Demana sempre" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" -msgstr "Es requereix un complement extra per importar aquest fitxer PDF. Voleu baixar-ho ara?" +msgstr "Es requereix un complement extra per a importar aquest fitxer PDF. Voleu baixar-ho ara?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "S'ha produït un error en instal·lar l'actualització." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "S'ha produït un error en iniciar l'OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -151,7 +143,11 @@ msgstr "S'ha produït un error amb l'autorització." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." -msgstr "S'ha produït un error amb el desat automàtic." +msgstr "S'ha produït un error amb el desament automàtic." + +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "S'ha produït un error en instal·lar l'actualització." #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." @@ -163,19 +159,19 @@ msgstr "S'ha produït un error en enviar el correu electrònic." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." -msgstr "S'ha produït un error mentre s'enviava el correu-e ." +msgstr "S'ha produït un error en enviar un correu electrònic." #: MiscResources.resx$UnknownDriverError$Message #: SdkResources.resx$UnknownDriverError$Message msgid "An error occurred with the scanning driver." -msgstr "S'ha produït un error amb el driver del escaner." +msgstr "S'ha produït un error amb el controlador de l'escàner." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" msgstr "Encara s'està executant. Segur que voleu cancel·lar l'operació?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "S'ha produït un error desconegut en l'escaneig per lots." #: MiscResources.resx$UpdateAvailable$Message @@ -188,9 +184,17 @@ msgstr "Hi ha una actualització de l'OCR disponible." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Controlador d'Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Aplicació" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Aplica brillantor/contrast després d'escanejar" @@ -204,67 +208,73 @@ msgstr "Segur que voleu cancel·lar el procés d'escaneig per lots?" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" -msgstr "Segur de voler buidar {0} pagina(s)?" +msgstr "Segur que voleu buidar {0} pàgines?" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "Segur que voleu suprimir {0} element(s)?" +msgstr "Segur que voleu suprimir {0} elements?" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" -msgstr "Esteu segur que voleu suprimir el perfil {0}?" +msgstr "Segur que voleu suprimir el perfil {0}?" #: MiscResources.resx$ConfirmDeleteItems$Message msgid "Are you sure you want to delete {0} item(s)?" -msgstr "Esteu segur que voleu suprimir {0} element(s)?" +msgstr "Segur que voleu suprimir {0} elements?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "Esteu segur que voleu suprimir {0} perfils?" +msgstr "Segur que voleu suprimir {0} perfils?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Segur que voleu aturar la compartició de {0}?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Segur que voleu desfer els canvis en {0} imatges?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Vincula" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Nom de l'adjunt:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autoria:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autoritza" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "Automàtic" +msgstr "Automàtica" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" -msgstr "Opcions de desat automàtic" +msgstr "Opcions de desament automàtic" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "Increment automàtic del número (1 dígit)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Increment automàtic del número (2 dígits)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Increment automàtic del número (3 dígits)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Increment automàtic del número (4 dígits)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Inicia l'OCR automàticament després d'escanejar" @@ -275,10 +285,10 @@ msgstr "B4 (250 x 353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Escaneig per lots" @@ -296,9 +306,8 @@ msgstr "Un error ha aturat l'escaneig en lot." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Millor" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bit de profunditat:" @@ -309,10 +318,10 @@ msgstr "Fitxers Bitmap (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Blanc i negre" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Pàgines buides" @@ -320,33 +329,19 @@ msgstr "Pàgines buides" msgid "Brightness / Contrast" msgstr "Brillantor/Contrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Brillantor:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "No trobeu el vostre escàner? Informeu-vos de les limitacions del NAPS2 en versió Flatpak." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Cancel·la" @@ -357,13 +352,13 @@ msgstr "Cancel·la la tasca" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." -msgstr "S'està cancel·lant......." +msgstr "S'està cancel·lant..." #: SettingsResources.resx$HorizontalAlign_Center$Message msgid "Center" msgstr "Centre" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Canvia" @@ -373,9 +368,9 @@ msgstr "Comprova si hi ha actualitzacions" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "S'està comprovant......" +msgstr "S'està comprovant..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Trieu el proveïdor de correu" @@ -383,10 +378,9 @@ msgstr "Trieu el proveïdor de correu" msgid "Choose Profile" msgstr "Trieu el perfil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" -msgstr "Trieu dispositiu" +msgstr "Trieu un dispositiu" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message @@ -395,9 +389,9 @@ msgstr "Neteja" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Neteja-ho tot" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Neteja les imatges després de desar" @@ -405,19 +399,33 @@ msgstr "Neteja les imatges després de desar" msgid "Close" msgstr "Tanca" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Combina" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "S'ha interromput la comunicació amb el dispositiu d'escaneig." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Compatibilitat" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Compressió:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Connecta" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Error de connexió." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" -msgstr "" +msgstr "Contrast:" #: UiStrings.resx$Copy$Message msgid "Copy" @@ -429,13 +437,13 @@ msgstr "Progrés de la còpia" #: MiscResources.resx$Copying$Message msgid "Copying..." -msgstr "S'està copiant......" +msgstr "S'està copiant..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} Contribuïdors del NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Llindar de cobertura" @@ -443,7 +451,7 @@ msgstr "Llindar de cobertura" msgid "Crop" msgstr "Escapça" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Escapça a la mida de la pàgina" @@ -451,10 +459,14 @@ msgstr "Escapça a la mida de la pàgina" msgid "Custom ({0}x{1} {2})" msgstr "Personalitzat ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Mida personalitzada de la pàgina" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Resolució personalitzada" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Rotació personalitzada" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "SMTP personalitzat" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." -msgstr "Personalitzat..." +msgstr "Personalitzada..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Fosc" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Dia (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Per defecte" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Camí predeterminat:" @@ -487,11 +504,10 @@ msgstr "Camí predeterminat:" msgid "Deinterleave" msgstr "Desentrellaça" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" -msgstr "Esborra" +msgstr "Suprimeix" #: UiStrings.resx$Deskew$Message msgid "Deskew" @@ -501,43 +517,39 @@ msgstr "Alinea" msgid "Deskew Progress" msgstr "Progrés de l'alineació" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Alinea les pàgines escanejades" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." -msgstr "S'està alineant......" +msgstr "S'està alineant..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Dispositiu:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Mides" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" -msgstr "Mostra el nom:" +msgstr "Nom mostrat:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Correcció del document" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Donació" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Fet" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Baixa" @@ -550,64 +562,102 @@ msgstr "Error amb la baixada" msgid "Download Needed" msgstr "Baixada necessària" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Progrés de la baixada" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "ppp" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Dúplex" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Controlador ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "Controlador de xarxa ESCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "Controlador USB ESCL" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Edita" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Edita amb {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Edita amb..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Envia-ho tot" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Envia-ho tot com a PDF per correu-e" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "" +msgstr "PDF per correu-e" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" msgstr "Progrés d'enviament del PDF" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Seleccionat" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Envia la selecció com a PDF per correu-e" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" -msgstr "Paràmetres del correu electrònic" +msgstr "Configuració del correu electrònic" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" -msgstr "Habilita el desat automàtic" +msgstr "Habilita el desament automàtic" + +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Activa el Mode depuració" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Xifra el PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" -msgstr "Xifrat" +msgstr "Xifratge" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Metafitxer millorat (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" -msgstr "" +msgstr "Error" + +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Error en iniciar l'aplicació {0}" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,49 +665,55 @@ msgstr "Mida estimada de la baixada: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Fitxer d'intercanvi d'imatge (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Exclou les pàgines en blanc" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Ràpid" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Alimentador" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "Nom del fitxer" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Nom del fitxer:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Camí del fitxer:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Corregeix el balanç de blancs i suprimeix el soroll" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Gira" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Gireu el revers de les pàgines dúplex" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" -msgstr "" +msgstr "Gira les pàgines doblegades" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "" +msgstr "Per a obtenir imatges JPEG d'alta qualitat (80+), incrementeu també la resolució al perfil." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" -msgstr "" +msgstr "Fitxer GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" -msgstr "" +msgstr "Més idiomes" #: SettingsResources.resx$Source_Glass$Message msgid "Glass" @@ -665,18 +721,17 @@ msgstr "Vidre" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Escala de grisos" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Alineació horitzontal:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Hora (0-23)" @@ -684,9 +739,13 @@ msgstr "Hora (0-23)" msgid "Hue / Saturation" msgstr "To/Saturació" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Amfitrió" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" -msgstr "Icons desde:" +msgstr "Icones de:" #: UiStrings.resx$Image$Message msgid "Image" @@ -694,14 +753,14 @@ msgstr "Imatge" #: MiscResources.resx$FileTypeImageFiles$Message msgid "Image Files" -msgstr "Imatges" +msgstr "Fitxers d'imatge" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Qualitat de la imatge" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Opcions de la imatge" @@ -745,69 +804,101 @@ msgstr "S'ha instal·lat correctament. Voleu reiniciar el NAPS2 ara per aplicar msgid "Installation failed." msgstr "S'ha produït un error en la instal·lació." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Interfície" + #: UiStrings.resx$Interleave$Message msgid "Interleave" -msgstr "" +msgstr "Intercala" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" -msgstr "" +msgstr "Fitxer JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "Fitxer JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Qualitat Jpeg" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Conserva les imatges entre sessions" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Dreceres de teclat" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Paraules clau:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Idioma" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Deixeu una ressenya" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Esquerra" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" -msgstr "" +msgstr "Heretat (només interfície nativa)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Clar" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Us agrada el NAPS2?" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Carrega les imatges al NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Crea documents PDF amb opció de cerca mitjançant l'OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "IP manual" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Qualitat màxima (fitxers grans)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Transferència en memòria" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metadades" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minut (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Mes (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Més informació" @@ -819,11 +910,19 @@ msgstr "Mou avall" msgid "Move Up" msgstr "Mou amunt" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Diversos idiomes" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Diversos idiomes..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" -msgstr "Escaneig múltiple (retard fixe entre escaneig)" +msgstr "Escaneig múltiple (retard fix entre escaneig)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Escaneig múltiple (demana entre cada escaneig)" @@ -831,23 +930,27 @@ msgstr "Escaneig múltiple (demana entre cada escaneig)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "El NAPS2 és completament gratuït. Considereu fer una donació." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Nom (opcional)" #: MiscResources.resx$NameMissing$Message msgid "Name missing." -msgstr "Manca el nom." +msgstr "Falta el nom." + +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Transferència nativa" #: UiStrings.resx$New$Message msgid "New" @@ -861,21 +964,24 @@ msgstr "Perfil nou" msgid "Next" msgstr "Següent" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" -msgstr "" +msgstr "Proper escaneig" #: MiscResources.resx$NoDeviceSelected$Message #: SdkResources.resx$NoDeviceSelected$Message msgid "No device selected." msgstr "No s'ha seleccionat cap dispositiu." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "No s'ha trobat cap dispositiu." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "No hi ha cap pàgina a l'alimentador." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "No s'ha seleccionat cap proveïdor." @@ -895,21 +1001,20 @@ msgstr "Cap" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "(Not Another PDF Scanner)" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Ara no" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Nombre d'escanejos:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Baixada de l'OCR" @@ -918,37 +1023,23 @@ msgstr "Baixada de l'OCR" msgid "OCR Progress" msgstr "Progrés de l'OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Configuració de l'OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Idioma de l'OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "Mode de l'OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "D'acord" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Amplada de desplaçament segons l'alineació (WIA)" @@ -956,13 +1047,11 @@ msgstr "Amplada de desplaçament segons l'alineació (WIA)" msgid "Old DSM" msgstr "DSM antic" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Un fitxer per pàgina" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Un fitxer per escaneig" @@ -970,7 +1059,11 @@ msgstr "Un fitxer per escaneig" msgid "One or more files could not be downloaded." msgstr "No s'ha pogut baixar un o més fitxers." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Permet només una única instància del NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Obre una carpeta" @@ -978,11 +1071,15 @@ msgstr "Obre una carpeta" msgid "Operation in Progress" msgstr "Operació en progrés" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (nou)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "Accés web de l'Outlook" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Sortida" @@ -990,7 +1087,7 @@ msgstr "Sortida" msgid "Overwrite File" msgstr "Sobreescriu el fitxer" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Contrasenya del propietari:" @@ -998,8 +1095,8 @@ msgstr "Contrasenya del propietari:" msgid "PDF Document (*.pdf)" msgstr "Document PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Opcions del PDF" @@ -1009,35 +1106,33 @@ msgstr "S'ha desat el PDF." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "Fitxer PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Mida de la pàgina:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Origen del paper:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Contrasenya" @@ -1045,21 +1140,24 @@ msgstr "Contrasenya" msgid "Paste" msgstr "Enganxa" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Marcadors de posició" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" -msgstr "Post-processament" +msgstr "Tractament posterior" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Inicia l'OCR de forma preventiva després de l'escaneig" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Premeu Inicia per continuar." @@ -1067,7 +1165,7 @@ msgstr "Premeu Inicia per continuar." msgid "Preview" msgstr "Previsualitza" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Previsualitza:" @@ -1080,51 +1178,60 @@ msgstr "Anterior" msgid "Print" msgstr "Imprimeix" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Paràmetres del perfil" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" -msgstr "Perfils:" +msgstr "Perfil:" #: UiStrings.resx$Profiles$Message #: UiStrings.resx$ProfilesFormTitle$Message msgid "Profiles" msgstr "Perfils" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Demana en seleccionar" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" -msgstr "" +msgstr "Demana la ruta de destinació" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Proveïdor" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." -msgstr "" +msgstr "Preparat per l'escaneig {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" -msgstr "" +msgstr "Recupera" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" -msgstr "" +msgstr "Recuperació d'imatges escanejades" #: MiscResources.resx$Recovering$Message msgid "Recovering..." -msgstr "S'està recuperant......" +msgstr "S'està recuperant..." #: MiscResources.resx$RecoveryProgress$Message msgid "Recovery Progress" msgstr "Progrés de la recuperació" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Refés" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Refés {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Recorda aquesta configuració" @@ -1140,21 +1247,25 @@ msgstr "Restaura" msgid "Reset Image" msgstr "Restaura la imatge" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Resolució:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Restaura als valors per defecte" #: UiStrings.resx$Reverse$Message msgid "Reverse" -msgstr "" +msgstr "Inverteix" + +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Inverteix-ho tot" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Selecció inversa" #: UiStrings.resx$Revert$Message msgid "Revert" @@ -1166,7 +1277,7 @@ msgstr "Dreta" #: UiStrings.resx$Rotate$Message msgid "Rotate" -msgstr "" +msgstr "Gira" #: UiStrings.resx$RotateLeft$Message msgid "Rotate Left" @@ -1176,7 +1287,6 @@ msgstr "Gira a l'esquerra" msgid "Rotate Right" msgstr "Gira a la dreta" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Executa en rerefons" @@ -1185,22 +1295,26 @@ msgstr "Executa en rerefons" msgid "Running OCR..." msgstr "OCR en ús..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "Controlador SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Desa" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Desa-ho tot" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Desa com a imatges" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Desa com a PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1209,7 +1323,7 @@ msgstr "Desa les imatges" #: MiscResources.resx$SaveImagesProgress$Message msgid "Save Images Progress" -msgstr "Progrés del desat d'imatges" +msgstr "Progrés del desament d'imatges" #: MiscResources.resx$SavePdf$Message #: UiStrings.resx$SavePdf$Message @@ -1218,55 +1332,76 @@ msgstr "Desa (PDF)" #: MiscResources.resx$SavePdfProgress$Message msgid "Save PDF Progress" -msgstr "Progrés del desat en PDF" +msgstr "Progrés del desament en PDF" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Desa la selecció" #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Desa la selecció com a imatges" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Desa la selecció com a PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Desa en un únic fitxer" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Desa en fitxers múltiples" #: MiscResources.resx$BatchStatusSaving$Message msgid "Saving batch results..." -msgstr "Resultats del desat en lot..." +msgstr "Resultats del desament en lot..." #: MiscResources.resx$SavingFormat$Message msgid "Saving {0}..." msgstr "S'està desant {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" -msgstr "" +msgstr "Ajusta a la finestra" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Escala:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Escaneja" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Configuració de l’escàner" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Escaneja amb el perfil per defecte" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Escaneja amb un perfil nou" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Escaneja amb el perfil {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Imatge escanejada" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Compartició d'escàner" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "S'està escanejant la pàgina {0}" @@ -1280,11 +1415,14 @@ msgstr "S'està escanejant la pàgina {0} (escàner {1})..." msgid "Scanning page {0}..." msgstr "S'està escanejant la pàgina {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "S'estan cercant dispositius..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Segons (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Selecciona" @@ -1293,26 +1431,27 @@ msgstr "Selecciona" msgid "Select All" msgstr "Selecciona-ho tot" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Selecció del dispositiu" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Origen" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." -msgstr "Selecciona un perfil abans de fer click a Scanejar." +msgstr "Seleccioneu un perfil abans de fer clic a Escaneja." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Seleccioneu un o més idiomes:" #: MiscResources.resx$SelectedCount$Message msgid "Selected ({0})" -msgstr "Seleccionat ({0})" +msgstr "Selecció ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Separació amb codis Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Separació amb codis Patch-T" msgid "Set Default" msgstr "Estableix per defecte" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Configuració" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Comparteix" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Comparteix fins i tot quan NAPS2 és tancat" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Configuració de l'escàner compartit" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Els escàners compartits es poden utilitzar des d'altres ordinadors de la xarxa local seleccionant «Controlador ESCL» a la configuració del perfil NAPS2 de l'altre ordinador." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Nitidesa" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Drecera" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Mostra" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Mostra la barra d'eines «Perfils»" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Mostra la interfície de progrés nativa del TWAIN" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Mostra els números de pàgina" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Barra lateral" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Un fitxer per cada pàgina" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" -msgstr "" +msgstr "Escaneig senzill" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" -msgstr "Omet les opcions de desat" +msgstr "Omet les opcions de desament" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Divideix" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Inicia" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Atura la compartició d'escàner" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Ajusta a la mida de la pàgina" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Assumpte:" @@ -1358,12 +1544,11 @@ msgstr "Assumpte:" msgid "TIFF File (*.tiff, *.tif)" msgstr "Fitxer TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" -msgstr "Dispositiu TWAIN" +msgstr "Controlador TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Detalls tècnics" @@ -1372,6 +1557,10 @@ msgstr "Detalls tècnics" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "No s'ha trobat el motor OCR. Assegureu-vos d'instal·lar el paquet necessari:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "S'ha excedit el temps d'espera d'OCR." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "No es pot sobreescriure el fitxer perquè està en ús." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "El fitxer {0} ja existeix. Voleu sobreescriure'l?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Aquest fitxer està xifrat per contrasenya i és requereix per accedir: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Aquest fitxer està xifrat per contrasenya i és necessària per obrir-lo:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1406,7 +1595,7 @@ msgstr "L'escàner té un embús de paper." #: MiscResources.resx$DeviceWarmingUp$Message #: SdkResources.resx$DeviceWarmingUp$Message msgid "The scanner is warming up." -msgstr "S'està posant a punt l'escàner...." +msgstr "L'escàner s'està escalfant." #: MiscResources.resx$DeviceCoverOpen$Message #: SdkResources.resx$DeviceCoverOpen$Message @@ -1416,12 +1605,12 @@ msgstr "La tapa de l'escàner està oberta." #: MiscResources.resx$DriverNotSupported$Message #: SdkResources.resx$DriverNotSupported$Message msgid "The selected driver is not supported on this system." -msgstr "El controlador seleccionat no està suportat en aquest sistema." +msgstr "El controlador seleccionat no és compatible amb aquest sistema." #: MiscResources.resx$DeviceNotFound$Message #: SdkResources.resx$DeviceNotFound$Message msgid "The selected scanner could not be found." -msgstr "El scanner seleccionat no s'ha pogut trobar ." +msgstr "No s'ha pogut trobar l'escàner seleccionat." #: MiscResources.resx$NoFeederSupport$Message #: SdkResources.resx$NoFeederSupport$Message @@ -1436,36 +1625,72 @@ msgstr "L'escàner seleccionat no suporta dúplex (escaneig per dues cares). Si #: MiscResources.resx$DeviceBusy$Message #: SdkResources.resx$DeviceBusy$Message msgid "The selected scanner is busy." -msgstr "L'escàner seleccionat esta ocupat." +msgstr "L'escàner seleccionat està ocupat." #: MiscResources.resx$DeviceOffline$Message #: SdkResources.resx$DeviceOffline$Message msgid "The selected scanner is offline." -msgstr "El scaner seleccionat esta apagat." +msgstr "L'escàner seleccionat està apagat." + +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "S'ha produït un error." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Ha fallat la tasca. Reviseu el Visor d'esdeveniments de Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Mode:" -#: FImageSettings.resx$groupTiff.Text$Message +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" -msgstr "Opcions Tiff" +msgstr "Opcions TIFF" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Temps entre escanejos (segons):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Títol:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Eines" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" -msgstr "Implementació Twain:" +msgstr "Implementació TWAIN:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 in)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" -msgstr "" +msgstr "Carta US (8.5x11 in)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Desvincula" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Desfés" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Desfés {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Escàner desconegut" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" @@ -1477,31 +1702,28 @@ msgstr "Progrés de l'actualització" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "La comprovació d'actualitzacions està inhabilitada." #: MiscResources.resx$Updating$Message msgid "Updating..." -msgstr "S'està actualitzant......" +msgstr "S'està actualitzant..." #: MiscResources.resx$UploadingEmail$Message msgid "Uploading email..." msgstr "Enviament per correu-e..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Utilitza la interfície nativa" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" -msgstr "Utiltza els ajustos predeterminats" +msgstr "Utilitza les opcions per defecte" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Contrasenya de l'usuari:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "La tecnologia OCR requereix baixar fitxers dels idiomes que voleu escanejar." @@ -1515,82 +1737,78 @@ msgstr "Versió {0}" msgid "View" msgstr "Visualitza" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" -msgstr "Dispositiu WIA" +msgstr "Controlador WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." -msgstr "Esperant a TWAIN per completar..." +msgstr "Esperant a TWAIN per a completar..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." -msgstr "S'està esperant autorització..." +msgstr "S'està esperant l'autorització..." #: MiscResources.resx$BatchStatusWaitingForScan$Message msgid "Waiting for scan {0}..." msgstr "S'està esperant l'escàner {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Llindar del blanc" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Versió Wia:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Any" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Any (00-99)" #: MiscResources.resx$PdfNoPermissionToExtractContent$Message #: SdkResources.resx$PdfNoPermissionToExtractContent$Message msgid "You do not have permission to copy content from the file '{0}'." -msgstr "No teniu prou permisos per copiar el contingut del fitxer '{0}'." +msgstr "No teniu prou permisos per a copiar el contingut del fitxer '{0}'." #: MiscResources.resx$DontHavePermission$Message msgid "You don't have permission to save files at this location." -msgstr "No teniu permís per desar fitxers en aquesta ubicació." +msgstr "No teniu prou permisos per a desar fitxers en aquesta ubicació." #: MiscResources.resx$ExitWithUnsavedChanges$Message msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Teniu modificacions sense desar. Voleu tancar i descartar els canvis?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Ampliació" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Escala actual" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Amplia" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Allunya" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "in" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,30 +1816,35 @@ msgstr "de {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "{0}/{1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0}/{1} fitxers" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} dispositius trobats." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} ppp" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" -msgstr "Possiblement {0} imatges escanejades a {1} el {2} no s'han desat però es poden recuperar. Voleu continuar amb la recuperació?" +msgstr "Possiblement {0} imatges escanejades el {1} a les {2} no s'han desat, però es poden recuperar. Voleu continuar amb la recuperació?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." diff --git a/NAPS2.Lib/Lang/po/cs.po b/NAPS2.Lib/Lang/po/cs.po index a11a46e8ca..f04226c9e6 100644 --- a/NAPS2.Lib/Lang/po/cs.po +++ b/NAPS2.Lib/Lang/po/cs.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Czech\n" "Language: cs\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Výchozí akce tlačítka \"Uložit\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "1 200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Výchozí akce tlačítka \"Sken\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Nabídka \"Skenovat\" změní výchozí profil" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "Nalezeno 1 zařízení." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24bitové barvy" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "300 dpi" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "400 dpi" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "600 dpi" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "800 dpi" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "A3 (297 × 420 mm)" @@ -76,12 +60,15 @@ msgstr "O aplikaci" msgid "Acquiring data..." msgstr "Získávání dat..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Akce" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Pokročilé" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Pokročilé nastavení profilu" @@ -93,35 +80,35 @@ msgstr "Vše ({0})" msgid "All Files" msgstr "Všechny soubory" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Povolit anotace" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Povolit kopírování obsahu" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Povolit kopírování obsahu pro přístupnost" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Povolit sestavení dokumentu" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Povolit úpravu dokumentu" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Povolit vyplňování formuláře" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Povolit tisk v plné kvalitě" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Povolit tisk" @@ -133,17 +120,22 @@ msgstr "Střídavé prokládání" msgid "Alternate Interleave" msgstr "Střídavé vložení listů" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternativní přenos" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Vždy se zeptat" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Vždy se zeptat" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "K importu tohoto PDF je nutná doplňující komponenta. Chcete ji teď stáhnout?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Při pokusu o aktualizaci došlo k chybě.." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Došlo k chybě při běhu OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Došlo k chybě při pokusu o autorizaci." msgid "An error occurred when trying to auto save." msgstr "Při pokusu o automatické uložení došlo k chybě." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Při pokusu o aktualizaci došlo k chybě." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Při pokusu o uložení souboru došlo k chybě." @@ -175,7 +171,7 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Probíhá operace. Určitě chcete odejít a přerušit tuto operaci?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "Během dávkového skenování došlo k chybě." #: MiscResources.resx$UpdateAvailable$Message @@ -188,9 +184,17 @@ msgstr "Je k dispozici aktualizace OCR." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Ovladač Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Aplikace" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Použít jas/kontrast po skenování" @@ -220,51 +224,57 @@ msgstr "Opravdu si přejete smazat položky {0}?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "Jste si jisti, že chcete smazat profily {0}? " +msgstr "Jste si jisti, že chcete smazat profily {0}?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Opravdu chcete přestat sdílet {0}?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Jste si jisti, že chcete zrušit změny obrázků {0}?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Přiřadit" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Název přílohy:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autor:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autorizovat" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "Auto" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Nastavení automatického uložení" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Automatické zvyšování čísla (1 číslice)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Automatický přírůstek čísla (1 znak)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Automatické zvyšování čísla (2 číslice)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Automatické zvyšování čísla (3 číslice)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Automatické zvyšování čísla (4 číslice)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Po skenování automaticky spustit OCR" @@ -277,8 +287,8 @@ msgstr "B4 (250 × 353 mm)" msgid "B5 (176x250 mm)" msgstr "B5 (176 × 250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Dávkové skenování" @@ -296,9 +306,8 @@ msgstr "Dávka byla zastavena kvůli chybě." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Nejlepší" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bitová hloubka:" @@ -309,10 +318,10 @@ msgstr "Soubory Bitmap (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Černobíle" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Prázdné stránky" @@ -320,33 +329,19 @@ msgstr "Prázdné stránky" msgid "Brightness / Contrast" msgstr "Jas / Kontrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Jas:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Nemůžete najít skener? Přečtěte si o omezeních Flatpaku NAPS2." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Zrušit" @@ -363,7 +358,7 @@ msgstr "Ruší se...." msgid "Center" msgstr "Centrovat" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Změnit" @@ -375,7 +370,7 @@ msgstr "Vyhledat aktualizace" msgid "Checking..." msgstr "Vyhledávání..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Vybrat poskytovatele mailu" @@ -383,7 +378,6 @@ msgstr "Vybrat poskytovatele mailu" msgid "Choose Profile" msgstr "Vybrat profil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Vybrat zařízení" @@ -395,9 +389,9 @@ msgstr "Vymazat" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Smazat vše" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Vyčistit obrázky po skenování" @@ -405,16 +399,30 @@ msgstr "Vyčistit obrázky po skenování" msgid "Close" msgstr "Zavřít" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Spojit" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Komunikace se skenovacím zařízením byla přerušena." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Kompatibilita" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Komprese:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Připojit" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Chyba připojení." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrast:" @@ -433,9 +441,9 @@ msgstr "Kopírování..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Autorská práva {0} přispěvatelů NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Práh pokrytí" @@ -443,7 +451,7 @@ msgstr "Práh pokrytí" msgid "Crop" msgstr "Ořez" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Oříznout na velikost stránky" @@ -451,10 +459,14 @@ msgstr "Oříznout na velikost stránky" msgid "Custom ({0}x{1} {2})" msgstr "Vlastní ({0} × {1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Vlastní velikost papíru" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Vlastní rozlišení" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Vlastní otočení" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "Vlastní SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Vlastní..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Tmavý" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Den (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Výchozí" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Výchozí umístění souboru:" @@ -487,7 +504,6 @@ msgstr "Výchozí umístění souboru:" msgid "Deinterleave" msgstr "Prokládání" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Odstranit zešikmení" msgid "Deskew Progress" msgstr "Průběh odstraňování zešikmení" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Odstranit zešikmení skenovaných stránek" @@ -509,35 +525,31 @@ msgstr "Odstranit zešikmení skenovaných stránek" msgid "Deskewing..." msgstr "Odstraňování zešikmení..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Zařízení:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Rozměry" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Zobrazovaný název:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Korekce dokumentu" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Darovat" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Hotovo" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Stáhnout" @@ -550,22 +562,50 @@ msgstr "Chyba stahování" msgid "Download Needed" msgstr "Potřebné stažení" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Průběh stahování" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "Dpi" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Duplexní" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Ovladač ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "Síťový ovladač ESCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "Ovladač ESCL USB" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Upravit" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Upravit pomocí {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Upravit pomocí..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Odeslat vše" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Odeslat vše jako PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,25 +616,32 @@ msgstr "PDF e-mailem" msgid "Email PDF Progress" msgstr "Průběh PDF e-mailem" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Odeslat vybrané" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Odeslat vybrané jako PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Nastavení e-mailu" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Povolit automatické ukládání" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Povolit protokolování ladění" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Zašifrovat PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Šifrování" @@ -602,12 +649,15 @@ msgstr "Šifrování" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Rozšířený Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Chyba" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Chyba při spouštění aplikace {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,36 +667,43 @@ msgstr "Odhadovaná velikost stažení: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "Soubor ExIF (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Vyloučit prázdné stránky" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Rychlý" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Podavač" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "Název souboru" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Název souboru:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Cesta k souboru:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Opravit vyvážení bílé a odstranění šumu" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Překlopit" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Obrácení zadních stran oboustranných stránek" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Překlopit oboustranné stánky" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Pro nejlepší výsledky při vysokých kvalitách JPEG (80+) zvyšte ve svém profilu Kvalitu obrázku." @@ -654,7 +711,6 @@ msgstr "Pro nejlepší výsledky při vysokých kvalitách JPEG (80+) zvyšte ve msgid "GIF File (*.gif)" msgstr "Soubor GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Instalovat více jazyků" @@ -665,18 +721,17 @@ msgstr "Sklo" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Stupně šedi" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Překlopit vodorovně:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Hodina (0-23)" @@ -684,6 +739,10 @@ msgstr "Hodina (0-23)" msgid "Hue / Saturation" msgstr "Odstín / Sytost" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Hostitel" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Použité ikony:" @@ -696,12 +755,12 @@ msgstr "Obrázek" msgid "Image Files" msgstr "Soubory obrázků" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Kvalita obrázku" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Nastavení obrázku" @@ -711,7 +770,7 @@ msgstr "Obrázek uložen." #: UiStrings.resx$Import$Message msgid "Import" -msgstr "" +msgstr "Importovat" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" @@ -745,34 +804,51 @@ msgstr "Instalace dokončena. Chcete nyní restartovat NAPS2?" msgid "Installation failed." msgstr "Instalace selhala." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Rozhraní" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Vložit listy" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" -msgstr "Soubor JPEG (*.jpg, *.jpeg) " +msgstr "Soubor JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "Soubor JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Kvalita JPEG" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Uchovávat obrázky napříč relacemi" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Klávesové zkratky" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Klíčová slova:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Jazyk" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Napsat recenzi" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Vlevo" @@ -781,33 +857,48 @@ msgstr "Vlevo" msgid "Legacy (native UI only)" msgstr "Legacy (pouze nativní UI)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Světlý" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Líbí se vám NAPS2?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Nahrát obrázky do NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Umožnit prohledávání PDF pomocí OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Manuální IP" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Nejlepší kvalita (velký soubor)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Přenos paměti," + +#: UiStrings.resx$Metadata$Message msgid "Metadata" -msgstr "" +msgstr "Metadata" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minuta (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Měsíc (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Více informací" @@ -819,11 +910,19 @@ msgstr "Posunout níž" msgid "Move Up" msgstr "Posunout výš" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Více jazyků" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Více jazyků..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Více skenů (fixní pauza mezi skeny)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Více skenů (oznámení mezi skeny)" @@ -831,17 +930,17 @@ msgstr "Více skenů (oznámení mezi skeny)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 je zcela zdarma. Zvažte poskytnutí daru." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Název (volitelné)" @@ -849,6 +948,10 @@ msgstr "Název (volitelné)" msgid "Name missing." msgstr "Název chybí." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Nativní přenos" + #: UiStrings.resx$New$Message msgid "New" msgstr "Nový" @@ -861,7 +964,7 @@ msgstr "Nový profil" msgid "Next" msgstr "Další" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Další sken" @@ -870,12 +973,15 @@ msgstr "Další sken" msgid "No device selected." msgstr "Nebylo vybráno žádné zařízení." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Zařízení nenalezena." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "V podavači nejsou žádné papíry." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Nebyl vybrán poskytovatel." @@ -895,21 +1001,20 @@ msgstr "Žádný" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Teď ne" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Počet skenů:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Stažení OCR" @@ -918,37 +1023,23 @@ msgstr "Stažení OCR" msgid "OCR Progress" msgstr "Průběh OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Nastavení OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Jazyk OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "Mód OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Šířka odsazení založená na zarovnání (WIA)" @@ -956,13 +1047,11 @@ msgstr "Šířka odsazení založená na zarovnání (WIA)" msgid "Old DSM" msgstr "Staré DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Jeden soubor na stránku" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Jeden soubor na sken" @@ -970,7 +1059,11 @@ msgstr "Jeden soubor na sken" msgid "One or more files could not be downloaded." msgstr "Jeden nebo více souborů nelze stáhnout." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Povolit pouze jednu instanci NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Otevřít složku" @@ -978,11 +1071,15 @@ msgstr "Otevřít složku" msgid "Operation in Progress" msgstr "Probíhá operace" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (nový)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Web Access" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Výstup" @@ -990,7 +1087,7 @@ msgstr "Výstup" msgid "Overwrite File" msgstr "Přepsat soubor" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Heslo vlastníka:" @@ -998,8 +1095,8 @@ msgstr "Heslo vlastníka:" msgid "PDF Document (*.pdf)" msgstr "PDF dokument (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Nastavení PDF" @@ -1009,35 +1106,33 @@ msgstr "PDF uloženo." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "Soubor PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Velikost stránky:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Zdroj papíru:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Heslo" @@ -1045,21 +1140,24 @@ msgstr "Heslo" msgid "Paste" msgstr "Vložit" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Zástupné znaky" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Následné zpracování" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Předběžné spuštění OCR po skenování" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Až budete připraveni, stiskněte Start." @@ -1067,7 +1165,7 @@ msgstr "Až budete připraveni, stiskněte Start." msgid "Preview" msgstr "Náhled" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Náhled:" @@ -1080,12 +1178,11 @@ msgstr "Předchozí" msgid "Print" msgstr "Tisk" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Nastavení profilu" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profil:" @@ -1094,23 +1191,27 @@ msgstr "Profil:" msgid "Profiles" msgstr "Profily" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Výzva Pokud je vybráno" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Výzva pro cestu k souboru" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Poskytovatel" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Připraven ke skenu {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Obnovit" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Obnovit skenované obrázky" @@ -1122,9 +1223,15 @@ msgstr "Obnovuje se..." msgid "Recovery Progress" msgstr "Průběh obnovení" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Znovu" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Znovu {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Zapamatovat si tato nastavení" @@ -1140,15 +1247,11 @@ msgstr "Resetovat" msgid "Reset Image" msgstr "Resetovat obrázek" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Rozlišení:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Obnovit výchozí" @@ -1156,6 +1259,14 @@ msgstr "Obnovit výchozí" msgid "Reverse" msgstr "Obrátit" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Obrátit vše" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Obrátit vybrané" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Vrátit" @@ -1176,7 +1287,6 @@ msgstr "Otočit vlevo" msgid "Rotate Right" msgstr "Otočit vpravo" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Spustit na pozadí" @@ -1185,22 +1295,26 @@ msgstr "Spustit na pozadí" msgid "Running OCR..." msgstr "Spuštěno OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "SANE ovladač" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Uložit" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Uložit vše" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Uložit vše jako obrázky" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Uložit vše jako PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Uložit PDF" msgid "Save PDF Progress" msgstr "Průběh ukládání PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Uložit vybrané" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Uložit vybrané jako obrázky" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Uložit vybrané jako PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Uložit do jednoho souboru" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Uložit do více souborů" @@ -1244,29 +1363,45 @@ msgstr "Ukládání výsledků dávky..." msgid "Saving {0}..." msgstr "Ukládá se {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Stupnice s oknem" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Měřítko:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skenovat" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Nastavení skenování" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Skenovat pomocí výchozího profilu" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Skenovat pomocí nového profilu" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Skenovat pomocí profilu {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Naskenovaný obrázek" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Sdílení skeneru" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Skenuje se stránka {0}" @@ -1280,11 +1415,14 @@ msgstr "Skenuje se stránka {0} (sken {1})..." msgid "Scanning page {0}..." msgstr "Skenuje se stránka {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Hledání zařízení..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekunda (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Vybrat" @@ -1293,7 +1431,10 @@ msgstr "Vybrat" msgid "Select All" msgstr "Vybrat vše" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Výběr zařízení" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Vybrat zdroj" @@ -1302,7 +1443,6 @@ msgstr "Vybrat zdroj" msgid "Select a profile before clicking Scan." msgstr "Vyberte profil před spuštěním skenování." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Vyberte jeden nebo více jazyků:" @@ -1311,8 +1451,7 @@ msgstr "Vyberte jeden nebo více jazyků:" msgid "Selected ({0})" msgstr "Vybráno ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Oddělit soubory pomocí Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Oddělit soubory pomocí Patch-T" msgid "Set Default" msgstr "Nastavit výchozí" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Nastavení" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Sdílet" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Sdílet, i když je NAPS2 zavřený" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Nastavení sdíleného skeneru" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Sdílené skenery lze používat z jiných počítačů v místní síti výběrem možnosti \"ESCL ovladač\" v nastavení profilu NAPS2 jiného počítače." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Zaostřit" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Zkratka" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Ukázat" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Zobrazit panel nástrojů \"Profily\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Zobrazit nativní průběh TWAIN" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Zobrazit čísla stránek" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Postranní panel" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Jednostránkové soubory" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Jeden sken" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Přeskočit dotaz na uložení" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Rozdělit" + +#: UiStrings.resx$Start$Message msgid "Start" -msgstr "" +msgstr "Start" + +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Zastavit sdílení skeneru" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Roztáhnout na velikost stránky" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Předmět:" @@ -1358,12 +1544,11 @@ msgstr "Předmět:" msgid "TIFF File (*.tiff, *.tif)" msgstr "Soubor TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "Ovladač TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Technické detaily" @@ -1372,6 +1557,10 @@ msgstr "Technické detaily" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "OCR není dostupné. Ověřte instalaci požadovaného balíčku:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Vypršel časový limit operace OCR." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Soubor nemůže být přepsán, protože je právě používán." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Soubor {0} již existuje. Přejete si jej přepsat?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Následující soubor je zašifrován a k otevření vyžaduje heslo: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Následující soubor je zašifrován a k otevření vyžaduje heslo:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "Vybraný skener je zaneprázdněn." msgid "The selected scanner is offline." msgstr "Vybraný skener je vypnutý." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Pracovní proces spadl." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Pracovní proces spadl. Zkontrolujte prohlížeč událostí systému Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Motiv:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Možnosti Tiff" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Čas mezi skeny (vteřiny):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Titul:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Nástroje" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Implementace TWAIN:" @@ -1467,6 +1676,22 @@ msgstr "US Legal (8,5 × 14 in)" msgid "US Letter (8.5x11 in)" msgstr "US Letter (8.5 × 11 in)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Zrušit přiřazení" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Zpět" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Zpět {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Neznámý skener" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Neuložené změny" @@ -1477,7 +1702,7 @@ msgstr "Průběh aktualizace" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Kontrola aktualizací je vypnuta." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Aktualizace..." msgid "Uploading email..." msgstr "Odesílání emailu..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Použít nativní rozhraní" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Použít výchozí nastavení" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Heslo uživatele:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Pro použití OCR musíte stáhnout každý jazyk, který chcete skenovat." @@ -1515,16 +1737,15 @@ msgstr "Verze {0}" msgid "View" msgstr "Zobrazení" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "Ovladač WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Čekání na dokončení TWAIN..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Čekání na autorizaci..." @@ -1532,19 +1753,19 @@ msgstr "Čekání na autorizaci..." msgid "Waiting for scan {0}..." msgstr "Čekání na sken {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Práh bílé" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Wia verze:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Rok" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Rok (00-99)" @@ -1561,36 +1782,33 @@ msgstr "Nemáte oprávnění zapisovat do tohoto umístění." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Máte neuložené změny. Jste si jisti, že chcete odejít a zahodit tyto změny?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Zvětšení" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Aktuální zvětšení" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Přiblížit" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Oddálit" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "in" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,28 +1816,33 @@ msgstr "z {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "{0} ({1} × {2} {3})" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "{0}/{1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0}/{1} souborů" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "Nalezeno {0} zařízení." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} obrázků skenovaných na {1} v {2} možná nebylo uloženo a je možno je obnovit. Chcete je obnovit?" diff --git a/NAPS2.Lib/Lang/po/da.po b/NAPS2.Lib/Lang/po/da.po index c287d5755c..52c0ef2ec1 100644 --- a/NAPS2.Lib/Lang/po/da.po +++ b/NAPS2.Lib/Lang/po/da.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Danish\n" "Language: da\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Standardhandling for knappen \"Gem\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Standardhandling for knappen \"Scan\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "\"Scan\" menu ændrer standardprofil" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 enhed fundet." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-bit farve" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -76,12 +60,15 @@ msgstr "Om" msgid "Acquiring data..." msgstr "Henter data..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Avanceret" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Avancerede Profil Indstillinger" @@ -93,35 +80,35 @@ msgstr "Alle ({0})" msgid "All Files" msgstr "Alle filer" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Tillad Annoteringer" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Tillad kopiering af indhold" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Tillad kopiering af indhold for tilgængelighed" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Tillad samling af dokumenter" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Tillad ændring af dokument" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Tillad formular udfyldning" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Tillad fuld kvalitetsudskrivning" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Tillad udskrivning" @@ -133,37 +120,46 @@ msgstr "Skift Deinterleave" msgid "Alternate Interleave" msgstr "Skift Interleave" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternativ overførsel" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Spørg altid" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Spørg altid" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "En ekstra komponent er krævet for at importere denne fil. Vil du downloade komponenten nu?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "" +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Der opstod en fejl under kørsel af OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "Der opstod en fejl forsøg på godkendelse." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." msgstr "En fejl opstod under forsøg på på at gemme automatisk." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Der opstod en fejl under opdateringen." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "En fejl opstod under forsøg på at gemme filen." #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "" +msgstr "Der opstod en fejl under forsøget på at sende e-mailen." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." -msgstr "En fejl opstod under forsøg på at sende en email." +msgstr "Der opstod en fejl under forsøget på at sende en e-mail." #: MiscResources.resx$UnknownDriverError$Message #: SdkResources.resx$UnknownDriverError$Message @@ -172,15 +168,15 @@ msgstr "Der opstod en fejl med scanner driveren." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "" +msgstr "En handling er i gang. Er du sikker på, at du vil afslutte og annullere handlingen?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "En ukendt fejl opstod under batch scanning." +msgid "An unknown error occurred during the batch scan." +msgstr "En ukendt fejl opstod under batch-scanning." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "" +msgstr "En opdatering er tilgængelig." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." @@ -188,9 +184,17 @@ msgstr "En opdatering til OCR er tilgængelig." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple driver" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Program" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Tilføj lysstyrke/kontrast efter scanning" @@ -200,7 +204,7 @@ msgstr "Tilføj til alle {0} valgte billeder" #: MiscResources.resx$ConfirmCancelBatch$Message msgid "Are you sure you want to cancel the batch scan?" -msgstr "" +msgstr "Er du sikker på du vil annullere batch-scanningen?" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" @@ -222,65 +226,71 @@ msgstr "Vil du virkelig slette {0} element(er)?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Er du sikker på, at du vil slette {0} profiler?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Er du sikker på, at du vil stoppe med at dele {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Er du sikker på at du vil fortryde din ændring(er) af {0} billede(r)?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Navn på vedhæftning:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Forfatter:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "Autoriser" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "Auto" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Autogem indstillinger" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Autoforøgelsesnummer (1 ciffer)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Automatisk stigende tal (1 ciffer)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Autoforøgelsesnummer (2 cifre)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Autoforøgelsesnummer (3 cifre)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Autoforøgelsesnummer (4 cifre)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "" +msgstr "Kør OCR automatisk efter scanning" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" -msgstr "" +msgstr "Batch-scanning" #: MiscResources.resx$BatchStatusCancelled$Message msgid "Batch cancelled." @@ -296,9 +306,8 @@ msgstr "Batch scan stoppede på grund af en fejl." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Bedst" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Antal bit:" @@ -309,10 +318,10 @@ msgstr "Bitmap filer (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Sort/hvid" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Blanke sider" @@ -320,40 +329,26 @@ msgstr "Blanke sider" msgid "Brightness / Contrast" msgstr "Lysstyrke / Kontrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Lysstyrke:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Kan du ikke finde din scanner? Læs om begrænsninger i NAPS2 Flatpak." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Fortryd" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" -msgstr "" +msgstr "Annuller batch" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." @@ -363,27 +358,26 @@ msgstr "Annullerer...." msgid "Center" msgstr "Centrér" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" -msgstr "" +msgstr "Skift" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "" +msgstr "Søg efter opdateringer" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "" +msgstr "Søger..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "" +msgstr "Vælg e-mail udbyder" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" msgstr "Vælg profil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Vælg enhed" @@ -395,9 +389,9 @@ msgstr "Ryd" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Ryd alle" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Ryd billeder efter gem" @@ -405,16 +399,30 @@ msgstr "Ryd billeder efter gem" msgid "Close" msgstr "Luk" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Kombiner" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Kommunikation med scanningsenheden blev afbrudt." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Kompatibilitet" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" -msgstr "" +msgstr "Komprimering:" + +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Forbind" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Forbindelsesfejl." -#: FEditProfile.resx$label7.Text$Message #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrast:" @@ -433,9 +441,9 @@ msgstr "Kopierer..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} NAPS2 Contributors" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Dækningstærskel" @@ -443,7 +451,7 @@ msgstr "Dækningstærskel" msgid "Crop" msgstr "Beskær" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Beskær til sidestørrelse" @@ -451,35 +459,44 @@ msgstr "Beskær til sidestørrelse" msgid "Custom ({0}x{1} {2})" msgstr "Tilpasset ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Tilpasset sidestørrelse" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Brugerdefineret rotation" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" -msgstr "" +msgstr "Brugerdefineret SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Brugertilpasset..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Dag (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Standard" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Standard filsti:" @@ -487,7 +504,6 @@ msgstr "Standard filsti:" msgid "Deinterleave" msgstr "Indflette" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Ret op" msgid "Deskew Progress" msgstr "Ret op fremskridt" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Ret scannede sider op" @@ -509,35 +525,31 @@ msgstr "Ret scannede sider op" msgid "Deskewing..." msgstr "Retter op..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Enhed:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Dimensioner" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Vist navn:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Dokumentkorrektion" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "" +msgstr "Doner" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Fuldført" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Hent" @@ -550,64 +562,102 @@ msgstr "Overførselsfejl" msgid "Download Needed" msgstr "Download krævet" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Status på hentning" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Dupleks" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL driver" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL netværksdriver" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB-driver" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Rediger" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Send alle via e-mail" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Send alle via e-mail som PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "" +msgstr "E-mail til PDF" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" -msgstr "Email PDF fremskridt" +msgstr "E-mail PDF fremskridt" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "E-mail valgt" #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Send det valgte via e-mail som PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" -msgstr "Email indstillinger" +msgstr "E-mail indstillinger" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Aktiver Autogem" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Aktiver logning af fejl" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Kryptér PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Kryptering" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "" +msgstr "Enhanced Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Fejl" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,46 +665,52 @@ msgstr "Anslået download størrelse: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Udelad blanke sider" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Hurtig" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Føder" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "Filnavn" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Filnavn:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Filsti:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Ret hvidbalance og fjern støj" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Spejling" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Vend bagsiden af duplex-sider" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Vend duplex sider" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "" +msgstr "For høje JPEG-kvaliteter (80+), bør du også øge billedkvaliteten i din profil for de bedste resultater." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" -msgstr "" +msgstr "GIF-fil (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Hent flere sprog" @@ -665,18 +721,17 @@ msgstr "Glas" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Gråskala" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Vandret justering:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Time (0-23)" @@ -684,6 +739,10 @@ msgstr "Time (0-23)" msgid "Hue / Saturation" msgstr "Farve / Mætning" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/vært" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Ikoner fra:" @@ -696,12 +755,12 @@ msgstr "Billede" msgid "Image Files" msgstr "Billedfiler" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" -msgstr "billedkvalitet" +msgstr "Billedkvalitet" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Billedindstillinger" @@ -711,7 +770,7 @@ msgstr "Billede gemt." #: UiStrings.resx$Import$Message msgid "Import" -msgstr "Importér" +msgstr "Importer" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" @@ -727,7 +786,7 @@ msgstr "Importerer..." #: MiscResources.resx$Install$Message msgid "Install {0}" -msgstr "" +msgstr "Installer {0}" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" @@ -745,34 +804,51 @@ msgstr "Installationen er færdig. Ønsker du at genstarte NAPS2 nu?" msgid "Installation failed." msgstr "Installationen mislykkedes." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Grænseflade" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Indflette" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" -msgstr "" +msgstr "JPEG- Fil (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000-fil (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Jpeg kvalitet" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Behold billeder på tværs af sessioner" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Nøgleord:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Sprog" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Venstre" @@ -781,33 +857,48 @@ msgstr "Venstre" msgid "Legacy (native UI only)" msgstr "Arv (kun oprindelig brugergrænseflade)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Hent billeder ind i NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Lav PDF'er søgebar med OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Manuel IP" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Maximum kvalitet (store filer)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Overførsel af hukommelse" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" -msgstr "" +msgstr "Metadata" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minut (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Måned (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Mere information" @@ -819,11 +910,19 @@ msgstr "Flyt ned" msgid "Move Up" msgstr "Flyt op" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Flere sprog" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Flere sprog..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Flere scanninger (fast forsinkelse mellem scanninger)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Flere scanninger (prompt mellem scanninger)" @@ -831,17 +930,17 @@ msgstr "Flere scanninger (prompt mellem scanninger)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "" +msgstr "NAPS2 er helt gratis. Overvej at foretage en donation." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Navn (valgfrit)" @@ -849,6 +948,10 @@ msgstr "Navn (valgfrit)" msgid "Name missing." msgstr "Navn mangler." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Indbygget overførsel" + #: UiStrings.resx$New$Message msgid "New" msgstr "Ny" @@ -861,7 +964,7 @@ msgstr "Ny profil" msgid "Next" msgstr "Næste" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Næste scanning" @@ -870,85 +973,73 @@ msgstr "Næste scanning" msgid "No device selected." msgstr "Ingen enheder valgt." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Ingen enheder fundet." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Der er ingen billeder i føderen." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "" +msgstr "Ingen udbyder valgt." #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message msgid "No scanning device was found." -msgstr "Ingen skannerenheder blev fundet." +msgstr "Ingen scannincsenheder blev fundet." #: MiscResources.resx$NoUpdates$Message msgid "No updates available." -msgstr "" +msgstr "Der er ingen tilgængelige opdateringer." #: SettingsResources.resx$TiffComp_None$Message msgid "None" -msgstr "" +msgstr "Ingen" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Ikke nu" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Antal scanninger:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR download" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "" +msgstr "OCR-fremskridt" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCR opsætning" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR sprog:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "" +msgstr "OCR-tilstand:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Forskydningsbredde baseret på justering (WIA)" @@ -956,13 +1047,11 @@ msgstr "Forskydningsbredde baseret på justering (WIA)" msgid "Old DSM" msgstr "Gammel DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "En fil pr. side" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "En fil pr. scanning" @@ -970,27 +1059,35 @@ msgstr "En fil pr. scanning" msgid "One or more files could not be downloaded." msgstr "En eller flere filer, kunne ikke blive hentet." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Tillad kun en enkelt åben NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Åben mappe" #: MiscResources.resx$ActiveOperations$Message msgid "Operation in Progress" +msgstr "Handling er i gang" + +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" msgstr "" #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Web Access" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" -msgstr "" +msgstr "Resultat" #: MiscResources.resx$OverwriteFile$Message msgid "Overwrite File" msgstr "Overskriv fil" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Ejers password:" @@ -998,8 +1095,8 @@ msgstr "Ejers password:" msgid "PDF Document (*.pdf)" msgstr "PDF dokument (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF indstillinger" @@ -1009,35 +1106,33 @@ msgstr "PDF gemt." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" -msgstr "" +msgstr "PNG-fil (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Sidestørrelse:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Papirkilde:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Adgangskode" @@ -1045,21 +1140,24 @@ msgstr "Adgangskode" msgid "Paste" msgstr "Indsæt" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Pladsholdere" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Efterprocessering" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Kør forebyggende OCR efter scanning" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Tryk start når du er klar." @@ -1067,7 +1165,7 @@ msgstr "Tryk start når du er klar." msgid "Preview" msgstr "Eksempel" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Eksempel:" @@ -1078,14 +1176,13 @@ msgstr "Forrige" #: MiscResources.resx$Print$Message #: UiStrings.resx$Print$Message msgid "Print" -msgstr "" +msgstr "Udskriv" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Profilindstillinger" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profil:" @@ -1094,25 +1191,29 @@ msgstr "Profil:" msgid "Profiles" msgstr "Profiler" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Spørg hvis valgt" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Spørg efter filsti" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "Udbyder" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Klar til scanning {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Gendan" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" -msgstr "Gendan skannet billeder" +msgstr "Gendan scannet billede" #: MiscResources.resx$Recovering$Message msgid "Recovering..." @@ -1122,15 +1223,21 @@ msgstr "Gendanner..." msgid "Recovery Progress" msgstr "Gendannelsesfremskridt" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Gentag" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Gentag {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Husk disse indstillinger" #: UiStrings.resx$Reorder$Message msgid "Reorder" -msgstr "Sortere" +msgstr "Sorter" #: UiStrings.resx$Reset$Message msgid "Reset" @@ -1140,15 +1247,11 @@ msgstr "Nulstil" msgid "Reset Image" msgstr "Nulstil billede" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Opløsning:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Gendan standardindstillinger" @@ -1156,6 +1259,14 @@ msgstr "Gendan standardindstillinger" msgid "Reverse" msgstr "Omvendt" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Omvend alle" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Omvend markerede" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Vend tilbage" @@ -1166,41 +1277,44 @@ msgstr "Højre" #: UiStrings.resx$Rotate$Message msgid "Rotate" -msgstr "Rotér" +msgstr "Roter" #: UiStrings.resx$RotateLeft$Message msgid "Rotate Left" -msgstr "Rotér til venstre" +msgstr "Roter til venstre" #: UiStrings.resx$RotateRight$Message msgid "Rotate Right" -msgstr "Rotér til højre" +msgstr "Roter til højre" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "" +msgstr "Kør i baggrunden" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "" +msgstr "Udfører OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "SANE driver" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Gem" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Gem alle" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Gem alle som billeder" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Gem alle som PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Gem PDF" msgid "Save PDF Progress" msgstr "Gem PDF fremskridt" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Gem valgte" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Gem valgte som billeder" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Gem valgte som PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Gem som en enkelt fil" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Gem som flere filer" @@ -1244,28 +1363,44 @@ msgstr "Gemmer batch resultater..." msgid "Saving {0}..." msgstr "Gemmer {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Skalér med vindue" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Skalér:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" -msgstr "Skan" +msgstr "Scan" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Scan konfiguration" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Scan med standardprofil" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" -msgstr "Skannet billede" +msgstr "Scannet billede" + +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Deling af scanner" #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" @@ -1280,11 +1415,14 @@ msgstr "Scanner side {0} (scan {1})..." msgid "Scanning page {0}..." msgstr "Scanner side {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Søger efter enheder..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekund (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Vælg" @@ -1293,16 +1431,18 @@ msgstr "Vælg" msgid "Select All" msgstr "Markér alle" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Vælg enhed" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Vælg kilde" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." -msgstr "Vælg profil før klik på skan." +msgstr "Vælg en profil før du klikker på scan." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Vælg et eller flere sprog:" @@ -1311,8 +1451,7 @@ msgstr "Vælg et eller flere sprog:" msgid "Selected ({0})" msgstr "Valgte ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Separer filer ved hjælp af Patch-T" @@ -1320,62 +1459,112 @@ msgstr "Separer filer ved hjælp af Patch-T" msgid "Set Default" msgstr "Sæt standard" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Indstillinger" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Del" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Del selv når NAPS2 er lukket" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Delt scanner-indstillinger" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Delte scannere kan bruges fra andre computere på det lokale netværk, ved at vælge \"ESCL Driver\" i den anden computers NAPS2-profilindstillinger." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Gør skarpere" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Vis" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Vis værktøjslinjen \"Profiler\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Vis indbygget TWAIN-fremskridt" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Vis sidenumre" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Sidebar" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" -msgstr "" +msgstr "Enkelt side-filer" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Enkelt scanning" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Spring over gemme-prompt" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Opdel" + +#: UiStrings.resx$Start$Message msgid "Start" +msgstr "Start" + +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Stræk til sidestørrelse" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Emne:" #: MiscResources.resx$FileTypeTiff$Message msgid "TIFF File (*.tiff, *.tif)" -msgstr "" +msgstr "TIFF-fil (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" -msgstr "" +msgstr "TWAIN-driver" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Tekniske detaljer" #: MiscResources.resx$TesseractNotAvailable$Message #: SdkResources.resx$TesseractNotAvailable$Message msgid "The OCR engine is not available. Make sure to install the required package:" -msgstr "" +msgstr "OCR-motoren er ikke tilgængelig. Kontroller at du har installeret den påkrævede pakke:" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "OCR-handlingen fik timeout." #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" -msgstr "" +msgstr "SANE-driveren er ikke tilgængelig. Kontroller at du har installeret den påkrævede pakke:" #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message @@ -1394,9 +1583,9 @@ msgstr "Filen kunne ikke overskrives, fordi den er i brug." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Filen {0} findes allerede. Vil du overskrive den?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Følgende fil er krypteret og kræver en adgangskode, for at åbne den: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Følgende fil er krypteret og kræver en adgangskode, for at åbne den:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1416,12 +1605,12 @@ msgstr "Scannerens cover er åbent." #: MiscResources.resx$DriverNotSupported$Message #: SdkResources.resx$DriverNotSupported$Message msgid "The selected driver is not supported on this system." -msgstr "" +msgstr "Den valgte driver er ikke understøttet på dette system." #: MiscResources.resx$DeviceNotFound$Message #: SdkResources.resx$DeviceNotFound$Message msgid "The selected scanner could not be found." -msgstr "Den valgte skanner kan ikke findes." +msgstr "Den valgte scanner blev ikke fundet." #: MiscResources.resx$NoFeederSupport$Message #: SdkResources.resx$NoFeederSupport$Message @@ -1441,30 +1630,66 @@ msgstr "Den valgte scanner er optaget." #: MiscResources.resx$DeviceOffline$Message #: SdkResources.resx$DeviceOffline$Message msgid "The selected scanner is offline." -msgstr "Den valgte skanner er offline." +msgstr "Den valgte scanner er offline." -#: FImageSettings.resx$groupTiff.Text$Message -msgid "Tiff Options" +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Arbejdsprocessen fejlede." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Arbejdsprocessen fejlede. Kontroller Windows event-fremviseren." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message +msgid "Tiff Options" +msgstr "TIFF-indstillinger" + +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Tid mellem scanninger (sekunder):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Titel:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Værktøjer" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Twain Implementering:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 in)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" +msgstr "US Letter (8.5x11 in)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Fortryd" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Fortryd {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" msgstr "" #: MiscResources.resx$UnsavedChanges$Message @@ -1473,35 +1698,32 @@ msgstr "Ikke-gemte ændringer" #: MiscResources.resx$UpdateProgress$Message msgid "Update Progress" -msgstr "" +msgstr "Opdateringsstatus" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Opdateringstjek er slået fra." #: MiscResources.resx$Updating$Message msgid "Updating..." -msgstr "" +msgstr "Opdaterer..." #: MiscResources.resx$UploadingEmail$Message msgid "Uploading email..." -msgstr "" +msgstr "Uploader e-mail..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Brug oprindelig brugergrænseflade" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Brug prædefinerede indstillinger" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Bruger adgangskode:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Ved brug af OCR kræver det, at du henter hvert sprog, som du vil scanne i." @@ -1509,42 +1731,41 @@ msgstr "Ved brug af OCR kræver det, at du henter hvert sprog, som du vil scanne #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message msgid "Version {0}" -msgstr "" +msgstr "Version {0}" #: UiStrings.resx$View$Message msgid "View" msgstr "Vis" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" -msgstr "" +msgstr "WIA driver" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Venter på TWAIN for at fortsætte..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." -msgstr "" +msgstr "Venter på godkendelse..." #: MiscResources.resx$BatchStatusWaitingForScan$Message msgid "Waiting for scan {0}..." msgstr "Venter på scanning {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Hvidgrænse" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "WIA-version:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "År" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "År (00-99)" @@ -1561,21 +1782,18 @@ msgstr "Du har ikke rettigheder til gemme filer på dette sted." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Du har ændringer, der ikke er gemt. Er du sikker på, at du vil afslutte uden at gemme ændringerne?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" -msgstr "" +msgstr "Zoom" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Zoom Aktuel" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Zoom ind" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Zoom ud" @@ -1598,30 +1816,35 @@ msgstr "af {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} filer" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} enheder fundet." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" -msgstr "{0} billede(r) skannet på {1} af {2} er måske ikke blevet gemt, men er mulige at genskabe. Vil du genskabe dem?" +msgstr "{0} billede(r) scannet på {1} af {2} er måske ikke blevet gemt, men er mulige at genskabe. Vil du genskabe dem?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." diff --git a/NAPS2.Lib/Lang/po/de.po b/NAPS2.Lib/Lang/po/de.po index d1edeadd44..d6d16f6aef 100644 --- a/NAPS2.Lib/Lang/po/de.po +++ b/NAPS2.Lib/Lang/po/de.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: German\n" "Language: de\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "\"Speichern\"-Button Standardaktion:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "\"Scan\"-Button Standardaktion:" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Menü \"Scan\" ändert das Standardprofil" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 Gerät gefunden." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-Bit Farbtiefe" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "A3 (297 × 420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -74,14 +58,17 @@ msgstr "Über" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." -msgstr "Daten erfassen..." +msgstr "Datenerfassung..." + +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Aktion" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Erweitert" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Erweiterte Profileinstellungen" @@ -93,35 +80,35 @@ msgstr "Alle ({0})" msgid "All Files" msgstr "Alle Dateien" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Anmerkungen erlauben" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" -msgstr "Kopieren des Inhalts erlauben" +msgstr "Kopieren von Inhalten zulassen" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Kopieren des Inhalts für erleichterte Bedienung erlauben" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Erlaube Dokumentenzusammenführung" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Erlaube Dokumentveränderung" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Erlaube das Ausfüllen von Formularen" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Erlaube Druck in höchster Qualität" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Drucken erlauben" @@ -133,17 +120,22 @@ msgstr "Alternatives Deinterleave" msgid "Alternate Interleave" msgstr "Alternatives Interleave" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternative Übertragung" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Immer fragen" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Immer nachfragen" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Eine zusätzliche Komponente wird benötigt, um diese PDF-Datei zu importieren. Möchten Sie diese jetzt herunterladen?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Beim Versuch das Update zu installieren ist ein Fehler aufgetreten." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Beim Ausführen von OCR ist ein Fehler aufgetreten." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Bei der Anmeldung ist ein Fehler aufgetreten." msgid "An error occurred when trying to auto save." msgstr "Beim automatischen Speichern der Datei ist ein Fehler aufgetreten." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Beim Versuch das Update zu installieren ist ein Fehler aufgetreten." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Beim Speichern der Datei ist ein Fehler aufgetreten." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Verarbeitung läuft noch. Wollen Sie wirklich beenden und die Verarbeitung abbrechen?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Ein unbekannter Fehler ist während der Stapelverarbeitung aufgetreten." +msgid "An unknown error occurred during the batch scan." +msgstr "Beim Stapel-Scan ist ein unbekannter Fehler aufgetreten." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -188,9 +184,17 @@ msgstr "Ein Update für OCR ist verfügbar." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple Treiber" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Anwendung" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Passe Helligkeit und Kontrast nach dem Scannen an" @@ -222,19 +226,27 @@ msgstr "{0} Element(e) wirklich löschen?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Möchten Sie wirklich {0} Profile löschen?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Sind Sie sicher, dass Sie {0} nicht mehr teilen möchten?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Sollen die Änderungen an {0} Bildern rückgängig gemacht werden?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Zuweisen" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Name des Anhangs:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autor:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Anmelden" @@ -242,43 +254,41 @@ msgstr "Anmelden" msgid "Auto" msgstr "Automatisch" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Einstellungen zum automatischen Speichern" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "Automatisches Hochzählen (1 Stelle)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Automatisches Hochzählen (2 Stellen)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Automatisches Hochzählen (3 Stellen)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Automatisches Hochzählen (4 Stellen)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "OCR nach dem Scannen automatisch starten" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Stapel-Scan" @@ -296,9 +306,8 @@ msgstr "Die Stapelverarbeitung wurde auf Grund eines Fehlers abgebrochen." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Am besten" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Farbtiefe:" @@ -309,10 +318,10 @@ msgstr "Bitmap Datei (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Schwarz-Weiß" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Leere Seiten" @@ -320,40 +329,26 @@ msgstr "Leere Seiten" msgid "Brightness / Contrast" msgstr "Helligkeit / Kontrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Helligkeit:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Sie können Ihren Scanner nicht finden? Lesen Sie über die Einschränkungen des NAPS2 Flatpaks." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Abbrechen" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" -msgstr " Stapelverarbeitung abbrechen" +msgstr "Stapelverarbeitung abbrechen" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." @@ -363,7 +358,7 @@ msgstr "Wird abgebrochen...." msgid "Center" msgstr "Zentriert" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Ändern" @@ -375,7 +370,7 @@ msgstr "Auf Updates prüfen" msgid "Checking..." msgstr "Prüfe..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "E-Mail-Anbieter auswählen" @@ -383,7 +378,6 @@ msgstr "E-Mail-Anbieter auswählen" msgid "Choose Profile" msgstr "Profil wählen" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Gerät wählen" @@ -395,9 +389,9 @@ msgstr "Alles Löschen" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Alles löschen" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Entferne Bilder nach dem Speichern" @@ -405,16 +399,30 @@ msgstr "Entferne Bilder nach dem Speichern" msgid "Close" msgstr "Schließen" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Kombinieren" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Die Kommunikation mit dem Scan-Gerät wurde unterbrochen." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Kompatibilität" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Komprimierung:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Verbinden" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Verbindungsfehler." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrast:" @@ -433,9 +441,9 @@ msgstr "Kopieren..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} NAPS2 Mitwirkende" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Deckungsgrad" @@ -443,7 +451,7 @@ msgstr "Deckungsgrad" msgid "Crop" msgstr "Zuschneiden" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Auf Seitengröße zuschneiden" @@ -451,10 +459,14 @@ msgstr "Auf Seitengröße zuschneiden" msgid "Custom ({0}x{1} {2})" msgstr "Benutzerdefiniert ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Benutzerdefinierte Seitengröße" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Angepasste Auflösung" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Benutzerdefinierte Rotation" @@ -464,30 +476,34 @@ msgid "Custom SMTP" msgstr "Benutzerdefinierter SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Benutzerdefiniert..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Dunkel" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Tag (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Standard" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Standarddateipfad:" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" -msgstr "Zeilensprünge entfernen" +msgstr "Auseinander-Fächern" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Schräglagenkorrektur" msgid "Deskew Progress" msgstr "Fortschritt Schräglagenkorrektur" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Schräglagenkorrektur gescannter Seiten" @@ -509,38 +525,34 @@ msgstr "Schräglagenkorrektur gescannter Seiten" msgid "Deskewing..." msgstr "Ausrichten..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Gerät:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Abmessungen" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Angezeigter Name:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Dokumentkorrektur" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Spenden" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Fertig" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" -msgstr "Herunterladen" +msgstr "Download" #: MiscResources.resx$DownloadError$Message msgid "Download Error" @@ -550,64 +562,102 @@ msgstr "Fehler beim Herunterladen" msgid "Download Needed" msgstr "Download erforderlich" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Download-Fortschritt" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "DPI" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" -msgstr "" +msgstr "Duplex" + +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL Treiber" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL Netzwerktreiber" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB-Treiber" #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Bearbeiten" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Bearbeiten mit {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Bearbeiten mit..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "E-Mail alle" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "E-Mail alle als PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "" +msgstr "E-Mail als PDF" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" -msgstr "Email PDF Fortschritt" +msgstr "E-Mail PDF Fortschritt" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Auswahl per E-Mail versenden" #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Auswahl als PDF per E-Mail versenden" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" -msgstr "Einstellungen für EMail" +msgstr "E-Mail Einstellungen" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Erlaube automatisches Speichern" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Fehlerprotokollierung aktivieren" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "PDF verschlüsseln" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Verschlüsselung" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "" +msgstr "Enhanced Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Fehler" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Fehler beim Starten der Anwendung {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,38 +665,45 @@ msgstr "Geschätzte Download-Größe: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Leerseiten unterdrücken" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Schnell" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Einzug" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "Dateiname" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Dateiname:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Dateipfad:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Weißabgleich korrigieren und Rauschen entfernen" + #: UiStrings.resx$Flip$Message msgid "Flip" -msgstr "Spiegeln" +msgstr "um 180° drehen" + +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Rückseite von Duplex-Seiten um 180° drehen" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Doppelseiten umdrehen" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Für eine hohe JPEG-Qualität (80+) ist ebenfalls die Bildqualität in Ihrem Profil zu erhöhen, um beste Ergebnisse zu erhalten." @@ -654,7 +711,6 @@ msgstr "Für eine hohe JPEG-Qualität (80+) ist ebenfalls die Bildqualität in I msgid "GIF File (*.gif)" msgstr "GIF Datei (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Weitere Sprachen installieren" @@ -671,12 +727,11 @@ msgstr "GMail" msgid "Grayscale" msgstr "Graustufen" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Horizontale Ausrichtung:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Stunde (0-23)" @@ -684,6 +739,10 @@ msgstr "Stunde (0-23)" msgid "Hue / Saturation" msgstr "Farbton / Sättigung" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Host" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Icons von:" @@ -696,12 +755,12 @@ msgstr "Bild" msgid "Image Files" msgstr "Bilddateien" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Bildqualität" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Bildeinstellungen" @@ -745,9 +804,13 @@ msgstr "Installation abgeschlossen. Soll NAPS2 jetzt neugestartet werden?" msgid "Installation failed." msgstr "Installation fehlgeschlagen." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Benutzeroberfläche" + #: UiStrings.resx$Interleave$Message msgid "Interleave" -msgstr "" +msgstr "Ineinander-Fächern" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" @@ -755,59 +818,87 @@ msgstr "JPEG Datei (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000 Datei (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "JPEG-Qualität" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Bilder über Sitzungen hinweg behalten" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Tastaturbefehle" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Schlagwörter:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Sprache" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Eine Bewertung hinterlassen" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Links" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" -msgstr "Altsystem (nur ursprüngliche Oberfläche)" +msgstr "Altsystem (nur native Oberfläche)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Hell" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Gefällt Ihnen NAPS2?" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Bilder in NAPS2 laden" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "PDF mittels OCR durchsuchbar machen" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Manuelle IP" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Maximale Qualität (große Dateien)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Gepufferte Übertragung" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metadaten" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" -msgstr "" +msgstr "Minute (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Monat (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Mehr Info" @@ -819,11 +910,19 @@ msgstr "Nach unten" msgid "Move Up" msgstr "Nach oben" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Mehrere Sprachen" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Mehrere Sprachen..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Mehrere Scans (Konstante Verzögerung zwischen Scans)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Mehrere Scans (Zwischen den Scans nachfragen)" @@ -831,24 +930,28 @@ msgstr "Mehrere Scans (Zwischen den Scans nachfragen)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 ist vollständig gratis. Unterstützen Sie uns mit einer Spende." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" -msgstr "" +msgstr "Name (optional)" #: MiscResources.resx$NameMissing$Message msgid "Name missing." msgstr "Name fehlt." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Native Übertragung" + #: UiStrings.resx$New$Message msgid "New" msgstr "Neu" @@ -861,7 +964,7 @@ msgstr "Neues Profil" msgid "Next" msgstr "Weiter" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Nächster Scan" @@ -870,12 +973,15 @@ msgstr "Nächster Scan" msgid "No device selected." msgstr "Keine Geräte ausgewählt." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Keine Geräte gefunden." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "In der Zuführung sind keine Seiten." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Kein Anbieter ausgewählt." @@ -895,13 +1001,13 @@ msgstr "Keine" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Jetzt nicht" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Anzahl der Scans:" @@ -909,7 +1015,6 @@ msgstr "Anzahl der Scans:" msgid "OCR" msgstr "Optische Texterkennung" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR-Sprachpaket herunterladen" @@ -918,37 +1023,23 @@ msgstr "OCR-Sprachpaket herunterladen" msgid "OCR Progress" msgstr "OCR-Fortschritt" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Texterkennung einrichten" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Texterkennungs-Sprache:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "OCR-Modus:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Versatzbreite basierend auf Ausrichtung (WIA)" @@ -956,13 +1047,11 @@ msgstr "Versatzbreite basierend auf Ausrichtung (WIA)" msgid "Old DSM" msgstr "Alte Datenquellenverwaltung" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Eine Datei je Seite" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Eine Datei je Scan" @@ -970,7 +1059,11 @@ msgstr "Eine Datei je Scan" msgid "One or more files could not be downloaded." msgstr "Eine oder mehrere Dateien konnten nicht heruntergeladen werden." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Nur eine einzige NAPS2-Instanz erlauben" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Ordner öffnen" @@ -978,11 +1071,15 @@ msgstr "Ordner öffnen" msgid "Operation in Progress" msgstr "Verarbeitung läuft" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (neu)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook im Web" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Ausgabe" @@ -990,7 +1087,7 @@ msgstr "Ausgabe" msgid "Overwrite File" msgstr "Datei überschreiben" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Besitzer-Passwort:" @@ -998,8 +1095,8 @@ msgstr "Besitzer-Passwort:" msgid "PDF Document (*.pdf)" msgstr "PDF-Dokument (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF-Einstellungen" @@ -1009,35 +1106,33 @@ msgstr "PDF gespeichert." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "PNG Datei (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Seitengröße:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Papiereinzug:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Passwort" @@ -1045,21 +1140,24 @@ msgstr "Passwort" msgid "Paste" msgstr "Einfügen" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Platzhalter" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Nachbearbeitung" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "OCR nach dem Scannen automatisch starten" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Start drücken wenn bereit." @@ -1067,7 +1165,7 @@ msgstr "Start drücken wenn bereit." msgid "Preview" msgstr "Vorschau" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Vorschau:" @@ -1080,12 +1178,11 @@ msgstr "Vorheriger" msgid "Print" msgstr "Drucken" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Profileinstellungen" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profil:" @@ -1094,23 +1191,27 @@ msgstr "Profil:" msgid "Profiles" msgstr "Profile" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Nachfrage, wenn ausgewählt" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Dateipfad bestätigen" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Anbieter" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Bereit für Scan {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Wiederherstellen" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Eingescannte Bilder wiederherstellen" @@ -1122,9 +1223,15 @@ msgstr "Wiederherstellen..." msgid "Recovery Progress" msgstr "Wiederherstellungsfortschritt" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Wiederholen" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Wiederholen {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Diese Einstellungen merken" @@ -1140,15 +1247,11 @@ msgstr "Zurücksetzen" msgid "Reset Image" msgstr "Bild zurücksetzen" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Auflösung:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Standardeinstellungen wiederherstellen" @@ -1156,6 +1259,14 @@ msgstr "Standardeinstellungen wiederherstellen" msgid "Reverse" msgstr "Umkehren" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Alle umkehren" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Auswahl umkehren" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Rückgängig" @@ -1176,7 +1287,6 @@ msgstr "Links drehen" msgid "Rotate Right" msgstr "Rechts drehen" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Ausführung im Hintergrund" @@ -1185,22 +1295,26 @@ msgstr "Ausführung im Hintergrund" msgid "Running OCR..." msgstr "Führe OCR aus..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "SANE-Treiber" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Speichern" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Alles speichern" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Alles als Bilder speichern" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Alles als PDF speichern" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "PDF speichern" msgid "Save PDF Progress" msgstr "Fortschritt des PDF-Speicherns" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Auswahl speichern" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Auswahl als Bilder speichern" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Auswahl als PDF speichern" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "In eine einzige Datei speichern" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "In mehrere Dateien speichern" @@ -1244,29 +1363,45 @@ msgstr "Speichere das Ergebnis der Stapelverarbeitung..." msgid "Saving {0}..." msgstr "{0} gespeichert..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Skaliere mit Fenster" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Skalieren:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Scannen" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Scan-Einstellungen" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Mit Standardprofil scannen" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Mit neuem Profil scannen" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Mit Profil {0} scannen" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Eingelesenes Bild" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Scanner teilen" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Seite {0} wird eingescannt" @@ -1280,11 +1415,14 @@ msgstr "Seite {0} wird eingescannt (Scan {1})..." msgid "Scanning page {0}..." msgstr "Seite {0} wird eingescannt..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Suche nach Geräten..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekunde (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Auswählen" @@ -1293,7 +1431,10 @@ msgstr "Auswählen" msgid "Select All" msgstr "Alle auswählen" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Gerät auswählen" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Quelle auswählen" @@ -1302,7 +1443,6 @@ msgstr "Quelle auswählen" msgid "Select a profile before clicking Scan." msgstr "Wählen Sie ein Profil vor dem Scannen." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Wählen Sie eine oder mehrere Sprachen aus:" @@ -1311,8 +1451,7 @@ msgstr "Wählen Sie eine oder mehrere Sprachen aus:" msgid "Selected ({0})" msgstr "Ausgewählt ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Dateien per Patch-T trennen" @@ -1320,37 +1459,84 @@ msgstr "Dateien per Patch-T trennen" msgid "Set Default" msgstr "Auf Standard setzen" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Einstellungen" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Teilen" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Auch teilen, wenn NAPS2 geschlossen ist" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Geteilte Scanner Einstellungen" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Geteilte Scanner können von anderen Computern im lokalen Netzwerk verwendet werden, indem Sie auf dem anderen Computer in den NAPS2 Profileinstellungen \"ESCL Treiber\" auswählen." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Schärfen" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Tastenkürzel" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Anzeigen" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Zeige \"Profile\" Symbolleiste" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Nativen TWAIN-Fortschritt anzeigen" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Zeige Seitenzahlen" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Seitenleiste" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Dateien mit einer Seite" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Einzelner Scan" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Überspringe Speicherdialog" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Teilen" + +#: UiStrings.resx$Start$Message msgid "Start" -msgstr "" +msgstr "Start" + +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Scanner nicht mehr teilen" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Auf Seitengröße dehnen" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Betreff:" @@ -1358,12 +1544,11 @@ msgstr "Betreff:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF Datei (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN Treiber" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Technische Details" @@ -1372,6 +1557,10 @@ msgstr "Technische Details" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "Das OCR-Modul ist nicht verfügbar. Stelle sicher, dass das benötigte Paket installiert ist:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Zeitüberschreitung bei der Texterkennung." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Die Datei konnte nicht überschrieben werden, da sie im Moment verwendet msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Die Datei {0} existiert bereits. Möchten Sie diese überschreiben?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Die folgend Datei ist verschlüsselt und benötigt ein Passwort zum Öffnen: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Die folgende Datei ist verschlüsselt und benötigt ein Passwort zum Öffnen:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,29 +1632,65 @@ msgstr "Der gewählte Scanner ist in Benutzung." msgid "The selected scanner is offline." msgstr "Der gewählte Scanner ist ausgeschaltet (offline)." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Der Arbeitsprozess ist abgestürzt." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Der Arbeitsprozess ist abgestürzt. Überprüfen Sie die Windows-Ereignisanzeige." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Erscheinungsbild:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Tiff Einstellungen" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Zeit zwischen den Scans (Sekunden):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Titel:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Werkzeuge" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Twain Implementierung:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 Zoll)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" -msgstr "" +msgstr "US Letter (8.5x11 Zoll)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Zuweisung aufheben" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Rückgängig" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Rückgängig {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Unbekannter Scanner" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" @@ -1477,7 +1702,7 @@ msgstr "Update-Fortschritt" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Updateprüfung ist deaktiviert." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Aktualisiere..." msgid "Uploading email..." msgstr "Hochladen der E-Mail..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" -msgstr "Ursprüngliche Oberfäche verwenden" +msgstr "Scanner-Dialog verwenden" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Vordefinierte Einstellungen verwenden" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Benutzerpasswort:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Um die Texterkennung verwenden zu können, müssen die benötigten Sprachen einzeln heruntergeladen werden." @@ -1509,22 +1731,21 @@ msgstr "Um die Texterkennung verwenden zu können, müssen die benötigten Sprac #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message msgid "Version {0}" -msgstr "" +msgstr "Version {0}" #: UiStrings.resx$View$Message msgid "View" msgstr "Betrachten" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA Treiber" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Warte auf TWAIN zur Fertigstellung..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Warten auf Anmeldung..." @@ -1532,19 +1753,19 @@ msgstr "Warten auf Anmeldung..." msgid "Waiting for scan {0}..." msgstr "Warte auf Scan {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Weiß Schwellwert" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Wia Version:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Jahr" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Jahr (00-99)" @@ -1561,36 +1782,33 @@ msgstr "Sie haben nicht die erforderlichen Zugriffsrechte, um an diesem Speicher msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Es gibt ungespeicherte Änderungen. Möchten Sie das Programm beenden und diese Änderungen verwerfen?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" -msgstr "" +msgstr "Zoom" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Zoom Originalgröße" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Vergrößern" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Verkleinern" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "in" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,37 +1816,42 @@ msgstr "von {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} Dateien" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} Geräte gefunden." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} Bilder wurden am {1} um {2} gescannt und wurden möglicherweise nicht gespeichert, sind aber wiederherstellbar. Sollen sie wiederhergestellt werden?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." -msgstr "{0} bilder gespeichert." +msgstr "{0} Bilder gespeichert." #: MiscResources.resx$PdfStatus$Message #: UiStrings.resx$XOfY$Message msgid "{0} of {1}" -msgstr "" +msgstr "{0} von {1}" diff --git a/NAPS2.Lib/Lang/po/dv.po b/NAPS2.Lib/Lang/po/dv.po new file mode 100644 index 0000000000..3eae77a9ba --- /dev/null +++ b/NAPS2.Lib/Lang/po/dv.po @@ -0,0 +1,1857 @@ +msgid "" +msgstr "" +"Project-Id-Version: naps2\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-04-19 21:51+0000\n" +"PO-Revision-Date: 2025-08-30 22:28\n" +"Last-Translator: \n" +"Language-Team: Dhivehi\n" +"Language: dv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Translate Toolkit 1.13.0\n" +"X-Poedit-SourceCharset: iso-8859-1\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: naps2\n" +"X-Crowdin-Project-ID: 531762\n" +"X-Crowdin-Language: dv\n" +"X-Crowdin-File: templates.pot\n" +"X-Crowdin-File-ID: 75\n" + +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "" + +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "\"ސްކޭން\" ފިތުގެ ޑިފޯލްޓް އަމަލު:" + +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "" + +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "" + +#: SettingsResources.resx$BitDepth_24Color$Message +msgid "24-bit Color" +msgstr "" + +#: SettingsResources.resx$PageSize_A3$Message +msgid "A3 (297x420 mm)" +msgstr "އޭ 3 (297x420 މމ)" + +#: SettingsResources.resx$PageSize_A4$Message +msgid "A4 (210x297 mm)" +msgstr "" + +#: SettingsResources.resx$PageSize_A5$Message +msgid "A5 (148x210 mm)" +msgstr "އޭ5 (148x210 މމ)" + +#: UiStrings.resx$About$Message +#: UiStrings.resx$AboutFormTitle$Message +msgid "About" +msgstr "ތަޢާރަފު" + +#: MiscResources.resx$AcquiringData$Message +msgid "Acquiring data..." +msgstr "" + +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + +#: UiStrings.resx$Advanced$Message +msgid "Advanced" +msgstr "" + +#: UiStrings.resx$AdvancedProfileFormTitle$Message +msgid "Advanced Profile Settings" +msgstr "" + +#: MiscResources.resx$AllCount$Message +msgid "All ({0})" +msgstr "" + +#: MiscResources.resx$FileTypeAllFiles$Message +msgid "All Files" +msgstr "" + +#: UiStrings.resx$AllowAnnotations$Message +msgid "Allow Annotations" +msgstr "" + +#: UiStrings.resx$AllowContentCopying$Message +msgid "Allow Content Copying" +msgstr "" + +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message +msgid "Allow Content Copying for Accessibility" +msgstr "" + +#: UiStrings.resx$AllowDocumentAssembly$Message +msgid "Allow Document Assembly" +msgstr "" + +#: UiStrings.resx$AllowDocumentModification$Message +msgid "Allow Document Modification" +msgstr "" + +#: UiStrings.resx$AllowFormFilling$Message +msgid "Allow Form Filling" +msgstr "" + +#: UiStrings.resx$AllowFullQualityPrinting$Message +msgid "Allow Full Quality Printing" +msgstr "" + +#: UiStrings.resx$AllowPrinting$Message +msgid "Allow Printing" +msgstr "" + +#: UiStrings.resx$AltDeinterleave$Message +msgid "Alternate Deinterleave" +msgstr "" + +#: UiStrings.resx$AltInterleave$Message +msgid "Alternate Interleave" +msgstr "" + +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "" + +#: MiscResources.resx$PdfImportComponentNeeded$Message +msgid "An additional component is needed to import this PDF file. Would you like to download it now?" +msgstr "" + +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "" + +#: MiscResources.resx$AuthError$Message +msgid "An error occurred when trying to authorize." +msgstr "" + +#: MiscResources.resx$AutoSaveError$Message +msgid "An error occurred when trying to auto save." +msgstr "" + +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "" + +#: MiscResources.resx$ErrorSaving$Message +msgid "An error occurred when trying to save the file." +msgstr "" + +#: MiscResources.resx$ErrorEmailing$Message +msgid "An error occurred when trying to send the email." +msgstr "" + +#: MiscResources.resx$EmailError$Message +msgid "An error occurred while trying to send an email." +msgstr "" + +#: MiscResources.resx$UnknownDriverError$Message +#: SdkResources.resx$UnknownDriverError$Message +msgid "An error occurred with the scanning driver." +msgstr "" + +#: MiscResources.resx$ExitWithActiveOperations$Message +msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" +msgstr "" + +#: MiscResources.resx$BatchError$Message +msgid "An unknown error occurred during the batch scan." +msgstr "" + +#: MiscResources.resx$UpdateAvailable$Message +msgid "An update is available." +msgstr "" + +#: MiscResources.resx$OcrUpdateAvailable$Message +msgid "An update to OCR is available." +msgstr "" + +#: UiStrings.resx$AppleDriver$Message +msgid "Apple Driver" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "އެޕްލިކޭޝަން" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message +msgid "Apply brightness/contrast after scan" +msgstr "" + +#: UiStrings.resx$ApplyToSelected$Message +msgid "Apply to all {0} selected images" +msgstr "" + +#: MiscResources.resx$ConfirmCancelBatch$Message +msgid "Are you sure you want to cancel the batch scan?" +msgstr "" + +#: MiscResources.resx$ConfirmClearItems$Message +msgid "Are you sure you want to clear {0} item(s)?" +msgstr "" + +#: MiscResources.resx$ConfirmDelete$Message +msgid "Are you sure you want to delete \"{0}\"?" +msgstr "" + +#: MiscResources.resx$ConfirmDeleteSingleProfile$Message +msgid "Are you sure you want to delete the profile {0}?" +msgstr "" + +#: MiscResources.resx$ConfirmDeleteItems$Message +msgid "Are you sure you want to delete {0} item(s)?" +msgstr "" + +#: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message +msgid "Are you sure you want to delete {0} profiles?" +msgstr "" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + +#: MiscResources.resx$ConfirmResetImages$Message +msgid "Are you sure you want undo your changes to {0} image(s)?" +msgstr "" + +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message +msgid "Attachment Name:" +msgstr "" + +#: UiStrings.resx$AuthorLabel$Message +msgid "Author:" +msgstr "" + +#: UiStrings.resx$AuthorizeFormTitle$Message +msgid "Authorize" +msgstr "" + +#: SettingsResources.resx$TiffComp_Auto$Message +msgid "Auto" +msgstr "" + +#: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message +msgid "Auto Save Settings" +msgstr "" + +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "" + +#: UiStrings.resx$AutoIncrementing2Digit$Message +msgid "Auto-incrementing number (2 digits)" +msgstr "" + +#: UiStrings.resx$AutoIncrementing3Digit$Message +msgid "Auto-incrementing number (3 digits)" +msgstr "" + +#: UiStrings.resx$AutoIncrementing4Digit$Message +msgid "Auto-incrementing number (4 digits)" +msgstr "" + +#: UiStrings.resx$RunOcrAfterScanning$Message +msgid "Automatically run OCR after scanning" +msgstr "" + +#: SettingsResources.resx$PageSize_B4$Message +msgid "B4 (250x353 mm)" +msgstr "" + +#: SettingsResources.resx$PageSize_B5$Message +msgid "B5 (176x250 mm)" +msgstr "" + +#: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message +msgid "Batch Scan" +msgstr "" + +#: MiscResources.resx$BatchStatusCancelled$Message +msgid "Batch cancelled." +msgstr "" + +#: MiscResources.resx$BatchStatusComplete$Message +msgid "Batch completed successfully." +msgstr "" + +#: MiscResources.resx$BatchStatusError$Message +msgid "Batch scan stopped due to error." +msgstr "" + +#: SettingsResources.resx$OcrMode_Best$Message +msgid "Best" +msgstr "" + +#: UiStrings.resx$BitDepthLabel$Message +msgid "Bit depth:" +msgstr "" + +#: MiscResources.resx$FileTypeBmp$Message +msgid "Bitmap Files (*.bmp)" +msgstr "" + +#: SettingsResources.resx$BitDepth_1BlackAndWhite$Message +#: UiStrings.resx$BlackAndWhite$Message +msgid "Black and White" +msgstr "" + +#: UiStrings.resx$BlankPages$Message +msgid "Blank Pages" +msgstr "" + +#: UiStrings.resx$BrightnessContrast$Message +msgid "Brightness / Contrast" +msgstr "" + +#: UiStrings.resx$BrightnessLabel$Message +msgid "Brightness:" +msgstr "" + +#: SettingsResources.resx$TiffComp_Ccitt4$Message +msgid "CCITT4" +msgstr "" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + +#: MiscResources.resx$Cancel$Message +#: UiStrings.resx$Cancel$Message +msgid "Cancel" +msgstr "" + +#: MiscResources.resx$CancelBatch$Message +msgid "Cancel Batch" +msgstr "" + +#: MiscResources.resx$BatchStatusCancelling$Message +msgid "Cancelling...." +msgstr "" + +#: SettingsResources.resx$HorizontalAlign_Center$Message +msgid "Center" +msgstr "" + +#: UiStrings.resx$Change$Message +msgid "Change" +msgstr "" + +#: UiStrings.resx$CheckForUpdates$Message +msgid "Check for updates" +msgstr "" + +#: MiscResources.resx$CheckingForUpdates$Message +msgid "Checking..." +msgstr "" + +#: UiStrings.resx$EmailProviderFormTitle$Message +msgid "Choose Email Provider" +msgstr "" + +#: MiscResources.resx$ChooseProfile$Message +msgid "Choose Profile" +msgstr "" + +#: UiStrings.resx$ChooseDevice$Message +msgid "Choose device" +msgstr "" + +#: MiscResources.resx$Clear$Message +#: UiStrings.resx$Clear$Message +msgid "Clear" +msgstr "" + +#: UiStrings.resx$ClearAll$Message +msgid "Clear All" +msgstr "" + +#: UiStrings.resx$ClearAfterSaving$Message +msgid "Clear images after saving" +msgstr "" + +#: MiscResources.resx$Close$Message +msgid "Close" +msgstr "" + +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message +msgid "Compatibility" +msgstr "" + +#: UiStrings.resx$CompressionLabel$Message +msgid "Compression:" +msgstr "" + +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "ގުޅުމުގެ އެރަރެއް." + +#: UiStrings.resx$ContrastLabel$Message +msgid "Contrast:" +msgstr "" + +#: UiStrings.resx$Copy$Message +msgid "Copy" +msgstr "" + +#: MiscResources.resx$CopyProgress$Message +msgid "Copy Progress" +msgstr "" + +#: MiscResources.resx$Copying$Message +msgid "Copying..." +msgstr "" + +#: UiStrings.resx$CopyrightFormat$Message +msgid "Copyright {0} NAPS2 Contributors" +msgstr "" + +#: UiStrings.resx$CoverageThreshold$Message +msgid "Coverage Threshold" +msgstr "" + +#: UiStrings.resx$Crop$Message +msgid "Crop" +msgstr "" + +#: UiStrings.resx$CropToPageSize$Message +msgid "Crop to page size" +msgstr "" + +#: MiscResources.resx$CustomPageSizeFormat$Message +msgid "Custom ({0}x{1} {2})" +msgstr "" + +#: UiStrings.resx$PageSizeFormTitle$Message +msgid "Custom Page Size" +msgstr "" + +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + +#: UiStrings.resx$CustomRotation$Message +msgid "Custom Rotation" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_CustomSmtp$Message +msgid "Custom SMTP" +msgstr "" + +#: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message +msgid "Custom..." +msgstr "" + +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message +msgid "Day (01-31)" +msgstr "" + +#: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message +#: SettingsResources.resx$TwainImpl_Default$Message +#: SettingsResources.resx$WiaVersion_Default$Message +#: UiStrings.resx$Default$Message +msgid "Default" +msgstr "" + +#: UiStrings.resx$DefaultFilePathLabel$Message +msgid "Default File Path:" +msgstr "" + +#: UiStrings.resx$Deinterleave$Message +msgid "Deinterleave" +msgstr "" + +#: MiscResources.resx$Delete$Message +#: UiStrings.resx$Delete$Message +msgid "Delete" +msgstr "" + +#: UiStrings.resx$Deskew$Message +msgid "Deskew" +msgstr "" + +#: MiscResources.resx$AutoDeskewProgress$Message +msgid "Deskew Progress" +msgstr "" + +#: UiStrings.resx$DeskewScannedPages$Message +msgid "Deskew scanned pages" +msgstr "" + +#: MiscResources.resx$AutoDeskewing$Message +msgid "Deskewing..." +msgstr "" + +#: UiStrings.resx$DeviceLabel$Message +msgid "Device:" +msgstr "" + +#: UiStrings.resx$Dimensions$Message +msgid "Dimensions" +msgstr "" + +#: UiStrings.resx$DisplayNameLabel$Message +msgid "Display name:" +msgstr "" + +#: UiStrings.resx$DocumentCorrection$Message +msgid "Document Correction" +msgstr "" + +#: MiscResources.resx$Donate$Message +#: UiStrings.resx$Donate$Message +msgid "Donate" +msgstr "" + +#: UiStrings.resx$Done$Message +msgid "Done" +msgstr "" + +#: UiStrings.resx$Download$Message +msgid "Download" +msgstr "" + +#: MiscResources.resx$DownloadError$Message +msgid "Download Error" +msgstr "" + +#: MiscResources.resx$DownloadNeeded$Message +msgid "Download Needed" +msgstr "" + +#: UiStrings.resx$DownloadProgressFormTitle$Message +msgid "Download Progress" +msgstr "" + +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + +#: SettingsResources.resx$Source_Duplex$Message +msgid "Duplex" +msgstr "" + +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + +#: UiStrings.resx$Edit$Message +msgid "Edit" +msgstr "" + +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + +#: UiStrings.resx$EmailAllAsPdf$Message +msgid "Email All as PDF" +msgstr "" + +#: MiscResources.resx$EmailPdf$Message +#: UiStrings.resx$EmailPdf$Message +msgid "Email PDF" +msgstr "" + +#: MiscResources.resx$EmailPdfProgress$Message +msgid "Email PDF Progress" +msgstr "" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + +#: UiStrings.resx$EmailSelectedAsPdf$Message +msgid "Email Selected as PDF" +msgstr "" + +#: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message +msgid "Email Settings" +msgstr "" + +#: UiStrings.resx$EnableAutoSave$Message +msgid "Enable Auto Save" +msgstr "" + +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message +msgid "Encrypt PDF" +msgstr "" + +#: UiStrings.resx$Encryption$Message +msgid "Encryption" +msgstr "" + +#: MiscResources.resx$FileTypeEmf$Message +msgid "Enhanced Windows MetaFile (*.emf)" +msgstr "" + +#: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message +msgid "Error" +msgstr "" + +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + +#: MiscResources.resx$EstimatedDownloadSize$Message +#: UiStrings.resx$EstimatedDownloadSize$Message +msgid "Estimated download size: {0} MB" +msgstr "" + +#: MiscResources.resx$FileTypeExif$Message +msgid "Exchangeable Image File (*.exif)" +msgstr "" + +#: UiStrings.resx$ExcludeBlankPages$Message +msgid "Exclude blank pages" +msgstr "" + +#: SettingsResources.resx$OcrMode_Fast$Message +msgid "Fast" +msgstr "" + +#: SettingsResources.resx$Source_Feeder$Message +msgid "Feeder" +msgstr "" + +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "" + +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" +msgstr "" + +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "" + +#: UiStrings.resx$Flip$Message +msgid "Flip" +msgstr "" + +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message +msgid "Flip duplexed pages" +msgstr "" + +#: UiStrings.resx$JpegQualityHelp$Message +msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." +msgstr "" + +#: MiscResources.resx$FileTypeGif$Message +msgid "GIF File (*.gif)" +msgstr "" + +#: UiStrings.resx$GetMoreLanguages$Message +msgid "Get more languages" +msgstr "" + +#: SettingsResources.resx$Source_Glass$Message +msgid "Glass" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Gmail$Message +msgid "Gmail" +msgstr "" + +#: SettingsResources.resx$BitDepth_8Grayscale$Message +msgid "Grayscale" +msgstr "" + +#: UiStrings.resx$HorizontalAlignLabel$Message +msgid "Horizontal align:" +msgstr "" + +#: UiStrings.resx$Hour2Digit$Message +msgid "Hour (0-23)" +msgstr "" + +#: UiStrings.resx$HueSaturation$Message +msgid "Hue / Saturation" +msgstr "" + +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + +#: UiStrings.resx$IconsFrom$Message +msgid "Icons from:" +msgstr "" + +#: UiStrings.resx$Image$Message +msgid "Image" +msgstr "" + +#: MiscResources.resx$FileTypeImageFiles$Message +msgid "Image Files" +msgstr "" + +#: UiStrings.resx$ImageQuality$Message +msgid "Image Quality" +msgstr "" + +#: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message +msgid "Image Settings" +msgstr "" + +#: MiscResources.resx$ImageSaved$Message +msgid "Image saved." +msgstr "" + +#: UiStrings.resx$Import$Message +msgid "Import" +msgstr "" + +#: MiscResources.resx$ImportProgress$Message +msgid "Import Progress" +msgstr "" + +#: MiscResources.resx$ImportingFormat$Message +msgid "Importing {0}..." +msgstr "" + +#: MiscResources.resx$Importing$Message +msgid "Importing..." +msgstr "" + +#: MiscResources.resx$Install$Message +msgid "Install {0}" +msgstr "" + +#: MiscResources.resx$InstallComplete$Message +msgid "Installation Complete" +msgstr "" + +#: MiscResources.resx$InstallFailedTitle$Message +msgid "Installation Failed" +msgstr "" + +#: MiscResources.resx$InstallCompletePromptRestart$Message +msgid "Installation complete. Do you want to restart NAPS2 now?" +msgstr "" + +#: MiscResources.resx$InstallFailed$Message +msgid "Installation failed." +msgstr "" + +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + +#: UiStrings.resx$Interleave$Message +msgid "Interleave" +msgstr "" + +#: MiscResources.resx$FileTypeJpeg$Message +msgid "JPEG File (*.jpg, *.jpeg)" +msgstr "" + +#: MiscResources.resx$FileTypeJp2$Message +msgid "JPEG2000 File (*.jp2, *.jpx)" +msgstr "" + +#: UiStrings.resx$JpegQuality$Message +msgid "Jpeg Quality" +msgstr "" + +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message +msgid "Keywords:" +msgstr "" + +#: SettingsResources.resx$TiffComp_Lzw$Message +msgid "LZW" +msgstr "" + +#: UiStrings.resx$Language$Message +msgid "Language" +msgstr "" + +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + +#: SettingsResources.resx$HorizontalAlign_Left$Message +msgid "Left" +msgstr "" + +#: SettingsResources.resx$TwainImpl_Legacy$Message +msgid "Legacy (native UI only)" +msgstr "" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message +msgid "Load images into NAPS2" +msgstr "" + +#: UiStrings.resx$MakePdfsSearchable$Message +msgid "Make PDFs searchable using OCR" +msgstr "" + +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message +msgid "Maximum quality (large files)" +msgstr "" + +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message +msgid "Metadata" +msgstr "" + +#: UiStrings.resx$Minute2Digit$Message +msgid "Minute (00-59)" +msgstr "" + +#: UiStrings.resx$Month2Digit$Message +msgid "Month (01-12)" +msgstr "" + +#: UiStrings.resx$MoreInfo$Message +msgid "More info" +msgstr "" + +#: UiStrings.resx$MoveDown$Message +msgid "Move Down" +msgstr "" + +#: UiStrings.resx$MoveUp$Message +msgid "Move Up" +msgstr "" + +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message +msgid "Multiple scans (fixed delay between scans)" +msgstr "" + +#: UiStrings.resx$MultipleScansPrompt$Message +msgid "Multiple scans (prompt between scans)" +msgstr "" + +#: MiscResources.resx$NAPS2$Message +#: SdkResources.resx$NAPS2$Message +#: UiStrings.resx$Naps2$Message +msgid "NAPS2" +msgstr "" + +#: UiStrings.resx$Naps2TitleFormat$Message +msgid "NAPS2 - {0}" +msgstr "" + +#: MiscResources.resx$DonatePrompt$Message +msgid "NAPS2 is completely free. Consider making a donation." +msgstr "" + +#: UiStrings.resx$NameOptional$Message +msgid "Name (optional)" +msgstr "" + +#: MiscResources.resx$NameMissing$Message +msgid "Name missing." +msgstr "" + +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + +#: UiStrings.resx$New$Message +msgid "New" +msgstr "" + +#: UiStrings.resx$NewProfile$Message +msgid "New Profile" +msgstr "" + +#: UiStrings.resx$Next$Message +msgid "Next" +msgstr "" + +#: UiStrings.resx$BatchPromptFormTitle$Message +msgid "Next Scan" +msgstr "" + +#: MiscResources.resx$NoDeviceSelected$Message +#: SdkResources.resx$NoDeviceSelected$Message +msgid "No device selected." +msgstr "" + +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + +#: MiscResources.resx$NoPagesInFeeder$Message +#: SdkResources.resx$NoPagesInFeeder$Message +msgid "No pages are in the feeder." +msgstr "" + +#: SettingsResources.resx$EmailProvider_NotSelected$Message +msgid "No provider selected." +msgstr "" + +#: MiscResources.resx$NoDevicesFound$Message +#: SdkResources.resx$NoDevicesFound$Message +msgid "No scanning device was found." +msgstr "" + +#: MiscResources.resx$NoUpdates$Message +msgid "No updates available." +msgstr "" + +#: SettingsResources.resx$TiffComp_None$Message +msgid "None" +msgstr "" + +#: UiStrings.resx$Naps2FullName$Message +msgid "Not Another PDF Scanner" +msgstr "" + +#: UiStrings.resx$NotNow$Message +msgid "Not Now" +msgstr "" + +#: UiStrings.resx$NumberOfScansLabel$Message +msgid "Number of scans:" +msgstr "" + +#: UiStrings.resx$Ocr$Message +msgid "OCR" +msgstr "" + +#: UiStrings.resx$OcrDownloadFormTitle$Message +msgid "OCR Download" +msgstr "" + +#: MiscResources.resx$OcrProgress$Message +msgid "OCR Progress" +msgstr "" + +#: UiStrings.resx$OcrSetupFormTitle$Message +msgid "OCR Setup" +msgstr "" + +#: UiStrings.resx$OcrLanguageLabel$Message +msgid "OCR language:" +msgstr "" + +#: UiStrings.resx$OcrModeLabel$Message +msgid "OCR mode:" +msgstr "" + +#: UiStrings.resx$OK$Message +msgid "OK" +msgstr "" + +#: UiStrings.resx$OffsetWidth$Message +msgid "Offset width based on alignment (WIA)" +msgstr "" + +#: SettingsResources.resx$TwainImpl_OldDsm$Message +msgid "Old DSM" +msgstr "" + +#: UiStrings.resx$OneFilePerPage$Message +msgid "One file per page" +msgstr "" + +#: UiStrings.resx$OneFilePerScan$Message +msgid "One file per scan" +msgstr "" + +#: MiscResources.resx$FilesCouldNotBeDownloaded$Message +msgid "One or more files could not be downloaded." +msgstr "" + +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message +msgid "Open Folder" +msgstr "" + +#: MiscResources.resx$ActiveOperations$Message +msgid "Operation in Progress" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_OutlookWeb$Message +msgid "Outlook Web Access" +msgstr "" + +#: UiStrings.resx$Output$Message +msgid "Output" +msgstr "" + +#: MiscResources.resx$OverwriteFile$Message +msgid "Overwrite File" +msgstr "" + +#: UiStrings.resx$OwnerPasswordLabel$Message +msgid "Owner Password:" +msgstr "" + +#: MiscResources.resx$FileTypePdf$Message +msgid "PDF Document (*.pdf)" +msgstr "" + +#: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message +msgid "PDF Settings" +msgstr "" + +#: MiscResources.resx$PdfSaved$Message +msgid "PDF saved." +msgstr "" + +#: SettingsResources.resx$PdfCompat_PdfA1B$Message +msgid "PDF/A-1b" +msgstr "" + +#: SettingsResources.resx$PdfCompat_PdfA2B$Message +msgid "PDF/A-2b" +msgstr "" + +#: SettingsResources.resx$PdfCompat_PdfA3B$Message +msgid "PDF/A-3b" +msgstr "" + +#: SettingsResources.resx$PdfCompat_PdfA3U$Message +msgid "PDF/A-3u" +msgstr "" + +#: MiscResources.resx$FileTypePng$Message +msgid "PNG File (*.png)" +msgstr "" + +#: UiStrings.resx$PageSizeLabel$Message +msgid "Page size:" +msgstr "" + +#: UiStrings.resx$PaperSourceLabel$Message +msgid "Paper source:" +msgstr "" + +#: UiStrings.resx$PdfPasswordFormTitle$Message +msgid "Password" +msgstr "" + +#: UiStrings.resx$Paste$Message +msgid "Paste" +msgstr "" + +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message +msgid "Placeholders" +msgstr "" + +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message +msgid "Post-processing" +msgstr "" + +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message +msgid "Press Start when ready." +msgstr "" + +#: UiStrings.resx$PreviewFormTitle$Message +msgid "Preview" +msgstr "" + +#: UiStrings.resx$PreviewLabel$Message +msgid "Preview:" +msgstr "" + +#: UiStrings.resx$Previous$Message +msgid "Previous" +msgstr "" + +#: MiscResources.resx$Print$Message +#: UiStrings.resx$Print$Message +msgid "Print" +msgstr "" + +#: UiStrings.resx$EditProfileFormTitle$Message +msgid "Profile Settings" +msgstr "" + +#: UiStrings.resx$ProfileLabel$Message +msgid "Profile:" +msgstr "" + +#: UiStrings.resx$Profiles$Message +#: UiStrings.resx$ProfilesFormTitle$Message +msgid "Profiles" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message +msgid "Prompt for file path" +msgstr "" + +#: UiStrings.resx$Provider$Message +msgid "Provider" +msgstr "" + +#: UiStrings.resx$ReadyForScan$Message +msgid "Ready for scan {0}." +msgstr "" + +#: UiStrings.resx$Recover$Message +msgid "Recover" +msgstr "" + +#: UiStrings.resx$RecoverFormTitle$Message +msgid "Recover Scanned Images" +msgstr "" + +#: MiscResources.resx$Recovering$Message +msgid "Recovering..." +msgstr "" + +#: MiscResources.resx$RecoveryProgress$Message +msgid "Recovery Progress" +msgstr "" + +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message +msgid "Remember these settings" +msgstr "" + +#: UiStrings.resx$Reorder$Message +msgid "Reorder" +msgstr "" + +#: UiStrings.resx$Reset$Message +msgid "Reset" +msgstr "" + +#: MiscResources.resx$ResetImage$Message +msgid "Reset Image" +msgstr "" + +#: UiStrings.resx$ResolutionLabel$Message +msgid "Resolution:" +msgstr "" + +#: UiStrings.resx$RestoreDefaults$Message +msgid "Restore Defaults" +msgstr "" + +#: UiStrings.resx$Reverse$Message +msgid "Reverse" +msgstr "" + +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + +#: UiStrings.resx$Revert$Message +msgid "Revert" +msgstr "" + +#: SettingsResources.resx$HorizontalAlign_Right$Message +msgid "Right" +msgstr "" + +#: UiStrings.resx$Rotate$Message +msgid "Rotate" +msgstr "" + +#: UiStrings.resx$RotateLeft$Message +msgid "Rotate Left" +msgstr "" + +#: UiStrings.resx$RotateRight$Message +msgid "Rotate Right" +msgstr "" + +#: UiStrings.resx$RunInBackground$Message +msgid "Run in Background" +msgstr "" + +#: MiscResources.resx$RunningOcr$Message +msgid "Running OCR..." +msgstr "" + +#: UiStrings.resx$SaneDriver$Message +msgid "SANE Driver" +msgstr "" + +#: UiStrings.resx$Save$Message +msgid "Save" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + +#: UiStrings.resx$SaveAllAsImages$Message +msgid "Save All as Images" +msgstr "" + +#: UiStrings.resx$SaveAllAsPdf$Message +msgid "Save All as PDF" +msgstr "" + +#: MiscResources.resx$SaveImages$Message +#: UiStrings.resx$SaveImages$Message +msgid "Save Images" +msgstr "" + +#: MiscResources.resx$SaveImagesProgress$Message +msgid "Save Images Progress" +msgstr "" + +#: MiscResources.resx$SavePdf$Message +#: UiStrings.resx$SavePdf$Message +msgid "Save PDF" +msgstr "" + +#: MiscResources.resx$SavePdfProgress$Message +msgid "Save PDF Progress" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + +#: UiStrings.resx$SaveSelectedAsImages$Message +msgid "Save Selected as Images" +msgstr "" + +#: UiStrings.resx$SaveSelectedAsPdf$Message +msgid "Save Selected as PDF" +msgstr "" + +#: UiStrings.resx$SaveToSingleFile$Message +msgid "Save to a single file" +msgstr "" + +#: UiStrings.resx$SaveToMultipleFiles$Message +msgid "Save to multiple files" +msgstr "" + +#: MiscResources.resx$BatchStatusSaving$Message +msgid "Saving batch results..." +msgstr "" + +#: MiscResources.resx$SavingFormat$Message +msgid "Saving {0}..." +msgstr "" + +#: UiStrings.resx$ScaleWithWindow$Message +msgid "Scale With Window" +msgstr "" + +#: UiStrings.resx$ScaleLabel$Message +msgid "Scale:" +msgstr "" + +#: MiscResources.resx$Scan$Message +#: UiStrings.resx$Scan$Message +msgid "Scan" +msgstr "" + +#: UiStrings.resx$ScanConfig$Message +msgid "Scan Configuration" +msgstr "" + +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + +#: MiscResources.resx$ScannedImage$Message +msgid "Scanned Image" +msgstr "" + +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + +#: MiscResources.resx$ScanPageProgress$Message +msgid "Scanning page {0}" +msgstr "" + +#: MiscResources.resx$BatchStatusScanPage$Message +msgid "Scanning page {0} (scan {1})..." +msgstr "" + +#: MiscResources.resx$BatchStatusPage$Message +#: MiscResources.resx$ScanProgressPage$Message +msgid "Scanning page {0}..." +msgstr "" + +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message +msgid "Second (00-59)" +msgstr "" + +#: UiStrings.resx$Select$Message +msgid "Select" +msgstr "" + +#: UiStrings.resx$SelectAll$Message +msgid "Select All" +msgstr "" + +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + +#: UiStrings.resx$SelectSource$Message +msgid "Select Source" +msgstr "" + +#: MiscResources.resx$SelectProfileBeforeScan$Message +msgid "Select a profile before clicking Scan." +msgstr "" + +#: UiStrings.resx$OcrSelectLanguageLabel$Message +msgid "Select one or more languages:" +msgstr "" + +#: MiscResources.resx$SelectedCount$Message +msgid "Selected ({0})" +msgstr "" + +#: UiStrings.resx$SeparateByPatchT$Message +msgid "Separate files by Patch-T" +msgstr "" + +#: UiStrings.resx$SetDefault$Message +msgid "Set Default" +msgstr "" + +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + +#: UiStrings.resx$Sharpen$Message +msgid "Sharpen" +msgstr "" + +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message +msgid "Show" +msgstr "" + +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message +msgid "Single page files" +msgstr "" + +#: UiStrings.resx$SingleScan$Message +msgid "Single scan" +msgstr "" + +#: UiStrings.resx$SkipSavePrompt$Message +msgid "Skip save prompt" +msgstr "" + +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message +msgid "Start" +msgstr "" + +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message +msgid "Stretch to page size" +msgstr "" + +#: UiStrings.resx$SubjectLabel$Message +msgid "Subject:" +msgstr "" + +#: MiscResources.resx$FileTypeTiff$Message +msgid "TIFF File (*.tiff, *.tif)" +msgstr "" + +#: UiStrings.resx$TwainDriver$Message +msgid "TWAIN Driver" +msgstr "" + +#: UiStrings.resx$TechnicalDetails$Message +msgid "Technical Details" +msgstr "" + +#: MiscResources.resx$TesseractNotAvailable$Message +#: SdkResources.resx$TesseractNotAvailable$Message +msgid "The OCR engine is not available. Make sure to install the required package:" +msgstr "" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + +#: MiscResources.resx$SaneNotAvailable$Message +#: SdkResources.resx$SaneNotAvailable$Message +msgid "The SANE driver is not available. Make sure to install the required packages:" +msgstr "" + +#: MiscResources.resx$ImportErrorCouldNot$Message +#: SdkResources.resx$ImportErrorCouldNot$Message +msgid "The file '{0}' could not be imported." +msgstr "" + +#: MiscResources.resx$ImportErrorNAPS2Pdf$Message +msgid "The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported." +msgstr "" + +#: MiscResources.resx$FileInUse$Message +msgid "The file could not be overwritten because it is currently in use." +msgstr "" + +#: MiscResources.resx$ConfirmOverwriteFile$Message +msgid "The file {0} already exists. Do you want to overwrite it?" +msgstr "" + +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "" + +#: MiscResources.resx$DevicePaperJam$Message +#: SdkResources.resx$DevicePaperJam$Message +msgid "The scanner has a paper jam." +msgstr "" + +#: MiscResources.resx$DeviceWarmingUp$Message +#: SdkResources.resx$DeviceWarmingUp$Message +msgid "The scanner is warming up." +msgstr "" + +#: MiscResources.resx$DeviceCoverOpen$Message +#: SdkResources.resx$DeviceCoverOpen$Message +msgid "The scanner's cover is open." +msgstr "" + +#: MiscResources.resx$DriverNotSupported$Message +#: SdkResources.resx$DriverNotSupported$Message +msgid "The selected driver is not supported on this system." +msgstr "" + +#: MiscResources.resx$DeviceNotFound$Message +#: SdkResources.resx$DeviceNotFound$Message +msgid "The selected scanner could not be found." +msgstr "" + +#: MiscResources.resx$NoFeederSupport$Message +#: SdkResources.resx$NoFeederSupport$Message +msgid "The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver." +msgstr "" + +#: MiscResources.resx$NoDuplexSupport$Message +#: SdkResources.resx$NoDuplexSupport$Message +msgid "The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver." +msgstr "" + +#: MiscResources.resx$DeviceBusy$Message +#: SdkResources.resx$DeviceBusy$Message +msgid "The selected scanner is busy." +msgstr "" + +#: MiscResources.resx$DeviceOffline$Message +#: SdkResources.resx$DeviceOffline$Message +msgid "The selected scanner is offline." +msgstr "" + +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message +msgid "Tiff Options" +msgstr "" + +#: UiStrings.resx$TimeBetweenScansLabel$Message +msgid "Time between scans (seconds):" +msgstr "" + +#: UiStrings.resx$TitleLabel$Message +msgid "Title:" +msgstr "" + +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message +msgid "Twain Implementation:" +msgstr "" + +#: SettingsResources.resx$PageSize_Legal$Message +msgid "US Legal (8.5x14 in)" +msgstr "" + +#: SettingsResources.resx$PageSize_Letter$Message +msgid "US Letter (8.5x11 in)" +msgstr "" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + +#: MiscResources.resx$UnsavedChanges$Message +msgid "Unsaved Changes" +msgstr "" + +#: MiscResources.resx$UpdateProgress$Message +msgid "Update Progress" +msgstr "" + +#: MiscResources.resx$UpdateCheckDisabled$Message +msgid "Update checking is disabled." +msgstr "" + +#: MiscResources.resx$Updating$Message +msgid "Updating..." +msgstr "" + +#: MiscResources.resx$UploadingEmail$Message +msgid "Uploading email..." +msgstr "" + +#: UiStrings.resx$UseNativeUi$Message +msgid "Use native UI" +msgstr "" + +#: UiStrings.resx$UsePredefinedSettings$Message +msgid "Use predefined settings" +msgstr "" + +#: UiStrings.resx$UserPasswordLabel$Message +msgid "User Password:" +msgstr "" + +#: UiStrings.resx$OcrDownloadSummaryText$Message +msgid "Using OCR requires you to download each language you want to scan." +msgstr "" + +#: MiscResources.resx$Version$Message +#: SdkResources.resx$Version$Message +msgid "Version {0}" +msgstr "" + +#: UiStrings.resx$View$Message +msgid "View" +msgstr "" + +#: UiStrings.resx$WiaDriver$Message +msgid "WIA Driver" +msgstr "" + +#: UiStrings.resx$WaitingForTwain$Message +msgid "Waiting for TWAIN to complete..." +msgstr "" + +#: UiStrings.resx$WaitingForAuthorization$Message +msgid "Waiting for authorization..." +msgstr "" + +#: MiscResources.resx$BatchStatusWaitingForScan$Message +msgid "Waiting for scan {0}..." +msgstr "" + +#: UiStrings.resx$WhiteThreshold$Message +msgid "White Threshold" +msgstr "" + +#: UiStrings.resx$WiaVersionLabel$Message +msgid "Wia Version:" +msgstr "" + +#: UiStrings.resx$Year4Digit$Message +msgid "Year" +msgstr "" + +#: UiStrings.resx$Year2Digit$Message +msgid "Year (00-99)" +msgstr "" + +#: MiscResources.resx$PdfNoPermissionToExtractContent$Message +#: SdkResources.resx$PdfNoPermissionToExtractContent$Message +msgid "You do not have permission to copy content from the file '{0}'." +msgstr "" + +#: MiscResources.resx$DontHavePermission$Message +msgid "You don't have permission to save files at this location." +msgstr "" + +#: MiscResources.resx$ExitWithUnsavedChanges$Message +msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" +msgstr "" + +#: UiStrings.resx$Zoom$Message +msgid "Zoom" +msgstr "" + +#: UiStrings.resx$ZoomActual$Message +msgid "Zoom Actual" +msgstr "" + +#: UiStrings.resx$ZoomIn$Message +msgid "Zoom In" +msgstr "" + +#: UiStrings.resx$ZoomOut$Message +msgid "Zoom Out" +msgstr "" + +#: SettingsResources.resx$PageSizeUnit_Centimetre$Message +msgid "cm" +msgstr "" + +#: SettingsResources.resx$PageSizeUnit_Inch$Message +msgid "in" +msgstr "" + +#: SettingsResources.resx$PageSizeUnit_Millimetre$Message +msgid "mm" +msgstr "" + +#: MiscResources.resx$OfN$Message +msgid "of {0}" +msgstr "" + +#: SettingsResources.resx$TwainImpl_X64$Message +msgid "x64" +msgstr "" + +#: MiscResources.resx$NamedPageSizeFormat$Message +msgid "{0} ({1}x{2} {3})" +msgstr "" + +#: MiscResources.resx$ProgressFormat$Message +msgid "{0} / {1}" +msgstr "" + +#: MiscResources.resx$SizeProgress$Message +msgid "{0} / {1} MB" +msgstr "" + +#: MiscResources.resx$FilesProgressFormat$Message +msgid "{0} / {1} files" +msgstr "" + +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message +msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" +msgstr "" + +#: MiscResources.resx$ImagesSaved$Message +msgid "{0} images saved." +msgstr "" + +#: MiscResources.resx$PdfStatus$Message +#: UiStrings.resx$XOfY$Message +msgid "{0} of {1}" +msgstr "" + diff --git a/NAPS2.Lib/Lang/po/el.po b/NAPS2.Lib/Lang/po/el.po index 50a31fbd3f..cc2c6d3f56 100644 --- a/NAPS2.Lib/Lang/po/el.po +++ b/NAPS2.Lib/Lang/po/el.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Greek\n" "Language: el\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "Χρώμα 24-bit" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "A3 (297x420 χιλ.)" @@ -74,14 +58,17 @@ msgstr "Σχετικά" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." -msgstr "Απόκτηση δεδομένων..." +msgstr "Συλλογή δεδομένων..." + +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Για προχωρημένους" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Προηγμένες Ρυθμίσεις Προφίλ" @@ -93,35 +80,35 @@ msgstr "Όλα ({0})" msgid "All Files" msgstr "Όλα τα Αρχεία" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Να Επιτρέπεται Σχολιασμός" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Να Επιτρέπεται Αντιγραφή Περιεχομένου" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Να Επιτρέπεται Αντιγραφή Περιεχομένου για Προσβασιμότητα" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Να Επιτρέπεται Συναρμολόγηση Εγγράφου" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Να Επιτρέπεται Τροποποίηση Εγγράφου" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Να Επιτρέπεται Συμπλήρωση Φορμών" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Να Επιτρέπεται Εκτύπωση Μέγιστης Ποιότητας" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Να Επιτρέπεται Εκτύπωση" @@ -133,17 +120,22 @@ msgstr "Εναλλακτική Αποπαρεμβολή [135642...]" msgid "Alternate Interleave" msgstr "Εναλλακτική Παρεμβολή" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Εναλλακτική Μεταφορά" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Ένα πρόσθετο συστατικό είναι απαραίτητο για την εισαγωγή αυτού του PDF. Θέλετε την λήψη του τώρα;" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "" +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Σφάλμα κατά την εκτέλεση της OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Παρουσιάστηκε ένα σφάλμα κατά την προσ msgid "An error occurred when trying to auto save." msgstr "Σφάλμα κατά την αυτόματη αποθήκευση." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Προέκυψε σφάλμα κατά την εγκατάσταση της ενημέρωσης." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Σφάλμα κατά την αποθήκευση του αρχείου." @@ -172,11 +168,11 @@ msgstr "Σφάλμα του οδηγού σάρωσης." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "" +msgstr "Υπάρχει εργασία σε εξέλιξη. Είστε σίγουρος ότι θέλετε την ακύρωσή της και έξοδο;" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Άγνωστο σφάλμα κατά την μαζική σάρωση." +msgid "An unknown error occurred during the batch scan." +msgstr "Ένα άγνωστο σφάλμα προέκυψε κατά την μαζική σάρωση." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -188,9 +184,17 @@ msgstr "Υπάρχει διαθέσιμη ενημέρωση για το OCR." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" +msgstr "Οδηγός Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Εφαρμογή φωτεινότητας/αντίθεσης μετά την σάρωση" @@ -222,52 +226,58 @@ msgstr "Θέλετε σίγουρα να διαγραφούν {0} αντικεί msgid "Are you sure you want to delete {0} profiles?" msgstr "Θέλετε σίγουρα να διαγραφούν {0} προφίλ;" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Θέλετε σίγουρα να αναιρεθούν οι αλλαγές σε {0} εικόνες;" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Όνομα Επισύναψης:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Συντάκτης:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "Εξουσιοδότηση" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" msgstr "Αυτόματη" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Ρυθμίσεις Αυτόματης Αποθήκευσης" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "Αυτόματη αρίθμηση (1 ψηφίο)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Αυτόματη αρίθμηση (2 ψηφία)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Αυτόματη αρίθμηση (3 ψηφία)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Αυτόματη αρίθμηση (4 ψηφία)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "" +msgstr "Αυτόματη εκτέλεση OCR μετά την σάρωση" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" @@ -277,14 +287,14 @@ msgstr "B4 (250x353 χιλ.)" msgid "B5 (176x250 mm)" msgstr "B5 (176x250 χιλ.)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Μαζική Σάρωση" #: MiscResources.resx$BatchStatusCancelled$Message msgid "Batch cancelled." -msgstr "Η μαζική ακυρώθηκε." +msgstr "Η μαζική επεξεργασία ακυρώθηκε." #: MiscResources.resx$BatchStatusComplete$Message msgid "Batch completed successfully." @@ -296,23 +306,22 @@ msgstr "Η μαζική διακόπηκε λόγω σφάλματος." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Βέλτιστα" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Βάθος bit:" #: MiscResources.resx$FileTypeBmp$Message msgid "Bitmap Files (*.bmp)" -msgstr "Αρχεία BMP (*.bmp)" +msgstr "Αρχεία Bitmap (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Ασπρόμαυρο" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Κενές Σελίδες" @@ -320,36 +329,22 @@ msgstr "Κενές Σελίδες" msgid "Brightness / Contrast" msgstr "Φωτεινότητα / Αντίθεση" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Φωτεινότητα:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" -msgstr "Άκυρο" +msgstr "Ακύρωση" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" @@ -363,19 +358,19 @@ msgstr "Ακύρωση...." msgid "Center" msgstr "Κέντρο" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Αλλαγή" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "Ελέγξτε για ενημερώσεις." +msgstr "Έλεγχος για ενημερώσεις" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." msgstr "Έλεγχος..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Επιλέξτε πάροχο Email" @@ -383,7 +378,6 @@ msgstr "Επιλέξτε πάροχο Email" msgid "Choose Profile" msgstr "Επιλογή Προφίλ" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Επιλογή συσκευής" @@ -391,13 +385,13 @@ msgstr "Επιλογή συσκευής" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message msgid "Clear" -msgstr "Ολική Διαγραφή" +msgstr "Εκκαθάριση" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Εκκαθάριση Όλων" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Διαγραφή εικόνων μετά την αποθήκευση" @@ -405,16 +399,30 @@ msgstr "Διαγραφή εικόνων μετά την αποθήκευση" msgid "Close" msgstr "Κλείσιμο" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Συμβατότητα" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Συμπίεση:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Αντίθεση:" @@ -433,9 +441,9 @@ msgstr "Αντιγραφή..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Πνευματικά δικαιώματα {0} Συνεισφέροντες NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Κατώτατο Όριο Κάλυψης" @@ -443,7 +451,7 @@ msgstr "Κατώτατο Όριο Κάλυψης" msgid "Crop" msgstr "Περικοπή" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Περικοπή στο μέγεθος σελίδας" @@ -451,35 +459,44 @@ msgstr "Περικοπή στο μέγεθος σελίδας" msgid "Custom ({0}x{1} {2})" msgstr "Προσαρμογή ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Προσαρμογή Μεγέθους Σελίδας" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Προσαρμοσμένη Περιστροφή" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" -msgstr "" +msgstr "Προσαρμοσμένο SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Προσαρμογή..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Ημέρα (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Προεπιλεγμένο" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Προεπιλεγμένη Διαδρομή Αρχείου:" @@ -487,7 +504,6 @@ msgstr "Προεπιλεγμένη Διαδρομή Αρχείου:" msgid "Deinterleave" msgstr "Αποπαρεμβολή" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Διόρθωση Ασυμμετρίας" msgid "Deskew Progress" msgstr "Πρόοδος Διόρθωσης Ασυμμετρίας" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Διόρθωση ασυμμετρίας σαρωμένων σελίδων" @@ -509,35 +525,31 @@ msgstr "Διόρθωση ασυμμετρίας σαρωμένων σελίδω msgid "Deskewing..." msgstr "Διόρθωση Ασυμμετρίας..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Συσκευή:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Διαστάσεις" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Εμφανιζόμενο Όνομα:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Διόρθωση Κειμένου" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Δωρεά" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Ολοκλήρωση" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Λήψη" @@ -550,22 +562,50 @@ msgstr "Σφάλμα Λήψης" msgid "Download Needed" msgstr "Λήψη Απαραίτητη" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Πρόοδος Λήψης" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Διπλή Όψη" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Επεξεργασία" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Αποστολή Όλων" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Αποστολή Όλων ως PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,25 +616,32 @@ msgstr "Αποστολή PDF (Email)" msgid "Email PDF Progress" msgstr "Πρόοδος Αποστολής PDF (Email)" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Αποστολή Επιλεγμένων" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Αποστολή Επιλεγμένων ως PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Ρυθμίσεις Email" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Αυτόματη Αποθήκευση Ενεργή" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Κρυπτογραφημένο PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Κρυπτογράφηση" @@ -602,12 +649,15 @@ msgstr "Κρυπτογράφηση" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Αρχεία EMF (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Σφάλμα" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,36 +667,43 @@ msgstr "Εκτιμώμενο μέγεθος λήψης: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "Αρχεία EXIF (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Εξαίρεση κενών σελίδων" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Γρήγορα" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Τροφοδότης" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "Όνομα Αρχείου" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Όνομα Αρχείου:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Διαδρομή αρχείου:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Αναστροφή" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Αναστροφή σελίδων διπλής όψης" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Για υψηλές ποιότητες JPEG (80+), η Ποιότητα Εικόνας μπορεί να αυξηθεί από τις ρυθμίσεις προφίλ." @@ -654,7 +711,6 @@ msgstr "Για υψηλές ποιότητες JPEG (80+), η Ποιότητα msgid "GIF File (*.gif)" msgstr "Αρχεία GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Περισσότερες γλώσσες" @@ -665,18 +721,17 @@ msgstr "Γυαλί" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Κλίμακα του Γκρι" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Οριζόντια στοίχιση:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Ώρα (0-23)" @@ -684,6 +739,10 @@ msgstr "Ώρα (0-23)" msgid "Hue / Saturation" msgstr "Χροιά / Κορεσμός" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Προέλευση εικονιδίων:" @@ -696,12 +755,12 @@ msgstr "Εικόνα" msgid "Image Files" msgstr "Αρχεία Εικόνας" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Ποιότητα Εικόνας" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Ρυθμίσεις Εικόνας" @@ -727,7 +786,7 @@ msgstr "Εισαγωγή..." #: MiscResources.resx$Install$Message msgid "Install {0}" -msgstr "" +msgstr "Εγκατάσταση {0}" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" @@ -745,6 +804,10 @@ msgstr "Η εγκατάσταση ολοκληρώθηκε. Θέλετε να ε msgid "Installation failed." msgstr "Η εγκατάσταση απέτυχε." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Παρεμβολή" @@ -755,24 +818,37 @@ msgstr "Αρχεία JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "Αρχείο JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Ποιότητα Jpeg" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Λέξεις Κλειδιά:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Γλώσσα" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Αριστερά" @@ -781,33 +857,48 @@ msgstr "Αριστερά" msgid "Legacy (native UI only)" msgstr "Παρωχημένο (εγγενές περιβάλλον μόνο)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Φόρτωση εικόνων στο NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Χρήση OCR για δυνατότητα αναζήτησης στα PDF" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Μέγιστη ποιότητα (μεγάλα αρχεία)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Μεταδεδομένα (Metadata)" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Λεπτά (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Μήνας (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Περισσότερες πληροφορίες" @@ -819,11 +910,19 @@ msgstr "Κύλιση Κάτω" msgid "Move Up" msgstr "Κύλιση Πάνω" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Πολλαπλές σαρώσεις (χρονοκαθυστέρηση μεταξύ σαρώσεων)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Πολλαπλές σαρώσεις (προτροπή μεταξύ σαρώσεων)" @@ -831,17 +930,17 @@ msgstr "Πολλαπλές σαρώσεις (προτροπή μεταξύ σα #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "Το NAPS2 είναι ελεύθερο λογισμικό. Αξιολογήστε την επιλογή μιας δωρεάς." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Όνομα (προαιρετικό)" @@ -849,6 +948,10 @@ msgstr "Όνομα (προαιρετικό)" msgid "Name missing." msgstr "Απουσιάζει το όνομα." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "Νέο" @@ -861,7 +964,7 @@ msgstr "Νέο Προφίλ" msgid "Next" msgstr "Επόμενη" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Επόμενη Σάρωση" @@ -870,15 +973,18 @@ msgstr "Επόμενη Σάρωση" msgid "No device selected." msgstr "Δεν επιλέχθηκε συσκευή." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Δεν υπάρχουν σελίδες στον τροφοδότη." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "" +msgstr "Δεν έχει επιλεγεί πάροχος." #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message @@ -895,60 +1001,45 @@ msgstr "Καμία" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Όχι άλλος σαρωτής PDF" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Αργότερα" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Αριθμός σαρώσεων:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Λήψη OCR" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "" +msgstr "Πρόοδος OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Διαμόρφωση OCR (Οπτική Αναγνώριση Χαρακτήρων)" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Γλώσσα OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "" +msgstr "Λειτουργία OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "Εντάξει" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Πλάτος απόκλισης βάση στοίχισης (WIA)" @@ -956,13 +1047,11 @@ msgstr "Πλάτος απόκλισης βάση στοίχισης (WIA)" msgid "Old DSM" msgstr "Παλαιός Διαχειριστής Πηγής Δεδομένων (DSM)" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Ένα αρχείο ανά σελίδα" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Ένα αρχείο ανά σάρωση" @@ -970,19 +1059,27 @@ msgstr "Ένα αρχείο ανά σάρωση" msgid "One or more files could not be downloaded." msgstr "Αποτυχία λήψης για ένα ή περισσότερα αρχεία." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Άνοιγμα Φακέλου" #: MiscResources.resx$ActiveOperations$Message msgid "Operation in Progress" +msgstr "Εργασία σε Εξέλιξη" + +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" msgstr "" #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Πρόσβαση μέσω Web" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Αποτέλεσμα" @@ -990,7 +1087,7 @@ msgstr "Αποτέλεσμα" msgid "Overwrite File" msgstr "Αντικατάσταση Αρχείου" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Κωδικός Κατόχου:" @@ -998,8 +1095,8 @@ msgstr "Κωδικός Κατόχου:" msgid "PDF Document (*.pdf)" msgstr "Αρχεία PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Ρυθμίσεις PDF" @@ -1009,35 +1106,33 @@ msgstr "Το PDF αποθηκεύτηκε." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "Αρχεία PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Μέγεθος σελίδας:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Προέλευση χαρτιού:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Κωδικός" @@ -1045,21 +1140,24 @@ msgstr "Κωδικός" msgid "Paste" msgstr "Επικόλληση" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Δεσμευτικά Θέσεων (Placeholders)" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Μετά την επεξεργασία" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Σε αναμονή για κλικ στο Έναρξη." @@ -1067,7 +1165,7 @@ msgstr "Σε αναμονή για κλικ στο Έναρξη." msgid "Preview" msgstr "Προεπισκόπηση" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Προεπισκόπηση:" @@ -1080,12 +1178,11 @@ msgstr "Προηγούμενη" msgid "Print" msgstr "Εκτύπωση" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Ρυθμίσεις Προφίλ" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Προφίλ:" @@ -1094,23 +1191,27 @@ msgstr "Προφίλ:" msgid "Profiles" msgstr "Προφίλ" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Προτροπή για διαδρομή αρχείου" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "Πάροχος" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Σε ετοιμότητα για σάρωση {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Ανάκτηση" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Ανάκτηση Σαρωμένων Εικόνων" @@ -1122,9 +1223,15 @@ msgstr "Ανάκτηση..." msgid "Recovery Progress" msgstr "Πρόοδος Ανάκτησης" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Απομνημόνευση αυτών των ρυθμίσεων" @@ -1140,15 +1247,11 @@ msgstr "Αρχικοποίηση" msgid "Reset Image" msgstr "Αρχικοποίηση Εικόνας" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Ανάλυση:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Επαναφορά Προεπιλογών" @@ -1156,6 +1259,14 @@ msgstr "Επαναφορά Προεπιλογών" msgid "Reverse" msgstr "Αντιστροφή" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Αναστροφή Όλων" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Αναστροφή Επιλεγμένων" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Επαναφορά" @@ -1176,31 +1287,34 @@ msgstr "Περιστροφή Αριστερά" msgid "Rotate Right" msgstr "Περιστροφή Δεξιά" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "" +msgstr "Εκτέλεση στο Παρασκήνιο" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." msgstr "Γίνεται Οπτική Αναγνώριση Χαρακτήρων...." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "Οδηγός SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Αποθήκευση" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Αποθήκευση Όλων" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Αποθήκευση Όλων ως Εικόνες" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Αποθήκευση Όλων ως PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Αποθήκευση PDF" msgid "Save PDF Progress" msgstr "Πρόοδος Αποθήκευσης PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Αποθήκευση Επιλεγμένων" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Αποθήκευση Επιλεγμένων ως Εικόνες" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Αποθήκευση Επιλεγμένων ως PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Αποθήκευση σε ένα αρχείο" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Αποθήκευση σε πολλαπλά αρχεία" @@ -1244,29 +1363,45 @@ msgstr "Αποθήκευση αποτελεσμάτων μαζικής..." msgid "Saving {0}..." msgstr "Αποθήκευση '{0}'..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Μέγεθος Βάση Παραθύρου" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Κλίμακα:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Σάρωση" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Διαμόρφωση Σάρωσης" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Σαρωμένη Εικόνα" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Σάρωση σελίδας {0}" @@ -1280,11 +1415,14 @@ msgstr "Σάρωση σελίδας {0} (σάρωση {1})..." msgid "Scanning page {0}..." msgstr "Σάρωση σελίδας {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Δευτερόλεπτα (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Επιλογή" @@ -1293,7 +1431,10 @@ msgstr "Επιλογή" msgid "Select All" msgstr "Επιλογή Όλων" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Επιλογή Προέλευσης" @@ -1302,7 +1443,6 @@ msgstr "Επιλογή Προέλευσης" msgid "Select a profile before clicking Scan." msgstr "Δεν έχει επιλεγεί προφίλ." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Επιλογή μίας ή περισσοτέρων γλωσσών:" @@ -1311,8 +1451,7 @@ msgstr "Επιλογή μίας ή περισσοτέρων γλωσσών:" msgid "Selected ({0})" msgstr "Επιλεγμένα ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Διαχωρισμός αρχείων με Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Διαχωρισμός αρχείων με Patch-T" msgid "Set Default" msgstr "Ορισμός Προεπιλεγμένου" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Οξύτητα" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Εμφάνιση" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Αρχεία μιας σελίδας" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Μία σάρωση" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Χωρίς προτροπή αποθήκευσης" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Έναρξη" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Επέκταση στο μέγεθος σελίδας" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Θέμα:" @@ -1358,24 +1544,27 @@ msgstr "Θέμα:" msgid "TIFF File (*.tiff, *.tif)" msgstr "Αρχεία TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "Οδηγός TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Τεχνικές Λεπτομέρειες" #: MiscResources.resx$TesseractNotAvailable$Message #: SdkResources.resx$TesseractNotAvailable$Message msgid "The OCR engine is not available. Make sure to install the required package:" -msgstr "" +msgstr "Η εφαρμογή OCR δεν είναι διαθέσιμη. Βεβαιωθείτε ότι έχετε εγκαταστήσει το απαιτούμενο πακέτο:" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Εξέπνευσε ο χρόνος για την λειτουργία OCR." #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" -msgstr "" +msgstr "Ο οδηγός SANE δεν είναι διαθέσιμος. Βεβαιωθείτε ότι έχετε εγκαταστήσει τα απαιτούμενα πακέτα:" #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message @@ -1394,9 +1583,9 @@ msgstr "Το αρχείο είναι σε χρήση και είναι αδύν msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Το αρχείο '{0}' υπάρχει ήδη. Θέλετε να αντικατασταθεί;" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Το ακόλουθο αρχείο είναι κρυπτογραφημένο και απαιτεί κωδικό για να ανοίξει: '{0}'" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Το ακόλουθο αρχείο είναι κρυπτογραφημένο και απαιτεί συνθηματικό για να ανοίξει:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1416,7 +1605,7 @@ msgstr "Το κάλυμμα του σαρωτή είναι ανοικτό." #: MiscResources.resx$DriverNotSupported$Message #: SdkResources.resx$DriverNotSupported$Message msgid "The selected driver is not supported on this system." -msgstr "" +msgstr "Ο επιλεγμένος οδηγός δεν υποστηρίζεται σε αυτό το σύστημα." #: MiscResources.resx$DeviceNotFound$Message #: SdkResources.resx$DeviceNotFound$Message @@ -1443,19 +1632,39 @@ msgstr "Ο επιλεγμένος σαρωτής είναι απασχολημέ msgid "The selected scanner is offline." msgstr "Ο επιλεγμένος σαρωτής βρίσκεται εκτός σύνδεσης (offline)." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Επιλογές Tiff" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Χρόνος μεταξύ σαρώσεων (δευτερόλεπτα):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Τίτλος:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Εργαλεία" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Υλοποίηση Twain:" @@ -1467,17 +1676,33 @@ msgstr "US Legal (8.5x14 ιν.)" msgid "US Letter (8.5x11 in)" msgstr "US Letter (8.5x11 ιν.)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Μη Αποθηκευμένες Αλλαγές" #: MiscResources.resx$UpdateProgress$Message msgid "Update Progress" -msgstr "" +msgstr "Εξέλιξη ενημέρωσης" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Ο έλεγχος ενημερώσεων είναι απενεργοποιημένος." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1485,23 +1710,20 @@ msgstr "Γίνεται ενημέρωση..." #: MiscResources.resx$UploadingEmail$Message msgid "Uploading email..." -msgstr "" +msgstr "Ανέβασμα email..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Εγγενές περιβάλλον" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Προκαθορισμένες ρυθμίσεις" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Κωδικός Χρήστη:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Για την χρήση του OCR, απαιτείται η λήψη της κάθε γλώσσας προς σάρωση." @@ -1515,36 +1737,35 @@ msgstr "Έκδοση {0}" msgid "View" msgstr "Προβολή" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "Οδηγός WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Αναμονή ολοκλήρωσης TWAIN..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." -msgstr "" +msgstr "Αναμονή για εξουσιοδότηση..." #: MiscResources.resx$BatchStatusWaitingForScan$Message msgid "Waiting for scan {0}..." msgstr "Σε αναμονή για σάρωση {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Κατώτατο Όριο Λευκού" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Έκδοση WIA:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Έτος" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Έτος (00-99)" @@ -1561,21 +1782,18 @@ msgstr "Δεν υπάρχει δικαίωμα αποθήκευσης αρχεί msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Υπάρχουν αλλαγές που δεν έχουν αποθηκευτεί. Θέλετε σίγουρα να τερματιστεί η εφαρμογή;" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Ποσοστό Μεγέθυνσης" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Πραγματικό Μέγεθος" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Μεγέθυνση" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Σμίκρυνση" @@ -1598,28 +1816,33 @@ msgstr "από {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} αρχεία" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "Υπάρχουν {0} εικόνες που σαρώθηκαν στις {1} και ώρα {2} οι οποίες ενδέχεται να μην έχουν αποθηκευτεί. Θέλετε να ανακτηθούν;" diff --git a/NAPS2.Lib/Lang/po/es.po b/NAPS2.Lib/Lang/po/es.po index 8376a5a13b..4233f1e1eb 100644 --- a/NAPS2.Lib/Lang/po/es.po +++ b/NAPS2.Lib/Lang/po/es.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Spanish\n" "Language: es\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "100 ppp" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Acción predeterminada del botón \"Guardar\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "1200 ppp" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Acción predeterminada del botón \"Escanear\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "150 ppp" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "El menú \"Escanear\" cambia el perfil por defecto" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "200 ppp" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 dispositivo encontrado." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "Color de 24 bits" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "300 ppp" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "400 ppp" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "600 ppp" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "800 ppp" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "A3 (297 x 420 mm)" @@ -76,12 +60,15 @@ msgstr "Acerca de" msgid "Acquiring data..." msgstr "Obteniendo datos..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Avanzadas" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Configuración de perfil avanzada" @@ -93,57 +80,62 @@ msgstr "Todo ({0})" msgid "All Files" msgstr "Todos los archivos" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Permitir anotaciones" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Permitir copia de contenido" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Permitir copia de contenido para accesibilidad" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Permitir montaje del documento" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Permitir modificación del documento" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Permitir rellenar el formulario" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Permitir impresión de calidad total" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Permitir impresión" #: UiStrings.resx$AltDeinterleave$Message msgid "Alternate Deinterleave" -msgstr "Desentrelazado alternativo" +msgstr "Des entrelazado alternativo" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" msgstr "Entrelazado alternativo" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Transferencia alternativa" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Preguntar siempre" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Preguntar siempre" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Se necesita un componente adicional para importar este archivo PDF. ¿Quiere descargarlo ahora?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Ha ocurrido un error al intentar instalar la actualización." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Se ha producido un error ejecutando OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Ha ocurrido un error al intentar conceder la autorización." msgid "An error occurred when trying to auto save." msgstr "Ocurrió un error al intentar guardar el archivo automáticamente." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Ha ocurrido un error al intentar instalar la actualización." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Ocurrió un error al intentar guardar el archivo." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Una operación está en marcha. ¿Está seguro de que quiere salir y cancelar la operación?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Ocurrió un error desconocido durante el escaneo por lotes." +msgid "An unknown error occurred during the batch scan." +msgstr "Se ha producido un error desconocido durante el escaneo por lotes." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -188,9 +184,17 @@ msgstr "Hay una actualización de OCR." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Controlador de Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Aplicación" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Aplicar el brillo/contraste tras el escaneo" @@ -222,19 +226,27 @@ msgstr "¿Está seguro de que desea eliminar {0} elemento(s)?" msgid "Are you sure you want to delete {0} profiles?" msgstr "¿Está seguro de que desea eliminar {0} perfiles?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "¿Estás seguro de que quieres dejar de compartir {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "¿Está seguro de que quiere deshacer los cambios a {0} imagen(es)?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Asignar" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Nombre del archivo adjunto:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autor:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autorizar" @@ -242,43 +254,41 @@ msgstr "Autorizar" msgid "Auto" msgstr "Automático" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Configuración de autoguardado" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Número autoincremental (1 dígito)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Número autoincremental (1 dígitos)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Número autoincremental (2 dígitos)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Número autoincremental (3 dígitos)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Número autoincremental (4 dígitos)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Ejecutar OCR automáticamente después del escaneo" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Escanear por lotes" @@ -296,9 +306,8 @@ msgstr "Escaneo por lotes detenido debido a un error." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Mejor" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Profundidad de color:" @@ -309,10 +318,10 @@ msgstr "Imagen BMP (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Blanco y negro" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Páginas en blanco" @@ -320,33 +329,19 @@ msgstr "Páginas en blanco" msgid "Brightness / Contrast" msgstr "Brillo / Contraste" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Brillo:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "¿No encuentras tu escáner? Lee las limitaciones del Flatpak NAPS2." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Cancelar" @@ -363,7 +358,7 @@ msgstr "Cancelando...." msgid "Center" msgstr "Centro" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Cambiar" @@ -375,7 +370,7 @@ msgstr "Comprobar si hay actualizaciones" msgid "Checking..." msgstr "Comprobando..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Elegir proveedor de correo electrónico" @@ -383,7 +378,6 @@ msgstr "Elegir proveedor de correo electrónico" msgid "Choose Profile" msgstr "Seleccione un perfil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Seleccione un dispositivo" @@ -395,9 +389,9 @@ msgstr "Limpiar" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Limpiar todo" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Borrar las imágenes después de guardar" @@ -405,16 +399,30 @@ msgstr "Borrar las imágenes después de guardar" msgid "Close" msgstr "Cerrar" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Combinar" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Se interrumpió la comunicación con el dispositivo de escaneo." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Compatibilidad" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Compresión:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Conectar" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Error de conexión." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Contraste:" @@ -433,9 +441,9 @@ msgstr "Copiando..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} colaboradores NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Umbral de cobertura" @@ -443,7 +451,7 @@ msgstr "Umbral de cobertura" msgid "Crop" msgstr "Recortar" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Recortar a tamaño de página" @@ -451,10 +459,14 @@ msgstr "Recortar a tamaño de página" msgid "Custom ({0}x{1} {2})" msgstr "Personalizado ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Tamaño de página personalizado" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Resolución predeterminada" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Rotación personalizada" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "SMTP personalizado" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Personalizar..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Oscuro" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Día (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Predeterminado" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Ruta predeterminada:" @@ -487,7 +504,6 @@ msgstr "Ruta predeterminada:" msgid "Deinterleave" msgstr "Desentrelazar" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Corregir sesgo" msgid "Deskew Progress" msgstr "Progreso de la corrección de sesgo" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Corregir sesgo de las páginas escaneadas" @@ -509,35 +525,31 @@ msgstr "Corregir sesgo de las páginas escaneadas" msgid "Deskewing..." msgstr "Corrigiendo sesgo..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Dispositivo:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Dimensiones" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Nombre mostrado:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Corrección de documento" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Donar" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Terminado" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Descargar" @@ -550,22 +562,50 @@ msgstr "Error de descarga" msgid "Download Needed" msgstr "Descarga necesaria" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Progreso de la descarga" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "PPP" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Dúplex" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Dispositivo ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "Dispositivo red ESCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "Dispositivo ESCL USB" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Editar" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Editar con {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Editar con..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Enviar a todos" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Enviar todo como PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,25 +616,32 @@ msgstr "Enviar PDF por correo electrónico" msgid "Email PDF Progress" msgstr "Progreso del envío del PDF por correo electrónico" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Enviar seleccionado" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Enviar seleccionado como PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Configuración del correo electrónico" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Activar autoguardado" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Activar registro de depuración" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Cifrar PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Cifrado" @@ -602,12 +649,15 @@ msgstr "Cifrado" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Metaarchivo mejorado de Windows (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" -msgstr "" +msgstr "Error" + +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Error al iniciar la aplicación {0}" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,36 +667,43 @@ msgstr "Tamaño estimado de la descarga: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "Archivo de Imagen Intercambiable (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Excluir las páginas en blanco" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Rápido" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Alimentador" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "Nombre de archivo" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Nombre de archivo:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Ruta del archivo:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Corregir el balance de blancos y eliminar el ruido" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Voltear" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Voltea el reverso de las páginas dúplex" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Voltear las páginas a doble cara" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Para calidades JPEG altas (más de 80), incremente también la calidad de imagen en su perfil para obtener los mejores resultados." @@ -654,7 +711,6 @@ msgstr "Para calidades JPEG altas (más de 80), incremente también la calidad d msgid "GIF File (*.gif)" msgstr "Imagen GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Obtener más idiomas" @@ -665,18 +721,17 @@ msgstr "Cristal" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "GMail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Escala de grises" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Alineación horizontal:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Hora (0-23)" @@ -684,6 +739,10 @@ msgstr "Hora (0-23)" msgid "Hue / Saturation" msgstr "Tono / Saturación" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Host" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Iconos de:" @@ -696,12 +755,12 @@ msgstr "Imagen" msgid "Image Files" msgstr "Archivos de imagen" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Calidad de imagen" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Configuración de la imagen" @@ -745,6 +804,10 @@ msgstr "Instalación completa. ¿Desea reiniciar NAPS2 ahora?" msgid "Installation failed." msgstr "La instalación ha fallado." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Interfaz" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Entrelazar" @@ -755,24 +818,37 @@ msgstr "Imagen JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "Archivo JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Calidad Jpeg" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Mantener las imágenes entre sesiones" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Atajos del teclado" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Palabras clave:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Idioma" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Dejar una reseña" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Izquierda" @@ -781,33 +857,48 @@ msgstr "Izquierda" msgid "Legacy (native UI only)" msgstr "Heredado (solo interfaz nativa)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Claro" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "¿Te gusta NAPS2?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Cargar imágenes en NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Hacer que los PDFs sean buscables usando OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "IP manual" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Máxima calidad (ficheros grandes)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Transferencia de memoria" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metadatos" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minuto (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Mes (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Más información" @@ -819,11 +910,19 @@ msgstr "Bajar" msgid "Move Up" msgstr "Subir" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Varios idiomas" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Varios idiomas..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Múltiples escaneos (pausa fija entre escaneos)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Múltiples escaneos (preguntar entre escaneos)" @@ -831,17 +930,17 @@ msgstr "Múltiples escaneos (preguntar entre escaneos)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 es completamente gratuito. Anímese a hacer una donación." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Nombre (opcional)" @@ -849,6 +948,10 @@ msgstr "Nombre (opcional)" msgid "Name missing." msgstr "Falta nombre." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Transferencia nativa" + #: UiStrings.resx$New$Message msgid "New" msgstr "Nuevo" @@ -861,7 +964,7 @@ msgstr "Nuevo perfil" msgid "Next" msgstr "Siguiente" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Siguiente escaneo" @@ -870,12 +973,15 @@ msgstr "Siguiente escaneo" msgid "No device selected." msgstr "No ha seleccionado ningún dispositivo." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "No se han encontrado dispositivos." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Sin hojas en el alimentador." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "No se ha seleccionado ningún proveedor." @@ -895,21 +1001,20 @@ msgstr "Ninguno" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "No es otro escáner PDF" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Ahora no" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Número de escaneos:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "Reconocimiento óptico de caracteres (OCR)" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Descarga de OCR" @@ -918,37 +1023,23 @@ msgstr "Descarga de OCR" msgid "OCR Progress" msgstr "Progreso OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Ajustes de OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Idioma de OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "Modo OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "Aceptar" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Ancho de desplazamiento basado en alineación (WIA)" @@ -956,13 +1047,11 @@ msgstr "Ancho de desplazamiento basado en alineación (WIA)" msgid "Old DSM" msgstr "Viejo DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Un archivo por página" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Un archivo por escaneo" @@ -970,7 +1059,11 @@ msgstr "Un archivo por escaneo" msgid "One or more files could not be downloaded." msgstr "Uno o más archivos no se han podido descargar." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Permitir sólo una sola instancia de NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Abrir carpeta" @@ -978,11 +1071,15 @@ msgstr "Abrir carpeta" msgid "Operation in Progress" msgstr "Operación en marcha" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (nuevo)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Acceso web de Outlook" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Destino" @@ -990,7 +1087,7 @@ msgstr "Destino" msgid "Overwrite File" msgstr "Sobreescribir archivo" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Contraseña de propietario:" @@ -998,8 +1095,8 @@ msgstr "Contraseña de propietario:" msgid "PDF Document (*.pdf)" msgstr "Documento PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Configuración de PDF" @@ -1009,35 +1106,33 @@ msgstr "PDF guardado." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "Imagen PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Tamaño de página:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Origen del papel:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Contraseña" @@ -1045,21 +1140,24 @@ msgstr "Contraseña" msgid "Paste" msgstr "Pegar" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Marcadores de posición" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Posprocesamiento" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Ejecutar OCR automáticamente después de escanear" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Cuando esté listo pulse Iniciar." @@ -1067,7 +1165,7 @@ msgstr "Cuando esté listo pulse Iniciar." msgid "Preview" msgstr "Vista previa" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Vista previa:" @@ -1080,12 +1178,11 @@ msgstr "Anterior" msgid "Print" msgstr "Imprimir" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Configuración de perfil" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Perfil:" @@ -1094,23 +1191,27 @@ msgstr "Perfil:" msgid "Profiles" msgstr "Perfiles" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Preguntar si se selecciona" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Preguntar ruta de archivo" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Proveedor" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Listo para el escaneo {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Recuperar" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Recuperar imágenes digitalizadas" @@ -1122,9 +1223,15 @@ msgstr "Recuperando..." msgid "Recovery Progress" msgstr "Progreso de la recuperación" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Rehacer" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Rehacer {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Recuerde esta configuración" @@ -1140,15 +1247,11 @@ msgstr "Reiniciar" msgid "Reset Image" msgstr "Reiniciar imagen" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Resolución:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Restaurar los valores predeterminados" @@ -1156,6 +1259,14 @@ msgstr "Restaurar los valores predeterminados" msgid "Reverse" msgstr "Invertir" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Revertir todo" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Revertir todo" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Revertir" @@ -1176,7 +1287,6 @@ msgstr "Girar a la izquierda" msgid "Rotate Right" msgstr "Girar a la derecha" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Ejecutar en segundo plano" @@ -1185,22 +1295,26 @@ msgstr "Ejecutar en segundo plano" msgid "Running OCR..." msgstr "Ejecutando OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "Controlador SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Guardar" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Guardar todo" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Guardar todo como imágenes" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Guardar todo como PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Guardar PDF" msgid "Save PDF Progress" msgstr "Progreso del guardado del PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Guardar seleccionado" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Guardar seleccionado como imágenes" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Guardar seleccionado como PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Guardar en un único archivo" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Guardar en múltiples archivos" @@ -1244,29 +1363,45 @@ msgstr "Guardando resultados de lote..." msgid "Saving {0}..." msgstr "Guardando {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Escalar con ventana" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Escalar:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Escanear" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Configuración del escaneo" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Escanear con perfil predeterminado" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Escanear con perfil nuevo" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Escanear con perfil {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Imagen escaneada" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Compartir escáner" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Escaneando página {0}" @@ -1280,11 +1415,14 @@ msgstr "Escaneando página {0} (escaneo {1})..." msgid "Scanning page {0}..." msgstr "Escaneando página {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Buscando dispositivo..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Segundo (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Seleccionar" @@ -1293,7 +1431,10 @@ msgstr "Seleccionar" msgid "Select All" msgstr "Seleccionar todo" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Seleccionar dispositivo" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Seleccione el origen" @@ -1302,7 +1443,6 @@ msgstr "Seleccione el origen" msgid "Select a profile before clicking Scan." msgstr "Seleccione un perfil antes de escanear." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Seleccione uno o más idiomas:" @@ -1311,8 +1451,7 @@ msgstr "Seleccione uno o más idiomas:" msgid "Selected ({0})" msgstr "Seleccionado ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Separar archivos usando Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Separar archivos usando Patch-T" msgid "Set Default" msgstr "Configurar como predeterminado" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Configuración" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Compartir" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Compartir incluso cuando NAPS2 esté cerrado" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Configuración de Escáner Compartido" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Los escáneres compartidos se pueden utilizar desde otros ordenadores de la red local seleccionando \"ESCL Driver\" en la configuración del perfil NAPS2 del resto de equipos." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Enfoque" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Atajo" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Mostrar" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Mostrar la barra de herramientas \"Perfiles\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Mostrar progreso nativo de TWAIN" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Mostrar números de página" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Barra lateral" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Archivos de una única página" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Escaneo sencillo" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Omitir aviso de guardado" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Dividir" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Iniciar" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Detener escáner compartido" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Estirar hasta tamaño de página" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Sujeto:" @@ -1358,12 +1544,11 @@ msgstr "Sujeto:" msgid "TIFF File (*.tiff, *.tif)" msgstr "Imagen TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "Controlador TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Detalles técnicos" @@ -1372,6 +1557,10 @@ msgstr "Detalles técnicos" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "El motor OCR no está disponible. Asegúrese de instalar el paquete necesario:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "La operación OCR demoró" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "El archivo no se pudo sobrescribir porque actualmente está en uso." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "El archivo {0} ya existe. ¿Desea sobreescribirlo?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "El siguiente archivo está cifrado y requiere una contraseña para ser abierto: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "El archivo siguiente está encriptado y requiere una contraseña para abrir:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "El escáner seleccionado está ocupado." msgid "The selected scanner is offline." msgstr "El escáner seleccionado está inactivo." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "El proceso de trabajo falló." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "El proceso de trabajo falló. Verifique el visor de eventos de Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Tema:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Opciones Tiff" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Tiempo entre escaneos (segundos):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Título:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Herramientas" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Implementación Twain:" @@ -1467,6 +1676,22 @@ msgstr "Oficio (356 x 216 mm)" msgid "US Letter (8.5x11 in)" msgstr "Carta (279 x 216 mm)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Sin asignar" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Deshacer" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Deshacer {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Escáner desconocido" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Cambios sin guardar" @@ -1477,7 +1702,7 @@ msgstr "Progreso de la actualización" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "La verificación de actualizaciones está desactivada." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Actualizando..." msgid "Uploading email..." msgstr "Subiendo correo electrónico..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Usar interfaz nativa" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Usar configuración predefinida" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Contraseña de usuario:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "El uso de OCR requiere la descarga de cada idioma que desea escanear." @@ -1515,16 +1737,15 @@ msgstr "Versión {0}" msgid "View" msgstr "Ver" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "Controlador WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Esperando que TWAIN finalice..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Esperando la autorización..." @@ -1532,19 +1753,19 @@ msgstr "Esperando la autorización..." msgid "Waiting for scan {0}..." msgstr "En espera para escanear {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Umbral de blanco" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Versión Wia:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Año" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Año (00-99)" @@ -1561,28 +1782,25 @@ msgstr "No tiene permiso para guardar archivos en esa ubicación." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Tiene cambios sin guardar. ¿Está seguro de que desea salir y descartar los cambios?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" -msgstr "" +msgstr "Zoom" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Zoom actual" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Acercarse" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Alejarse" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" @@ -1590,7 +1808,7 @@ msgstr "pulg" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,28 +1816,33 @@ msgstr "de {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} archivos" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} Dispositivos encontrados." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} ppp" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} imagen(es) escaneada(s) el {1} a las {2} quizás no se ha(n) guardado y se puede(n) recuperar. ¿Deseas recuperarla(s)?" diff --git a/NAPS2.Lib/Lang/po/et.po b/NAPS2.Lib/Lang/po/et.po index 7ef3ae7910..f408295d86 100644 --- a/NAPS2.Lib/Lang/po/et.po +++ b/NAPS2.Lib/Lang/po/et.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Estonian\n" "Language: et\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-bitine värv" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "A3 (297 x 420 mm)" @@ -76,12 +60,15 @@ msgstr "Meist" msgid "Acquiring data..." msgstr "Andmeid pärimas..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Profiili lisavalikud" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Profiili lisavalikud" @@ -93,35 +80,35 @@ msgstr "Kõik ({0})" msgid "All Files" msgstr "Kõik failid" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Luba märkused" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Luba sisu kopeerimine" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Luba dokumendi muutmine" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Luba vormi täitmine" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Luba täiskvaliteetne printimine" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Luba printimine" @@ -133,17 +120,22 @@ msgstr "" msgid "Alternate Interleave" msgstr "" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Selle PDF-faili importimiseks on vaja täiendavat komponenti. Kas soovite selle kohe alla laadida?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Värskenduse installimisel tekkis viga." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "" #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Autoriseerimisel tekkis viga." msgid "An error occurred when trying to auto save." msgstr "Automaatsel salvestamisel tekkis viga." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Värskenduse installimisel tekkis viga." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Faili salvestamisel tekkis viga." @@ -175,7 +171,7 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Käimas on toiming. Kas olete kindel, et soovite tegevuse lõpetada ja tühistada?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "" #: MiscResources.resx$UpdateAvailable$Message @@ -190,7 +186,15 @@ msgstr "OCR-i värskendus on saadaval." msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Rakendage heledust / kontrasti pärast skaneerimist" @@ -222,19 +226,27 @@ msgstr "Oled sa kindel, et soovid {0} elementi kustutada?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Oled sa kindel, et soovid kustutada {0} profiili?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Kas olete kindel, et soovite tühistada {0} pildi muudatused?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Manuse nimi:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autor:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autoriseeri" @@ -242,29 +254,27 @@ msgstr "Autoriseeri" msgid "Auto" msgstr "" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Automaatselt salvestamise seaded" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "" @@ -277,8 +287,8 @@ msgstr "" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "" @@ -298,7 +308,6 @@ msgstr "" msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bitisügavus:" @@ -309,10 +318,10 @@ msgstr "Bitmap-failid (* .bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Must ja valge" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Tühjad lehed" @@ -320,7 +329,6 @@ msgstr "Tühjad lehed" msgid "Brightness / Contrast" msgstr "Heledus / kontrastsus" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Heledus:" @@ -329,24 +337,11 @@ msgstr "Heledus:" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Loobu" @@ -363,7 +358,7 @@ msgstr "Tühistamine...." msgid "Center" msgstr "Keskel" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Muuda" @@ -375,7 +370,7 @@ msgstr "Kontrolli kas uuendused on saadaval" msgid "Checking..." msgstr "Kontrollimine..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Valige e-posti pakkuja" @@ -383,7 +378,6 @@ msgstr "Valige e-posti pakkuja" msgid "Choose Profile" msgstr "Vali profiil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Seadme valik" @@ -397,7 +391,7 @@ msgstr "Puhasta" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "" @@ -405,16 +399,30 @@ msgstr "" msgid "Close" msgstr "Sulge" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Ühilduvus" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Tihendus:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrast:" @@ -435,7 +443,7 @@ msgstr "Kopeerimine..." msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "" @@ -443,7 +451,7 @@ msgstr "" msgid "Crop" msgstr "Lõika" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "" @@ -451,10 +459,14 @@ msgstr "" msgid "Custom ({0}x{1} {2})" msgstr "" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "" -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Päev (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Vaikimisi" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Vaikimisi faili asukoht:" @@ -487,7 +504,6 @@ msgstr "Vaikimisi faili asukoht:" msgid "Deinterleave" msgstr "" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "" msgid "Deskew Progress" msgstr "" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" @@ -509,16 +525,14 @@ msgstr "" msgid "Deskewing..." msgstr "" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Seade:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Näidatav nimi:" @@ -532,12 +546,10 @@ msgstr "" msgid "Donate" msgstr "" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Valmis" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Lae alla" @@ -550,19 +562,47 @@ msgstr "Allalaadimise viga" msgid "Download Needed" msgstr "" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Allalaadimise edenemine" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Muuda" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +616,32 @@ msgstr "PDF e-mailile" msgid "Email PDF Progress" msgstr "" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "E-posti sätted" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Luba automaatne salvestamine" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Krüpti PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Krüpteering" @@ -602,12 +649,15 @@ msgstr "Krüpteering" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Laiendatud metafail (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Viga" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "Hinnanguline allalaadimise suurus: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Tühjade lehtede eemaldamine" @@ -629,24 +679,31 @@ msgstr "" msgid "Feeder" msgstr "Söötur" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Faili nimi" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Faili asukoht:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" @@ -654,7 +711,6 @@ msgstr "" msgid "GIF File (*.gif)" msgstr "GIF fail (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Hankige rohkem keeli" @@ -671,12 +727,11 @@ msgstr "" msgid "Grayscale" msgstr "Halltoonid" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Horisontaalne peegeldamine:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Tund (0-23)" @@ -684,6 +739,10 @@ msgstr "Tund (0-23)" msgid "Hue / Saturation" msgstr "" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Ikoonid pärinevad:" @@ -696,12 +755,12 @@ msgstr "Pilt" msgid "Image Files" msgstr "Pildifailid" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Pildi kvaliteet" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Pildi seadistused" @@ -745,6 +804,10 @@ msgstr "Paigaldamine on lõppenud. Kas soovite taaskäivitada NAPS2 kohe?" msgid "Installation failed." msgstr "Paigaldamine nurjus." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "" @@ -757,11 +820,20 @@ msgstr "JPEG fail (*.jpg, *.jpeg)" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Märksõnad:" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "Keel" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Vasak" @@ -781,33 +857,48 @@ msgstr "Vasak" msgid "Legacy (native UI only)" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Tehke OCR-i abil PDF-id otsitavaks" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metaandmed" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minutid (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Kuu (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Rohkem infot" @@ -819,11 +910,19 @@ msgstr "Liiguta alla" msgid "Move Up" msgstr "Liiguta üles" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Nimi (valikuline)" @@ -849,6 +948,10 @@ msgstr "Nimi (valikuline)" msgid "Name missing." msgstr "Nimi puudub." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "Uus" @@ -861,7 +964,7 @@ msgstr "Uus profiil" msgid "Next" msgstr "Järgmine" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Uus skaneerimine" @@ -870,12 +973,15 @@ msgstr "Uus skaneerimine" msgid "No device selected." msgstr "Ükski seade pole valitud." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Sööturis ei ole ühtegi lehte." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Ükski pakkuja pole valitud." @@ -897,11 +1003,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Mitte praegu" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Skaneerimiste arv:" @@ -909,7 +1015,6 @@ msgstr "Skaneerimiste arv:" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR allalaadimine" @@ -918,37 +1023,23 @@ msgstr "OCR allalaadimine" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCR häälestaja" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR keel:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "Olgu" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Üks fail lehe kohta" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Üks fail skaneerimise kohta" @@ -970,7 +1059,11 @@ msgstr "Üks fail skaneerimise kohta" msgid "One or more files could not be downloaded." msgstr "Ühte või mitut faili ei saanud alla laadida." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Ava kaust" @@ -978,11 +1071,15 @@ msgstr "Ava kaust" msgid "Operation in Progress" msgstr "Töö käib" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Väljund" @@ -990,7 +1087,7 @@ msgstr "Väljund" msgid "Overwrite File" msgstr "Kirjuta fail üle" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Omaniku parool:" @@ -998,8 +1095,8 @@ msgstr "Omaniku parool:" msgid "PDF Document (*.pdf)" msgstr "PDF dokument (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF seaded" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "PNG fail (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Lehe suurus:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Paberi allikas:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Parool" @@ -1045,21 +1140,24 @@ msgstr "Parool" msgid "Paste" msgstr "Aseta" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Kohahoidjad" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Kui olete valmis, vajutage nuppu Start." @@ -1067,7 +1165,7 @@ msgstr "Kui olete valmis, vajutage nuppu Start." msgid "Preview" msgstr "Eelvaade" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Eelvaade:" @@ -1080,12 +1178,11 @@ msgstr "Eelmine" msgid "Print" msgstr "Prindi" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Profiili sätted" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profiilid:" @@ -1094,23 +1191,27 @@ msgstr "Profiilid:" msgid "Profiles" msgstr "Profiilid" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Pakkuja" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "" -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Taasta" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Taasta skännitud pildid" @@ -1122,9 +1223,15 @@ msgstr "Taastamine..." msgid "Recovery Progress" msgstr "" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Mälesta neid seadeid" @@ -1140,15 +1247,11 @@ msgstr "Lähtesta" msgid "Reset Image" msgstr "Lähtesta pilt" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Resolutsioon:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Taasta algsed sätted" @@ -1156,6 +1259,14 @@ msgstr "Taasta algsed sätted" msgid "Reverse" msgstr "" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "" @@ -1176,7 +1287,6 @@ msgstr "Pööramine vasakule" msgid "Rotate Right" msgstr "Pööramine paremale" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Käivita taustal" @@ -1185,7 +1295,6 @@ msgstr "Käivita taustal" msgid "Running OCR..." msgstr "OCR-i käivitamine..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1303,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "Salvesta PDF" msgid "Save PDF Progress" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Salvesta ühte faili" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Salvesta mitmesse faili" @@ -1244,29 +1363,45 @@ msgstr "" msgid "Saving {0}..." msgstr "" -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Mõõtuviimine:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skaneeri" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Skaneerimise seaded" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Skaneeritud pilt" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "" @@ -1280,11 +1415,14 @@ msgstr "" msgid "Scanning page {0}..." msgstr "" -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekund (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Vali" @@ -1293,7 +1431,10 @@ msgstr "Vali" msgid "Select All" msgstr "Vali kõik" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Vali allikas" @@ -1302,7 +1443,6 @@ msgstr "Vali allikas" msgid "Select a profile before clicking Scan." msgstr "Enne skaneerimist valige profiil." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Valige üks või mitu keelt:" @@ -1311,8 +1451,7 @@ msgstr "Valige üks või mitu keelt:" msgid "Selected ({0})" msgstr "" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,37 +1459,84 @@ msgstr "" msgid "Set Default" msgstr "Määra vaikimisi" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Teravdama" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Näita" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Ühelehelised failid" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Ühekordne skaneerimine" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Venita lehe suuruseni" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Teema:" @@ -1358,12 +1544,11 @@ msgstr "Teema:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF fail (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN draiver" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Tehnilised detailid" @@ -1372,6 +1557,10 @@ msgstr "Tehnilised detailid" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "OCR-mootor pole saadaval. Paigaldage vajalik osa:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Faili ei saa üle kirjutada, kuna see on praegu kasutusel." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Fail {0} on juba olemas. Kas soovite selle üle kirjutada?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Järgmine fail on krüptitud ja nõuab parooli avamiseks: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "Valitud skanner on hõivatud." msgid "The selected scanner is offline." msgstr "" -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Skaneerimiste vaheline aeg (sekundid):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Pealkiri:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Salvestamata muudatused" @@ -1487,21 +1712,18 @@ msgstr "Uuendamine..." msgid "Uploading email..." msgstr "E-posti üleslaadimine..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Kasutage eelmääratud sätteid" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Kasutaja parool:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "OCR-i kasutamine eeldab, et laaditakse alla kõik keeled, mida soovite skaneerida." @@ -1515,16 +1737,15 @@ msgstr "" msgid "View" msgstr "" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA draiver" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "" -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1532,19 +1753,19 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "" -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Aasta" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Aasta (00-99)" @@ -1561,21 +1782,18 @@ msgstr "Teil ei ole õigust failide salvestamiseks selles asukohas." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Teil on salvestamata muudatusi. Kas olete kindel, et soovite neist muudatustest loobuda?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Suurendus" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "" diff --git a/NAPS2.Lib/Lang/po/fa.po b/NAPS2.Lib/Lang/po/fa.po index a544852332..9dc88674d8 100644 --- a/NAPS2.Lib/Lang/po/fa.po +++ b/NAPS2.Lib/Lang/po/fa.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Persian\n" "Language: fa\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "۱۰۰ نقطه در اینچ" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "ذخیره:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "۱۲۰۰ نقطه در اینچ" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "۱۵۰ نقطه در اینچ" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "۲۰۰ نقطه در اینچ" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "رنگ ۲۴ بیتی" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "۳۰۰ نقطه در اینچ" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "۴۰۰ نقطه در اینچ" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "۶۰۰ نقطه در اینچ" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "۸۰۰ نقطه در اینچ" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "A3 ۲۹۷ × ۴۲۰ میلی‌متر" @@ -76,12 +60,15 @@ msgstr "درباره" msgid "Acquiring data..." msgstr "دستیابی به داده‌ها..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "پیشرفته" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "تنظیمات پیشرفته پروفایل" @@ -93,35 +80,35 @@ msgstr "همه ({0})" msgid "All Files" msgstr "تمام پرونده‌ها" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "اجازه یادداشت" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "اجازه کپی محتوا" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "اجازه کپی محتوا برای دستیابی پذیری" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "اجازه مونتاژ سند" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "اجازه تغییر سند" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "اجازه پرکردن فرم" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "اجازه چاپ با کیفیت کامل" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "اجازه چاپ" @@ -133,17 +120,22 @@ msgstr "صفحات سفید را بصورت متناوب حذف کردن" msgid "Alternate Interleave" msgstr "جایگذاری متناوب" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "روش انتقال جایگزین" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "برای درون‌ریزی این پرونده PDF به کامپونت جدیدی نیاز است. آیا مایل به دانلود آن هستید؟" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "خطایی هنگام تلاش برای بروزرسانی رخ داد.." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "خطایی در OCR رخ داد." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "خطایی هنگام تلاش برای احراز هویت رخ داد. msgid "An error occurred when trying to auto save." msgstr "یک خطا هنگام ذخیره خودکار رخ داد." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "خطایی هنگام تلاش برای بروزرسانی رخ داد.." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "هنگام تلاش برای ذخیره کردن این پرونده خطایی رخ داد." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "عملیات در حال اجراست، آیا مطمئن هستید می‌خواهد خارج شده و عملیات را لغو کنید؟" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "خطای ناشناخته‌ای هنگام اسکن دسته‌ای." +msgid "An unknown error occurred during the batch scan." +msgstr "" #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -190,7 +186,15 @@ msgstr "یک بروزرسانی برای OCR موجود است.." msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "اعمال روشنایی و درخشش پس از اسکن" @@ -222,19 +226,27 @@ msgstr "آیا مطمئنید که می‌خواهید آیتم {0} حذف کن msgid "Are you sure you want to delete {0} profiles?" msgstr "آیا مطمئنید که می‌خواهید پروفایل {0} را حذف کنید؟" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "آیا مطمئن هستند می‌خواهید تغییرات خود را روی {0} تصویر واگردانی کنید؟" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "نام های ضمیمه:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "مؤلف:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "اعطای اجازه" @@ -242,29 +254,27 @@ msgstr "اعطای اجازه" msgid "Auto" msgstr "خودکار" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "ذخیره خودکار تنظیمات‌" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "افزایش خودکار شماره ( ۱ رقم)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "افزایش خودکار شماره ( ۲ رقم)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "افزایش خودکار شماره ( ۳ رقم)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "افزایش خودکار شماره ( ۴ رقم)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "پس از اسکن OCR بصورت خودکار اجرا شود." @@ -277,8 +287,8 @@ msgstr "ب۴ (۲۵۰ در ۳۵۳ م.م)" msgid "B5 (176x250 mm)" msgstr "ب۵ (۱۷۶ در ۲۵۰ م.م)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "اسکن دسته‌ای" @@ -298,7 +308,6 @@ msgstr "اسکن دسته‌ای به دلیل بروز خطا متوقف شد." msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "عمق بیت:" @@ -309,10 +318,10 @@ msgstr "پرونده Bitmap (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "سیاه و سفید" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "صفحات خالی" @@ -320,7 +329,6 @@ msgstr "صفحات خالی" msgid "Brightness / Contrast" msgstr "روشنایی / سایه روشن" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "روشنایی:" @@ -329,24 +337,11 @@ msgstr "روشنایی:" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "لغو" @@ -363,7 +358,7 @@ msgstr "درحال لغو...." msgid "Center" msgstr "مرکز" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "تغییر" @@ -375,7 +370,7 @@ msgstr "بررسی وجود نسخه جدید" msgid "Checking..." msgstr "درحال بررسی......" -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "یک پست الکترونیکی انتخاب کنید" @@ -383,7 +378,6 @@ msgstr "یک پست الکترونیکی انتخاب کنید" msgid "Choose Profile" msgstr "انتخاب پروفایل" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "انتخاب دستگاه" @@ -397,7 +391,7 @@ msgstr "پاک‌کردن" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "پاک کردن تصویر پس از ذخیره سازی" @@ -405,16 +399,30 @@ msgstr "پاک کردن تصویر پس از ذخیره سازی" msgid "Close" msgstr "بستن" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "سازگاری" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "فشردگی:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "شفافیت:" @@ -435,7 +443,7 @@ msgstr "در حال کپی..." msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "آستانه پوشش" @@ -443,7 +451,7 @@ msgstr "آستانه پوشش" msgid "Crop" msgstr "برش کناره‌ها" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "برش به اندازه صفحه" @@ -451,10 +459,14 @@ msgstr "برش به اندازه صفحه" msgid "Custom ({0}x{1} {2})" msgstr "سفارشی ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "اندازه صفحه سفارشی" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "چرخش سفارشی" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "SMTP خاص" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "سفارشی..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "روز (۳۱-۰)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "پیش‌فرض" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "مسیر پیش‌فرض:" @@ -487,7 +504,6 @@ msgstr "مسیر پیش‌فرض:" msgid "Deinterleave" msgstr "صفحات سفید را حذف کردن" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "میز" msgid "Deskew Progress" msgstr "مرتب‌سازی درحال اجراست" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "مرتب‌سازی صفحات اسکن شده" @@ -509,16 +525,14 @@ msgstr "مرتب‌سازی صفحات اسکن شده" msgid "Deskewing..." msgstr "مرتب‌سازی..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "دستگاه:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "ابعاد" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "نام ظاهری:" @@ -532,12 +546,10 @@ msgstr "" msgid "Donate" msgstr "کمک مالی" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "انجام شد" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "بارگیری" @@ -550,19 +562,47 @@ msgstr "خطای بارگیری" msgid "Download Needed" msgstr "به دانلود نیاز است" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "پیشرفت دانلود" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "دورو" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "ویرایش" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +616,32 @@ msgstr "رایانامه PDF" msgid "Email PDF Progress" msgstr "پیشرفت رایانامه PDF" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "تنظیمات رایانامه" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "فعال سازی ذخیره خودکار" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "رمزگذاری PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "رمزگذاری" @@ -602,12 +649,15 @@ msgstr "رمزگذاری" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "خطا" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "تخمین حجم دانلود: {0} مگابایت" msgid "Exchangeable Image File (*.exif)" msgstr "" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "جلوگیری از صفحات خالی" @@ -629,24 +679,31 @@ msgstr "" msgid "Feeder" msgstr "خوراننده" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "نام پرونده" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "مسیر پرونده:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "پشت و رو" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "صفحات دورو را قرینه کن" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "برای JPEG کیفیت بالا (۸۰+)، همچنین افزایش کیفیت تصویر پروفایل شما با بهترین نتیجه." @@ -654,7 +711,6 @@ msgstr "برای JPEG کیفیت بالا (۸۰+)، همچنین افزایش ک msgid "GIF File (*.gif)" msgstr "پرونده GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "دریافت زبان‌های بیشتر" @@ -671,12 +727,11 @@ msgstr "پست الکترونیکی جی‌میل" msgid "Grayscale" msgstr "بی‌رنگ" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "چیدمان افقی:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "ساعت (۰-۲۳)" @@ -684,6 +739,10 @@ msgstr "ساعت (۰-۲۳)" msgid "Hue / Saturation" msgstr "پرده / غلظت" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "آیکون‌ها از:" @@ -696,12 +755,12 @@ msgstr "تصویر" msgid "Image Files" msgstr "پرونده تصاویر" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "کیفیت تصویر" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "تنظیمات تصویر" @@ -745,6 +804,10 @@ msgstr "نصب تمام شد. آیا می‌خواهید NAPS2 دوباره را msgid "Installation failed." msgstr "نصب شکست خورد." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "جایگذاری" @@ -757,11 +820,20 @@ msgstr "پرونده JPEG (*.jpg, *.jpeg)" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "کیفیت Jpeg" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "کلیدواژه‌ها:" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "زبان" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "چپ" @@ -781,33 +857,48 @@ msgstr "چپ" msgid "Legacy (native UI only)" msgstr "ارث (فقط رابط کاربری بومی)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "بارگذاری تصویر در NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "ایجاد PDF قابل جستجو بوسیله OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "حداکثر کیفیت (پرونده‌های بزرگ)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "اَبَرداده" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "دقیقه (۵۹-۰۰)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "ماه (۱۲-۰۱)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "اطلاعات بیشتر" @@ -819,11 +910,19 @@ msgstr "بردن به پایین" msgid "Move Up" msgstr "بردن به بالا" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "اسکن چندتایی (تاخیر ثابت بین اسکن‌ها)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "اسکن چندتایی (اعلان بین اسکن‌ها)" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 کاملا رایگان است. کمک مالی کنید.." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "نام (اختیاری)" @@ -849,6 +948,10 @@ msgstr "نام (اختیاری)" msgid "Name missing." msgstr "فاقد نام." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "جدید" @@ -861,7 +964,7 @@ msgstr "مجموعه تنظیمات جدید" msgid "Next" msgstr "بعدی" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "اسکن بعدی" @@ -870,12 +973,15 @@ msgstr "اسکن بعدی" msgid "No device selected." msgstr "هیچ دستگاهی انتخاب نشده." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "کاغذی داخل خوراک‌دهنده نیست." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "هیچ فراهم‌کننده‌ای‌ انتخاب نشده." @@ -897,11 +1003,11 @@ msgstr "هیچ‌کدام" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "الان نه" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "تعداد اسکن‌ها:" @@ -909,7 +1015,6 @@ msgstr "تعداد اسکن‌ها:" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "دریافت OCR" @@ -918,37 +1023,23 @@ msgstr "دریافت OCR" msgid "OCR Progress" msgstr "در حال اجرای OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "پیکربندی OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "زبان OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "حالت OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "تأیید" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "عرض Offset براساس چینش (WIA)" @@ -956,13 +1047,11 @@ msgstr "عرض Offset براساس چینش (WIA)" msgid "Old DSM" msgstr "DSM قدیمی" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "یک فایل به ازای هر صفحه" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "یک فایل به ازای هر اسکن" @@ -970,7 +1059,11 @@ msgstr "یک فایل به ازای هر اسکن" msgid "One or more files could not be downloaded." msgstr "یک یا چند پرونده قابل بارگیری نیستند." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "بازکردن پوشه" @@ -978,11 +1071,15 @@ msgstr "بازکردن پوشه" msgid "Operation in Progress" msgstr "عملیات در حال پیش‌روی است" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "دسترسی وب Outlook" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "خروجی" @@ -990,7 +1087,7 @@ msgstr "خروجی" msgid "Overwrite File" msgstr "جای‌نوشت پرونده" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "گذرواژه مالک:" @@ -998,8 +1095,8 @@ msgstr "گذرواژه مالک:" msgid "PDF Document (*.pdf)" msgstr "پرونده PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "تنظیمات پی‌دی‌اف" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "پرونده PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "اندازه صفحه:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "منبع کاغذ:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "گذرواژه" @@ -1045,21 +1140,24 @@ msgstr "گذرواژه" msgid "Paste" msgstr "چسباندن" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "جانگهدار" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "پس پردازش" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "وقتی آماده شدید، شروع را فشار دهید." @@ -1067,7 +1165,7 @@ msgstr "وقتی آماده شدید، شروع را فشار دهید." msgid "Preview" msgstr "پیش‌نمایش" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "پیش‌نمایش:" @@ -1080,12 +1178,11 @@ msgstr "قبلی" msgid "Print" msgstr "چاپ" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "تنظیمات پروفایل" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "پروفایل:" @@ -1094,23 +1191,27 @@ msgstr "پروفایل:" msgid "Profiles" msgstr "پروفایل‌ها" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "اعلان برای مسیر فایل" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "ارائه‌دهنده" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "آماده برای اسکن {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "بازیابی" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "در حال بازیابی" @@ -1122,9 +1223,15 @@ msgstr "پیشرفت بازیابی..." msgid "Recovery Progress" msgstr "پیشروی بازیابی" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "این تنظیمات را بخاطر بسپار" @@ -1140,15 +1247,11 @@ msgstr "تنظیم مجدد" msgid "Reset Image" msgstr "تنظیم مجدد تصویر" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "دقت:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "تنظيمات اوليه" @@ -1156,6 +1259,14 @@ msgstr "تنظيمات اوليه" msgid "Reverse" msgstr "معکوس" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "برگرداندن" @@ -1176,7 +1287,6 @@ msgstr "چرخش چپ" msgid "Rotate Right" msgstr "چرخش راست" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "اجرا در پس‌زمینه" @@ -1185,7 +1295,6 @@ msgstr "اجرا در پس‌زمینه" msgid "Running OCR..." msgstr "اجرای OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "درایور SANE" @@ -1194,6 +1303,11 @@ msgstr "درایور SANE" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "ذخیره PDF" msgid "Save PDF Progress" msgstr "پیشرفت ذخیره PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "ذخیره در یک پرونده" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "ذخیره در چند پرونده" @@ -1244,29 +1363,45 @@ msgstr "ذخیره نتیجه اسکن دسته‌ای..." msgid "Saving {0}..." msgstr "ذخیره {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "بزرگنمایی اندازه پنجره" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "مقیاس:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "اسکن" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "پیکربندی اسکن" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "تصویر اسکن شده" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "اسکن صفحه {0}" @@ -1280,11 +1415,14 @@ msgstr "اسکن صفحه {0} (اسکن {1})..." msgid "Scanning page {0}..." msgstr "اسکن صفحه {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "ثانیه (۰۰-۵۹)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "انتخاب" @@ -1293,7 +1431,10 @@ msgstr "انتخاب" msgid "Select All" msgstr "انتخاب همه" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "انتخاب منبع" @@ -1302,7 +1443,6 @@ msgstr "انتخاب منبع" msgid "Select a profile before clicking Scan." msgstr "قبل از اسکن پروفایلی را انتخاب کنید." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "یک یا چند زبان انتخاب کنید:" @@ -1311,8 +1451,7 @@ msgstr "یک یا چند زبان انتخاب کنید:" msgid "Selected ({0})" msgstr "انتخاب شده ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "جداسازی پرونده‌ها با Patch-T" @@ -1320,37 +1459,84 @@ msgstr "جداسازی پرونده‌ها با Patch-T" msgid "Set Default" msgstr "پیش‌فرض کن" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "وضوح" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "نمایش" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "پرونده‌های تک صفحه‌ای" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "تک اسکن" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "اعلان ذخیره‌سازی را نادیده بگیر" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "شروع" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "کشیدن به اندازه صفحه" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "موضوع:" @@ -1358,12 +1544,11 @@ msgstr "موضوع:" msgid "TIFF File (*.tiff, *.tif)" msgstr "پرونده TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "درایور TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "جزئیات فنی" @@ -1372,6 +1557,10 @@ msgstr "جزئیات فنی" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "موتور OCR در دسترس نیست. مطمئن شوید بسته‌های مورد نیاز را نصب کرده‌اید.:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "پرونده قابل بازنویسی نیست زیاد در حال ا msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "پرونده {0} از قبل وجود دارد. می‌خواهید آن را جای‌نوشت کنید؟" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "این پرونده رمزنگاری شده و برای باز کردن آن به گذرواژه نیاز است : {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "اسکنر انتخابی مشغول است." msgid "The selected scanner is offline." msgstr "اسکنر انتخابی خاموش است." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "گزینه‌های Tiff" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "زمان بین اسکن‌ها (ثانیه):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "عنوان:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "پیاده‌سازی Twain:" @@ -1467,6 +1676,22 @@ msgstr "US Legal (۸٫۵x۱۴ اینچ)" msgid "US Letter (8.5x11 in)" msgstr "US Letter (۸٫۵x۱۱ اینچ)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "تغییرات ذخیره نشده" @@ -1487,21 +1712,18 @@ msgstr "درحال بروزرسانی..." msgid "Uploading email..." msgstr "بارگزاری ایمیل..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "استفاده از رابط کاربری بومی" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "استفاده از تنظیمات پیش تعریف شده" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "گذرواژه کاربر:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "برای استفاده از OCR نیاز است هر زبانی را که می‌خواهید اسکن کنید را دانلود کنید." @@ -1515,16 +1737,15 @@ msgstr "نسخه {0}" msgid "View" msgstr "نما" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "درایور WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "منتظر اتمام کار TWAIN..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "انتظار برای اجازه..." @@ -1532,19 +1753,19 @@ msgstr "انتظار برای اجازه..." msgid "Waiting for scan {0}..." msgstr "منتظر اسکن {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "آستانه سفید" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "سال" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "سال (۹۹-۰۰)" @@ -1561,21 +1782,18 @@ msgstr "شما اجازه ذخیره پرونده در این محل را ندا msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "شما تغییرات ذخیره شده دارید. آیا مطمئنید می‌خواهید خارج شوید و تغییرات را نادیده بگیرید؟" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "بزرگنمایی" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "اندازه واقعی" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "زوم به داخل" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "زوم به خارج" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "{0} / {1} مگابایت" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} پرونده‌ها" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} نقطه در اینچ" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} تصویر اسکن شده از {1} روی {2} ذخیره نشده‌اند و قابل بازیابی هستندُ آیا مایل به بازیابی آنها هستید؟" diff --git a/NAPS2.Lib/Lang/po/fi.po b/NAPS2.Lib/Lang/po/fi.po index 87866c2a50..97909ce746 100644 --- a/NAPS2.Lib/Lang/po/fi.po +++ b/NAPS2.Lib/Lang/po/fi.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Finnish\n" "Language: fi\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "\"Tallenna\" painikkeen oletustoiminto:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "\"Skannaa\" painikkeen oletustoiminto:" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "\"Skannaa\" valikko muuttaa oletusprofiilia" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 laite löytyi." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-bittinen väri" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "800 dpi" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "" @@ -76,12 +60,15 @@ msgstr "Tietoja" msgid "Acquiring data..." msgstr "Haetaan tietoja..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Toiminto" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Lisäasetukset" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Profiilin lisäasetukset" @@ -93,35 +80,35 @@ msgstr "Kaikki ({0})" msgid "All Files" msgstr "Kaikki tiedostot" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Salli kommentit" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Salli sisällön kopiointi" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Salli sisällön kopioiminen helppokäyttötoimintoja varten" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Salli asiakirjan kokoaminen" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Salli asiakirjan muokkaus" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Salli lomakkeiden täyttäminen" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Salli korkealaatuinen tulostus" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Salli tulostaminen" @@ -133,17 +120,22 @@ msgstr "Vaihtoehtoinen lomituksen poisto" msgid "Alternate Interleave" msgstr "Vaihtoehtoinen lomitus" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Vaihtoehtoinen muunnos" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Kysy aina" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Kysy aina" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Tämän PDF-tiedoston tuomiseen tarvitaan lisäkomponentti. Haluatko ladata sen nyt?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Virhe päivityksen asennuksessa." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "OCR:n käytössä tapahtui virhe." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Virhe yritettäessä valtuuttaa." msgid "An error occurred when trying to auto save." msgstr "Virhe automaattisessa tallennuksessa." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Virhe päivityksen asennuksessa." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Virhe tiedoston tallennuksessa." @@ -175,7 +171,7 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Toiminto on käynnissä. Haluatko varmasti poistua ja peruuttaa toiminnon?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "Tuntematon virhe sarjaskannauksessa." #: MiscResources.resx$UpdateAvailable$Message @@ -188,9 +184,17 @@ msgstr "OCR-päivitys on saatavana." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Applen Ajuri" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Sähköposti" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Sovellus" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Käytä kirkkautta/kontrastia skannauksen jälkeen" @@ -222,19 +226,27 @@ msgstr "Haluatko varmasti poistaa {0} tiedostoa?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Haluatko varmasti poistaa {0} profiilia?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Haluatko varmasti lopettaa jakamisen {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Haluatko varmasti peruuttaa kuvien ( {0} ) muutokset?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Määritä" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Liitteen nimi:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Tekijä:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Valtuuta" @@ -242,29 +254,27 @@ msgstr "Valtuuta" msgid "Auto" msgstr "Automaattinen" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Automaattisen tallennuksen asetukset" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "Automaattinen lisäys (1 numero)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Automaattinen lisäys (2 numeroa)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Automaattinen lisäys (3 numeroa)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Automaattinen lisäys (4 numeroa)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Tunnista teksti optisesti automaattisesti skannauksen jälkeen" @@ -277,8 +287,8 @@ msgstr "" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Sarjaskannaus" @@ -296,9 +306,8 @@ msgstr "Sarjaskannaus pysäytetty virheen takia." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Paras" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bittisyvyys:" @@ -309,10 +318,10 @@ msgstr "Bittikarttatiedosto (.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Mustavalkoinen" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Tyhjät sivut" @@ -320,7 +329,6 @@ msgstr "Tyhjät sivut" msgid "Brightness / Contrast" msgstr "Kirkkaus / kontrasti" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Kirkkaus:" @@ -329,24 +337,11 @@ msgstr "Kirkkaus:" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Skanneria ei löytynyt? Lue NAPS2 Flatpakin rajoituksista." + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Peruuta" @@ -363,7 +358,7 @@ msgstr "Peruutetaan...." msgid "Center" msgstr "Keskitä" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Muuta" @@ -375,7 +370,7 @@ msgstr "Tarkista päivitykset" msgid "Checking..." msgstr "Tarkistetaan..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Valitse sähköpostin lähetystapa" @@ -383,7 +378,6 @@ msgstr "Valitse sähköpostin lähetystapa" msgid "Choose Profile" msgstr "Valitse profiili" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Valitse laite" @@ -395,9 +389,9 @@ msgstr "Tyhjennä" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Poista kaikki" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Pyyhi kuvat tallennukset jälkeen" @@ -405,16 +399,30 @@ msgstr "Pyyhi kuvat tallennukset jälkeen" msgid "Close" msgstr "Sulje" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Yhdistä" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Yhteys skannauslaitteen kanssa keskeytyi." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Yhteensopivuus" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Pakkaus:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Yhdistä" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Yhteysvirhe." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrasti:" @@ -433,9 +441,9 @@ msgstr "Kopioidaan..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Tekijänoikeudet {0} NAPS2 Avustajat" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Peitto" @@ -443,7 +451,7 @@ msgstr "Peitto" msgid "Crop" msgstr "Rajaa" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Rajaa sivun kokoon" @@ -451,10 +459,14 @@ msgstr "Rajaa sivun kokoon" msgid "Custom ({0}x{1} {2})" msgstr "Mukautettu ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Mukautettu sivun koko" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Mukautettu kääntäminen" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "Mukautettu SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Mukautettu..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Päivä (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Oletus" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Oletuspolku:" @@ -487,7 +504,6 @@ msgstr "Oletuspolku:" msgid "Deinterleave" msgstr "Lomituksen poisto" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Suorista" msgid "Deskew Progress" msgstr "Suoristuksen eteneminen" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Suorista skannatut sivut" @@ -509,35 +525,31 @@ msgstr "Suorista skannatut sivut" msgid "Deskewing..." msgstr "Suoristetaan..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Laite:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Mittasuhteet" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Näyttönimi:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Dokumentin Korjaus" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Lahjoita" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Valmis" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Lataa" @@ -550,22 +562,50 @@ msgstr "Latausvirhe" msgid "Download Needed" msgstr "Tarvitaan lataus" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Latauksen eteneminen" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Kaksipuoleinen" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL- Ajuri" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL Verkkoajuri" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB-ajuri" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Muokkaa" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Muokkaa sovelluksella {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Muokkaa sovelluksella..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Lähetä kaikki sähköpostitse" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Lähetä kaikki sähköpostilla PDF-muodossa" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,25 +616,32 @@ msgstr "Liitä PDF sähköpostiin" msgid "Email PDF Progress" msgstr "PDF - postituksen eteneminen" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Lähetä valittu sähköpostilla" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Lähetä valittu PDF-tiedostona" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Sähköpostiasetukset" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Salli automaattinen tallennus" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Ota virheenkorjausloki käyttöön" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Salaa PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Salaus" @@ -602,12 +649,15 @@ msgstr "Salaus" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Edistynyt Windows-metatiedosto (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Virhe" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,38 +665,45 @@ msgstr "Arvioitu latauskoko {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Exif-tiedosto (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Ohita tyhjät sivut" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Nopea" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Syöttölaite" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Tiedoston nimi" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Tiedostopolku:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Korjaa valkotasapaino ja poista kohinaa" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Käännä" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Käännä kaksipuoleiset sivut" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Lisää myös kuvan laatua profiilissasi laadukkaita JPEG (80+) kuvia varten." @@ -654,7 +711,6 @@ msgstr "Lisää myös kuvan laatua profiilissasi laadukkaita JPEG (80+) kuvia va msgid "GIF File (*.gif)" msgstr "GIF-tiedosto (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Hae lisää kieliä" @@ -665,18 +721,17 @@ msgstr "Lasi" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Harmaasävy" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Vaaka-asettelu:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Tunti (0-23)" @@ -684,6 +739,10 @@ msgstr "Tunti (0-23)" msgid "Hue / Saturation" msgstr "Sävy / kylläisyys" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Isäntä" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Käytettyjen kuvakkeiden lähde:" @@ -696,12 +755,12 @@ msgstr "Kuva" msgid "Image Files" msgstr "Kuvatiedostot" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Kuvan laatu" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Kuva-asetukset" @@ -745,6 +804,10 @@ msgstr "Asennus on valmis. Halutatko käynnistään NAPS2:n uudelleen?" msgid "Installation failed." msgstr "Asennus epäonnistui." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Käyttöliittymä" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Lomita" @@ -755,13 +818,22 @@ msgstr "JPEG-tiedosto (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000-tiedosto (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Jpeg - laatu" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Säilytä kuvat eri istunnoissa" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Pikanäppäimet" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Avainsanat:" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "Kieli" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Jätä arviosi" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Vasen" @@ -781,33 +857,48 @@ msgstr "Vasen" msgid "Legacy (native UI only)" msgstr "Perinteinen (vain natiivikäyttöliittymä)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Pidätkö NAPS2:sta?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Lataa kuvia NAPS2:een" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Mahdollista haku PDF:issä optisen tekstintunnistuksen avulla" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Manuaalinen IP" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Korkein laatu (suuret tiedostot)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metatiedot" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minuutti (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Kuukausi (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Lisätietoja" @@ -819,11 +910,19 @@ msgstr "Siirrä alas" msgid "Move Up" msgstr "Siirrä ylös" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Useita kieliä" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Useita kieliä..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Monta skannausta (vakioviive skannausten välillä)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Monta skannausta (kysy skannausten välillä)" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 on täysin ilmainen. Harkitsethan lahjoitusta." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Nimi (valinnainen)" @@ -849,6 +948,10 @@ msgstr "Nimi (valinnainen)" msgid "Name missing." msgstr "Nimi puuttuu." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Natiivi Siirto" + #: UiStrings.resx$New$Message msgid "New" msgstr "Uusi" @@ -861,7 +964,7 @@ msgstr "Uusi profiili" msgid "Next" msgstr "Seuraava" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Seuraava skannaus" @@ -870,12 +973,15 @@ msgstr "Seuraava skannaus" msgid "No device selected." msgstr "Laitetta ei valittu." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Laitteita ei löytynyt." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Syöttölaite on tyjä." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Tapaa ei ole valittu." @@ -897,11 +1003,11 @@ msgstr "Ei mitään" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Ei nyt" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Skannauksia:" @@ -909,7 +1015,6 @@ msgstr "Skannauksia:" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR lataus" @@ -918,37 +1023,23 @@ msgstr "OCR lataus" msgid "OCR Progress" msgstr "OCR edistyminen" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCR asetus" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR kieli:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "OCR muoto:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Siirtymän suurus kohdistuksen perusteella (WIA)" @@ -956,13 +1047,11 @@ msgstr "Siirtymän suurus kohdistuksen perusteella (WIA)" msgid "Old DSM" msgstr "Vanha DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Tiedosto joka sivusta" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Skannauksesta yksi tiedosto" @@ -970,7 +1059,11 @@ msgstr "Skannauksesta yksi tiedosto" msgid "One or more files could not be downloaded." msgstr "Tiedosto(j)a ei voitu ladata." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Salli vain yksi NAPS2-instanssi" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Avaa kansio" @@ -978,11 +1071,15 @@ msgstr "Avaa kansio" msgid "Operation in Progress" msgstr "Toiminto käynnissä" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Web-käyttö" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Tulos" @@ -990,7 +1087,7 @@ msgstr "Tulos" msgid "Overwrite File" msgstr "Korvaa tiedosto" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Omistajan salasana:" @@ -998,8 +1095,8 @@ msgstr "Omistajan salasana:" msgid "PDF Document (*.pdf)" msgstr "PDF-tiedosto (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF - asetukset" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "PNG-tiedosto (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Sivun koko:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Paperilähde:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Salasana" @@ -1045,21 +1140,24 @@ msgstr "Salasana" msgid "Paste" msgstr "Liitä" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Paikkamerkit" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Portti" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Jälkikäsittely" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Paina Aloita, kun olet vlamis." @@ -1067,7 +1165,7 @@ msgstr "Paina Aloita, kun olet vlamis." msgid "Preview" msgstr "Esikatsele" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Esikatsele:" @@ -1080,12 +1178,11 @@ msgstr "Edellinen" msgid "Print" msgstr "Tulosta" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Profiiliasetukset" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profiilit:" @@ -1094,23 +1191,27 @@ msgstr "Profiilit:" msgid "Profiles" msgstr "Profiilit" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Kysy jos valittu" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Kysy tiedostopolkua" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Palveluntarjoaja" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Valmis skannaamaan {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Palauta" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Palauta skannatut kuvat" @@ -1122,9 +1223,15 @@ msgstr "Palautetaan..." msgid "Recovery Progress" msgstr "Palauttamisen eteneminen" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Tee uudelleen" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Tee Uudelleen {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Muista asetukset" @@ -1140,15 +1247,11 @@ msgstr "Palauta" msgid "Reset Image" msgstr "Palauta kuva" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Tarkkuus:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Palauta oletusarvoihin" @@ -1156,6 +1259,14 @@ msgstr "Palauta oletusarvoihin" msgid "Reverse" msgstr "Taaksepäin" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Vaihda kaikkien arvot" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Vaihda valittujen arvot" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Palauta" @@ -1176,7 +1287,6 @@ msgstr "Käännä vasemmalle" msgid "Rotate Right" msgstr "Käännä oikealle" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Aja taustalla" @@ -1185,22 +1295,26 @@ msgstr "Aja taustalla" msgid "Running OCR..." msgstr "Optinen tunnistus meneillään..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "SANE - ajuri" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Tallenna" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Tallenna kaikki" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Tallenna kaikki kuvina" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Tallenna kaikki PDF-muodossa" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Tallenna PDF" msgid "Save PDF Progress" msgstr "PDF:n tallennuksen eteneminen" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Tallenna valitut" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Tallenna valitut kuvina" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Tallenna valitut PDF-muodossa" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Tallenna yhteen tiedostoon" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Tallenna useaan tiedostoon" @@ -1244,29 +1363,45 @@ msgstr "Tallennetaan sarjaskannauksen tuloksia..." msgid "Saving {0}..." msgstr "Tallennetaan {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Skaalaa ikkunan mukaan" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Kuvasuhde:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skannaa" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Skannausasetukset" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Skannaa Oletusprofiililla" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Skannaa Uudella Profiili" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Skannaa Profiililla {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Skannattu kuva" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Skannerin Jakaminen" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Skannataan sivua {0}" @@ -1280,11 +1415,14 @@ msgstr "Skannataan sivua {0} (skannaus {1})..." msgid "Scanning page {0}..." msgstr "Skannataan sivua {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Etsitään laitteita..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekunti (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Valitse" @@ -1293,7 +1431,10 @@ msgstr "Valitse" msgid "Select All" msgstr "Valitse kaikki" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Valitse laite" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Valitse mistä" @@ -1302,7 +1443,6 @@ msgstr "Valitse mistä" msgid "Select a profile before clicking Scan." msgstr "Valitse profiili ennen kuin painat Skannaa." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Valitse yksi tai useampi kieli:" @@ -1311,8 +1451,7 @@ msgstr "Valitse yksi tai useampi kieli:" msgid "Selected ({0})" msgstr "Valittu ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Erota tiedostot Patch-T - koodilla" @@ -1320,37 +1459,84 @@ msgstr "Erota tiedostot Patch-T - koodilla" msgid "Set Default" msgstr "Aseta oletus" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Asetukset" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Jaa" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Jaa vaikka NAPS2 olisi suljettu" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Jaetut Skannerin Asetukset" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Jaettuja skannereita voidaan käyttää muista paikallisessa verkossa olevista tietokoneista valitsemalla \"ESCL Driver\" toisen tietokoneen NAPS2-profiiliasetuksista." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Terävöitä" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Pikavalinta" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Näytä" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Näytä \"Profiilit\" työkalupalkki" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Näytä sivunnumerot" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Yhden sivun tiedostot" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Yksittäisskannaus" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Ohita talleta - kysymys" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Jaa" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Aloita" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Venytä sivun kokoon" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Aihe:" @@ -1358,12 +1544,11 @@ msgstr "Aihe:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF-tiedosto (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN-ajuri" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Tekniset yksityiskohdat" @@ -1372,6 +1557,10 @@ msgstr "Tekniset yksityiskohdat" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "OCR - moottori ei ole käytettävissä. Asenna tarvittava osat:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "OCR-operaatio aikakatkaistiin." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Tiedostoa ei voitu korvata, koska se on käytössä." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Tiedosto {0} on jo olemassa. Haluatko korvata tiedoston?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Seuraava tiedosto on salattu ja tarvitsee salasanan avaukseen: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Seuraava tiedosto on salattu ja tarvitsee salasanan avaukseen:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "Valittu skanneri on varattu." msgid "The selected scanner is offline." msgstr "Valittu skanneri on offline-tilassa." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Työprosessi kaatui." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Työprosessi kaatui. Tarkista Windows-tapahtuman katselija." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Tiff - vaihtoehdot" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Aika skannausten välillä (sekuntia):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Otsikko:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Työkalut" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Twain - toteutus:" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Poista määritys" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Kumoa" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Kumoa {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Tallentamattomia muutoksia" @@ -1477,7 +1702,7 @@ msgstr "Päivityksen eteneminen" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Päivityksen tarkistus on poistettu käytöstä." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Päivitetään..." msgid "Uploading email..." msgstr "Lähetään sähköpostia..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Käytä laitteen käyttöliittymää" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Käytä esimääritettyjä asetuksia" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Käyttäjän salasana:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "OCR:n käyttäminen vaatii, että lataat kaikki kielet, joilla haluat skannata." @@ -1515,16 +1737,15 @@ msgstr "Versio {0}" msgid "View" msgstr "Näytä" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA-ajuri" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Odotetaan TWAINin valmistumista..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Odotetaan valtuutusta..." @@ -1532,19 +1753,19 @@ msgstr "Odotetaan valtuutusta..." msgid "Waiting for scan {0}..." msgstr "Odotetaan skannausta {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Valkoisen raja-arvo" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Wia-versio:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Vuosi" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Vuosi (00-99)" @@ -1561,21 +1782,18 @@ msgstr "Sinulla ei ole oikeuksia tallentaa tiedostoja tänne." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Sinulla on tallentamattomia muutoksia. Oletko varma, että haluat lopettaa ja menettää muutokset?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Muuta kokoa" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Alkuperäiseen kokoon" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Suurenna" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Pienennä" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} tiedostoa" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} laitetta löytyi." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} kuva(a) skannattu {1} {2} ei ehkä ole tallennettu ja voidaan palauttaa. Haluatko palauttaa?" diff --git a/NAPS2.Lib/Lang/po/fr.po b/NAPS2.Lib/Lang/po/fr.po index aea47843b0..96b9778bb5 100644 --- a/NAPS2.Lib/Lang/po/fr.po +++ b/NAPS2.Lib/Lang/po/fr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: French\n" "Language: fr\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "100 ppp" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Action par défaut du bouton « Enregistrer » :" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "1200 ppp" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Action par défaut du bouton « Numériser » :" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "150 ppp" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Modifier le profil par défaut avec le menu « Numériser »" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "200 ppp" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "Un périphérique trouvé." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "Couleurs sur 24 bits" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "300 ppp" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "400 ppp" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "600 ppp" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "800 ppp" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "A3 (297 x 420 mm)" @@ -74,14 +58,17 @@ msgstr "À propos" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." -msgstr "Acquisition des données..." +msgstr "Acquisition des données…" + +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Action" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" -msgstr "Avancés" +msgstr "Avancés…" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Paramètres de profil avancés" @@ -93,66 +80,75 @@ msgstr "Tout ({0})" msgid "All Files" msgstr "Tous les fichiers" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Permettre les annotations" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Permettre la copie du contenu" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Permettre la copie du contenu pour l'accessibilité" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Permettre l'assemblage des documents" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Permettre la modification des documents" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Permettre l'édition de formulaires" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Permettre l'impression en qualité maximale" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Permettre l'impression" #: UiStrings.resx$AltDeinterleave$Message msgid "Alternate Deinterleave" -msgstr "Désappairer alternative. [162534>123456]" +msgstr "Désappairer alternativement [162534>123456]" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" -msgstr "Appairer alternative. [135642>123456]" +msgstr "Appairer alternativement [135642>123456]" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Transfert alternatif" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Toujours demander" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Toujours demander" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Un composant supplémentaire est requis pour importer ce fichier PDF. Faut-il le télécharger maintenant ?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Une erreur est survenue en essayant d'installer la mise à jour." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Une erreur est survenue lors de l'exécution de l'OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "Une erreur est survenue en essayent d'autoriser." +msgstr "Une erreur est survenue en essayant d'autoriser." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." msgstr "Une erreur est survenue en essayant d'enregistrer automatiquement." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Une erreur est survenue en essayant d'installer la mise à jour." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Une erreur est survenue en essayant d'enregistrer le fichier." @@ -175,12 +171,12 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Une opération est en cours. Faut-il vraiment quitter et annuler l'opération ?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Une erreur inconnue est survenue lors de l'acquisition par lot." +msgid "An unknown error occurred during the batch scan." +msgstr "Une erreur inconnue s'est produite lors de l'acquisition par lot." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "Une mise à jour est disponible." +msgstr "Mise à jour disponible." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." @@ -188,9 +184,17 @@ msgstr "Une mise à jour pour l'OCR est disponible." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Pilote Apple" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Application" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Appliquer la luminosité/contraste après acquisition" @@ -220,21 +224,29 @@ msgstr "Faut-il vraiment supprimer {0} élément(s) ?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "Faut-il vraiment supprimer {0} profils ?" +msgstr "Faut-il vraiment supprimer {0} profil(s) ?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Faut-il vraiment arrêter de partager « {0} » ?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Faut-il vraiment défaire les modifications sur {0} image(s) ?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Affecter" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" -msgstr "Nom de la pièce jointe:" +msgstr "Nom de la pièce jointe :" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" -msgstr "Auteur:" +msgstr "Auteur :" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autoriser" @@ -242,43 +254,41 @@ msgstr "Autoriser" msgid "Auto" msgstr "Auto" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Paramètres d'enregistrement automatique" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "Numéro auto-incrémenté (1 chiffre)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Numéro auto-incrémenté (2 chiffres)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Numéro auto-incrémenté (3 chiffres)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Numéro auto-incrémenté (4 chiffres)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Exécuter automatiquement l'OCR après la numérisation" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Acquisition par lot" @@ -292,16 +302,15 @@ msgstr "Lot terminé avec succès." #: MiscResources.resx$BatchStatusError$Message msgid "Batch scan stopped due to error." -msgstr "Acquisition par lot arrêtée avec l'erreur." +msgstr "Acquisition par lot arrêtée en raison d'une erreur." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Plus précis" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" -msgstr "Profondeur d'échantillonnage:" +msgstr "Profondeur d'échantillonnage :" #: MiscResources.resx$FileTypeBmp$Message msgid "Bitmap Files (*.bmp)" @@ -309,10 +318,10 @@ msgstr "Fichiers Bitmap (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Noir et blanc" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Pages blanches" @@ -320,33 +329,19 @@ msgstr "Pages blanches" msgid "Brightness / Contrast" msgstr "Luminosité / Contraste" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" -msgstr "Luminosité:" +msgstr "Luminosité :" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Scanner introuvable ? Afficher les limites du Flatpak NAPS2." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Annuler" @@ -357,13 +352,13 @@ msgstr "Annuler le lot" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." -msgstr "Annulation...." +msgstr "Annulation…" #: SettingsResources.resx$HorizontalAlign_Center$Message msgid "Center" msgstr "Centré" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Modifier" @@ -373,9 +368,9 @@ msgstr "Vérifier les mises à jour" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "Vérification..." +msgstr "Vérification…" -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Choisir un fournisseur de messagerie" @@ -383,7 +378,6 @@ msgstr "Choisir un fournisseur de messagerie" msgid "Choose Profile" msgstr "Choisir un profil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Sélectionner" @@ -391,13 +385,13 @@ msgstr "Sélectionner" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message msgid "Clear" -msgstr "Tout supprimer" +msgstr "Tout effacer" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Tout effacer" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Effacer les images après l'enregistrement" @@ -405,19 +399,33 @@ msgstr "Effacer les images après l'enregistrement" msgid "Close" msgstr "Fermer" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Combiner" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "La communication avec le périphérique de numérisation a été interrompue." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Compatibilité" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" -msgstr "" +msgstr "Compression :" + +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Connecter" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Erreur de connexion." -#: FEditProfile.resx$label7.Text$Message #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" -msgstr "Contraste:" +msgstr "Contraste :" #: UiStrings.resx$Copy$Message msgid "Copy" @@ -429,13 +437,13 @@ msgstr "Progression de la copie" #: MiscResources.resx$Copying$Message msgid "Copying..." -msgstr "Copie..." +msgstr "Copie…" #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} Contributeurs NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Seuil de couverture" @@ -443,7 +451,7 @@ msgstr "Seuil de couverture" msgid "Crop" msgstr "Recadrer" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Recadrer aux dimensions de la page" @@ -451,10 +459,14 @@ msgstr "Recadrer aux dimensions de la page" msgid "Custom ({0}x{1} {2})" msgstr "Personnalisé ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Taille de page personnalisée" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Résolution personnalisée" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Rotation personnalisée" @@ -464,30 +476,34 @@ msgid "Custom SMTP" msgstr "SMTP personnalisé" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." -msgstr "Personnalisé..." +msgstr "Personnalisée…" -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Sombre" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Jour (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Prédéfinie" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" -msgstr "Chemin de fichier par défaut:" +msgstr "Chemin de fichier par défaut :" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" msgstr "Désappairer [142536>123456]" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,43 +517,39 @@ msgstr "Réaligner" msgid "Deskew Progress" msgstr "Progression du réalignement" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Réaligner les pages numérisées" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." -msgstr "Réalignement..." +msgstr "Réalignement…" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" -msgstr "Périphérique:" +msgstr "Périphérique" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" -msgstr "" +msgstr "Dimensions" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" -msgstr "Nom d'affichage:" +msgstr "Nom d'affichage :" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Correction de document" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "Don" +msgstr "Faire un don" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Fermer" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Télécharger" @@ -550,22 +562,50 @@ msgstr "Erreur de téléchargement" msgid "Download Needed" msgstr "Téléchargement requis" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Progression du téléchargement" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "PPP" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Recto-verso" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Pilote eSCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "Pilote réseau eSCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "Pilote USB eSCL" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Modifier" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Modifier avec {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Modifier avec..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Courriel à tous" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Tout envoyer en PDF par courriel" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,25 +616,32 @@ msgstr "PDF par courriel" msgid "Email PDF Progress" msgstr "Progression du PDF par courriel" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Courriel sélectionné" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Envoyer la sélection en PDF par courriel" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Paramètres de messagerie" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Activer l'enregistrement automatique" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Activer le journal de débogage" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Chiffrer le PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Chiffrement" @@ -602,12 +649,15 @@ msgstr "Chiffrement" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Fichier EMF (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Erreur" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Erreur lors du démarrage de l’application {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,36 +667,43 @@ msgstr "Taille estimée du téléchargement : {0} Mo" msgid "Exchangeable Image File (*.exif)" msgstr "Fichier EXIF (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Exclure les pages blanches" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Plus rapide" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" -msgstr "Dispositif d'alimentation" +msgstr "Chargeur de documents" + +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Nom du fichier :" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "Nom du fichier" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" +msgstr "Chemin du fichier :" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" -msgstr "Chemin du fichier:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Corriger la balance des blancs et supprimer le bruit" #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Retourner" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Retourner les pages recto-verso" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Retourner les pages recto-verso" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "En JPEG haute qualité (80+), augmenter aussi la Qualité d'Image dans le profil pour de meilleurs résultats." @@ -654,7 +711,6 @@ msgstr "En JPEG haute qualité (80+), augmenter aussi la Qualité d'Image dans l msgid "GIF File (*.gif)" msgstr "Fichier GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Obtenir plus de langues" @@ -665,18 +721,17 @@ msgstr "Vitre" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Niveaux de gris" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" -msgstr "Alignement horizontal:" +msgstr "Alignement horizontal :" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Heure (0-23)" @@ -684,24 +739,28 @@ msgstr "Heure (0-23)" msgid "Hue / Saturation" msgstr "Teinte / Saturation" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Hôte" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" -msgstr "Icônes provenant de:" +msgstr "Icônes provenant de :" #: UiStrings.resx$Image$Message msgid "Image" -msgstr "" +msgstr "Image" #: MiscResources.resx$FileTypeImageFiles$Message msgid "Image Files" msgstr "Fichiers image" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Qualité d'image" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Paramètres d'image" @@ -719,11 +778,11 @@ msgstr "Progression de l'importation" #: MiscResources.resx$ImportingFormat$Message msgid "Importing {0}..." -msgstr "Importation de « {0} »..." +msgstr "Importation de « {0} »…" #: MiscResources.resx$Importing$Message msgid "Importing..." -msgstr "Importation..." +msgstr "Importation…" #: MiscResources.resx$Install$Message msgid "Install {0}" @@ -745,6 +804,10 @@ msgstr "Installation terminée. Faut-il redémarrer NAPS2 maintenant ?" msgid "Installation failed." msgstr "Échec de l'installation." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Interface" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Appairer [135246>123456]" @@ -755,59 +818,87 @@ msgstr "Fichier JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "Fichier JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Qualité JPEG" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Conserver les images d'une session à l'autre" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Raccourcis clavier" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" -msgstr "Mots-clés:" +msgstr "Mots-clés :" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Langue" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Laisser un avis" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" -msgstr "Gauche" +msgstr "À gauche" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" msgstr "Ancienne (interface native seulement)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Clair" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Aimez-vous NAPS2 ?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Charger les images dans NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Rendre possible la recherche dans les PDF issus d'OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "IP manuelle" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Qualité maximale (fichiers volumineux)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Transfert en mémoire" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Métadonnées" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" -msgstr "" +msgstr "Minute (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Mois (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Plus d'informations" @@ -819,11 +910,19 @@ msgstr "Descendre" msgid "Move Up" msgstr "Monter" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Multilingue" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Multilingue…" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Acquisitions multiples (délai fixe entre les numérisations)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Acquisitions multiples (demande à chaque numérisation)" @@ -831,17 +930,17 @@ msgstr "Acquisitions multiples (demande à chaque numérisation)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 est gratuit, mais il est possible de faire un don." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Nom (optionnel)" @@ -849,6 +948,10 @@ msgstr "Nom (optionnel)" msgid "Name missing." msgstr "Nom manquant." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Transfert natif" + #: UiStrings.resx$New$Message msgid "New" msgstr "Nouveau" @@ -861,7 +964,7 @@ msgstr "Nouveau profil" msgid "Next" msgstr "Suivant" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Prochaine acquisition" @@ -870,12 +973,15 @@ msgstr "Prochaine acquisition" msgid "No device selected." msgstr "Aucun périphérique sélectionné." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Aucun périphérique trouvé." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Il n'y a aucune page dans le dispositif d'alimentation." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Aucun fournisseur sélectionné." @@ -895,21 +1001,20 @@ msgstr "Aucun" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Pas maintenant" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" -msgstr "Nombre de numérisations:" +msgstr "Nombre de numérisations :" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Téléchargement OCR (reconnaissance optique de caractères)" @@ -918,37 +1023,23 @@ msgstr "Téléchargement OCR (reconnaissance optique de caractères)" msgid "OCR Progress" msgstr "Progression de l'OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Configuration OCR (reconnaissance optique de caractères)" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" -msgstr "Langue OCR:" +msgstr "Langue de l'OCR :" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "Mode de l'OCR:" - -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message +msgstr "Mode de l'OCR :" + #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Largeur de décalage basée sur l'alignement (WIA)" @@ -956,21 +1047,23 @@ msgstr "Largeur de décalage basée sur l'alignement (WIA)" msgid "Old DSM" msgstr "Ancien DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Un fichier par page" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Un fichier par numérisation" #: MiscResources.resx$FilesCouldNotBeDownloaded$Message msgid "One or more files could not be downloaded." -msgstr "Au moins un fichier n'a pas pu être téléchargé." +msgstr "Un ou plusieurs fichiers n'ont pas pu être téléchargés." + +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "N'autoriser qu'une seule instance de NAPS2" -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Ouvrir un dossier" @@ -978,11 +1071,15 @@ msgstr "Ouvrir un dossier" msgid "Operation in Progress" msgstr "Progression de l'opération" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (nouveau)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "Accès Web pour Outlook" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Sortie" @@ -990,16 +1087,16 @@ msgstr "Sortie" msgid "Overwrite File" msgstr "Écraser le fichier" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" -msgstr "Mot de passe du propriétaire:" +msgstr "Mot de passe du propriétaire :" #: MiscResources.resx$FileTypePdf$Message msgid "PDF Document (*.pdf)" msgstr "Document PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Paramètres PDF" @@ -1009,35 +1106,33 @@ msgstr "PDF enregistré." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "Fichier PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" -msgstr "Taille de la page:" +msgstr "Taille de la page :" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" -msgstr "Source du papier:" +msgstr "Source du papier :" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Mot de passe" @@ -1045,21 +1140,24 @@ msgstr "Mot de passe" msgid "Paste" msgstr "Coller" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Chaînes de substitution" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Post-traitement" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Exécuter l'OCR immédiatement après la numérisation" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Presser « Démarrer » une fois prêt." @@ -1067,9 +1165,9 @@ msgstr "Presser « Démarrer » une fois prêt." msgid "Preview" msgstr "Aperçu" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" -msgstr "Aperçu:" +msgstr "Aperçu :" #: UiStrings.resx$Previous$Message msgid "Previous" @@ -1080,51 +1178,60 @@ msgstr "Précédent" msgid "Print" msgstr "Imprimer" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Paramètres de profil" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" -msgstr "Profil:" +msgstr "Profil :" #: UiStrings.resx$Profiles$Message #: UiStrings.resx$ProfilesFormTitle$Message msgid "Profiles" msgstr "Profils" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Demander si sélectionné" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" -msgstr "Demander le chemin pour le fichier" +msgstr "Demander un chemin pour le fichier" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Fournisseur" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Prêt à numériser {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Récupérer" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Récupérer des images numérisées" #: MiscResources.resx$Recovering$Message msgid "Recovering..." -msgstr "Récupération..." +msgstr "Récupération…" #: MiscResources.resx$RecoveryProgress$Message msgid "Recovery Progress" msgstr "Progression de la récupération" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Rétablir" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Rétablir {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Mémoriser ces paramètres" @@ -1138,17 +1245,13 @@ msgstr "Réinitialiser" #: MiscResources.resx$ResetImage$Message msgid "Reset Image" -msgstr "Réinitialisation de l'image" +msgstr "Réinitialiser l'image" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" -msgstr "Résolution:" +msgstr "Résolution :" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Valeurs par défaut" @@ -1156,13 +1259,21 @@ msgstr "Valeurs par défaut" msgid "Reverse" msgstr "Inverser" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Tout inverser" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Inverser la sélection" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Rétablir" #: SettingsResources.resx$HorizontalAlign_Right$Message msgid "Right" -msgstr "Droite" +msgstr "À droite" #: UiStrings.resx$Rotate$Message msgid "Rotate" @@ -1176,31 +1287,34 @@ msgstr "Pivoter à gauche" msgid "Rotate Right" msgstr "Pivoter à droite" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Exécuter en tâche de fond" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "OCR en exécution..." +msgstr "OCR en cours…" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "Pilote SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Enregistrer" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Tout enregistrer" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Tout enregistrer en images" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Tout enregistrer en PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,71 +1334,95 @@ msgstr "Enregistrer en PDF" msgid "Save PDF Progress" msgstr "Progression de l'enregistrement en PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Enregistrer la sélection" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Enregistrer la sélection en images" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Enregistrer la sélection en PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Enregistrer en un fichier multipages unique" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Enregistrer en fichiers uniques" #: MiscResources.resx$BatchStatusSaving$Message msgid "Saving batch results..." -msgstr "Enregistrement des acquisitions par lot..." +msgstr "Enregistrement des acquisitions par lot…" #: MiscResources.resx$SavingFormat$Message msgid "Saving {0}..." -msgstr "Enregistrement de « {0} »..." +msgstr "Enregistrement de « {0} »…" -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Mettre à l'échelle de la fenêtre" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" -msgstr "Échelle:" +msgstr "Échelle :" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Numériser" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Configuration d'acquisition" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Numériser avec le profil par défaut" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Numériser avec un nouveau profil" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Numériser avec le profil {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Image numérisée" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Partage de scanner" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Acquisition de la page {0}" #: MiscResources.resx$BatchStatusScanPage$Message msgid "Scanning page {0} (scan {1})..." -msgstr "Acquisition page {0} (numérisation {1})..." +msgstr "Acquisition page {0} (numérisation {1})…" #: MiscResources.resx$BatchStatusPage$Message #: MiscResources.resx$ScanProgressPage$Message msgid "Scanning page {0}..." msgstr "Acquisition de la page {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Recherche de périphériques…" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Seconde (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Sélectionner" @@ -1293,7 +1431,10 @@ msgstr "Sélectionner" msgid "Select All" msgstr "Tout sélectionner" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Choisir un périphérique" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Sélectionner une source" @@ -1302,17 +1443,15 @@ msgstr "Sélectionner une source" msgid "Select a profile before clicking Scan." msgstr "Sélectionner un profil avant de cliquer sur « Numériser »." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" -msgstr "Sélectionner une ou plusieurs langues:" +msgstr "Sélectionner une ou plusieurs langues :" #: MiscResources.resx$SelectedCount$Message msgid "Selected ({0})" msgstr "Sélection ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Délimitation des fichiers par Patch-T" @@ -1320,62 +1459,112 @@ msgstr "Délimitation des fichiers par Patch-T" msgid "Set Default" msgstr "Définir par défaut" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Paramètres" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Partager" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Partager même lorsque NAPS2 est fermé" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Paramètres de partage de scanner" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Les scanners partagés peuvent être utilisés à partir d'autres ordinateurs du réseau local en sélectionnant « Pilote eSCL » dans les paramètres de profil NAPS2 de l'autre ordinateur." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" -msgstr "Accentuation" +msgstr "Améliorer la netteté" + +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Raccourci" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Afficher" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Afficher la barre d'outils « Profils »" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Afficher la progression native TWAIN" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Afficher les numéros de page" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Barre latérale" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Fichiers à page unique" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Acquisition unique" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Ignorer l'invite d'enregistrement" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Scinder" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Démarrer" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Arrêt du partage du scanner" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Étirer aux dimensions de la page" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" -msgstr "Sujet:" +msgstr "Sujet :" #: MiscResources.resx$FileTypeTiff$Message msgid "TIFF File (*.tiff, *.tif)" msgstr "Fichier TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "Pilote TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Détails techniques" #: MiscResources.resx$TesseractNotAvailable$Message #: SdkResources.resx$TesseractNotAvailable$Message msgid "The OCR engine is not available. Make sure to install the required package:" -msgstr "Le moteur OCR n'est pas disponible. Merci d'installer le paquet requis:" +msgstr "Le moteur OCR n'est pas disponible. Merci d'installer le paquet requis :" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "L'opération OCR a expiré." #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" -msgstr "Le pilote SANE n'est pas disponible. Merci d'installer les paquets requis:" +msgstr "Le pilote SANE n'est pas disponible. Merci d'installer les paquets nécessaires :" #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message @@ -1394,9 +1583,9 @@ msgstr "Le fichier ne peut pas être écrasé car il est en cours d'utilisation. msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Le fichier « {0} » existe déjà. Faut-il vraiment l'écraser ?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Le fichier suivant est chiffré, son ouverture nécessite un mot de passe : {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Le fichier suivant est chiffré et son ouverture nécessite un mot de passe :" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1426,12 +1615,12 @@ msgstr "Le scanner sélectionné est introuvable." #: MiscResources.resx$NoFeederSupport$Message #: SdkResources.resx$NoFeederSupport$Message msgid "The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver." -msgstr "Le scanner sélectionné ne gère pas de dispositif d'alimentation. Si ce scanner en possède un, essayer d'utiliser un autre pilote." +msgstr "Le scanner sélectionné ne semble pas disposer de dispositif d'alimentation. S'il en possède un, essayer d'utiliser un autre pilote." #: MiscResources.resx$NoDuplexSupport$Message #: SdkResources.resx$NoDuplexSupport$Message msgid "The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver." -msgstr "Le scanner sélectionné ne gère pas le recto-verso. Si ce scanner est supposé le gérer, essayer d'utiliser un autre pilote." +msgstr "Le scanner sélectionné ne semble pas gérer pas le recto-verso (duplex). S'il est supposé le prendre en charge, essayer d'utiliser un autre pilote." #: MiscResources.resx$DeviceBusy$Message #: SdkResources.resx$DeviceBusy$Message @@ -1443,21 +1632,41 @@ msgstr "Le scanner sélectionné est occupé." msgid "The selected scanner is offline." msgstr "Le scanner sélectionné est éteint." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Le processus de travail a planté." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Le processus de travail a planté. Vérifier dans l'Observateur d'évènements Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Thème :" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" -msgstr "Options Tiff" +msgstr "Options TIFF" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Délai entre 2 numérisations (secondes) :" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" -msgstr "Titre:" +msgstr "Titre :" + +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Outils" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" -msgstr "Implémentation Twain:" +msgstr "Implémentation TWAIN :" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" @@ -1467,6 +1676,22 @@ msgstr "US Legal (8.5x14 p)" msgid "US Letter (8.5x11 in)" msgstr "US Letter (8.5x11 p)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Désaffecter" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Annuler" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Annuler {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Scanner inconnu" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Modifications non enregistrées" @@ -1477,81 +1702,77 @@ msgstr "Progression de la mise à jour" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Vérification des mises à jour désactivée." #: MiscResources.resx$Updating$Message msgid "Updating..." -msgstr "Mise à jour..." +msgstr "Mise à jour…" #: MiscResources.resx$UploadingEmail$Message msgid "Uploading email..." -msgstr "Envoi du courriel..." +msgstr "Envoi du courriel…" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Interface native" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Paramètres prédéfinis" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" -msgstr "Mot de passe utilisateur:" +msgstr "Mot de passe utilisateur :" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." -msgstr "Télécharger chaque langue OCR appropriée au document numérisé." +msgstr "Télécharger chaque langue OCR appropriée aux documents numérisés." #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message msgid "Version {0}" -msgstr "" +msgstr "Version {0}" #: UiStrings.resx$View$Message msgid "View" msgstr "Afficher" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "Pilote WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." -msgstr "En attente de la fin du transfert TWAIN..." +msgstr "En attente de la fin du transfert TWAIN…" -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "En attente d'autorisation..." #: MiscResources.resx$BatchStatusWaitingForScan$Message msgid "Waiting for scan {0}..." -msgstr "En attente de l'acquisition {0}..." +msgstr "En attente de l'acquisition {0}…" -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Seuil du blanc" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Version WIA :" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Année" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Année (00-99)" #: MiscResources.resx$PdfNoPermissionToExtractContent$Message #: SdkResources.resx$PdfNoPermissionToExtractContent$Message msgid "You do not have permission to copy content from the file '{0}'." -msgstr "Vous n'avez pas la permission de copier le contenu du fichier « {0} ».." +msgstr "Vous n'avez pas la permission de copier le contenu du fichier « {0} »." #: MiscResources.resx$DontHavePermission$Message msgid "You don't have permission to save files at this location." @@ -1561,28 +1782,25 @@ msgstr "Vous n'avez pas la permission d'enregistrer des fichiers à cet emplacem msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Il existe des modifications non enregistrées. Faut-il vraiment quitter et les perdre ?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" -msgstr "" +msgstr "Zoom" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Zoom courant" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Zoom avant" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Zoom arrière" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" @@ -1590,7 +1808,7 @@ msgstr "pouce" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,34 +1816,39 @@ msgstr "de {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "{0} / {1} Mo" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "Fichier(s) {0} / {1}" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} périphériques trouvés." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} ppp" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" -msgstr "{0} image(s) numérisée(s) le {1} à {2} n'a(ont) peut-être pas été enregistrée(s), et est(sont) récupérable(s). Faut-il procéder à la récupération ?" +msgstr "{0} image(s) numérisée(s) le {1} à {2} n'a(ont) peut-être pas été enregistrée(s), et est(sont) récupérable(s). Faut-il procéder à leur récupération ?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." -msgstr "{0} images enregistrées." +msgstr "{0} image(s) enregistrée(s)." #: MiscResources.resx$PdfStatus$Message #: UiStrings.resx$XOfY$Message diff --git a/NAPS2.Lib/Lang/po/he.po b/NAPS2.Lib/Lang/po/he.po index 827eef660f..f0f18f0971 100644 --- a/NAPS2.Lib/Lang/po/he.po +++ b/NAPS2.Lib/Lang/po/he.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Hebrew\n" "Language: he\n" @@ -19,69 +19,56 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "פעולת ברירת מחדל לכפתור „שמירה”:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "פעולת ברירת מחדל לכפתור „סריקה”:" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "תפריט „סריקה” משנה את פרופיל ברירת המחדל" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "נמצא מכשיר." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "צבע 24 סיביות" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "A3 (297x240 מ\"מ)" +msgstr "A3 (297x240 מ״מ)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "A4 (210x297 מ\"מ)" +msgstr "A4 (210x297 מ״מ)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "A5 (148x210 מ\"מ)" +msgstr "A5 (148x210 מ״מ)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message msgid "About" -msgstr "אודות" +msgstr "על אודות" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." -msgstr "מייבא נתונים..." +msgstr "נתונים מתקבלים…" + +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "פעולה" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "מתקדם" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "הגדרות שרת מתקדמות" @@ -93,35 +80,35 @@ msgstr "הכל ({0})" msgid "All Files" msgstr "כל הקבצים" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "לאפשר ביאורים" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "לאפשר העתקת תוכן" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "לאפשר העתקת תוכן עבור נגישות" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "לאפשר הרכבת מסמך" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "לאפשר שינויים במסמך" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "לאפשר מילוי טופס" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "לאפשר הדפסה באיכות מירבית" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "לאפשר הדפסה" @@ -133,25 +120,34 @@ msgstr "ביטול הפרדה מסורג" msgid "Alternate Interleave" msgstr "הפרדה מסורג" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "תמיד לשאול" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "תמיד לבקש אישור" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "נדרש רכיב נוסף כדי לייבא קובץ PDF זה. האם ברצונך להוריד אותו כעת?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "אירעה שגיאה בעת ניסיון להתקין את העדכון." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "אירעה שגיאה בהרצת זיהוי תווים אופטי." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "אירעה שגיאה בניסיון לאימות." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." -msgstr "אירעה שגיאה בעת שמירת אוטומטי." +msgstr "אירעה שגיאה בעת שמירה אוטומטית." + +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "אירעה שגיאה בעת ניסיון להתקין את העדכון." #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." @@ -159,7 +155,7 @@ msgstr "תקלה בשמירת הקובץ." #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "שגיאה התרחשה בעת הנסיון לשלוח מייל.." +msgstr "שגיאה התרחשה בעת הנסיון לשלוח דוא״ל." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." @@ -172,11 +168,11 @@ msgstr "אירעה שגיאה במנהל ההתקן של הסורק." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "פעולה מתבצעת. האם אתה בטוח שברצונך לצאת ולבטל את הפעולה?" +msgstr "פעולה מתבצעת. לצאת ולבטל את הפעולה?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "שגיאה בלתי ידוע אירע בעת סריקת אצווה." +msgid "An unknown error occurred during the batch scan." +msgstr "אירעה שגיאה בלתי־ידועה במהלך הסריקה במרוכז." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -184,237 +180,249 @@ msgstr "קיים עדכון זמין." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "עדכון ל- OCR זמין." +msgstr "יש עדכון לזיהוי התווים האופטי." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "מנהל התקן של Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "דוא״ל Apple" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "יישום" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" -msgstr "החל בהירות/ניגודיות לאחר הסריקה" +msgstr "החלת בהירות/ניגודיות לאחר הסריקה" #: UiStrings.resx$ApplyToSelected$Message msgid "Apply to all {0} selected images" -msgstr "החל על כל {0} התמונות שנבחרו" +msgstr "החלה על כל {0} התמונות שנבחרו" #: MiscResources.resx$ConfirmCancelBatch$Message msgid "Are you sure you want to cancel the batch scan?" -msgstr "האם אתה בטוח שאתה רוצה לבטל את קבוצות הסריקות? " +msgstr "לבטל את הסריקות במרוכז?" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" -msgstr "בטוחים שתרצו למחוק {0} פריט/ים?" +msgstr "לפנות {0} פריט/ים?" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "בטוחים שתרצו למחוק \"{0}\"?" +msgstr "למחוק את „{0}”?" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" -msgstr "בטוחים שתרצו למחוק את הפרופיל {0}?" +msgstr "למחוק את הפרופיל {0}?" #: MiscResources.resx$ConfirmDeleteItems$Message msgid "Are you sure you want to delete {0} item(s)?" -msgstr "בטוחים שתרצו למחוק {0} פריט/ים?" +msgstr "למחוק {0} פריט/ים?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "בטוחים שתרצו למחוק את הפרופיל/ים {0}?" +msgstr "למחוק {0} פרופיל/ים?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "להפסיק לשתף את {0}?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" -msgstr "לבטל שינויים ב-{0} תמונה/תמונות?" +msgstr "להחזיר את השינויים שביצעת ב־{0} תמונות?" + +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "הקצאה" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "שם קובץ מצורף:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "מאת:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "אימות" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" msgstr "אוטומטי" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "הגדרות שמירה אוטומטית" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "מס' מונה אוטומטי (ספרה אחת)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "מס׳ מונה אוטומטי (ספרה אחת)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" -msgstr "מס' מונה אוטומטי (שתי ספרות)" +msgstr "מס׳ מונה אוטומטי (שתי ספרות)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" -msgstr "מס' מונה אוטומטי (שלוש ספרות)" +msgstr "מס׳ מונה אוטומטי (שלוש ספרות)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" -msgstr "מס' מונה אוטומטי (ארבע ספרות)" +msgstr "מס׳ מונה אוטומטי (ארבע ספרות)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "הרץ OCR בצורת אוטומטית בסוף הסריקה" +msgstr "הרצת זיהוי תווים אופטי אוטומטית בסוף הסריקה" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "B4 (250x353 מ\"מ)" +msgstr "B4 (250x353 מ״מ)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "B5 (176x250 מ\"מ)" +msgstr "B5 (176x250 מ״מ)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" -msgstr "סריקה באצווה" +msgstr "סריקה במרוכז" #: MiscResources.resx$BatchStatusCancelled$Message msgid "Batch cancelled." -msgstr "האצווה בוטלה." +msgstr "הסריקה במרוכז בוטלה." #: MiscResources.resx$BatchStatusComplete$Message msgid "Batch completed successfully." -msgstr "אצווה הושלמה בהצלחה." +msgstr "סריקה במרוכז הושלמה בהצלחה." #: MiscResources.resx$BatchStatusError$Message msgid "Batch scan stopped due to error." -msgstr "סריקה אצווה נעצרה עקב שגיאה." +msgstr "סריקה במרוכז נעצרה עקב שגיאה." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "מיטבי" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "עומק סיביות:" #: MiscResources.resx$FileTypeBmp$Message msgid "Bitmap Files (*.bmp)" -msgstr "קובץ מפת סיביות (*.bmp)" +msgstr "קובץ מפת סיביות (‎*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "שחור/לבן" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "דפים ריקים" #: UiStrings.resx$BrightnessContrast$Message msgid "Brightness / Contrast" -msgstr "" +msgstr "בהירות / ניגודיות" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "בהירות:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "הסורק לא נמצא? להלן הפרטים על המגבלות של ה־Flatpak של NAPS2." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "ביטול" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" -msgstr "" +msgstr "ביטול סריקה במרוכז" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." -msgstr "" +msgstr "בהליכי ביטול…" #: SettingsResources.resx$HorizontalAlign_Center$Message msgid "Center" msgstr "ממורכז" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" -msgstr "" +msgstr "שינוי" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "בדוק עדכונים" +msgstr "איתור עדכונים" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "" +msgstr "מתבצעת בדיקה…" -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "" +msgstr "נא לבחור ספק דוא״ל" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" -msgstr "בחר פרופיל" +msgstr "בחירת פרופיל" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" -msgstr "בחר התקן" +msgstr "בחירת התקן" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message msgid "Clear" -msgstr "מחק הכל" +msgstr "פינוי" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "לפנות הכול" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" -msgstr "נקה תמונות לאחר שמירה" +msgstr "פינוי תמונות לאחר שמירה" #: MiscResources.resx$Close$Message msgid "Close" -msgstr "" +msgstr "סגירה" + +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "שילוב" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "התקשורת מול מכשיר הסריקה נקטעה." -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "תאימות" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "דחיסה:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "התחברות" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "שגיאת חיבור." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "ניגודיות:" @@ -433,9 +441,9 @@ msgstr "העתקה..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "כל הזכויות שמורות {0} לתורמים של לא עוד סתם סורק 2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "סף כיסוי" @@ -443,51 +451,59 @@ msgstr "סף כיסוי" msgid "Crop" msgstr "חיתוך" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" -msgstr "חתוך לגודל הדף" +msgstr "חיתוך לגודל הדף" #: MiscResources.resx$CustomPageSizeFormat$Message msgid "Custom ({0}x{1} {2})" msgstr "מותאם אישית ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "גודל נייר מותאם אישית" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "רזולוציה בהתאמה אישית" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "סיבוב מותאם אישית" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" -msgstr "" +msgstr "SMTP בהתאמה אישית" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." -msgstr "מותאם אישית..." +msgstr "התאמה אישית…" + +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "כהה" -#: FPlaceholders.resx$label6.Text$Message +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "יום (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "ברירת מחדל" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "שם קובץ ברירת מחדל:" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" -msgstr "בטל הפרדה" +msgstr "ביטול הפרדה" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -495,169 +511,209 @@ msgstr "מחיקה" #: UiStrings.resx$Deskew$Message msgid "Deskew" -msgstr "" +msgstr "תיקון הטייה" #: MiscResources.resx$AutoDeskewProgress$Message msgid "Deskew Progress" -msgstr "" +msgstr "התקדמות תיקון הטייה" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" -msgstr "" +msgstr "תיקון הטיית דפים שנסרקו" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." -msgstr "" +msgstr "ההטייה מתוקנת…" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "התקן:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" -msgstr "" +msgstr "ממדים" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "שם תצוגה:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "תיקון מסמך" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "תרום" +msgstr "תרומה" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "אישור" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "הורדה" #: MiscResources.resx$DownloadError$Message msgid "Download Error" -msgstr "תקלה בהורדה" +msgstr "שגיאה בהורדה" #: MiscResources.resx$DownloadNeeded$Message msgid "Download Needed" -msgstr "" +msgstr "נדרשת הורדה" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "התקדמות ההורדה" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "נק׳ לאינץ׳" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" -msgstr "דופלקס (דו-צדדי)" +msgstr "דופלקס (דו־צדדי)" + +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "מנהל התקן ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "מנהל התקן רשת מסוג ESCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "מנהל התקן USB מסוג ESCL" #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "עריכה" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "עריכה עם {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "עריכה עם…" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "שליחה של הכול בדוא״ל" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "שליחה של הכול כ־PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "שליחת PDF בדוא\"ל" +msgstr "שליחת PDF בדוא״ל" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" -msgstr "" +msgstr "התקדמות שליחת PDF בדוא״ל" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "שליחת הנבחרים בדוא״ל" #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "שליחת הנבחרים כ־PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" -msgstr "הגדרות דוא\"ל" +msgstr "הגדרות דוא״ל" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" -msgstr "הפעל שמירה אוטומטית" +msgstr "הפעלת שמירה אוטומטית" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "הפעלת תיעוד ניפוי שגיאות" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" -msgstr "הצפן PDF" +msgstr "הצפנת PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "הצפנה" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "קובצי מטא מורחבים (‎*.emf)" +msgstr "קובצי על מורחבים (‎*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "שגיאה" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "שגיאה בהפעלת היישום {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" -msgstr "גודל מוערך להורדה: {0} מ\"ב" +msgstr "גודל מוערך להורדה: {0} מ״ב" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "קובץ תמונה בר-החלפה (*.exif)" +msgstr "קובץ תמונה בר־החלפה (‎*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" -msgstr "" +msgstr "החרגת דפים ריקים" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "מהיר" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "מזין מסמכים" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "שם קובץ" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "שם קובץ:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "נתיב קובץ:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "תיקון איזון לבן והסרת רעש" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "היפוך" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "היפוך הצד האחורי בדפים דו־צדדיים" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" -msgstr "" +msgstr "היפוך דפים דו־צדדיים" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "" +msgstr "לקבלת איכויות JPEG גבוהות (80 ומעלה), רצוי להגדיל גם את איכות התמונה בפרופיל שלך לתוצאות מיטביות." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" -msgstr "קובץ GIF (*.gif)" +msgstr "קובץ GIF‏ (‎*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" -msgstr "קבל שפות נוספות" +msgstr "משיכת שפות נוספות" #: SettingsResources.resx$Source_Glass$Message msgid "Glass" @@ -665,28 +721,31 @@ msgstr "זכוכית (משטח)" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "גווני אפור" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "יישור אופקי:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "שעה (0-23)" #: UiStrings.resx$HueSaturation$Message msgid "Hue / Saturation" -msgstr "" +msgstr "גוון / רוויה" + +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/מארח" #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" -msgstr "צלמיות מתוך:" +msgstr "סמלים מתוך:" #: UiStrings.resx$Image$Message msgid "Image" @@ -694,14 +753,14 @@ msgstr "תמונה" #: MiscResources.resx$FileTypeImageFiles$Message msgid "Image Files" -msgstr "קבצי תמונה" +msgstr "קובצי תמונה" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "איכות תמונה" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "הגדרות תמונה" @@ -715,19 +774,19 @@ msgstr "ייבוא" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" -msgstr "" +msgstr "התקדמות ייבוא" #: MiscResources.resx$ImportingFormat$Message msgid "Importing {0}..." -msgstr "" +msgstr "מתבצע ייבוא של {0}…" #: MiscResources.resx$Importing$Message msgid "Importing..." -msgstr "מייבא..." +msgstr "מתבצע ייבוא…" #: MiscResources.resx$Install$Message msgid "Install {0}" -msgstr "" +msgstr "התקנת {0}" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" @@ -739,11 +798,15 @@ msgstr "תקלה בהתקנה" #: MiscResources.resx$InstallCompletePromptRestart$Message msgid "Installation complete. Do you want to restart NAPS2 now?" -msgstr "ההתקנה הושלמה. האם ברצונך לאתחל את התוכנה?" +msgstr "ההתקנה הושלמה. להפעיל את התוכנה מחדש עכשיו?" #: MiscResources.resx$InstallFailed$Message msgid "Installation failed." -msgstr "תקלה בהתקנה." +msgstr "ההתקנה נכשלה." + +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "מנשק" #: UiStrings.resx$Interleave$Message msgid "Interleave" @@ -751,63 +814,91 @@ msgstr "הפרדה" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" -msgstr "קובץ JPEG (*.jpg, *.jpeg)" +msgstr "קובץ JPEG‏ (‎*.jpg,‏ ‎*.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "קובץ JPEG2000‏ (‎*.jp2,‏ ‎*.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "איכות Jpeg" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "לשמור תמונות בין הפעלות" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "קיצורי מקלדת" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "מילות מפתח:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "שפה" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "כתיבת ביקורת" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "שמאל" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" -msgstr "" +msgstr "מיושן (ממשק משתמש מקורי בלבד)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "בהיר" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "אהבת את NAPS2?" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" -msgstr "טען תמונות לתוך NAPS2" +msgstr "טעינת תמונות לתוך NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" -msgstr "הכן קובצי PDF הניתנים לחיפוש באמצעות OCR" +msgstr "הפקת קובצי PDF שאפשר לחפש בהם עם זיהוי תווים אופטי" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "IP ידני" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "איכות מירבית (קבצים גדולים)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "העברת זיכרון" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" -msgstr "מטא-נתונים" +msgstr "נתוני־על" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "דקה (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "חודש (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "מידע נוסף" @@ -819,36 +910,48 @@ msgstr "להזיז למטה" msgid "Move Up" msgstr "להזיז למעלה" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "מגוון שפות" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "מגוון שפות…" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" -msgstr "" +msgstr "ריבוי סריקות (הפרש קבוע בין הסריקות)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" -msgstr "" +msgstr "ריבוי סריקות (הצגת בקשה בין הסריקות)" #: MiscResources.resx$NAPS2$Message #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "לא עוד סתם סורק 2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "לא עוד סתם סורק 2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "NAPS2 הוא לגמרי בחינם. שקול לבצע תרומה." +msgstr "NAPS2 הוא לגמרי בחינם. נא לשקול לתרום לנו." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" -msgstr "שם: (לא חובה)" +msgstr "שם (רשות)" #: MiscResources.resx$NameMissing$Message msgid "Name missing." msgstr "שם חסר." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "העברה טבעית" + #: UiStrings.resx$New$Message msgid "New" msgstr "חדש" @@ -861,24 +964,27 @@ msgstr "פרופיל חדש" msgid "Next" msgstr "הבא" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" -msgstr "" +msgstr "הסריקה הבאה" #: MiscResources.resx$NoDeviceSelected$Message #: SdkResources.resx$NoDeviceSelected$Message msgid "No device selected." msgstr "לא נבחר התקן." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "לא נמצאו מכשירים." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "אין דפים במזין המסמכים." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "" +msgstr "לא נבחר ספק." #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message @@ -887,82 +993,65 @@ msgstr "לא נמצא סורק." #: MiscResources.resx$NoUpdates$Message msgid "No updates available." -msgstr "" +msgstr "אין עדכונים זמינים." #: SettingsResources.resx$TiffComp_None$Message msgid "None" -msgstr "" +msgstr "אין" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "לא עוד סורק PDF" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "לא כעת" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" -msgstr "" +msgstr "מספר הסריקות:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "זיהוי תווים אופטי" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" -msgstr "הורדת OCR" +msgstr "הורדת זיהוי תווים אופטי" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "" +msgstr "התקדמות זיהוי תווים אופטי" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" -msgstr "הגדרות OCR" +msgstr "הגדרות זיהוי תווים אופטי" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" -msgstr "שפת OCR:" +msgstr "שפת זיהוי תווים אופטי:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "מצב OCR:" - -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message +msgstr "מצב זיהוי תווים אופטי:" + #: UiStrings.resx$OK$Message msgid "OK" msgstr "אישור" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" -msgstr "" +msgstr "רוחב היסט לפי יישור (WIA)" #: SettingsResources.resx$TwainImpl_OldDsm$Message msgid "Old DSM" -msgstr "" +msgstr "מנהל מקורות נתונים ישן" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "קובץ נפרד לכל דף" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "קובץ אחד לכל סריקה" @@ -970,36 +1059,44 @@ msgstr "קובץ אחד לכל סריקה" msgid "One or more files could not be downloaded." msgstr "תקלה בהורדת קובץ אחד או יותר." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "לאפשר רק עותק יחיד של NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" -msgstr "פתח תיקייה" +msgstr "פתיחת תיקייה" #: MiscResources.resx$ActiveOperations$Message msgid "Operation in Progress" -msgstr "" +msgstr "פעולה מתבצעת" + +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (חדש)" #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Web Access" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" -msgstr "" +msgstr "פלט" #: MiscResources.resx$OverwriteFile$Message msgid "Overwrite File" msgstr "החלפת קובץ" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "סיסמת בעלים:" #: MiscResources.resx$FileTypePdf$Message msgid "PDF Document (*.pdf)" -msgstr "" +msgstr "מסמך PDF‏ (‎*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "הגדרות PDF" @@ -1009,65 +1106,66 @@ msgstr "PDF נשמר." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "קובץ PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "גודל עמוד:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "מקור נייר:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "סיסמה" #: UiStrings.resx$Paste$Message msgid "Paste" -msgstr "הדבק" - -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +msgstr "הדבקה" + +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" -msgstr "שומרי מקום" +msgstr "ממלאי מקום" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "פתחה" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" -msgstr "" +msgstr "עיבוד סופי" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "הרצת זיהוי תווים אופטי מקדים בסוף הסריקה" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." -msgstr "" +msgstr "נא ללחוץ על התחלה כשאפשר להתחיל." #: UiStrings.resx$PreviewFormTitle$Message msgid "Preview" msgstr "תצוגה מקדימה" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "תצוגה מקדימה:" @@ -1080,85 +1178,98 @@ msgstr "הקודם" msgid "Print" msgstr "הדפסה" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "הגדרות פרופיל" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" -msgstr "" +msgstr "פרופיל:" #: UiStrings.resx$Profiles$Message #: UiStrings.resx$ProfilesFormTitle$Message msgid "Profiles" msgstr "פרופילים" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "לבקש אישור אם נבחר" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" -msgstr "" +msgstr "לבקש נתיב קובץ" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "ספק" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." -msgstr "" +msgstr "אפשר להתחיל לסרוק {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "שחזור" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "שחזור תמונות שנסרקו" #: MiscResources.resx$Recovering$Message msgid "Recovering..." -msgstr "" +msgstr "מתבצע שחזור…" #: MiscResources.resx$RecoveryProgress$Message msgid "Recovery Progress" -msgstr "" +msgstr "התקדמות שחזור" + +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "ביצוע מחדש" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "ביצוע {0} מחדש" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" -msgstr "זכור את ההגדרות" +msgstr "לזכור את ההגדרות" #: UiStrings.resx$Reorder$Message msgid "Reorder" -msgstr "סדר מחדש" +msgstr "סידור מחדש" #: UiStrings.resx$Reset$Message msgid "Reset" -msgstr "אתחל" +msgstr "איפוס" #: MiscResources.resx$ResetImage$Message msgid "Reset Image" -msgstr "אפס תמונה" +msgstr "איפוס תמונה" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "רזולוציה:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" -msgstr "שחזור ברירת מחדל" +msgstr "שחזור ברירות מחדל" #: UiStrings.resx$Reverse$Message msgid "Reverse" -msgstr "הפוך" +msgstr "היפוך" + +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "היפוך של הכול" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "היפוך הנבחרים" #: UiStrings.resx$Revert$Message msgid "Revert" -msgstr "בטל שינויים" +msgstr "ביטול שינויים" #: SettingsResources.resx$HorizontalAlign_Right$Message msgid "Right" @@ -1166,7 +1277,7 @@ msgstr "ימין" #: UiStrings.resx$Rotate$Message msgid "Rotate" -msgstr "סובב" +msgstr "סיבוב" #: UiStrings.resx$RotateLeft$Message msgid "Rotate Left" @@ -1176,31 +1287,34 @@ msgstr "סיבוב לשמאל" msgid "Rotate Right" msgstr "סיבוב לימין" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "" +msgstr "הרצה ברקע" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "" +msgstr "זיהוי התווים האופטי רץ…" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "מנהל התקן SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "שמירה" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "שמירה של הכול" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "שמירה של הכול כתמונות" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "שמירה של הכול כ־PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1209,7 +1323,7 @@ msgstr "לשמור תמונות" #: MiscResources.resx$SaveImagesProgress$Message msgid "Save Images Progress" -msgstr "" +msgstr "התקדמות שמירת תמונות" #: MiscResources.resx$SavePdf$Message #: UiStrings.resx$SavePdf$Message @@ -1218,205 +1332,280 @@ msgstr "לשמור PDF" #: MiscResources.resx$SavePdfProgress$Message msgid "Save PDF Progress" -msgstr "" +msgstr "התקדמות שמירת PDF" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "שמירת הנבחרים" #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "שמירת הנבחרים כתמונות" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "שמירת הנבחרים כ־PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" -msgstr "שמור לקובץ יחיד" +msgstr "לשמור לקובץ יחיד" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" -msgstr "שמור לקבצים מרובים" +msgstr "שמירה למספר קבצים" #: MiscResources.resx$BatchStatusSaving$Message msgid "Saving batch results..." -msgstr "" +msgstr "תוצאות הסריקה במרוכז נשמרות…" #: MiscResources.resx$SavingFormat$Message msgid "Saving {0}..." -msgstr "" +msgstr "{0} נשמר…" -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" -msgstr "התאם גודל עם החלון" +msgstr "התאמת גודל עם החלון" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "קנה מידה:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "סריקה" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" -msgstr "" +msgstr "הגדרות סריקה" + +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "לסרוק עם פרופיל ברירת המחדל" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "סריקה עם פרופיל חדש" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "סריקה עם פרופיל {0}" #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "תמונה שנסרקה" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "שיתוף סורק" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" -msgstr "סורק עמוד {0}" +msgstr "עמוד {0} נסרק" #: MiscResources.resx$BatchStatusScanPage$Message msgid "Scanning page {0} (scan {1})..." -msgstr "" +msgstr "עמוד {0} נסרק (סריקה {1})…" #: MiscResources.resx$BatchStatusPage$Message #: MiscResources.resx$ScanProgressPage$Message msgid "Scanning page {0}..." -msgstr "סורק עמוד {0}..." +msgstr "עמוד {0} נסרק…" + +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "מתבצע חיפוש אחר מכשירים…" -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "שניות (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" -msgstr "" +msgstr "בחירה" #: UiStrings.resx$SelectAll$Message msgid "Select All" -msgstr "בחר הכל" +msgstr "בחירה בהכול" + +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "בחירת מכשיר" -#: FSelectDevice.resx$$this.Text$Message #: UiStrings.resx$SelectSource$Message msgid "Select Source" -msgstr "בחר מקור" +msgstr "בחירת מקור" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." msgstr "יש לבחור פרופיל לפני לחיצה על כפתור סריקה." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" -msgstr "בחר שפה אחת או יותר:" +msgstr "נא לבחור שפה אחת או יותר:" #: MiscResources.resx$SelectedCount$Message msgid "Selected ({0})" msgstr "מסומנים ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" -msgstr "" +msgstr "הפרדת קבצים בשיטת Patch-T" #: UiStrings.resx$SetDefault$Message msgid "Set Default" -msgstr "הגדר ברירת מחדל" +msgstr "הגדרת ברירת מחדל" + +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "הגדרות" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "שיתוף" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "לשתף אפילו כש־NAPS2 סגור" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "הגדרות סורק משותף" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "אפשר להשתמש בסורקים משותפים במחשבים אחרים ברשת המקומית על ידי בחירה ב„מנהל התקן ESCL” בהגדרות הפרופיל של NAPS2 במחשב השני." #: UiStrings.resx$Sharpen$Message msgid "Sharpen" -msgstr "" +msgstr "חידוד" + +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "קיצור דרך" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Show$Message msgid "Show" -msgstr "הצג" +msgstr "הצגה" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "הצגת סרגל כלים „פרופילים”" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "הצגת תהליך TWAIN טבעי" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "הצגת מספרי עמודים" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "סרגל צד" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" -msgstr "" +msgstr "קבצים של עמוד אחד" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" -msgstr "" +msgstr "סריקה בודדת" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" -msgstr "" +msgstr "דילוג על בקשת שמירה" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "פיצול" + +#: UiStrings.resx$Start$Message msgid "Start" -msgstr "התחל" +msgstr "התחלה" + +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "הפסקת שיתוף סורק" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" -msgstr "מתח לגודל הדף" +msgstr "מתיחה לגודל הדף" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "נושא:" #: MiscResources.resx$FileTypeTiff$Message msgid "TIFF File (*.tiff, *.tif)" -msgstr "קובץ TIFF (*.tiff, *.tif)" +msgstr "קובץ TIFF‏ (‎*.tiff,‏ ‎*.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "מנהל התקן TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" -msgstr "" +msgstr "פרטים טכניים" #: MiscResources.resx$TesseractNotAvailable$Message #: SdkResources.resx$TesseractNotAvailable$Message msgid "The OCR engine is not available. Make sure to install the required package:" -msgstr "" +msgstr "מנוע זיהוי התווים האופטי לא זמין. נא לוודא שהתקנת את החבילות הנדרשות:" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "הזמן שהוקצב לפעילות זיהוי התווים האופטי נגמר." #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" -msgstr "" +msgstr "מנהל התקן ה־SANE לא זמין. נא לוודא שהתקנת את החבילות הנדרשות:" #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message msgid "The file '{0}' could not be imported." -msgstr "לא ניתן לייבא את הקובץ '{0}'." +msgstr "לא ניתן לייבא את הקובץ ‚{0}’." #: MiscResources.resx$ImportErrorNAPS2Pdf$Message msgid "The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported." -msgstr "לא ניתן לייבא את הקובץ '{0}'. ניתן לייבא רק קבצים שנוצרו על ידי תוכנה זו." +msgstr "לא ניתן לייבא את הקובץ ‚{0}’. ניתן לייבא רק קבצים שנוצרו על ידי תוכנה זו." #: MiscResources.resx$FileInUse$Message msgid "The file could not be overwritten because it is currently in use." -msgstr "" +msgstr "לא ניתן לדרוס את הקובץ כיוון שהוא בשימוש כרגע." #: MiscResources.resx$ConfirmOverwriteFile$Message msgid "The file {0} already exists. Do you want to overwrite it?" -msgstr "הקובץ {0} כבר קיים. האם להחליפו?" +msgstr "הקובץ {0} כבר קיים. להחליפו?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "הקוץ הבא מוצפן ונדרשת סיסמה על מנת להציגו: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "הקובץ הבא מוצפן ונדרשת סיסמה על מנת להציגו:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message msgid "The scanner has a paper jam." -msgstr "" +msgstr "נתקע דף בסורק." #: MiscResources.resx$DeviceWarmingUp$Message #: SdkResources.resx$DeviceWarmingUp$Message msgid "The scanner is warming up." -msgstr "" +msgstr "הסורק מתחמם." #: MiscResources.resx$DeviceCoverOpen$Message #: SdkResources.resx$DeviceCoverOpen$Message msgid "The scanner's cover is open." -msgstr "" +msgstr "המכסה של הסורק פתוח." #: MiscResources.resx$DriverNotSupported$Message #: SdkResources.resx$DriverNotSupported$Message msgid "The selected driver is not supported on this system." -msgstr "" +msgstr "מנהל ההתקן הנבחר לא נתמך במערכת הזאת." #: MiscResources.resx$DeviceNotFound$Message #: SdkResources.resx$DeviceNotFound$Message @@ -1426,46 +1615,82 @@ msgstr "הסורק שנבחר לא נמצא." #: MiscResources.resx$NoFeederSupport$Message #: SdkResources.resx$NoFeederSupport$Message msgid "The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver." -msgstr "הסורק שנבחר איננו תומך במזין מסמכים. אם לסורק יש מזין מסמכים, נסה להשתמש במנהל התקן אחר." +msgstr "הסורק שנבחר איננו תומך במזין מסמכים. אם לסורק יש מזין מסמכים, כדאי לנסות להשתמש במנהל התקן אחר." #: MiscResources.resx$NoDuplexSupport$Message #: SdkResources.resx$NoDuplexSupport$Message msgid "The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver." -msgstr "" +msgstr "הסורק שנבחר לא תומך במצב דופלקס (סריקה דו־צדדית). אם הסורק שלך אמור לתמוך בדופלקס, כדאי לנסות להשתמש במנהל התקן אחר." #: MiscResources.resx$DeviceBusy$Message #: SdkResources.resx$DeviceBusy$Message msgid "The selected scanner is busy." -msgstr "" +msgstr "הסורק הנבחר עסוק." #: MiscResources.resx$DeviceOffline$Message #: SdkResources.resx$DeviceOffline$Message msgid "The selected scanner is offline." -msgstr "הסורק שנבחר איננו מקוון (OFFLINE)." +msgstr "הסורק שנבחר איננו מקוון." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "תהליך הרקע קרס." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "תהליך הרקע קרס. נא לבדוק את הסיבה במציג האירועים של Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "ערכת עיצוב:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" -msgstr "" +msgstr "אפשרויות Tiff" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" -msgstr "" +msgstr "זמן בין סריקות (שניות):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "כותרת:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "כלים" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" -msgstr "" +msgstr "מימושים של TWAIN:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "נייר US Legal (8.5x14 in)" +msgstr "נייר US Legal (8.5x14 אינטש)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" -msgstr "נייר US Legal (8.5x14 in)" +msgstr "נייר US Letter (8.5x11 אינטש)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "ביטול הקצאה" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "הסגה" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "הסגת {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "סורק לא ידוע" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" @@ -1473,35 +1698,32 @@ msgstr "שינויים שלא נשמרו" #: MiscResources.resx$UpdateProgress$Message msgid "Update Progress" -msgstr "" +msgstr "התקדמות העדכון" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "בדיקת עדכונים מושבתת." #: MiscResources.resx$Updating$Message msgid "Updating..." -msgstr "" +msgstr "מתבצע עדכון…" #: MiscResources.resx$UploadingEmail$Message msgid "Uploading email..." -msgstr "" +msgstr "ההודעה נשלחת בדוא״ל…" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" -msgstr "השתמש בממשק מקורי (נטיבי)" +msgstr "שימוש בממשק מקורי (טבעי)" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" -msgstr "השתמש בהגדרות שמורות" +msgstr "שימוש בהגדרות שמורות" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "סיסמת משתמש:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "השימוש ב-OCR דורש הורדה של נתוני השפה במסמך הנסרק." @@ -1513,38 +1735,37 @@ msgstr "גרסה {0}" #: UiStrings.resx$View$Message msgid "View" -msgstr "הצג" +msgstr "הצגה" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "מנהל התקן WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." -msgstr "ממתין למנהל התקן TWAIN..." +msgstr "בהמתנה למנהל התקן TWAIN..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." -msgstr "" +msgstr "בהמתנה לאימות…" #: MiscResources.resx$BatchStatusWaitingForScan$Message msgid "Waiting for scan {0}..." -msgstr "" +msgstr "בהמתנה לסריקה {0}…" -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" -msgstr "" +msgstr "סף לבן" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "גרסת Wia:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "שנה" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "שנה (00-99)" @@ -1561,28 +1782,25 @@ msgstr "אין ברשותכם הרשאות מספיקות לשמירת הקוב msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "ישנם שינויים שלא נשמרו. האם לסגור את התוכנה ולאבד את השינויים הללו?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" -msgstr "זום" +msgstr "תקריב" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" -msgstr "זום 1:1" +msgstr "תקריב מקורי" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" -msgstr "זום פנימה" +msgstr "התקרבות" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" -msgstr "זום החוצה" +msgstr "התרחקות" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "ס\"מ" +msgstr "ס״מ" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" @@ -1590,7 +1808,7 @@ msgstr "אינטש" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "מ\"מ" +msgstr "מ״מ" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,34 +1816,39 @@ msgstr "מתוך {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "{0} / {1} מ\"ב" +msgstr "{0} / {1} מ״ב" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} קבצים" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "נמצאו {0} מכשירים." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} נק׳ לאינטש" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} תמונה/תמונות שנסרקו ב-{1} ב-{2} לא נשמרו, אך ניתן לשחזר אותן. האם לשחזר אותן?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." -msgstr "" +msgstr "{0} תמונות נשמרו." #: MiscResources.resx$PdfStatus$Message #: UiStrings.resx$XOfY$Message diff --git a/NAPS2.Lib/Lang/po/hi.po b/NAPS2.Lib/Lang/po/hi.po index e8493d2ed0..6a0d8d0d4f 100644 --- a/NAPS2.Lib/Lang/po/hi.po +++ b/NAPS2.Lib/Lang/po/hi.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:35\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Hindi\n" "Language: hi\n" @@ -19,548 +19,588 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "स्कैन करे" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "स्कैन करे" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "\"स्कैन\" मेनू डिफ़ॉल्ट प्रोफ़ाइल को बदल देता है" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" -msgstr "" - -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" +msgstr "24-बिट रंग" #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 मिलीमीटर)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 मिलीमीटर)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 मिलीमीटर)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message msgid "About" -msgstr "" +msgstr "सॉफ्टवेयर के बारे में" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." +msgstr "डेटा प्राप्त किया जा रहा है..." + +#: UiStrings.resx$Action$Message +msgid "Action" msgstr "" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" -msgstr "" +msgstr "उन्नत विकल्प" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" -msgstr "" +msgstr "उन्नत प्रोफ़ाइल सेटिंग्स" #: MiscResources.resx$AllCount$Message msgid "All ({0})" -msgstr "" +msgstr "सभी ({0})" #: MiscResources.resx$FileTypeAllFiles$Message msgid "All Files" -msgstr "" +msgstr "सभी फाइलें" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" -msgstr "" +msgstr "एनोटेशन की अनुमति दें" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" -msgstr "" +msgstr "सामग्री प्रतिलिपि बनाने की अनुमति दें" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" -msgstr "" +msgstr "पहुंच के लिए सामग्री की प्रतिलिपि बनाने की अनुमति दें" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" -msgstr "" +msgstr "दस्तावेज़ संयोजन की अनुमति दें" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" -msgstr "" +msgstr "दस्तावेज़ संशोधन की अनुमति दें" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" -msgstr "" +msgstr "फॉर्म भरने की अनुमति दें" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" -msgstr "" +msgstr "पूर्ण गुणवत्ता मुद्रण की अनुमति दें" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" -msgstr "" +msgstr "मुद्रण की अनुमति दें" #: UiStrings.resx$AltDeinterleave$Message msgid "Alternate Deinterleave" -msgstr "" +msgstr "वैकल्पिक डीइंटरलीव" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" -msgstr "" +msgstr "वैकल्पिक इंटरलीव" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "हमेशा पूछें" + #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" -msgstr "" +msgstr "इस पीडीएफ फ़ाइल को आयात करने के लिए एक अतिरिक्त घटक की आवश्यकता है। क्या आप इसे अभी डाउनलोड करना चाहेंगे?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "" +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "OCR चलाने में त्रुटि उत्पन्न हुई." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "अधिकृत करने का प्रयास करते समय एक त्रुटि उत्पन्न हुई।" #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." -msgstr "" +msgstr "स्वतः सहेजने का प्रयास करते समय एक त्रुटि उत्पन्न हुई." + +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "अद्यतन स्थापित करने का प्रयास करते समय एक त्रुटि उत्पन्न हुई।" #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." -msgstr "" +msgstr "फ़ाइल को सहेजने का प्रयास करते समय एक त्रुटि उत्पन्न हुई." #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "" +msgstr "ईमेल भेजने का प्रयास करते समय एक त्रुटि उत्पन्न हुई." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." -msgstr "" +msgstr "ईमेल भेजने का प्रयास करते समय एक त्रुटि उत्पन्न हुई." #: MiscResources.resx$UnknownDriverError$Message #: SdkResources.resx$UnknownDriverError$Message msgid "An error occurred with the scanning driver." -msgstr "" +msgstr "स्कैनिंग ड्राइवर के साथ कोई त्रुटि उत्पन्न हुई." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "" +msgstr "एक ऑपरेशन चल रहा है. क्या आप वाकई बाहर निकलना और ऑपरेशन रद्द करना चाहते हैं?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "" +msgid "An unknown error occurred during the batch scan." +msgstr "बैच स्कैन के दौरान एक अज्ञात त्रुटि उत्पन्न हुई." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "" +msgstr "एक अपडेट उपलब्ध है।" #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "" +msgstr "OCR का अपडेट उपलब्ध है." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "एप्पल ड्राइवर" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "एप्पल मेल" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "एप्लिकेशन" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" -msgstr "" +msgstr "स्कैन के बाद चमक/कंट्रास्ट लागू करें" #: UiStrings.resx$ApplyToSelected$Message msgid "Apply to all {0} selected images" -msgstr "" +msgstr "सभी {0} चयनित छवियों पर लागू करें" #: MiscResources.resx$ConfirmCancelBatch$Message msgid "Are you sure you want to cancel the batch scan?" -msgstr "" +msgstr "क्या आप बैच स्कैन रद्द करना चाहते हैं?" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" -msgstr "" +msgstr "क्या आप वाकई {0} आइटम साफ़ करना चाहते हैं?" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "" +msgstr "क्या आप {0} को डिलीट करना चाहते हैं?" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" -msgstr "" +msgstr "क्या आप वाकई प्रोफ़ाइल {0} को हटाना चाहते हैं?" #: MiscResources.resx$ConfirmDeleteItems$Message msgid "Are you sure you want to delete {0} item(s)?" -msgstr "" +msgstr "क्या आप वाकई {0} आइटम हटाना चाहते हैं?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "" +msgstr "क्या आप वाकई {0} प्रोफ़ाइल हटाना चाहते हैं?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "क्या आप वाकई शेयरिंग बंद करना चाहते हैं?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" +msgstr "क्या आप वाकई {0} छवि(छवियों) में अपने परिवर्तन पूर्ववत करना चाहते हैं?" + +#: UiStrings.resx$Assign$Message +msgid "Assign" msgstr "" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" -msgstr "" +msgstr "अनुलग्नक का नाम:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" -msgstr "" +msgstr "लेखक:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "अधिकृत" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "स्वतः" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" -msgstr "" +msgstr "स्वतः सहेजें सेटिंग्स" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "स्वत: बढ़ती संख्या (1 अंक)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" -msgstr "" +msgstr "स्वत: बढ़ती संख्या (2 अंक)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" -msgstr "" +msgstr "स्वत: बढ़ती संख्या (3 अंक)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" -msgstr "" +msgstr "स्वत: वृद्धिशील संख्या (4 अंक)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "" +msgstr "स्कैनिंग के बाद स्वचालित रूप से OCR चलाएँ" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "बी4 (250x353 मिलीमीटर)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "बी5 (176x250 मिलीमीटर)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" -msgstr "" +msgstr "बैच स्कैन" #: MiscResources.resx$BatchStatusCancelled$Message msgid "Batch cancelled." -msgstr "" +msgstr "बैच रद्द कर दिया गया." #: MiscResources.resx$BatchStatusComplete$Message msgid "Batch completed successfully." -msgstr "" +msgstr "बैच सफलतापूर्वक पूरा हुआ." #: MiscResources.resx$BatchStatusError$Message msgid "Batch scan stopped due to error." -msgstr "" +msgstr "त्रुटि के कारण बैच स्कैन रुक गया।" #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "श्रेष्ठ" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" -msgstr "" +msgstr "बिट गहराई:" #: MiscResources.resx$FileTypeBmp$Message msgid "Bitmap Files (*.bmp)" -msgstr "" +msgstr "बिटमैप फ़ाइलें (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" -msgstr "" +msgid "Black and White" +msgstr "काला और सफेद" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" -msgstr "" +msgstr "खाली पन्ने" #: UiStrings.resx$BrightnessContrast$Message msgid "Brightness / Contrast" -msgstr "" +msgstr "चमक/विपरीतता" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" -msgstr "" +msgstr "चमक:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" -msgstr "" +msgstr "रद्द करें" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" -msgstr "" +msgstr "बैच रद्द करें" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." -msgstr "" +msgstr "रद्द किया जा रहा है..." #: SettingsResources.resx$HorizontalAlign_Center$Message msgid "Center" -msgstr "" +msgstr "केन्द्र" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" -msgstr "" +msgstr "परिवर्तन" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "" +msgstr "अद्यतन के लिए जाँच" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "" +msgstr "जांच की जा रही है" -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "" +msgstr "ईमेल प्रदाता चुनें" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" -msgstr "" +msgstr "प्रोफ़ाइल चुनें" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" -msgstr "" +msgstr "डिवाइस का चयन करें" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message msgid "Clear" -msgstr "" +msgstr "क्लियर करें" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "सभी साफ करें" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" -msgstr "" +msgstr "सहेजने के बाद छवियाँ साफ़ करें" #: MiscResources.resx$Close$Message msgid "Close" -msgstr "" +msgstr "बंद करे" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "मिलाएँ" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "स्कैनिंग डिवाइस के साथ संचार बाधित हो गया।" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" -msgstr "" +msgstr "संगतता" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" +msgstr "कम्प्रेशन" + +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." msgstr "" -#: FEditProfile.resx$label7.Text$Message #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" -msgstr "" +msgstr "कन्ट्रास्ट:" #: UiStrings.resx$Copy$Message msgid "Copy" -msgstr "" +msgstr "प्रतिलिपि बनाएँ" #: MiscResources.resx$CopyProgress$Message msgid "Copy Progress" -msgstr "" +msgstr "कॉपी प्रगति" #: MiscResources.resx$Copying$Message msgid "Copying..." -msgstr "" +msgstr "कॉपी करी जा रही है |" #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "कॉपीराइट {0} NAPS2 योगदानकर्ता" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" -msgstr "" +msgstr "कवरेज सीमा" #: UiStrings.resx$Crop$Message msgid "Crop" -msgstr "" +msgstr "क्रॉप करें" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" -msgstr "" +msgstr "पृष्ठ आकार के अनुसार काटें" #: MiscResources.resx$CustomPageSizeFormat$Message msgid "Custom ({0}x{1} {2})" -msgstr "" +msgstr "पसंद के अनुसार ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" +msgstr "कस्टम पेज आकार" + +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" msgstr "" #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" -msgstr "" +msgstr "कस्टम घुमाव" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" -msgstr "" +msgstr "स्वयं का एसएमटीपी(SMTP)" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." +msgstr "कस्टम..." + +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" msgstr "" -#: FPlaceholders.resx$label6.Text$Message +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" -msgstr "" +msgstr "दिन (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" -msgstr "" +msgstr "डिफ़ॉल्ट (न्यून)" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" -msgstr "" +msgstr "डिफ़ॉल्ट फ़ाइल पथ" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" -msgstr "" +msgstr "बीच में छूड़ाव हटाये" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" -msgstr "" +msgstr "नष्ट करें" #: UiStrings.resx$Deskew$Message msgid "Deskew" -msgstr "" +msgstr "तिरछापन दूर करें" #: MiscResources.resx$AutoDeskewProgress$Message msgid "Deskew Progress" -msgstr "" +msgstr "तिरछापन दूर करने की प्रगति" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" -msgstr "" +msgstr "स्कैन किए गए पृष्ठों से तिरछापन दूर करें" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." -msgstr "" +msgstr "तिरछापन दूर किया जा रहा है..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" -msgstr "" +msgstr "उपकरण:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" -msgstr "" +msgstr "आयाम" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" -msgstr "" +msgstr "प्रदर्शित होने वाला नाम:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "दस्तावेज़ सुधार" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "" +msgstr "दान करें" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" -msgstr "" +msgstr "पूर्ण" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" -msgstr "" +msgstr "डाउनलोड करें" #: MiscResources.resx$DownloadError$Message msgid "Download Error" -msgstr "" +msgstr "डाउनलोड एरर" #: MiscResources.resx$DownloadNeeded$Message msgid "Download Needed" -msgstr "" +msgstr "डाउनलोड की आवश्यकता है" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" +msgstr "डाउनलोड प्रगति" + +#: UiStrings.resx$Dpi$Message +msgid "Dpi" msgstr "" #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" +msgstr "डुप्लेक्स" + +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ईएससीएल (ESCL) ड्राइवर" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" msgstr "" #: UiStrings.resx$Edit$Message msgid "Edit" +msgstr "संपादित करें" + +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" msgstr "" #: UiStrings.resx$EmailAllAsPdf$Message @@ -570,44 +610,54 @@ msgstr "" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "" +msgstr "पीडीएफ(PDF) ईमेल करे" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" msgstr "" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message -msgid "Encrypt PDF" +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" msgstr "" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$EncryptPdf$Message +msgid "Encrypt PDF" +msgstr "PDF सुरक्षित करे" + +#: UiStrings.resx$Encryption$Message msgid "Encryption" -msgstr "" +msgstr "सुरक्षा अनुभाग" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" msgstr "" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" +msgstr "त्रुटि" + +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" msgstr "" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,36 +667,43 @@ msgstr "" msgid "Exchangeable Image File (*.exif)" msgstr "" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" -msgstr "" +msgstr "खाली पेज रहने दे" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "तेज" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "फ़ाइल का नाम" + +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" msgstr "" #: UiStrings.resx$Flip$Message msgid "Flip" +msgstr "पलटें" + +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" msgstr "" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" @@ -654,7 +711,6 @@ msgstr "" msgid "GIF File (*.gif)" msgstr "" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "" @@ -665,45 +721,48 @@ msgstr "" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "जीमेल" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" -msgstr "" +msgstr "ग्रेस्केल" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" -msgstr "" +msgstr "घंटा (0-23)" #: UiStrings.resx$HueSaturation$Message msgid "Hue / Saturation" +msgstr "रंग / संतृप्ति" + +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" msgstr "" #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" -msgstr "" +msgstr "चिह्न प्रदाता:" #: UiStrings.resx$Image$Message msgid "Image" -msgstr "" +msgstr "छवि" #: MiscResources.resx$FileTypeImageFiles$Message msgid "Image Files" -msgstr "" +msgstr "इमेज फाइल" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" -msgstr "" +msgstr "छवि की गुणवत्ता" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" -msgstr "" +msgstr "छवि समायोजन" #: MiscResources.resx$ImageSaved$Message msgid "Image saved." @@ -711,39 +770,43 @@ msgstr "" #: UiStrings.resx$Import$Message msgid "Import" -msgstr "" +msgstr "आयात करें" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" -msgstr "" +msgstr "आयात की प्रगति" #: MiscResources.resx$ImportingFormat$Message msgid "Importing {0}..." -msgstr "" +msgstr "आयत हो रहा है {0}..." #: MiscResources.resx$Importing$Message msgid "Importing..." -msgstr "" +msgstr "आयत हो रहा है" #: MiscResources.resx$Install$Message msgid "Install {0}" -msgstr "" +msgstr "अधिस्थापित {0}" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" -msgstr "" +msgstr "अधिस्थापना पूर्ण" #: MiscResources.resx$InstallFailedTitle$Message msgid "Installation Failed" -msgstr "" +msgstr "अधिस्थापना विफल" #: MiscResources.resx$InstallCompletePromptRestart$Message msgid "Installation complete. Do you want to restart NAPS2 now?" -msgstr "" +msgstr "अधिस्थापना पूर्ण। क्या आप अब NAPS2 को पुनः आरंभ करना चाहते हैं?" #: MiscResources.resx$InstallFailed$Message msgid "Installation failed." -msgstr "" +msgstr "अधिस्थापना विफल।" + +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "इंटरफेस" #: UiStrings.resx$Interleave$Message msgid "Interleave" @@ -751,143 +814,186 @@ msgstr "" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" -msgstr "" +msgstr "JPEG File (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000 File (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" +msgstr "Jpeg की गुणवत्ता" + +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" msgstr "" -#: FPdfSettings.resx$label6.Text$Message -msgid "Keywords:" +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" msgstr "" +#: UiStrings.resx$KeywordsLabel$Message +msgid "Keywords:" +msgstr "खोजशब्द:" + #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" msgstr "" #: UiStrings.resx$Language$Message msgid "Language" +msgstr "भाषा" + +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" msgstr "" #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" -msgstr "" +msgstr "बाएं" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" +msgstr "लीगेसी (केवल मूल यूआई)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message -msgid "Load images into NAPS2" +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" msgstr "" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message +#: UiStrings.resx$LoadIn$Message +msgid "Load images into NAPS2" +msgstr "NAPS2 में छवियाँ लोड करें" + #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" +msgstr "ओसीआर (OCR) का उपयोग करके पीडीएफ को खोजने योग्य बनाएं" + +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" msgstr "" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" -msgstr "" +msgstr "अधिकतम गुणवत्ता (बड़ी फ़ाइलें)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "मेमोरी ट्रांसफर" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" -msgstr "" +msgstr "मेटाडाटा" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" -msgstr "" +msgstr "मिनट (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" -msgstr "" +msgstr "महीना (0-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" -msgstr "" +msgstr "और जाने" #: UiStrings.resx$MoveDown$Message msgid "Move Down" -msgstr "" +msgstr "नीचे की ओर खिसकाए" #: UiStrings.resx$MoveUp$Message msgid "Move Up" -msgstr "" +msgstr "ऊपर की ओर खिसकाए" + +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "एकाधिक भाषाएँ" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "एकाधिक भाषाएँ..." -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" -msgstr "" +msgstr "एकाधिक स्कैन (स्कैन के बीच निश्चित विलंब)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" -msgstr "" +msgstr "एकाधिक स्कैन (स्कैन के बीच संकेत)" #: MiscResources.resx$NAPS2$Message #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "" +msgstr "NAPS2 पूरी तरह से मुफ़्त है। दान देने पर विचार करें।" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" -msgstr "" +msgstr "नाम (वैकल्पिक)" #: MiscResources.resx$NameMissing$Message msgid "Name missing." -msgstr "" +msgstr "नाम गायब है।" + +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "नेटिव ट्रांसफर" #: UiStrings.resx$New$Message msgid "New" -msgstr "" +msgstr "नया" #: UiStrings.resx$NewProfile$Message msgid "New Profile" -msgstr "" +msgstr "नया प्रोफ़ाइल" #: UiStrings.resx$Next$Message msgid "Next" -msgstr "" +msgstr "अगला" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" -msgstr "" +msgstr "अगला स्कैन" #: MiscResources.resx$NoDeviceSelected$Message #: SdkResources.resx$NoDeviceSelected$Message msgid "No device selected." +msgstr "कोई उपकरण चयनित नहीं।" + +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." msgstr "" #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." -msgstr "" +msgstr "फीडर में कोई पेज नहीं है।" -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "" +msgstr "कोई प्रदाता चयनित नहीं।" #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message msgid "No scanning device was found." -msgstr "" +msgstr "कोई स्कैनिंग उपकरण नहीं मिला।" #: MiscResources.resx$NoUpdates$Message msgid "No updates available." -msgstr "" +msgstr "कोई अपडेट उपलब्ध नहीं है।" #: SettingsResources.resx$TiffComp_None$Message msgid "None" @@ -897,11 +1003,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "" @@ -909,7 +1015,6 @@ msgstr "" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "" @@ -918,37 +1023,23 @@ msgstr "" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "" @@ -970,7 +1059,11 @@ msgstr "" msgid "One or more files could not be downloaded." msgstr "" -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "" @@ -978,11 +1071,15 @@ msgstr "" msgid "Operation in Progress" msgstr "" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "" @@ -990,7 +1087,7 @@ msgstr "" msgid "Overwrite File" msgstr "" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "" @@ -998,8 +1095,8 @@ msgstr "" msgid "PDF Document (*.pdf)" msgstr "" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "" @@ -1045,21 +1140,24 @@ msgstr "" msgid "Paste" msgstr "" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "" @@ -1067,7 +1165,7 @@ msgstr "" msgid "Preview" msgstr "" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "" @@ -1080,12 +1178,11 @@ msgstr "" msgid "Print" msgstr "" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "" @@ -1094,37 +1191,47 @@ msgstr "" msgid "Profiles" msgstr "" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "प्रदाता" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "" -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" -msgstr "" +msgstr "पुनर्प्राप्त करें" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" -msgstr "" +msgstr "स्कैन की गई छवियाँ पुनर्प्राप्त करें" #: MiscResources.resx$Recovering$Message msgid "Recovering..." -msgstr "" +msgstr "पुनर्प्राप्त हो रही है..." #: MiscResources.resx$RecoveryProgress$Message msgid "Recovery Progress" msgstr "" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "फिर से करें" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "" @@ -1140,15 +1247,11 @@ msgstr "" msgid "Reset Image" msgstr "" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "" @@ -1156,6 +1259,14 @@ msgstr "" msgid "Reverse" msgstr "" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "" @@ -1176,7 +1287,6 @@ msgstr "" msgid "Rotate Right" msgstr "" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1185,7 +1295,6 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1303,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1214,59 +1328,80 @@ msgstr "" #: MiscResources.resx$SavePdf$Message #: UiStrings.resx$SavePdf$Message msgid "Save PDF" -msgstr "" +msgstr "पीडीएफ सेव करें" #: MiscResources.resx$SavePdfProgress$Message msgid "Save PDF Progress" -msgstr "" +msgstr "पीडीएफ की प्रगति सेव करें" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "चयनित सेव करें" #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "चयनित को छवियों के रूप में सेव करें" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "चयनित को पीडीएफ के रूप में सेव करें" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" -msgstr "" +msgstr "एकल फ़ाइल में सेव करें" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" -msgstr "" +msgstr "एकाधिक फ़ाइलों में सेव करें" #: MiscResources.resx$BatchStatusSaving$Message msgid "Saving batch results..." -msgstr "" +msgstr "खेप परिणाम सेव कारा जा रहा है..." #: MiscResources.resx$SavingFormat$Message msgid "Saving {0}..." -msgstr "" +msgstr "सेव कारा जा रहा है {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" -msgstr "" +msgstr "खिड़की के साथ स्केल करें" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" -msgstr "" +msgstr "पैमाना" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" -msgstr "" +msgstr "स्कैन करें" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "" @@ -1280,11 +1415,14 @@ msgstr "" msgid "Scanning page {0}..." msgstr "" -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "" @@ -1293,7 +1431,10 @@ msgstr "" msgid "Select All" msgstr "" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "" @@ -1302,7 +1443,6 @@ msgstr "" msgid "Select a profile before clicking Scan." msgstr "" -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "" @@ -1311,8 +1451,7 @@ msgstr "" msgid "Selected ({0})" msgstr "" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,37 +1459,84 @@ msgstr "" msgid "Set Default" msgstr "" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "" @@ -1358,12 +1544,11 @@ msgstr "" msgid "TIFF File (*.tiff, *.tif)" msgstr "" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "" @@ -1372,6 +1557,10 @@ msgstr "" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,8 +1583,8 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" msgstr "" #: MiscResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "" msgid "The selected scanner is offline." msgstr "" -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "" @@ -1487,21 +1712,18 @@ msgstr "" msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "" @@ -1515,16 +1737,15 @@ msgstr "" msgid "View" msgstr "" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "" -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1532,19 +1753,19 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "" -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "" @@ -1561,21 +1782,18 @@ msgstr "" msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "" diff --git a/NAPS2.Lib/Lang/po/hr.po b/NAPS2.Lib/Lang/po/hr.po index 11c3332684..9f82c3947e 100644 --- a/NAPS2.Lib/Lang/po/hr.po +++ b/NAPS2.Lib/Lang/po/hr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Croatian\n" "Language: hr\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Zadana radnja gumba \"Spremi\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Zadana radnja gumba \"Skeniraj\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Izbornik \"Skeniraj\" mijenja zadani profil" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "Pronađen je 1 uređaj." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-bitna Boja" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -76,14 +60,17 @@ msgstr "O programu" msgid "Acquiring data..." msgstr "Prikupljam podatke..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Radnja" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Napredno" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" -msgstr "Napredna podešavanja profila" +msgstr "Napredne postavke profila" #: MiscResources.resx$AllCount$Message msgid "All ({0})" @@ -93,35 +80,35 @@ msgstr "Sve ({0})" msgid "All Files" msgstr "Sve datoteke" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Dozvoli zabilješke" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Dozvoli kopiranje sadržaja" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" -msgstr "Dozvoli kopiranje sadržaja za olakšani pristup" +msgstr "Dozvoli kopiranje sadržaja radi pristupačnosti" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" -msgstr "Dozvoli sastavljanje dokumenta" +msgstr "Dozvoli slaganje dokumenata" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Dozvoli izmjenu dokumenta" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" -msgstr "Dozvoli popunjavanje forme" +msgstr "Dopusti ispunjavanje obrazaca" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" -msgstr "Dozvoli punu kvalitetu ispisa" +msgstr "Dozvoli ispis u punoj kvaliteti" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Dozvoli ispis" @@ -133,70 +120,87 @@ msgstr "Alternativno Raspletanje" msgid "Alternate Interleave" msgstr "Alternativno Prepletanje" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternativni Prijenos" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Uvijek pitaj" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Uvijek upitaj" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" -msgstr "Dodatna komponenta je potrebna za uvoz ovog PDF-a. Želite li ju preuzeti?" +msgstr "Za uvoz ove PDF datoteke potrebna je dodatna komponenta. Želite li je sada preuzeti?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "" +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Došlo je do pogreške prilikom pokretanja OCR-a." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "Došlo je do pogreške prilikom pokušaja autorizacije." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." -msgstr "Pogreška pri automatskom spremanju." +msgstr "Došlo je do pogreške prilikom pokušaja automatskog spremanja." + +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Došlo je do pogreške prilikom pokušaja instaliranja ažuriranja." #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." -msgstr "Pogreška pri spremanju datoteke." +msgstr "Došlo je do pogreške prilikom pokušaja spremanja datoteke." #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "" +msgstr "Došlo je do pogreške prilikom pokušaja slanja e-pošte." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." -msgstr "Pogreška pri slanju e-pošte." +msgstr "Došlo je do pogreške prilikom pokušaja slanja e-pošte." #: MiscResources.resx$UnknownDriverError$Message #: SdkResources.resx$UnknownDriverError$Message msgid "An error occurred with the scanning driver." -msgstr "Pogreška s upravljačkim programom za skener." +msgstr "Dogodila se pogreška s upravljačkim programom za skeniranje." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "" +msgstr "Operacija je u tijeku. Jeste li sigurni da želite izaći i otkazati operaciju?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Nepoznata pogreška prilikom serijskog skeniranja." +msgid "An unknown error occurred during the batch scan." +msgstr "" #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "" +msgstr "Dostupno je ažuriranje." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "Nadogradnja za OCR je dostupna." +msgstr "Dostupno je ažuriranje za OCR." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple upr. prog." + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Aplikacija" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Primjeni podešenje svjetlline/kontrasta nakon skeniranja" #: UiStrings.resx$ApplyToSelected$Message msgid "Apply to all {0} selected images" -msgstr "Primjeni na sve {0} označene slike" +msgstr "Primijeni na sve odabrane slike ({0})." #: MiscResources.resx$ConfirmCancelBatch$Message msgid "Are you sure you want to cancel the batch scan?" @@ -204,11 +208,11 @@ msgstr "Jeste li sigurni da želite prekinuti serijsko skeniranje?" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" -msgstr "Jeste li sigurni da želite ukloniti sledeći broj datoteka: {0}?" +msgstr "Jeste li sigurni da želite obrisati {0} stavku(i)?" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "Jeste li sigurni da želite obrisati sljedeći broj datoteka: {0}?" +msgstr "Jeste li sigurni da želite izbrisati \"{0}\"?" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" @@ -216,69 +220,75 @@ msgstr "Jeste li sigurni da želite obrisati profil {0}?" #: MiscResources.resx$ConfirmDeleteItems$Message msgid "Are you sure you want to delete {0} item(s)?" -msgstr "Jeste li sigurni da želite obrisati sledeći broj datoteka: {0}?" +msgstr "Jeste li sigurni da želite obrisati sledeći broj stavki: {0}?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "Jeste li sigurni da želite obrisati {0} profila?" +msgstr "Jeste li sigurni da želite obrisati {0} profil(a)?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Jeste li sigurni da želite zaustaviti dijeljenje {0}?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" -msgstr "Jeste li sigurni da želite vratiti promjene na {0} slika?" +msgstr "Jeste li sigurni da želite poništiti promjene na {0} slike(a)?" + +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Dodijeli" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" -msgstr "Ime Privitka:" +msgstr "Naziv privitka:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autor:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "Autoriziraj" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" msgstr "Automatski" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" -msgstr "Automatski spremi postavke" +msgstr "Postavke automatskog spremanja" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Samo-povećavajući brojač (1 znamenka)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Samo-povećavajući brojač (2 znamenke)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Samo-povećavajući brojač (3 znamenke)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Samo-povećavajući brojač (4 znamenke)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "" +msgstr "Automatski pokreni OCR nakon skeniranja" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Serijsko Skeniranje" @@ -298,10 +308,9 @@ msgstr "Serijsko Skeniranje prekinuto zbog pogreške." msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" -msgstr "Dubina bitova:" +msgstr "Klaliteta boje u bitova:" #: MiscResources.resx$FileTypeBmp$Message msgid "Bitmap Files (*.bmp)" @@ -309,10 +318,10 @@ msgstr "Bitmap datoteka (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" -msgstr "Crno/bijelo" +msgid "Black and White" +msgstr "Crno-bijelo" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Prazne stranice" @@ -320,36 +329,22 @@ msgstr "Prazne stranice" msgid "Brightness / Contrast" msgstr "Svjetlina / Kontrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" -msgstr "Osvjetljenost:" +msgstr "Svjetlina:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" -msgstr "Prekini" +msgstr "Odustani" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" @@ -363,27 +358,26 @@ msgstr "Odustajanje...." msgid "Center" msgstr "Sredina" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" -msgstr "" +msgstr "Promijeni" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "" +msgstr "Provjeri ažuriranja" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." msgstr "" -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "" +msgstr "Odaberite pružatelja usluga e-pošte" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" msgstr "Odaberite profil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Odaberite uređaj" @@ -391,30 +385,44 @@ msgstr "Odaberite uređaj" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message msgid "Clear" -msgstr "Očisti" +msgstr "Izbriši" #: UiStrings.resx$ClearAll$Message msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" -msgstr "Prebriši slike nakon spremanja" +msgstr "Obriši slike nakon spremanja" #: MiscResources.resx$Close$Message msgid "Close" msgstr "Zatvori" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Prekinuta je komunikacija sa uređajem za skeniranje." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" -msgstr "Usklađenost" +msgstr "Kompatibilnost" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Sažimanje:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Poveži" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Greška kod povezivanja." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrast:" @@ -425,7 +433,7 @@ msgstr "Kopiraj" #: MiscResources.resx$CopyProgress$Message msgid "Copy Progress" -msgstr "Napredak Kopiranja" +msgstr "Napredak kopiranja" #: MiscResources.resx$Copying$Message msgid "Copying..." @@ -435,63 +443,71 @@ msgstr "Kopiranje..." msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Prag Pokrivanja" #: UiStrings.resx$Crop$Message msgid "Crop" -msgstr "" +msgstr "Obreži" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" -msgstr "" +msgstr "Izreži na veličinu stranice" #: MiscResources.resx$CustomPageSizeFormat$Message msgid "Custom ({0}x{1} {2})" msgstr "" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" -msgstr "" +msgstr "Prilagođena veličina stranice" + +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Prilagođena rezolucija" #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" -msgstr "" +msgstr "Prilagođeno rotiranje" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" -msgstr "" +msgstr "Prilagođeni SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." -msgstr "" +msgstr "Prilagođeno..." + +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Tamna" -#: FPlaceholders.resx$label6.Text$Message +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" -msgstr "" +msgstr "Dan (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" -msgstr "" +msgstr "Zadano" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" -msgstr "" +msgstr "Zadana putanja datoteke:" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" msgstr "Raspletanje" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" -msgstr "Obriši" +msgstr "Izbriši" #: UiStrings.resx$Deskew$Message msgid "Deskew" @@ -501,7 +517,7 @@ msgstr "Popravljanje iskrivljene perspektive" msgid "Deskew Progress" msgstr "Napredak popravljanja iskrivljene perspektive" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Popravljanje iskrivljene perspektive skeniranih stranica" @@ -509,16 +525,14 @@ msgstr "Popravljanje iskrivljene perspektive skeniranih stranica" msgid "Deskewing..." msgstr "Proces popravljanja iskrivljene perspektive..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Uređaj:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Dimenzije" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Ime za prikaz:" @@ -532,82 +546,118 @@ msgstr "" msgid "Donate" msgstr "Doniraj" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Gotovo" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Preuzmi" #: MiscResources.resx$DownloadError$Message msgid "Download Error" -msgstr "Greška u preuzimanju" +msgstr "Greška preuzimanja" #: MiscResources.resx$DownloadNeeded$Message msgid "Download Needed" -msgstr "Potrebno je preuzimanje" +msgstr "Potrebno preuzimanje" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Napredak preuzimanja" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "DPI" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Dvostrano" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL upr. prog." + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL mrežni upr. prog." + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB upr. prog." + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Uredi" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Pošalji sve e-poštom" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Pošalji sve e-poštom kao PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "" +msgstr "Pošalji PDF e-poštom" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" -msgstr "" +msgstr "Napredak slanja PDF-a e-poštom" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Pošalji odabrano e-poštom" #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Pošalji odabrano kao PDF putem e-pošte" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" -msgstr "" +msgstr "Postavke e-pošte" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" -msgstr "" +msgstr "Omogući automatsko spremanje" + +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Omogući zapise otklanjanja grešaka" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" -msgstr "" +msgstr "Šifriraj PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" -msgstr "" +msgstr "Šifriranje" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "Poboljšani Windows MetaFile (*.emf)" +msgstr "Enhanced Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Greška" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Greška pri pokretanju aplikacije {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,49 +665,55 @@ msgstr "Procijenjena veličina preuzimanja: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "Razmenjiva slika (*.exif)" +msgstr "Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" -msgstr "" +msgstr "Izuzmi prazne stranice" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Brzo" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" -msgstr "Umetač" +msgstr "Ulagač papira" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Naziv datoteke:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" -msgstr "" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" +msgstr "Putanja datoteke:" + +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Popravi balans bijele i ukloni šum" #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Okreni" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Okrenite stražnje strane obostranih stranica" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" -msgstr "" +msgstr "Okreni obostrane stranice" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "" +msgstr "Za visoku JPEG kvalitetu (80+), za najbolje rezultate potrebno je povećati i kvalitetu slike na svom profilu." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" msgstr "GIF datoteka (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" -msgstr "Instaliraj više jezika" +msgstr "Preuzmite više jezika" #: SettingsResources.resx$Source_Glass$Message msgid "Glass" @@ -665,57 +721,60 @@ msgstr "Staklo" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Sivi tonovi" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Vodoravno poravnanje:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" -msgstr "" +msgstr "Sat (0-23)" #: UiStrings.resx$HueSaturation$Message msgid "Hue / Saturation" msgstr "" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" -msgstr "Ikonice iz:" +msgstr "Ikone sa:" #: UiStrings.resx$Image$Message msgid "Image" -msgstr "" +msgstr "Slika" #: MiscResources.resx$FileTypeImageFiles$Message msgid "Image Files" -msgstr "" +msgstr "Slikovne datoteke" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" -msgstr "" +msgstr "Kvaliteta slike" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" -msgstr "" +msgstr "Postavke slike" #: MiscResources.resx$ImageSaved$Message msgid "Image saved." -msgstr "" +msgstr "Slika je spremljena." #: UiStrings.resx$Import$Message msgid "Import" -msgstr "Uvezi" +msgstr "Uvoz" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" -msgstr "" +msgstr "Napredak uvoza" #: MiscResources.resx$ImportingFormat$Message msgid "Importing {0}..." @@ -739,12 +798,16 @@ msgstr "Instalacija nije uspjela" #: MiscResources.resx$InstallCompletePromptRestart$Message msgid "Installation complete. Do you want to restart NAPS2 now?" -msgstr "Instalacija uspešna. Želite li sada restartovati NAPS2?" +msgstr "Instalacija je završena. Želite li sada ponovno pokrenuti NAPS2?" #: MiscResources.resx$InstallFailed$Message msgid "Installation failed." msgstr "Instalacija nije uspjela." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Sučelje" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "" @@ -755,24 +818,37 @@ msgstr "JPEG datoteka (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000 datoteka (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" +msgstr "JPEG kvaliteta" + +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" msgstr "" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Tipkovni prečaci" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" -msgstr "" +msgstr "Ključne riječi:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Jezik (language)" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Ostavite recenziju" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Lijevo" @@ -781,35 +857,50 @@ msgstr "Lijevo" msgid "Legacy (native UI only)" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Svjetla" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Sviđa Vam se NAPS2?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" -msgstr "" +msgstr "Učitajte slike u NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Učinite PDF-ove pretraživim koristeći OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Ručna IP adresa" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Maksimalna kvaliteta (velike datoteke)" -#: FPdfSettings.resx$groupMetadata.Text$Message -msgid "Metadata" +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" msgstr "" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Metadata$Message +msgid "Metadata" +msgstr "Metapodaci" + +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" -msgstr "" +msgstr "Minuta (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" -msgstr "" +msgstr "Mjesec (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" -msgstr "" +msgstr "Više informacija" #: UiStrings.resx$MoveDown$Message msgid "Move Down" @@ -819,75 +910,90 @@ msgstr "Pomakni dolje" msgid "Move Up" msgstr "Pomakni gore" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Višestruki jezici" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Višestruki jezici..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" -msgstr "" +msgstr "Više skeniranja (fiksna odgoda između skeniranja)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" -msgstr "" +msgstr "Više skeniranja (upit između skeniranja)" #: MiscResources.resx$NAPS2$Message #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "" +msgstr "NAPS2 je potpuno besplatan. Razmotrite mogućnost donacije." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" -msgstr "" +msgstr "Naziv (neobavezno)" #: MiscResources.resx$NameMissing$Message msgid "Name missing." msgstr "Nedostaje ime." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" -msgstr "" +msgstr "Novo" #: UiStrings.resx$NewProfile$Message msgid "New Profile" -msgstr "" +msgstr "Novi profil" #: UiStrings.resx$Next$Message msgid "Next" -msgstr "" +msgstr "Sljedeće" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" -msgstr "" +msgstr "Slijedeće skeniranje" #: MiscResources.resx$NoDeviceSelected$Message #: SdkResources.resx$NoDeviceSelected$Message msgid "No device selected." -msgstr "Uređaj nije odabran." +msgstr "Nije odabran uređaj." + +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Nisu pronađeni uređaji." #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." -msgstr "" +msgstr "U ulagaču nema stranica." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "" +msgstr "Nije odabran nijedan pružatelj usluga." #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message msgid "No scanning device was found." -msgstr "Uređaj za skeniranje nije pronađen." +msgstr "Nije pronađen uređaj za skeniranje." #: MiscResources.resx$NoUpdates$Message msgid "No updates available." -msgstr "" +msgstr "Nema dostupnih ažuriranja." #: SettingsResources.resx$TiffComp_None$Message msgid "None" @@ -895,270 +1001,275 @@ msgstr "" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Ne sada" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" -msgstr "" +msgstr "Broj skeniranja:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Preuzmi OCR" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "" +msgstr "Napredak OCR-a" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" -msgstr "Podesi OCR" +msgstr "OCR postavke" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR jezik:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "" +msgstr "OCR način:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "U redu" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" -msgstr "" +msgstr "Širina pomaka na temelju poravnanja (WIA)" #: SettingsResources.resx$TwainImpl_OldDsm$Message msgid "Old DSM" -msgstr "" +msgstr "Stari DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" -msgstr "" +msgstr "Jedna datoteka po stranici" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" -msgstr "" +msgstr "Jedna datoteka po skeniranju" #: MiscResources.resx$FilesCouldNotBeDownloaded$Message msgid "One or more files could not be downloaded." -msgstr "Jedna ili više datoteka nisu mogle biti skinute." +msgstr "Jednu ili više datoteka nije bilo moguće preuzeti." + +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Dopusti samo jednu NAPS2 instancu" -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" -msgstr "" +msgstr "Otvori mapu" #: MiscResources.resx$ActiveOperations$Message msgid "Operation in Progress" +msgstr "Radnja u tijeku" + +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" msgstr "" #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" -msgstr "" +msgstr "Izlaz" #: MiscResources.resx$OverwriteFile$Message msgid "Overwrite File" msgstr "Presnjimiti datoteku?" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" -msgstr "" +msgstr "Lozinka vlasnika:" #: MiscResources.resx$FileTypePdf$Message msgid "PDF Document (*.pdf)" -msgstr "" +msgstr "PDF dokument (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" -msgstr "" +msgstr "PDF postavke" #: MiscResources.resx$PdfSaved$Message msgid "PDF saved." -msgstr "" +msgstr "PDF spremljen." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "PNG datoteka (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Veličina stranice:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" -msgstr "" +msgstr "Izvor papira:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" -msgstr "" +msgstr "Lozinka" #: UiStrings.resx$Paste$Message msgid "Paste" -msgstr "" +msgstr "Zalijepi" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Priključak" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" -msgstr "" +msgstr "Naknadna obrada" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Preventivno pokretanje OCR-a nakon skeniranja" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." -msgstr "" +msgstr "Pritisnite Započni kada ste spremni." #: UiStrings.resx$PreviewFormTitle$Message msgid "Preview" msgstr "Pregled" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" -msgstr "Pregled:" +msgstr "Pretpregled:" #: UiStrings.resx$Previous$Message msgid "Previous" -msgstr "" +msgstr "Prethodno" #: MiscResources.resx$Print$Message #: UiStrings.resx$Print$Message msgid "Print" -msgstr "" +msgstr "Ispis" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Postavke profila" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" -msgstr "" +msgstr "Profil:" #: UiStrings.resx$Profiles$Message #: UiStrings.resx$ProfilesFormTitle$Message msgid "Profiles" -msgstr "Profil" +msgstr "Profili" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Pitaj ako je odabrano" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" -msgstr "" +msgstr "Pitaj za putanju datoteke" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "Davatelj usluga" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." -msgstr "" +msgstr "Spremno za skeniranje {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" -msgstr "" +msgstr "Obnovi" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" -msgstr "" +msgstr "Oporavak skeniranih slika" #: MiscResources.resx$Recovering$Message msgid "Recovering..." -msgstr "" +msgstr "Oporavljam..." #: MiscResources.resx$RecoveryProgress$Message msgid "Recovery Progress" -msgstr "" +msgstr "Napredak oporavka" + +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Ponovi" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Ponovi {0}" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" -msgstr "" +msgstr "Zapamti ove postavke" #: UiStrings.resx$Reorder$Message msgid "Reorder" -msgstr "Resortiraj" +msgstr "Prerasporedi" #: UiStrings.resx$Reset$Message msgid "Reset" -msgstr "" +msgstr "Resetiraj" #: MiscResources.resx$ResetImage$Message msgid "Reset Image" -msgstr "" +msgstr "Resetiraj sliku" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Rezolucija:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" -msgstr "" +msgstr "Vrati zadane postavke" #: UiStrings.resx$Reverse$Message msgid "Reverse" msgstr "" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" -msgstr "" +msgstr "Poništi" #: SettingsResources.resx$HorizontalAlign_Right$Message msgid "Right" @@ -1170,37 +1281,40 @@ msgstr " Rotiraj" #: UiStrings.resx$RotateLeft$Message msgid "Rotate Left" -msgstr "Rotiraj levo" +msgstr "Rotirajte ulijevo" #: UiStrings.resx$RotateRight$Message msgid "Rotate Right" -msgstr "Rotiraj desno" +msgstr "Rotiraj udesno" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "" +msgstr "Izvodi u pozadini" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "SANE upr. prog." #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Spremi" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Spremi sve" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Spremi sve kao slike" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Spremi sve kao PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1209,7 +1323,7 @@ msgstr "Spremi slike" #: MiscResources.resx$SaveImagesProgress$Message msgid "Save Images Progress" -msgstr "" +msgstr "Napredak spremanja slika" #: MiscResources.resx$SavePdf$Message #: UiStrings.resx$SavePdf$Message @@ -1218,21 +1332,26 @@ msgstr "Spremi PDF" #: MiscResources.resx$SavePdfProgress$Message msgid "Save PDF Progress" -msgstr "" +msgstr "Napredak spremanja PDF-a" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Spremi odabrano" #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Spremi odabrano kao slike" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Spremi odabrano kao PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Spremi u jednu datoteku" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Spremi u više datoteka" @@ -1244,138 +1363,208 @@ msgstr "" msgid "Saving {0}..." msgstr "" -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" -msgstr "Razmjer:" +msgstr "Omjer:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skeniraj" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" -msgstr "" +msgstr "Postavke skeniranja" + +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Skeniraj sa zadanim profilom" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Skeniraj s novim profilom" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Skeniraj s profilom {0}" #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Skenirana slika" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Dijeljenje skenera" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" -msgstr "" +msgstr "Skeniranje stranice {0}" #: MiscResources.resx$BatchStatusScanPage$Message msgid "Scanning page {0} (scan {1})..." -msgstr "" +msgstr "Skeniranje stranice {0} (skeniraj {1})..." #: MiscResources.resx$BatchStatusPage$Message #: MiscResources.resx$ScanProgressPage$Message msgid "Scanning page {0}..." -msgstr "" +msgstr "Skeniranje stranice {0}..." + +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Traženje uređaja..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" -msgstr "" +msgstr "Sekunda (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" -msgstr "" +msgstr "Odaberi" #: UiStrings.resx$SelectAll$Message msgid "Select All" msgstr "Odaberi sve" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Odaberi uređaj" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" -msgstr "" +msgstr "Odaberi izvor" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." -msgstr "Izaberite profil pre nego kliknete dugme za skeniranje." +msgstr "Odaberite profil prije nego što kliknete Skeniraj." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" -msgstr "Izaberite jedan ili više jezika:" +msgstr "Odaberite jedan ili više jezika:" #: MiscResources.resx$SelectedCount$Message msgid "Selected ({0})" msgstr "Odabrano ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" -msgstr "" +msgstr "Razdvojite datoteke pomoću Patch-T" #: UiStrings.resx$SetDefault$Message msgid "Set Default" -msgstr "" +msgstr "Postavi zadano" + +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Postavke" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Dijeli" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Dijeli čak i kada je NAPS2 zatvoren" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Postavke dijeljenog skenera" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Dijeljeni skeneri mogu se koristiti s drugih računala u lokalnoj mreži odabirom \"ESCL upr. prog.\" u postavkama NAPS2 profila drugog računala." #: UiStrings.resx$Sharpen$Message msgid "Sharpen" -msgstr "" +msgstr "Izoštri" + +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Prečac" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Show$Message msgid "Show" -msgstr "" +msgstr "Prikaži" + +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Prikaži alatnu traku \"Profili\"" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Prikaži napredak izvornog TWAIN-a" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Prikaži brojeve stranica" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Bočni izbornik" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" -msgstr "" +msgstr "Datoteke s jednom stranicom" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" -msgstr "" +msgstr "Jedno skeniranje" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" -msgstr "Preskoči upit prilikom spremanja" +msgstr "Preskoči upit za spremanje" + +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Podijeli" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Start$Message msgid "Start" -msgstr "" +msgstr "Započni" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Zaustavi dijeljenje skenera" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" -msgstr "" +msgstr "Raširi na veličinu stranice" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" -msgstr "" +msgstr "Predmet:" #: MiscResources.resx$FileTypeTiff$Message msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF datoteka (*.tiff, *tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" -msgstr "" +msgstr "TWAIN upr. prog." -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" -msgstr "" +msgstr "Tehnički detalji" #: MiscResources.resx$TesseractNotAvailable$Message #: SdkResources.resx$TesseractNotAvailable$Message msgid "The OCR engine is not available. Make sure to install the required package:" -msgstr "" +msgstr "OCR program nije dostupan. Provjerite jeste li instalirali potrebni paket:" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Isteklo je vrijeme za OCR operaciju." #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" -msgstr "" +msgstr "SANE upravljački program nije dostupan. Provjerite jeste li instalirali potrebne pakete:" #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message @@ -1384,24 +1573,24 @@ msgstr "Datoteka '{0}' nije mogla biti uvezena." #: MiscResources.resx$ImportErrorNAPS2Pdf$Message msgid "The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported." -msgstr "Datoteka '{0}' nije mogla biti uvezena. Samo PDF datoteke generirane od strane NAPS2 mogu biti uvezene." +msgstr "Datoteka '{0}' nije mogla biti uvezena. Moguće je uvesti samo PDF datoteke generirane pomoću NAPS2." #: MiscResources.resx$FileInUse$Message msgid "The file could not be overwritten because it is currently in use." -msgstr "" +msgstr "Datoteka se ne može prebrisati jer je trenutno u upotrebi." #: MiscResources.resx$ConfirmOverwriteFile$Message msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Datoteka {0} već postoji. Želite li ju presnimiti?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Sljedeća datoteka je šifrirana i za otvaranje je potrebna lozinka:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message msgid "The scanner has a paper jam." -msgstr "" +msgstr "U skeneru se zaglavio papir." #: MiscResources.resx$DeviceWarmingUp$Message #: SdkResources.resx$DeviceWarmingUp$Message @@ -1411,100 +1600,133 @@ msgstr "" #: MiscResources.resx$DeviceCoverOpen$Message #: SdkResources.resx$DeviceCoverOpen$Message msgid "The scanner's cover is open." -msgstr "" +msgstr "Poklopac skenera je otvoren." #: MiscResources.resx$DriverNotSupported$Message #: SdkResources.resx$DriverNotSupported$Message msgid "The selected driver is not supported on this system." -msgstr "" +msgstr "Odabrani upravljački program nije podržan na ovom sustavu." #: MiscResources.resx$DeviceNotFound$Message #: SdkResources.resx$DeviceNotFound$Message msgid "The selected scanner could not be found." -msgstr "Odabrani skener nije pronađen." +msgstr "Odabrani skener nije moguće pronaći." #: MiscResources.resx$NoFeederSupport$Message #: SdkResources.resx$NoFeederSupport$Message msgid "The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver." -msgstr "" +msgstr "Odabrani skener ne podržava korištenje ulagača. Ako vaš skener ima ulagač, pokušajte upotrijebiti drugi upravljački program." #: MiscResources.resx$NoDuplexSupport$Message #: SdkResources.resx$NoDuplexSupport$Message msgid "The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver." -msgstr "" +msgstr "Odabrani skener ne podržava obostrano skeniranje. Ako bi vaš skener trebao podržavati obostrano skeniranje, pokušajte upotrijebiti drugi upravljački program." #: MiscResources.resx$DeviceBusy$Message #: SdkResources.resx$DeviceBusy$Message msgid "The selected scanner is busy." -msgstr "" +msgstr "Odabrani skener je zauzet." #: MiscResources.resx$DeviceOffline$Message #: SdkResources.resx$DeviceOffline$Message msgid "The selected scanner is offline." -msgstr "Odabrani skener nije uključen." +msgstr "Odabrani skener je izvan mreže." + +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Radni proces se srušio." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Radni proces se srušio. Provjerite preglednik događaja u sustavu Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Tema:" -#: FImageSettings.resx$groupTiff.Text$Message +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" -msgstr "" +msgstr "TIFF opcije" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" -msgstr "" +msgstr "Vrijeme između skeniranja (sekunde):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" -msgstr "" +msgstr "Naslov:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Alati" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" -msgstr "" +msgstr "TWAIN implementacija:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 in)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" -msgstr "" +msgstr "US Letter (8.5x11 in)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Poništi dodjelu" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Poništi" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Poništi {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Nepoznat skener" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" -msgstr "" +msgstr "Nespremljene promjene" #: MiscResources.resx$UpdateProgress$Message msgid "Update Progress" -msgstr "" +msgstr "Napredak ažuriranja" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Provjera ažuriranja je onemogućena." #: MiscResources.resx$Updating$Message msgid "Updating..." -msgstr "" +msgstr "Ažuriranje..." #: MiscResources.resx$UploadingEmail$Message msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" -msgstr "Koristi nativni UI" +msgstr "Koristi izvorno sučelje" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Koristi predefinirane postavke" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" -msgstr "" +msgstr "Lozinka korisnika:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." -msgstr "Korištenje OCR-a zahteva da preuzmete svaki jezik koji želite da skenirate." +msgstr "Korištenje OCR-a zahtijeva preuzimanje svakog pojedinog jezika koji želite skenirati." #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message @@ -1513,119 +1735,120 @@ msgstr "Verzija {0}" #: UiStrings.resx$View$Message msgid "View" -msgstr "Pogled" +msgstr "Pogledaj" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" -msgstr "" +msgstr "WIA upravljački program" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Čekam da TWAIN završi..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." -msgstr "" +msgstr "Čekam na autorizaciju..." #: MiscResources.resx$BatchStatusWaitingForScan$Message msgid "Waiting for scan {0}..." -msgstr "" +msgstr "Čekam na skeniranje {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "WIA verzija:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" -msgstr "" +msgstr "Godina" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" -msgstr "" +msgstr "Godina (00-99)" #: MiscResources.resx$PdfNoPermissionToExtractContent$Message #: SdkResources.resx$PdfNoPermissionToExtractContent$Message msgid "You do not have permission to copy content from the file '{0}'." -msgstr "Nemate dozvolu za kopiranje sadržaja iz datoteke '{0}'." +msgstr "Nemate dopuštenje za kopiranje sadržaja iz datoteke '{0}'." #: MiscResources.resx$DontHavePermission$Message msgid "You don't have permission to save files at this location." -msgstr "Nemate dopuštenja da snimite datoteke na ovu lokaciju." +msgstr "Nemate dopuštenje za spremanje datoteka na ovu lokaciju." #: MiscResources.resx$ExitWithUnsavedChanges$Message msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" -msgstr "" +msgstr "Imate nespremljene promjene. Jeste li sigurni da želite izaći i odbaciti te promjene?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Uvećaj/Umanji" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" -msgstr "" +msgstr "Stvarna veličina" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" -msgstr "" +msgstr "Uvećaj" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" -msgstr "" +msgstr "Smanji" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "in" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" -msgstr "" +msgstr "od {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} datoteka" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} pronađen(a) uređaj(a)." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" -msgstr "" +msgstr "{0} slika(e) skenirana(e) {1} u {2} možda nisu spremljene i mogu se oporaviti. Želite li ih oporaviti?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." -msgstr "" +msgstr "{0} slike(a) spremljena(o)." #: MiscResources.resx$PdfStatus$Message #: UiStrings.resx$XOfY$Message diff --git a/NAPS2.Lib/Lang/po/hu.po b/NAPS2.Lib/Lang/po/hu.po index e59615f0b4..c2060a7e62 100644 --- a/NAPS2.Lib/Lang/po/hu.po +++ b/NAPS2.Lib/Lang/po/hu.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Hungarian\n" "Language: hu\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "\"Mentés\" gomb alapértelmezett művelete:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "\"Beolvasás\" gomb alapértelmezett művelete:" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "A \"Beolvasás\" menü megváltoztatja az alapértelmezett profilt" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 eszköz található." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" -msgstr "Szines (24 bit)" - -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" +msgstr "24 bites szín" #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -74,200 +58,226 @@ msgstr "Névjegy" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." -msgstr "Beolvasás..." +msgstr "Adatgyűjtés..." + +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Művelet" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Haladó" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" -msgstr "Bővebb profilbeállítások" +msgstr "Haladó profilbeállítások" #: MiscResources.resx$AllCount$Message msgid "All ({0})" -msgstr "Mind ({0})" +msgstr "Összes ({0})" #: MiscResources.resx$FileTypeAllFiles$Message msgid "All Files" -msgstr "Minden fájl" +msgstr "Összes fájl" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Jegyzet engedélyezése" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Tartalom másolásának engedélyezése" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" -msgstr "Tartalom másolásának engedélyezése hozzáféréshez" +msgstr "Tartalom másolásának engedélyezése az akadálymentesítés érdekében" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" -msgstr "Dokumentum szerkesztés engedélyezése" +msgstr "Dokumentum összeállításának engedélyezése" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Dkoumentum módosításának engedélyezése" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" -msgstr "Űrlap kitöltés engedélyezése" +msgstr "Űrlap kitöltésének engedélyezése" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Legjobb minőségű nyomtatás engedélyezése" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Nyomtatás engedélyezése" #: UiStrings.resx$AltDeinterleave$Message msgid "Alternate Deinterleave" -msgstr "Alternatív képminőség javítás" +msgstr "Alternatív Deinterleave" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" -msgstr "" +msgstr "Alternatív átlapolás" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Mindig kérdezzen rá" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Mindig kérdezzen rá" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" -msgstr "További bővitmény szükséges, hogy ezt a PDF fájlt importálhassa. Szeretné most letölteni?" +msgstr "A PDF-fájl importálásához egy további összetevőre van szükség. Szeretné letölteni most?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Hiba történt a frissités telepitésekor." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Hiba történt a karakterfelismerő futtatásakor." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "Hiba történt az engedélyezés során." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." -msgstr "" +msgstr "Hiba történt az automatikus mentés során." + +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Hiba történt a frissítés telepítésekor." #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." -msgstr "" +msgstr "Hiba történt a fájl mentésekor." #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "" +msgstr "Hiba történt az e-mail elküldésekor." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." -msgstr "Hiba történt az e-mail küldése során.." +msgstr "Hiba történt e-mail küldése közben." #: MiscResources.resx$UnknownDriverError$Message #: SdkResources.resx$UnknownDriverError$Message msgid "An error occurred with the scanning driver." -msgstr "Hiba történt a képolvasó meghajtójával.." +msgstr "Hiba történt a lapolvasó illesztőprogrammal." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "" +msgstr "Egy művelet folyamatban van. Biztos, hogy ki szeretne lépni és meg szeretné szakítani a műveletet?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "" +msgid "An unknown error occurred during the batch scan." +msgstr "Ismeretlen hiba történt a kötegelt lapolvasás során." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "" +msgstr "Elérhető egy frissítés." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "" +msgstr "Elérhető egy frissítés a karakterfelismeréshez." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple illesztőprogram" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Alkalmazás" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" -msgstr "" +msgstr "Fényerő/kontraszt alkalmazása a beolvasás után" #: UiStrings.resx$ApplyToSelected$Message msgid "Apply to all {0} selected images" -msgstr "" +msgstr "Alkalmazás az összes kiválasztott {0} képre" #: MiscResources.resx$ConfirmCancelBatch$Message msgid "Are you sure you want to cancel the batch scan?" -msgstr "" +msgstr "Biztos, hogy meg szeretné szakítani a kötegelt beolvasást?" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" -msgstr "Biztos hogy tisztítja a(z) {0} képet?" +msgstr "Biztos, hogy törölni szeretné a {0} elemet?" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "" +msgstr "Biztos, hogy törölni szeretne \"{0}\" elemet?" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" -msgstr "Biztos hogy törli a következő profilt: {0}?" +msgstr "Biztos, hogy törölni szeretné a következő profilt: {0}?" #: MiscResources.resx$ConfirmDeleteItems$Message msgid "Are you sure you want to delete {0} item(s)?" -msgstr "Biztos hogy törli mind a(z) {0} oldalt?" +msgstr "Biztos, hogy törölni szeretne {0} elemet?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "Biztos hogy törli mind a(z) {0} profilt?" +msgstr "Biztos, hogy törölni szeretne {0} profilt?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Biztos benne, hogy nem szeretné többé megosztani a {0} lapolvasót?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" -msgstr "" +msgstr "Biztos benne, hogy vissza szeretné vonni {0} kép módosítását?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Hozzárendelés" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" -msgstr "Csatolt állomány neve:" +msgstr "Melléklet neve:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Szerző:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "Engedélyezés" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "Automatikus" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Automatikus mentés beállításai" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Automatikusan növekvő szám (1 számjegy)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" -msgstr "" +msgstr "Automatikusan növekvő szám (2 számjegy)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" -msgstr "" +msgstr "Automatikusan növekvő szám (3 számjegy)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" -msgstr "" +msgstr "Automatikusan növekvő szám (4 számjegy)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "" +msgstr "Karakterfelismerés automatikus futtatása beolvasás után" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" @@ -277,83 +287,68 @@ msgstr "B4 (250 x 353 mm)" msgid "B5 (176x250 mm)" msgstr "B5 (176 x 250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Kötegelt beolvasás" #: MiscResources.resx$BatchStatusCancelled$Message msgid "Batch cancelled." -msgstr "" +msgstr "A kötegelt feldolgozás törlésre került." #: MiscResources.resx$BatchStatusComplete$Message msgid "Batch completed successfully." -msgstr "A folyamat befejeződött." +msgstr "A kötegelt feldolgozás sikeresen befejeződött." #: MiscResources.resx$BatchStatusError$Message msgid "Batch scan stopped due to error." -msgstr "Hiba történt a művelet során, a folyamat megszakadt." +msgstr "A kötegelt beolvasás hiba miatt megszakadt." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Legjobb" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bitmélység:" #: MiscResources.resx$FileTypeBmp$Message msgid "Bitmap Files (*.bmp)" -msgstr "BMP fájl (*.bmp)" +msgstr "BMP fájlok (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Fekete-fehér" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Üres oldalak" #: UiStrings.resx$BrightnessContrast$Message msgid "Brightness / Contrast" -msgstr "Fényerő / kontraszt" +msgstr "Fényerő / Kontraszt" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Fényerő:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Nem találja a lapolvasóját? Olvasson a NAPS2 Flatpak korlátairól." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Mégse" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" -msgstr "" +msgstr "A kötegelt feldolgozás törlése" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." @@ -363,58 +358,71 @@ msgstr "Visszavonás...." msgid "Center" msgstr "Középre" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" -msgstr "" +msgstr "Változtatás" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "Frissítések keresése" +msgstr "Frissítések ellenőrzése" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "" +msgstr "Ellenőrzés..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "" +msgstr "E-mail szolgáltató kiválasztása" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" msgstr "Profil kiválasztása" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" -msgstr "Képolvasó kiválasztása" +msgstr "Eszköz kiválasztása" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message msgid "Clear" -msgstr "Kép tisztítása" +msgstr "Összes törlése" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Összes törlése" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" -msgstr "Csak mentse, ne jelenjenek meg a programban" +msgstr "Képek törlése mentés után" #: MiscResources.resx$Close$Message msgid "Close" msgstr "Bezárás" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Egyesítés" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "A kommunikáció a lapolvasó eszközzel megszakadt." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Kompatibilitás" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Tömörítés:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Csatlakozás" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Csatlakozási hiba." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontraszt:" @@ -425,119 +433,123 @@ msgstr "Másolás" #: MiscResources.resx$CopyProgress$Message msgid "Copy Progress" -msgstr "Folyamatban" +msgstr "Másolás folyamata" #: MiscResources.resx$Copying$Message msgid "Copying..." -msgstr "Másolás folyamatban..." +msgstr "Másolás..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} NAPS2 Közreműködők" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" -msgstr "" +msgstr "Lefedettségi küszöbérték" #: UiStrings.resx$Crop$Message msgid "Crop" msgstr "Levágás" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" -msgstr "" +msgstr "Levágás oldalméretre" #: MiscResources.resx$CustomPageSizeFormat$Message msgid "Custom ({0}x{1} {2})" -msgstr "" +msgstr "Egyéni ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" -msgstr "Egyedi lap méret" +msgstr "Egyéni oldalméret" + +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Egyéni felbontás" #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" -msgstr "" +msgstr "Egyéni forgatás" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" -msgstr "" +msgstr "Egyéni SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." -msgstr "" +msgstr "Egyéni..." + +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Sötét" -#: FPlaceholders.resx$label6.Text$Message +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" -msgstr "" +msgstr "Nap (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Alapértelmezett" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" -msgstr "" +msgstr "Alapértelmezett fájl elérési útvonal:" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" msgstr "Elhagyás" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" -msgstr "Oldal törlése" +msgstr "Törlés" #: UiStrings.resx$Deskew$Message msgid "Deskew" -msgstr "Visszatorzítás" +msgstr "Kiegyenesítés" #: MiscResources.resx$AutoDeskewProgress$Message msgid "Deskew Progress" -msgstr "Visszatorzítás folyamatban" +msgstr "Kiegyenesítés folyamata" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" -msgstr "Szkennelt oldalak visszatorzítása" +msgstr "Beolvasott oldalak kiegyenesítése" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." -msgstr "Visszatorzítás..." +msgstr "Kiegyenesítés..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Eszköz:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Méretek" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" -msgstr "Név:" +msgstr "Megjelenített név:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Dokumentum javítás" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "Támogatás" +msgstr "Adományozás" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Kész" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Letöltés" @@ -550,51 +562,86 @@ msgstr "Letöltési hiba" msgid "Download Needed" msgstr "Letöltés szükséges" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Letöltési folyamat" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "Dpi" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Kétoldalas" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL illesztőprogram" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL hálózati illesztőprogram" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB illesztőprogram" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Szerkesztés" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Szerkesztés a következővel: {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Szerkesztés..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Összes küldése e-mailben" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Összes elküldése e-mailben, PDF-ként" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "PDF küldése Email-ben" +msgstr "PDF küldése e-mailben" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" -msgstr "" +msgstr "PDF e-mailben történő elküldésének folyamata" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Kiválasztott küldése e-mailben" #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Kiválasztottak elküldése e-mailbenm PDF-ként" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" -msgstr "Email beállítások" +msgstr "E-mail beállítások" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Automatikus mentés engedélyezése" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Hibakeresési naplózás engedélyezése" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "PDF titkosítása" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Titkosítás" @@ -602,12 +649,15 @@ msgstr "Titkosítás" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "EMF fájl (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Hiba" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Hiba az alkalmazás indításakor {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,44 +667,50 @@ msgstr "Becsült letöltési méret: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "EXIF fájl (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" -msgstr "Üres oldalak kihagyása" +msgstr "Üres oldalak kizárása" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Gyors" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Lapadagoló" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Fájlnév" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Fájl elérési útvonal:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Fehéregyensúly javítása és zaj eltávolítása" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Átfordítás" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Duplex (kétoldalas) oldalak hátoldalának megfordítása" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" -msgstr "" +msgstr "Kétoldalas oldalak megfordítása" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "" +msgstr "Magas JPEG minőség (80+) elérése érdekében növelje a képminőséget a profilban a legjobb eredmény eléréséhez." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" msgstr "GIF fájl (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "További nyelvek beszerzése" @@ -665,24 +721,27 @@ msgstr "Síkágy" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" -msgstr "Szürkeárnylatos" +msgstr "Szürkeárnyalatos" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Vízszintes igazítás:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Óra (0-23)" #: UiStrings.resx$HueSaturation$Message msgid "Hue / Saturation" -msgstr "" +msgstr "Színárnyalat / telítettség" + +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Gazdagép" #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" @@ -696,14 +755,14 @@ msgstr "Kép" msgid "Image Files" msgstr "Képfájlok" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Képminőség" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" -msgstr "Kép beállításai" +msgstr "Kép beállítások" #: MiscResources.resx$ImageSaved$Message msgid "Image saved." @@ -715,7 +774,7 @@ msgstr "Importálás" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" -msgstr "" +msgstr "Importálás folyamata" #: MiscResources.resx$ImportingFormat$Message msgid "Importing {0}..." @@ -727,7 +786,7 @@ msgstr "Importálás..." #: MiscResources.resx$Install$Message msgid "Install {0}" -msgstr "" +msgstr "{0} telepítése" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" @@ -739,15 +798,19 @@ msgstr "A telepítés sikertelen" #: MiscResources.resx$InstallCompletePromptRestart$Message msgid "Installation complete. Do you want to restart NAPS2 now?" -msgstr "A telepítés sikeres. Újra szeretnéd indítani a NAPS2 programot most?" +msgstr "A telepítés befejeződött. Szeretné most újraindítani a NAPS2 programot?" #: MiscResources.resx$InstallFailed$Message msgid "Installation failed." msgstr "A telepítés sikertelen." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Felület" + #: UiStrings.resx$Interleave$Message msgid "Interleave" -msgstr "Átrendezés" +msgstr "Átlapolás" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" @@ -755,59 +818,87 @@ msgstr "JPEG fájl (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000 fájl (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" -msgstr "JPEG tömörítés" +msgstr "Jpeg minőség" + +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Képek megtartása a munkamenetek között" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Billentyűparancsok" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Kulcsszavak:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Nyelv" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Hagyjon egy értékelést" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Balra" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" -msgstr "" +msgstr "Régi (csak natív felhasználói felület)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Világos" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Kedveli a NAPS2-t?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" -msgstr "" +msgstr "Képek betöltése a NAPS2-be" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "A PDF-ek kereshetővé tétele karakterfelismeréssel" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Kézi IP" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Legjobb minőség (nagy fájlok)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Memória átvitel" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metaadatok" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Perc (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Hónap (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "További információ" @@ -819,35 +910,47 @@ msgstr "Fel" msgid "Move Up" msgstr "Le" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Több nyelv" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Több nyelv..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" -msgstr "" +msgstr "Többszörös beolvasás (rögzített késleltetés a beolvasások között)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" -msgstr "" +msgstr "Többszörös beolvasás ( kérdés a beolvasások között)" #: MiscResources.resx$NAPS2$Message #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "" +msgstr "A NAPS2 teljesen ingyenes. Fontolja meg az adományozást." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" -msgstr "" +msgstr "Név (nem kötelező)" #: MiscResources.resx$NameMissing$Message msgid "Name missing." -msgstr "Hiányzó név." +msgstr "A név hiányzik." + +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Natív átvitel" #: UiStrings.resx$New$Message msgid "New" @@ -861,116 +964,106 @@ msgstr "Új profil" msgid "Next" msgstr "Következő" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" -msgstr "" +msgstr "Következő beolvasás" #: MiscResources.resx$NoDeviceSelected$Message #: SdkResources.resx$NoDeviceSelected$Message msgid "No device selected." msgstr "Nincs kiválasztott eszköz." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Nem található eszköz." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." -msgstr "Nincs lap a beolvasóban." +msgstr "Nincs lap a lapadagolóban." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "" +msgstr "Nincs szolgáltató kiválasztva." #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message msgid "No scanning device was found." -msgstr "Nincs elérhető képolvasó." +msgstr "Nem található lapolvasó eszköz." #: MiscResources.resx$NoUpdates$Message msgid "No updates available." -msgstr "Nem érhető el frissítés." +msgstr "Nem érhetők el frissítések." #: SettingsResources.resx$TiffComp_None$Message msgid "None" -msgstr "" +msgstr "Nincs" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Most nem" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" -msgstr "" +msgstr "A beolvasások száma:" #: UiStrings.resx$Ocr$Message msgid "OCR" msgstr "Karakterfelismerés" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" -msgstr "OCR Letöltés" +msgstr "Karakterfelismerés letöltés" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "" +msgstr "Karakterfelismerés folyamata" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" -msgstr "OCR Telepítés" +msgstr "Karakterfelismerés beállítás" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Karakterfelismerés nyelve:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "" +msgstr "Karakterfelismerés mód:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "Ok" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" -msgstr "" +msgstr "Igazításon alapuló eltolási szélesség (WIA)" #: SettingsResources.resx$TwainImpl_OldDsm$Message msgid "Old DSM" -msgstr "" +msgstr "Régi DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" -msgstr "" +msgstr "Egy fájl oldalanként" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" -msgstr "" +msgstr "Egy fájl beolvasásonként" #: MiscResources.resx$FilesCouldNotBeDownloaded$Message msgid "One or more files could not be downloaded." msgstr "Egy vagy több fájlt nem sikerült letölteni." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Csak egyetlen NAPS2 példány engedélyezése" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Mappa megnyitása" @@ -978,11 +1071,15 @@ msgstr "Mappa megnyitása" msgid "Operation in Progress" msgstr "Művelet folyamatban" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (új)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Web Access" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Kimenet" @@ -990,18 +1087,18 @@ msgstr "Kimenet" msgid "Overwrite File" msgstr "Fájl felülírása" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" -msgstr "" +msgstr "Tulajdonos jelszó:" #: MiscResources.resx$FileTypePdf$Message msgid "PDF Document (*.pdf)" -msgstr "" +msgstr "PDF dokumentum (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" -msgstr "PDF beállításai" +msgstr "PDF beállítások" #: MiscResources.resx$PdfSaved$Message msgid "PDF saved." @@ -1009,35 +1106,33 @@ msgstr "PDF mentve." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "PNG fájl (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Oldalméret:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" -msgstr "Forrás:" +msgstr "Papírforrás:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Jelszó" @@ -1045,29 +1140,32 @@ msgstr "Jelszó" msgid "Paste" msgstr "Beillesztés" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" -msgstr "" +msgstr "Helyettesítők" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Utófeldogozás" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Előzetes OCR futtatás a beolvasás után" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." -msgstr "" +msgstr "Ha kész, nyomja meg a Kezdés gombot." #: UiStrings.resx$PreviewFormTitle$Message msgid "Preview" msgstr "Előnézet" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Előnézet:" @@ -1080,12 +1178,11 @@ msgstr "Előző" msgid "Print" msgstr "Nyomtatás" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" -msgstr "Profil-beálítások" +msgstr "Profil beállítások" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profil:" @@ -1094,25 +1191,29 @@ msgstr "Profil:" msgid "Profiles" msgstr "Profilok" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Kérdezzen rá, ha kiválasztva" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" -msgstr "Kérdezzen rá a mentési helyre" +msgstr "Kérdezzen rá a fájl elérési útvonalára" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "Szolgáltató" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." -msgstr "" +msgstr "Készen áll {0} beolvasásra." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Helyreállítás" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" -msgstr "Szkennelt képek helyreállítása" +msgstr "Beolvasott képek helyreállítása" #: MiscResources.resx$Recovering$Message msgid "Recovering..." @@ -1120,17 +1221,23 @@ msgstr "Helyreállítás..." #: MiscResources.resx$RecoveryProgress$Message msgid "Recovery Progress" -msgstr "Helyreállítási folyamat" +msgstr "Helyreállítás folyamata" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Mégis" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Mégis {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" -msgstr "Jegyezd meg ezeket a beállításokat" +msgstr "Emlékezzen ezekre a beállításokra" #: UiStrings.resx$Reorder$Message msgid "Reorder" -msgstr "Rendezés" +msgstr "Átrendezés" #: UiStrings.resx$Reset$Message msgid "Reset" @@ -1140,25 +1247,29 @@ msgstr "Visszaállítás" msgid "Reset Image" msgstr "Kép visszaállítása" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Felbontás:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Alapértelmezések visszaállítása" #: UiStrings.resx$Reverse$Message msgid "Reverse" -msgstr "" +msgstr "Visszafelé" + +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Összes visszafordítása" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Kiválasztott visszafordítása" #: UiStrings.resx$Revert$Message msgid "Revert" -msgstr "" +msgstr "Alaphelyzet" #: SettingsResources.resx$HorizontalAlign_Right$Message msgid "Right" @@ -1176,31 +1287,34 @@ msgstr "Elforgatás balra" msgid "Rotate Right" msgstr "Elforgatás jobbra" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "Háttérbe" +msgstr "Futtatás a háttérben" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "OCR futtatása..." +msgstr "Karakterfelismerés futtatása..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "SANE illesztőprogram" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Mentés" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Összes mentése" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Összes mentése képekként" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Összes mentése PDF-ként" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1209,82 +1323,106 @@ msgstr "Képek mentése" #: MiscResources.resx$SaveImagesProgress$Message msgid "Save Images Progress" -msgstr "" +msgstr "Képmentés folyamata" #: MiscResources.resx$SavePdf$Message #: UiStrings.resx$SavePdf$Message msgid "Save PDF" -msgstr "PDF mentése" +msgstr "PDF mentés" #: MiscResources.resx$SavePdfProgress$Message msgid "Save PDF Progress" -msgstr "" +msgstr "PDF mentésének folyamata" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Kiválasztott mentése" #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Kiválasztottak mentése képekként" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Kiválasztott mentése PDF-ként" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" -msgstr "" +msgstr "Mentés egyetlen fájlba" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" -msgstr "" +msgstr "Mentés több fájlba" #: MiscResources.resx$BatchStatusSaving$Message msgid "Saving batch results..." -msgstr "" +msgstr "A kötegelt feldolgozás eredményének mentése..." #: MiscResources.resx$SavingFormat$Message msgid "Saving {0}..." -msgstr "" +msgstr "Mentés {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" -msgstr "" +msgstr "Méretezés az ablakhoz" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Méret:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" -msgstr "Képolvasás" +msgstr "Beolvasás" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" -msgstr "" +msgstr "Beolvasási konfiguráció" + +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Beolvasás az alapértelmezett profillal" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Lapolvasás új profillal" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Lapolvasás a {0} profillal" #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Beolvasott kép" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Lapolvasó megosztása" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" -msgstr "" +msgstr "{0}. oldal beolvasása" #: MiscResources.resx$BatchStatusScanPage$Message msgid "Scanning page {0} (scan {1})..." -msgstr "" +msgstr "{0}. oldal beolvasása ({1}. beolvasás)..." #: MiscResources.resx$BatchStatusPage$Message #: MiscResources.resx$ScanProgressPage$Message msgid "Scanning page {0}..." -msgstr "" +msgstr "{0}. oldal beolvasása..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Eszközök keresése..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" -msgstr "" +msgstr "Másodperc (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Kijelölés" @@ -1293,64 +1431,112 @@ msgstr "Kijelölés" msgid "Select All" msgstr "Összes kijelölése" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Eszköz kiválasztása" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Forrás kiválasztása" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." -msgstr "Válasszon profilt a képolvasás elött." +msgstr "Válasszon ki egy profilt, mielőtt a Beolvasás gombra kattint." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" -msgstr "Válasszon ki legalább egy nyelvet.:" +msgstr "Válasszon ki egy vagy több nyelvet:" #: MiscResources.resx$SelectedCount$Message msgid "Selected ({0})" -msgstr "Kiválasztva ({0})" +msgstr "Kiválasztott ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" -msgstr "" +msgstr "Különálló fájlok Patch-T szerint" #: UiStrings.resx$SetDefault$Message msgid "Set Default" -msgstr "" +msgstr "Beállítás alapértelmezettként" + +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Beállítások" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Megosztás" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Megosztás akkor is, ha a NAPS2 be van zárva" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Megosztott lapolvasó beállítások" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "A megosztott lapolvasók a helyi hálózat más számítógépeiről is használhatók, ha a másik számítógép NAPS2 profilbeállításaiban kiválasztja az \"ESCL illesztőprogramot\"." #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Élesítés" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Billentyűparancs" + +#: UiStrings.resx$Show$Message msgid "Show" -msgstr "" +msgstr "Megjelenítés" + +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "\"Profilok\" eszköztár megjelenítése" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "A natív TWAIN folyamat megjelenítése" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Oldalszámok megjelenítése" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Oldalsáv" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" -msgstr "" +msgstr "Egyoldalas fájlok" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" -msgstr "" +msgstr "Egyetlen beolvasás" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" -msgstr "" +msgstr "Mentési felszólítás kihagyása" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Felosztás" + +#: UiStrings.resx$Start$Message msgid "Start" -msgstr "" +msgstr "Kezdés" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "A lapolvasó megosztásának leállítása" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" -msgstr "" +msgstr "Nyújtás az oldal méretére" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Tárgy:" @@ -1358,114 +1544,153 @@ msgstr "Tárgy:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF fájl (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" -msgstr "TWAIN meghajtó" +msgstr "TWAIN illesztőprogram" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" -msgstr "" +msgstr "Technikai részletek" #: MiscResources.resx$TesseractNotAvailable$Message #: SdkResources.resx$TesseractNotAvailable$Message msgid "The OCR engine is not available. Make sure to install the required package:" -msgstr "" +msgstr "Az karakterfelismerő motor nem áll rendelkezésre. Győződjön meg róla, hogy telepítette a szükséges csomagot:" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Időtúllépés a karakterfelismerés során." #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" -msgstr "" +msgstr "A SANE illesztőprogram nem áll rendelkezésre. Győződjön meg róla, hogy telepítette a szükséges csomagokat:" #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message msgid "The file '{0}' could not be imported." -msgstr "A '{0}' fájl nem importálható.." +msgstr "A következő fájlt nem lehetett importálni: '{0}'." #: MiscResources.resx$ImportErrorNAPS2Pdf$Message msgid "The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported." -msgstr "A '{0}' fájlt nem lehet importálni. Csak a NAPS2 által generált PDF fájlokat lehet importálni." +msgstr "A következő fájlt nem lehetett importálni: '{0}'. Csak a NAPS2 által generált PDF fájlok importálhatók." #: MiscResources.resx$FileInUse$Message msgid "The file could not be overwritten because it is currently in use." -msgstr "" +msgstr "A fájlt nem lehetett felülírni, mert jelenleg használatban van." #: MiscResources.resx$ConfirmOverwriteFile$Message msgid "The file {0} already exists. Do you want to overwrite it?" -msgstr "Már létezik {0} nevű fájl, felül szeretné írni?" +msgstr "A következő fájl már létezik: {0}. Szeretné felülírni?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "A következő fájl titkosított, és a megnyitásához jelszó szükséges:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message msgid "The scanner has a paper jam." -msgstr "" +msgstr "A lapolvasóban papírelakadás van." #: MiscResources.resx$DeviceWarmingUp$Message #: SdkResources.resx$DeviceWarmingUp$Message msgid "The scanner is warming up." -msgstr "" +msgstr "A lapolvasó felmelegedik." #: MiscResources.resx$DeviceCoverOpen$Message #: SdkResources.resx$DeviceCoverOpen$Message msgid "The scanner's cover is open." -msgstr "" +msgstr "A lapolvasó fedele nyitva van." #: MiscResources.resx$DriverNotSupported$Message #: SdkResources.resx$DriverNotSupported$Message msgid "The selected driver is not supported on this system." -msgstr "" +msgstr "A kiválasztott illesztőprogram nem támogatott ezen a rendszeren." #: MiscResources.resx$DeviceNotFound$Message #: SdkResources.resx$DeviceNotFound$Message msgid "The selected scanner could not be found." -msgstr "A kiválasztott képolvasó nem található." +msgstr "A kiválasztott lapolvasó nem található." #: MiscResources.resx$NoFeederSupport$Message #: SdkResources.resx$NoFeederSupport$Message msgid "The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver." -msgstr "" +msgstr "A kiválasztott lapolvasó nem támogatja az lapadagoló használatát. Ha a lapolvasója rendelkezik lapadagolóval, próbáljon meg másik illesztőprogramot használni." #: MiscResources.resx$NoDuplexSupport$Message #: SdkResources.resx$NoDuplexSupport$Message msgid "The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver." -msgstr "" +msgstr "A kiválasztott lapolvasó nem támogatja a kétoldalas használatot. Ha a lapolvasó feltehetőleg támogatja a kétoldalas használatot, próbáljon meg másik illesztőprogramot használni." #: MiscResources.resx$DeviceBusy$Message #: SdkResources.resx$DeviceBusy$Message msgid "The selected scanner is busy." -msgstr "A kiválasztott képolvasó nem elérhető (foglalt)." +msgstr "A kiválasztott lapolvasó foglalt." #: MiscResources.resx$DeviceOffline$Message #: SdkResources.resx$DeviceOffline$Message msgid "The selected scanner is offline." -msgstr "A kiválasztott képolvasó nem elérhető (leválasztva)." +msgstr "A kiválasztott lapolvasó inaktív." + +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "A munkavégző folyamat összeomlott." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "A munkavégző folyamat összeomlott. Ellenőrizze a Windows eseménynaplóját." -#: FImageSettings.resx$groupTiff.Text$Message +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Téma:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" -msgstr "" +msgstr "Tiff beállítások" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" -msgstr "" +msgstr "Beolvasások közötti idő (másodperc):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Cím:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Eszközök" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" -msgstr "" +msgstr "Twain megvalósítás:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 hüvelyk)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" -msgstr "" +msgstr "US Letter (8,5x11 hüvelyk)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Hozzárendelés megszüntetése" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Visszavonás" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Visszavonás {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Ismeretlen lapolvasó" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" @@ -1473,38 +1698,35 @@ msgstr "Nem mentett módosítások" #: MiscResources.resx$UpdateProgress$Message msgid "Update Progress" -msgstr "" +msgstr "Frissítés folyamata" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "A frissítés ellenőrzése le van tiltva." #: MiscResources.resx$Updating$Message msgid "Updating..." -msgstr "" +msgstr "Frissítés..." #: MiscResources.resx$UploadingEmail$Message msgid "Uploading email..." -msgstr "" +msgstr "E-mail feltöltése..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" -msgstr "" +msgstr "Natív felhasználói felület használata" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" -msgstr "A következő bállítások használata" +msgstr "Előre meghatározott beállítások használata" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" -msgstr "" +msgstr "Felhasználó jelszó:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." -msgstr "Az karakterfelismerés használatához le kell tölteni az összes szkennelési nyelvet." +msgstr "A karakterfelismerés használatához minden olyan nyelvet le kell töltenie, amelyet be szeretne olvasni." #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message @@ -1513,119 +1735,120 @@ msgstr "Verzió: {0}" #: UiStrings.resx$View$Message msgid "View" -msgstr "Nézet" +msgstr "Megtekintés" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" -msgstr "WIA meghajtó" +msgstr "WIA illesztőprogram" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." -msgstr "Várakozás a TWAIN befejezésére...." +msgstr "Várakozás a TWAIN-re a befejezéshez..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." -msgstr "" +msgstr "Várakozás az engedélyezésre..." #: MiscResources.resx$BatchStatusWaitingForScan$Message msgid "Waiting for scan {0}..." -msgstr "" +msgstr "Várakozás a beolvasásra {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" -msgstr "" +msgstr "Fehér küszöbérték" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Wia verzió:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" -msgstr "" +msgstr "Év" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" -msgstr "" +msgstr "Év (00-99)" #: MiscResources.resx$PdfNoPermissionToExtractContent$Message #: SdkResources.resx$PdfNoPermissionToExtractContent$Message msgid "You do not have permission to copy content from the file '{0}'." -msgstr "" +msgstr "Nincs jogosultsága a következő fájl tartalmát másolni: '{0}'." #: MiscResources.resx$DontHavePermission$Message msgid "You don't have permission to save files at this location." -msgstr "Nincs hozzáférési egedélye az adott helyhez.." +msgstr "Nincs jogosultsága fájlok mentésére ezen a helyen." #: MiscResources.resx$ExitWithUnsavedChanges$Message msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" -msgstr "Nem mentett módosításai vannak, amik elvesznek ha bezárja a programot. Biztosan kilép?" +msgstr "Vannak el nem mentett módosításai. Biztos, hogy ki akar lépni és el akarja vetni ezeket a módosításokat?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Nagyítás" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Eredeti méret" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Nagyítás" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Kicsinyítés" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "hüvelyk" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" -msgstr "" +msgstr "/ {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" -msgstr "" +msgstr "{0} / {1} fájl" + +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} eszköz található." -#: FRecover.resx$lblPrompt.Text$Message +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" -msgstr "" +msgstr "A {1} {2}-kor beolvasott {0} kép lehet, hogy nem került mentésre, és visszaállítható. Szeretné visszaállítani őket?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." -msgstr "" +msgstr "{0} kép mentve." #: MiscResources.resx$PdfStatus$Message #: UiStrings.resx$XOfY$Message diff --git a/NAPS2.Lib/Lang/po/id.po b/NAPS2.Lib/Lang/po/id.po index 6816896ce1..137f1b79a2 100644 --- a/NAPS2.Lib/Lang/po/id.po +++ b/NAPS2.Lib/Lang/po/id.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:35\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Indonesian\n" "Language: id\n" @@ -19,674 +19,733 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Aksi bawaan tombol \"Simpan\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Aksi bawaan tombol \"Pindai\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Menu \"Pindai\" mengubah profil bawaan" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 perangkat ditemukan." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" -msgstr "" - -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" +msgstr "Kedalaman Warna 24-Bit" #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (29.7x42.0 cm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (21.0x29.7 cm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (14.8x21.0 cm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message msgid "About" -msgstr "" +msgstr "Tentang" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." +msgstr "Mengumpulkan Data..." + +#: UiStrings.resx$Action$Message +msgid "Action" msgstr "" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" -msgstr "" +msgstr "Tingkat Lanjutan" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" -msgstr "" +msgstr "Pengaturan Profile Tingkat Lanjut" #: MiscResources.resx$AllCount$Message msgid "All ({0})" -msgstr "" +msgstr "seluruhnya ({0})" #: MiscResources.resx$FileTypeAllFiles$Message msgid "All Files" -msgstr "" +msgstr "Semua Berkas" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" -msgstr "" +msgstr "Ijinkan pemberian annotasi" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" -msgstr "" +msgstr "Ijinkan penyalinan konten" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" -msgstr "" +msgstr "Ijinkan penyalinan konten untuk aksesibilitas" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" -msgstr "" +msgstr "Ijinkan perakitan dokumen dan penyisipan" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" -msgstr "" +msgstr "Ijinkan perubahan terhadap dokument" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" -msgstr "" +msgstr "Ijinkan pengisian isian formulir" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" -msgstr "" +msgstr "Ijinkan pencetakan berkualitas baik" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" -msgstr "" +msgstr "Ijinkan pencetakan" #: UiStrings.resx$AltDeinterleave$Message msgid "Alternate Deinterleave" -msgstr "" +msgstr "Alternatif Deinterleave" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" -msgstr "" +msgstr "Alternatif Interleave" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Selalu tanyakan" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Selalu tanya" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" -msgstr "" +msgstr "Dibutuhkan komponen tambahan untuk dapat mengimport berkas PDF ini. Apakah anda ingin mengunduhnya sekarang?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "" +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Galat ditemukan dalam proses OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "Galat ditemukan ketika memproses autorisasi." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." -msgstr "" +msgstr "Galat ditemukan ketika menyimpan otomatis." + +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Galat ditemukan ketika proses installasi update." #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." -msgstr "" +msgstr "Galat ditemukan saat proses menyimpan berkas." #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "" +msgstr "Galat ditemukan ketika proses pengiriman melalui surel." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." -msgstr "" +msgstr "Galat ditemukan saat proses pengiriman melalui surel berlangsung." #: MiscResources.resx$UnknownDriverError$Message #: SdkResources.resx$UnknownDriverError$Message msgid "An error occurred with the scanning driver." -msgstr "" +msgstr "Galat ditemukan didalam driver piranti." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "" +msgstr "Tindakan kegiatan sedang berlangsung. Yakin untuk keluar dan membatalkan kegiatan?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "" +msgid "An unknown error occurred during the batch scan." +msgstr "Galat tak diketahui ditemukan dalam proses pindai berkas tumpak." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "" +msgstr "Pemutakhiran aplikasi NAPS telah tersedia." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "" +msgstr "Pemutakhiran aplikasi untuk fitur OCR telah tersedia." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Driver utk Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Aplikasi" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" -msgstr "" +msgstr "Terapkan tingkat kontras/kecerahan setelah pindai selesai" #: UiStrings.resx$ApplyToSelected$Message msgid "Apply to all {0} selected images" -msgstr "" +msgstr "Terapkan ke semua {0} gambar yang dipilih" #: MiscResources.resx$ConfirmCancelBatch$Message msgid "Are you sure you want to cancel the batch scan?" -msgstr "" +msgstr "Batalkan Pemindaian Tumpak, anda yakin?" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" -msgstr "" +msgstr "Lakukan pengosongan {0} item(s), anda yakin?" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "" +msgstr "Lakukan penghapusan {0} item(s), anda yakin?" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" -msgstr "" +msgstr "Lakukan penghapusan profil {0}, anda yakin?" #: MiscResources.resx$ConfirmDeleteItems$Message msgid "Are you sure you want to delete {0} item(s)?" -msgstr "" +msgstr "Lakukan penghapusan {0} item(s), anda yakin?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "" +msgstr "Lakukan penghapusan profile(s) {0}, anda yakin?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Anda yakin ingin berhenti membagikan {0}?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" +msgstr "Lakukan Undo perubahan terhadap gambar: {0}, anda yakin?" + +#: UiStrings.resx$Assign$Message +msgid "Assign" msgstr "" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" -msgstr "" +msgstr "Nama berkas dilampirkan:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" -msgstr "" +msgstr "Author:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "Authorize" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "Auto" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" -msgstr "" +msgstr "Pengaturan (Auto Save)" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "penambahan nomor auto (1 digit)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" -msgstr "" +msgstr "penambahan nomor auto (2 digit)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" -msgstr "" +msgstr "penambahan nomor auto (3 digit)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" -msgstr "" +msgstr "penambahan nomor auto (4 digit)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "" +msgstr "Auto Jalankan OCR Teks setelah pemindaian" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (25.0x35.3 cm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (17.6x25.0 cm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" -msgstr "" +msgstr "Pindai Tumpak" #: MiscResources.resx$BatchStatusCancelled$Message msgid "Batch cancelled." -msgstr "" +msgstr "Tumpak dibatalkan." #: MiscResources.resx$BatchStatusComplete$Message msgid "Batch completed successfully." -msgstr "" +msgstr "Tumpak diselesaikan dengan sukses." #: MiscResources.resx$BatchStatusError$Message msgid "Batch scan stopped due to error." -msgstr "" +msgstr "Pindai Tumpak terhenti dikarenakan galat." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Terbaik" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" -msgstr "" +msgstr "Kedalaman bit:" #: MiscResources.resx$FileTypeBmp$Message msgid "Bitmap Files (*.bmp)" -msgstr "" +msgstr "berkas Bitmap (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" -msgstr "" +msgid "Black and White" +msgstr "Hitam Putih" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" -msgstr "" +msgstr "Laman Kosong" #: UiStrings.resx$BrightnessContrast$Message msgid "Brightness / Contrast" -msgstr "" +msgstr "Kontras / Kecerahan" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" -msgstr "" +msgstr "Kecerahan:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" -msgstr "" +msgstr "Batal" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" -msgstr "" +msgstr "Batalkan Tumpak" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." -msgstr "" +msgstr "Membatalkan...." #: SettingsResources.resx$HorizontalAlign_Center$Message msgid "Center" -msgstr "" +msgstr "Center" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" -msgstr "" +msgstr "Ubah" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "" +msgstr "Periksa pembaharuan" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "" +msgstr "Memeriksa..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "" +msgstr "Pilih penyedia email" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" -msgstr "" +msgstr "Pilih Profil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" -msgstr "" +msgstr "Pilih perangkat" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message msgid "Clear" -msgstr "" +msgstr "Bersihkan" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Bersihkan semua" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" -msgstr "" +msgstr "Bersihkan gambar setelah Menyimpan (Saving)" #: MiscResources.resx$Close$Message msgid "Close" -msgstr "" +msgstr "Tutup" + +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Gabungkan" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Komunikasi dengan alat pindai terganggu." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" -msgstr "" +msgstr "Kompatibilitas" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" -msgstr "" +msgstr "Kompresi:" + +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Hubungkan" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Kesalahan koneksi." -#: FEditProfile.resx$label7.Text$Message #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" -msgstr "" +msgstr "Kontras:" #: UiStrings.resx$Copy$Message msgid "Copy" -msgstr "" +msgstr "Salin" #: MiscResources.resx$CopyProgress$Message msgid "Copy Progress" -msgstr "" +msgstr "Proses Salin" #: MiscResources.resx$Copying$Message msgid "Copying..." -msgstr "" +msgstr "Menyalin..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} , Kontributor NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" -msgstr "" +msgstr "jangkauan Threshold" #: UiStrings.resx$Crop$Message msgid "Crop" -msgstr "" +msgstr "Crop" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" -msgstr "" +msgstr "Crop ke ukuran halaman" #: MiscResources.resx$CustomPageSizeFormat$Message msgid "Custom ({0}x{1} {2})" -msgstr "" +msgstr "Custom ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" +msgstr "Ukuran halaman Custom" + +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" msgstr "" #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" -msgstr "" +msgstr "Rotasi custom" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" -msgstr "" +msgstr "SMTP Custom" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." +msgstr "kostumasi..." + +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" msgstr "" -#: FPlaceholders.resx$label6.Text$Message +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" -msgstr "" +msgstr "Tanggal (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" -msgstr "" +msgstr "Default" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" -msgstr "" +msgstr "Path Default Berkas:" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" -msgstr "" +msgstr "Deinterleave" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" -msgstr "" +msgstr "Hapus" #: UiStrings.resx$Deskew$Message msgid "Deskew" -msgstr "" +msgstr "miring-mereng" #: MiscResources.resx$AutoDeskewProgress$Message msgid "Deskew Progress" -msgstr "" +msgstr "Proses Deskew - miring mereng" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" -msgstr "" +msgstr "Halaman terpindai cek miring-mereng" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." -msgstr "" +msgstr "ngelurusin halaman..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" -msgstr "" +msgstr "piranti:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" -msgstr "" +msgstr "Dimensi" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" -msgstr "" +msgstr "Nama Tampilan:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Koreksi Dokumen" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "" +msgstr "Donasi" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" -msgstr "" +msgstr "Selesai" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" -msgstr "" +msgstr "Unduh" #: MiscResources.resx$DownloadError$Message msgid "Download Error" -msgstr "" +msgstr "Galat Unduh" #: MiscResources.resx$DownloadNeeded$Message msgid "Download Needed" -msgstr "" +msgstr "Butuh mengunduh" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" +msgstr "Sedang mengunduh" + +#: UiStrings.resx$Dpi$Message +msgid "Dpi" msgstr "" #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" -msgstr "" +msgstr "Bolak Balik / 2 muka" + +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL Driver" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL Network Driver" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB Driver" #: UiStrings.resx$Edit$Message msgid "Edit" +msgstr "Edit" + +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" msgstr "" +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Emailkan Semua" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Emailkan Semua sebagai PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "" +msgstr "Emailkan PDF" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" -msgstr "" +msgstr "Emailkan Progres PDF" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Emailkan Pilihan" #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Emailkan Pilihan sebagai PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" -msgstr "" +msgstr "Pengaturan Email" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" -msgstr "" +msgstr "Aktifkan Penyimpanan Otomatis" + +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Aktifkan debug logging" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" -msgstr "" +msgstr "Enkripsi PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" -msgstr "" +msgstr "Enkripsi" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "" +msgstr "Tingkatkan Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" +msgstr "Kesalahan" + +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" msgstr "" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" -msgstr "" +msgstr "Estimasi ukuran unduhan: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" -msgstr "" +msgstr "Lewati halaman kosong" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Cepat" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" -msgstr "" +msgstr "Pengumpan" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Nama Berkas:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" -msgstr "" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" +msgstr "Lokasi berkas:" + +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Perbaiki white balance dan hilangkan noise" #: UiStrings.resx$Flip$Message msgid "Flip" -msgstr "" +msgstr "Putar Balik" + +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Balik sisi halaman bolak-balik" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" -msgstr "" +msgstr "Balik halaman bolak-balik" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "" +msgstr "Untuk JPEG kualitas tinggi, tingkatkan juga kualitas gambar di profilmu untuk hasil terbaik." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" -msgstr "" +msgstr "GIF File (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" -msgstr "" +msgstr "Dapatkan bahasa lain" #: SettingsResources.resx$Source_Glass$Message msgid "Glass" -msgstr "" +msgstr "Kaca" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" -msgstr "" +msgstr "Skala abu-abu" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" -msgstr "" +msgstr "Penyelarasan horizontal:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" -msgstr "" +msgstr "Jam (0-23)" #: UiStrings.resx$HueSaturation$Message msgid "Hue / Saturation" -msgstr "" +msgstr "Hue / Saturation" + +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Host" #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" -msgstr "" +msgstr "Ikon dari:" #: UiStrings.resx$Image$Message msgid "Image" @@ -696,12 +755,12 @@ msgstr "" msgid "Image Files" msgstr "" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "" @@ -745,6 +804,10 @@ msgstr "" msgid "Installation failed." msgstr "" +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Antarmuka" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "" @@ -757,11 +820,20 @@ msgstr "" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "" @@ -781,33 +857,48 @@ msgstr "" msgid "Legacy (native UI only)" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "" @@ -819,11 +910,19 @@ msgstr "" msgid "Move Up" msgstr "" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "" @@ -849,6 +948,10 @@ msgstr "" msgid "Name missing." msgstr "" +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "" @@ -861,7 +964,7 @@ msgstr "" msgid "Next" msgstr "" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "" @@ -870,12 +973,15 @@ msgstr "" msgid "No device selected." msgstr "" +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "" -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "" @@ -897,11 +1003,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "" @@ -909,7 +1015,6 @@ msgstr "" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "" @@ -918,37 +1023,23 @@ msgstr "" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "" @@ -970,7 +1059,11 @@ msgstr "" msgid "One or more files could not be downloaded." msgstr "" -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "" @@ -978,11 +1071,15 @@ msgstr "" msgid "Operation in Progress" msgstr "" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "" @@ -990,7 +1087,7 @@ msgstr "" msgid "Overwrite File" msgstr "" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "" @@ -998,8 +1095,8 @@ msgstr "" msgid "PDF Document (*.pdf)" msgstr "" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "" @@ -1045,21 +1140,24 @@ msgstr "" msgid "Paste" msgstr "" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "" @@ -1067,7 +1165,7 @@ msgstr "" msgid "Preview" msgstr "" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "" @@ -1080,12 +1178,11 @@ msgstr "" msgid "Print" msgstr "" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "" @@ -1094,23 +1191,27 @@ msgstr "" msgid "Profiles" msgstr "" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "" -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "" @@ -1122,9 +1223,15 @@ msgstr "" msgid "Recovery Progress" msgstr "" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "" @@ -1140,15 +1247,11 @@ msgstr "" msgid "Reset Image" msgstr "" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "" @@ -1156,6 +1259,14 @@ msgstr "" msgid "Reverse" msgstr "" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "" @@ -1176,7 +1287,6 @@ msgstr "" msgid "Rotate Right" msgstr "" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1185,7 +1295,6 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1303,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "" msgid "Save PDF Progress" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "" @@ -1244,29 +1363,45 @@ msgstr "" msgid "Saving {0}..." msgstr "" -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "" @@ -1280,11 +1415,14 @@ msgstr "" msgid "Scanning page {0}..." msgstr "" -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "" @@ -1293,7 +1431,10 @@ msgstr "" msgid "Select All" msgstr "" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "" @@ -1302,7 +1443,6 @@ msgstr "" msgid "Select a profile before clicking Scan." msgstr "" -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "" @@ -1311,8 +1451,7 @@ msgstr "" msgid "Selected ({0})" msgstr "" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,37 +1459,84 @@ msgstr "" msgid "Set Default" msgstr "" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "" @@ -1358,12 +1544,11 @@ msgstr "" msgid "TIFF File (*.tiff, *.tif)" msgstr "" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "" @@ -1372,6 +1557,10 @@ msgstr "" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,8 +1583,8 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" msgstr "" #: MiscResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "" msgid "The selected scanner is offline." msgstr "" -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "" @@ -1487,21 +1712,18 @@ msgstr "" msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "" @@ -1515,16 +1737,15 @@ msgstr "" msgid "View" msgstr "" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "" -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1532,19 +1753,19 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "" -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "" @@ -1561,21 +1782,18 @@ msgstr "" msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "" diff --git a/NAPS2.Lib/Lang/po/it.po b/NAPS2.Lib/Lang/po/it.po index 786c4c6013..e9ad428349 100644 --- a/NAPS2.Lib/Lang/po/it.po +++ b/NAPS2.Lib/Lang/po/it.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Italian\n" "Language: it\n" @@ -19,71 +19,58 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Azione predefinita del tasto \"Salva\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Azione predefinita del tasto \"Scansiona\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Menu \"Scansiona\" modifica il profilo predefinito" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 dispositivo trovato." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" -msgstr "Colore a 24-bit" - -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" +msgstr "Colore a 24bit" #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message msgid "About" -msgstr "Informazioni" +msgstr "Info sul programma" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." msgstr "Acquisizione dati..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Azione" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Avanzate" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" -msgstr "Impostazioni avanzate di profilo" +msgstr "Impostazioni profilo avanzate" #: MiscResources.resx$AllCount$Message msgid "All ({0})" @@ -93,66 +80,75 @@ msgstr "Tutti ({0})" msgid "All Files" msgstr "Tutti i file" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Consenti annotazioni" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" -msgstr "Consenti la copia dei contenuti" +msgstr "Consenti copia contenuti" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" -msgstr "Consenti la copia dei contenuti per accessibilità" +msgstr "Consenti copia contenuti per accessibilità" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Consenti composizione documento" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Consenti modifica documento" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Consenti compilazione modulo" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" -msgstr "Consenti stampa ad alta qualità" +msgstr "Consenti stampa in alta qualità" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Consenti stampa" #: UiStrings.resx$AltDeinterleave$Message msgid "Alternate Deinterleave" -msgstr "Deinterleave alternativo" +msgstr "Inverti interfoliazione alternativa" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" -msgstr "Interleave alternativo" +msgstr "Interfoliazione alternativa" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "E' neccessario un componente aggiuntivo per importare questo fle PDF. Vuoi scaricarlo ora?" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Chiedi sempre" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Chiedi sempre" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Per importare questo file PDF è necessario un componente aggiuntivo. Vuoi scaricarlo ora?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Si è verificato un errore durante l'installazione dell'aggiornamento." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Si è verificato un errore durante l'esecuzione dell'OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "Si è verificato un errore provando ad autorizzare." +msgstr "Si è verificato un errore durante il tentativo di autorizzazione." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." msgstr "Si è verificato un errore durante il salvataggio automatico." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Si è verificato un errore durante l'installazione dell'aggiornamento." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Si è verificato un errore durante il salvataggio del file." @@ -168,29 +164,37 @@ msgstr "Si è verificato un errore durante l'invio di una email." #: MiscResources.resx$UnknownDriverError$Message #: SdkResources.resx$UnknownDriverError$Message msgid "An error occurred with the scanning driver." -msgstr "Errore del driver di scansione." +msgstr "Si è verificato un errore con il driver di scansione." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "E' in corso un'operazione. Desideri uscire e cancellare tale operazione?" +msgstr "È in corso un'operazione. Sicuro di volere uscire e annullare questa operazione?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Errore sconosciuto durante la scansione batch." +msgid "An unknown error occurred during the batch scan." +msgstr "Si è verificato un errore sconosciuto durante la scansione in serie." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "E' disponibile un aggiornamento." +msgstr "È disponibile un aggiornamento." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "E' disponibile un aggiornamento per l'OCR." +msgstr "È disponibile un aggiornamento per l'OCR." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Driver Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Applicazione" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Applica luminosità/contrasto dopo la scansione" @@ -200,15 +204,15 @@ msgstr "Applica a tutte le {0} immagini selezionate" #: MiscResources.resx$ConfirmCancelBatch$Message msgid "Are you sure you want to cancel the batch scan?" -msgstr "Sei sicuro di voler annullare la scansione batch?" +msgstr "Vuoi annullare la scansione in serie?" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" -msgstr "Vuoi cancellare il(i) {0} oggetto(i)?" +msgstr "Vuoi eliminare {0} elemento/i?" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "Sicuro di voler eliminare \"{0}\"?" +msgstr "Vuoi eliminare \"{0}\"?" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" @@ -216,69 +220,75 @@ msgstr "Vuoi eliminare il profilo {0}?" #: MiscResources.resx$ConfirmDeleteItems$Message msgid "Are you sure you want to delete {0} item(s)?" -msgstr "Vuoi eliminare {0} oggetto(i)?" +msgstr "Vuoi eliminare {0} elemento/i?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "Vuoi eliminare {0} profilo(i)?" +msgstr "Vuoi eliminare {0} profilo/i?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Sei sicuro di voler interrompere la condivisione di {0}?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" -msgstr "Sicuro di voler annullare i cambiamenti a {0} immagine(i)?" +msgstr "Vuoi annullare le modifiche a {0} immagine/i?" + +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Assegna" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Nome allegato:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autore:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autorizza" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "Automatico" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" -msgstr "Salva impostazioni automaticamente" +msgstr "Salva automaticamente impostazioni" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Autoincrementa numero (1 cifra)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Incremento automatico numero (1 cifra)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" -msgstr "Autoincrementa numero (2 cifre)" +msgstr "Incremento automatico numero (2 cifre)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" -msgstr "Autoincrementa numero (3 cifre)" +msgstr "Incremento automatico numero (3 cifre)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" -msgstr "Autoincrementa numero (4 cifre)" +msgstr "Incremento automatico numero (4 cifre)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "Avvia automaticamente l'OCR dopo la scansione" +msgstr "Dopo la scansione avvia automaticamente l'OCR" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "B4 (250×353 mm)" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Scansione in serie" @@ -288,7 +298,7 @@ msgstr "Scansione in serie annullata." #: MiscResources.resx$BatchStatusComplete$Message msgid "Batch completed successfully." -msgstr "Serie completata." +msgstr "Scansione in serie completata." #: MiscResources.resx$BatchStatusError$Message msgid "Batch scan stopped due to error." @@ -296,23 +306,22 @@ msgstr "Scansione in serie arrestata a causa di un errore." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Migliore" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Profondità di bit:" #: MiscResources.resx$FileTypeBmp$Message msgid "Bitmap Files (*.bmp)" -msgstr "" +msgstr "File bitmap (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Bianco e nero" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Pagine vuote" @@ -320,40 +329,26 @@ msgstr "Pagine vuote" msgid "Brightness / Contrast" msgstr "Luminosità/contrasto" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Luminosità:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Non riesci a trovare lo scanner? Leggi le limitazioni del Flatpak NAPS2." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Annulla" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" -msgstr "Annulla batch" +msgstr "Annulla operazione in serie" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." @@ -363,58 +358,71 @@ msgstr "Annullamento...." msgid "Center" msgstr "Centra" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" -msgstr "Cambia" +msgstr "Modifica" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "Ricerca degli aggiornamenti:" +msgstr "Cerca aggiornamenti" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "Verifica..." +msgstr "Verifica in corso..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "Scegli un provider email" +msgstr "Scegli provider email" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" -msgstr "Scegli Profilo" +msgstr "Scegli profilo" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" -msgstr "Scegli Dispositivo" +msgstr "Scegli dispositivo" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message msgid "Clear" -msgstr "Cancella" +msgstr "Azzera" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Azzera tutto" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" -msgstr "Elimina immagini dopo il salvataggio" +msgstr "Cancella immagini dopo il salvataggio" #: MiscResources.resx$Close$Message msgid "Close" msgstr "Chiudi" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Combina" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "La comunicazione con il dispositivo di scansione è stata interrotta." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Compatibilità" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Compressione:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Connetti" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Errore connessione." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Contrasto:" @@ -425,69 +433,77 @@ msgstr "Copia" #: MiscResources.resx$CopyProgress$Message msgid "Copy Progress" -msgstr "Progresso della copia" +msgstr "Progresso copia" #: MiscResources.resx$Copying$Message msgid "Copying..." -msgstr "Copia in corso..." +msgstr "Copia..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} contributori NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" -msgstr "Soglia di copertura" +msgstr "Soglia copertura" #: UiStrings.resx$Crop$Message msgid "Crop" -msgstr "Taglia" +msgstr "Ritaglia" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" -msgstr "Taglia a dimensione pagina" +msgstr "Ritaglia alla dimensione pagina" #: MiscResources.resx$CustomPageSizeFormat$Message msgid "Custom ({0}x{1} {2})" msgstr "Personalizzato ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" -msgstr "Formato Pagina Personalizzato" +msgstr "Formato pagina personalizzato" + +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Risoluzione personalizzata" #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" -msgstr "Rotazione Personalizzata" +msgstr "Rotazione personalizzata" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" msgstr "SMTP personalizzato" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Personalizzato..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Scuro" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Giorno (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Predefinito" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" -msgstr "Percorso File Predefinito:" +msgstr "Percorso file predefinito:" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" -msgstr "Deinterlaccia" +msgstr "Deinterleave" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -495,13 +511,13 @@ msgstr "Elimina" #: UiStrings.resx$Deskew$Message msgid "Deskew" -msgstr "Ruota ed allinea" +msgstr "Ruota e allinea" #: MiscResources.resx$AutoDeskewProgress$Message msgid "Deskew Progress" msgstr "Progresso allineamento" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Pagine allineate scansionate" @@ -509,174 +525,213 @@ msgstr "Pagine allineate scansionate" msgid "Deskewing..." msgstr "Allineamento..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Dispositivo:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Dimensioni" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Nome visualizzato:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Correzione documento" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "Fai una donazione." +msgstr "Dona" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Fatto" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Scarica" #: MiscResources.resx$DownloadError$Message msgid "Download Error" -msgstr "Errore nel download" +msgstr "Errore download" #: MiscResources.resx$DownloadNeeded$Message msgid "Download Needed" -msgstr "Necessario download" +msgstr "Scaricamento necessario" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" -msgstr "Avanzamento download" +msgstr "Progresso download" + +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "DPI" #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" -msgstr "Fronte/Retro" +msgstr "Fronte/retro" + +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Driver ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "Driver di rete ESCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "Driver USB ESCL" #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Modifica" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Modifica con {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Modifica con..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Invia email a tutti" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Invia e-mail a tutti come PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "" +msgstr "Invia PDF via email" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" msgstr "Progresso invio PDF via email" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Invia selezionati via email" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Invia selezionati via email come PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Impostazioni email" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Abilita salvataggio automatico" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Abilita la registrazione dei log di debug" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" -msgstr "Cripta il PDF" +msgstr "Cifra il PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" -msgstr "Criptare" +msgstr "Crifratura" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "" +msgstr "Enhanced MetaFile di Windows (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Errore" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Errore avvio applicazione {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" -msgstr "Dimensione stimata del download: {0} MB" +msgstr "Dimensione stimata download: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" -msgstr "Esclude pagine vuote" +msgstr "Escludi pagine vuote" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Veloce" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" -msgstr "Caricatore" +msgstr "Alimentatore automatico" + +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Nome file:" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "Nome file" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" +msgstr "Percorso file:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" -msgstr "Percorso del file:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Correggi bilanciamento del bianco e rimuovi il rumore" #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Capovolgi" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Capovolgi lati posteriori pagine fronte/retro" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" -msgstr "Capovolgi pagine fronte-retro" +msgstr "Capovolgi pagine fronte retro" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "Per ottenere i migliori risultati con JPEG di qualità elevata (80+), aumenta anche il valore di Qualità Immagine nel tuo profilo." +msgstr "Per ottenere i migliori risultati con JPEG di qualità elevata (80+), aumenta nel profilo il valore 'Qualità Immagine'." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" -msgstr "" +msgstr "File GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Installa altre lingue" #: SettingsResources.resx$Source_Glass$Message msgid "Glass" -msgstr "Piano" +msgstr "Piano fisso" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Scala di grigi" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Allineamento orizzontale:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Ora (0-23)" @@ -684,9 +739,13 @@ msgstr "Ora (0-23)" msgid "Hue / Saturation" msgstr "Tonalità/saturazione" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/host" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" -msgstr "Trova icone su:" +msgstr "Libreria icone:" #: UiStrings.resx$Image$Message msgid "Image" @@ -694,14 +753,14 @@ msgstr "Immagine" #: MiscResources.resx$FileTypeImageFiles$Message msgid "Image Files" -msgstr "Files immagini" +msgstr "File immagini" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Qualità immagine" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Impostazioni immagine" @@ -727,7 +786,7 @@ msgstr "Importazione..." #: MiscResources.resx$Install$Message msgid "Install {0}" -msgstr "Installazione {0}" +msgstr "Installazione di {0}" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" @@ -745,69 +804,101 @@ msgstr "Installazione completata. Vuoi riavviare NAPS2 ora?" msgid "Installation failed." msgstr "Installazione fallita." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Interfaccia" + #: UiStrings.resx$Interleave$Message msgid "Interleave" -msgstr "Interlaccia" +msgstr "Interleave" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" -msgstr "" +msgstr "File JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "File JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Qualità Jpeg" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Mantieni le immagini tra una sezione e l'altra" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Scorciatoie tastiera" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Parole chiave:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Lingua" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Scrivi una recensione" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Sinistra" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" -msgstr "Eredita (solo UI nativo)" +msgstr "Legacy (solo interfaccia grafica nativa)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Chiaro" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Ti piace NAPS2?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Carica immagini in NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" -msgstr "Rendi i PDF ricercabili usando l'OCR" +msgstr "Rendi PDF ricercabili usando l'OCR" + +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "IP manuale" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" -msgstr "Massima qualità (files grandi)" +msgstr "Massima qualità (file di grandi dimensioni)" + +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Trasferimento in memoria" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metadati" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minuti (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Mese (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Maggiori informazioni" @@ -819,29 +910,37 @@ msgstr "Sposta giù" msgid "Move Up" msgstr "Sposta su" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Lingue multiple" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Lingue multiple..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" -msgstr "Scansioni multiple (ritardo fisso per ognuna)" +msgstr "Scansioni multiple (ritardo fisso tra le scansioni)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" -msgstr "Scansioni multiple (richiesta tra ognuna)" +msgstr "Scansioni multiple (richiesta dopo ogni scansione)" #: MiscResources.resx$NAPS2$Message #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 è totalmente gratuito. Considera la possibilità di fare una donazione." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Nome (opzionale)" @@ -849,33 +948,40 @@ msgstr "Nome (opzionale)" msgid "Name missing." msgstr "Nome mancante." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Trasferimento nativo" + #: UiStrings.resx$New$Message msgid "New" msgstr "Nuovo" #: UiStrings.resx$NewProfile$Message msgid "New Profile" -msgstr "Nuovo Profilo" +msgstr "Nuovo profilo" #: UiStrings.resx$Next$Message msgid "Next" -msgstr "Avanti" +msgstr "Successiva" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" -msgstr "Prossima Scansione" +msgstr "Scansione successiva" #: MiscResources.resx$NoDeviceSelected$Message #: SdkResources.resx$NoDeviceSelected$Message msgid "No device selected." msgstr "Nessun dispositivo selezionato." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Nessun dispositivo trovato." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." -msgstr "Non ci sono fogli nel caricatore." +msgstr "Non ci sono fogli nell'alimentatore automatico." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Nessun provider selezionato." @@ -883,7 +989,7 @@ msgstr "Nessun provider selezionato." #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message msgid "No scanning device was found." -msgstr "Nessuno scanner non trovato." +msgstr "Nessuno scanner trovato." #: MiscResources.resx$NoUpdates$Message msgid "No updates available." @@ -895,82 +1001,69 @@ msgstr "Nessuno" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Non ora" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" -msgstr "Numero di scansioni:" +msgstr "Numero scansioni:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Scarica OCR" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "OCR in corso" +msgstr "Progresso OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Installa OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Lingua OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "Modalità OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" -msgstr "Bilanciamento larghezza in base ad allineamento (WIA)" +msgstr "Offset larghezza in base ad allineamento (WIA)" #: SettingsResources.resx$TwainImpl_OldDsm$Message msgid "Old DSM" msgstr "Vecchio DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" -msgstr "Una pagina per file" +msgstr "Un file per pagina" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Un file per scansione" #: MiscResources.resx$FilesCouldNotBeDownloaded$Message msgid "One or more files could not be downloaded." -msgstr "Uno o più files non possono essere scaricati." +msgstr "Uno o più file non possono essere scaricati." + +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Consenti una sola istanza di NAPS2" -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Apri cartella" @@ -978,28 +1071,32 @@ msgstr "Apri cartella" msgid "Operation in Progress" msgstr "Operazione in corso" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (nuovo)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "Accesso Web di Outlook" +msgstr "Accesso web Outlook" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" -msgstr "" +msgstr "Destinazione" #: MiscResources.resx$OverwriteFile$Message msgid "Overwrite File" msgstr "Sovrascrivi file" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" -msgstr "Password Proprietario:" +msgstr "Password proprietario:" #: MiscResources.resx$FileTypePdf$Message msgid "PDF Document (*.pdf)" msgstr "Documento PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Impostazioni PDF" @@ -1009,65 +1106,66 @@ msgstr "PDF salvato." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" -msgstr "" +msgstr "File PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Dimensione pagina:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Alimentazione carta:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" -msgstr "" +msgstr "Password" #: UiStrings.resx$Paste$Message msgid "Paste" msgstr "Incolla" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Segnaposti" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Porta" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Post-processo" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Esegui preventivamente OCR dopo la scansione" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." -msgstr "Premere avvia quando pronti." +msgstr "Seleziona 'Avvia' quando sei pronto." #: UiStrings.resx$PreviewFormTitle$Message msgid "Preview" msgstr "Anteprima" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Anteprima:" @@ -1080,12 +1178,11 @@ msgstr "Precedente" msgid "Print" msgstr "Stampa" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" -msgstr "Configurazione Profilo" +msgstr "Impostazioni profilo" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profilo:" @@ -1094,25 +1191,29 @@ msgstr "Profilo:" msgid "Profiles" msgstr "Profili" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Chiedi se selezionato" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" -msgstr "Richiesta del percorso file" +msgstr "Richiesta percorso file" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "Fornitore" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Pronto per la scansione {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" -msgstr "Ripristino" +msgstr "Ripristina" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" -msgstr "Ripristino Immagini Digitalizzate" +msgstr "Ripristino immagini digitalizzate" #: MiscResources.resx$Recovering$Message msgid "Recovering..." @@ -1120,11 +1221,17 @@ msgstr "Ripristino..." #: MiscResources.resx$RecoveryProgress$Message msgid "Recovery Progress" -msgstr "Progresso del ripristino" +msgstr "Progresso ripristino" + +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Ripeti operazione" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Ripeti operazione {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Ricorda queste impostazioni" @@ -1134,27 +1241,31 @@ msgstr "Riordina" #: UiStrings.resx$Reset$Message msgid "Reset" -msgstr "Azzera" +msgstr "Ripristina" #: MiscResources.resx$ResetImage$Message msgid "Reset Image" -msgstr "Resetta Immagine" +msgstr "Ripristina immagine" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Risoluzione:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Ripristina predefiniti" #: UiStrings.resx$Reverse$Message msgid "Reverse" -msgstr "Inverso" +msgstr "Inverti" + +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Inverti tutto" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Inverti selezionati" #: UiStrings.resx$Revert$Message msgid "Revert" @@ -1176,40 +1287,43 @@ msgstr "Ruota a sinistra" msgid "Rotate Right" msgstr "Ruota a destra" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "Esegui in Background" +msgstr "Esegui in background" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "OCR in esecuzione..." +msgstr "Esecuzione OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "Driver SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Salva" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Salva tutti" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Salva tutti come immagini" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Salva tutti come PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message msgid "Save Images" -msgstr "Salva Immagine" +msgstr "Salva immagini" #: MiscResources.resx$SaveImagesProgress$Message msgid "Save Images Progress" -msgstr "Progresso del salvataggio immagini" +msgstr "Progresso salvataggio immagini" #: MiscResources.resx$SavePdf$Message #: UiStrings.resx$SavePdf$Message @@ -1218,58 +1332,79 @@ msgstr "Salva PDF" #: MiscResources.resx$SavePdfProgress$Message msgid "Save PDF Progress" -msgstr "Progresso del salvataggio PDF" +msgstr "Progresso salvataggio PDF" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Salva selezionati" #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Salva selezionati come immagini" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Salva selezionati come PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Salva in un singolo file" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" -msgstr "Salva in files multipli" +msgstr "Salva in file multipli" #: MiscResources.resx$BatchStatusSaving$Message msgid "Saving batch results..." -msgstr "Salvataggio risultati della serie..." +msgstr "Salvataggio risultati operazioni in serie..." #: MiscResources.resx$SavingFormat$Message msgid "Saving {0}..." msgstr "Salvataggio {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Adatta alla finestra" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Scala:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Scansiona" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" -msgstr "Configurazione della scansione" +msgstr "Configurazione scansione" + +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Scansiona con Profilo Predefinito" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Scansiona con nuovo profilo" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Scansiona con il profilo {0}" #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Immagine scansionata" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Condivisione scanner" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" -msgstr "Digitalizzazione pagina {0}" +msgstr "Scansione pagina {0}" #: MiscResources.resx$BatchStatusScanPage$Message msgid "Scanning page {0} (scan {1})..." @@ -1278,13 +1413,16 @@ msgstr "Scansione pagina {0} (scansione {1})..." #: MiscResources.resx$BatchStatusPage$Message #: MiscResources.resx$ScanProgressPage$Message msgid "Scanning page {0}..." -msgstr "Digitalizzazione pagina {0}..." +msgstr "Scansione pagina {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Ricerca dispositivi..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Secondi (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Seleziona" @@ -1293,16 +1431,18 @@ msgstr "Seleziona" msgid "Select All" msgstr "Seleziona tutto" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Seleziona dispositivo" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Seleziona sorgente" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." -msgstr "Seleziona profilo prima di scansionare." +msgstr "Seleziona profilo prima della scansione." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Seleziona una o più lingue:" @@ -1311,46 +1451,92 @@ msgstr "Seleziona una o più lingue:" msgid "Selected ({0})" msgstr "Selezionato ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" -msgstr "Separa files tramite Patch-T" +msgstr "Separa file tramite Patch-T" #: UiStrings.resx$SetDefault$Message msgid "Set Default" msgstr "Imposta predefinito" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Impostazioni" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Condividi" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Condividi anche quando NAPS2 è chiuso" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Impostazioni scanner condiviso" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Gli scanner condivisi possono essere usati da altri computer nella rete locale selezionando \"Driver ESCL\" nelle impostazioni del profilo NAPS2 dell'altro computer." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" -msgstr "Focalizzare" +msgstr "Nitidezza" + +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Collegamento" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Show$Message msgid "Show" -msgstr "Mostra" +msgstr "Visualizza" + +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Visualizza barra \"Profili\"" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Visualizza avanzamento TWAIN nativo" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Visualizza numeri di pagina" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Barra laterale" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "File a pagina singola" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Scansione singola" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" -msgstr "Salta richiesta di salvataggio" +msgstr "Salta richiesta salvataggio" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Dividi" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Avvia" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Stop condivisione scanner" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Adatta a dimensione pagina" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Oggetto:" @@ -1358,12 +1544,11 @@ msgstr "Oggetto:" msgid "TIFF File (*.tiff, *.tif)" msgstr "File TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "Driver TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Dettagli tecnici" @@ -1372,6 +1557,10 @@ msgstr "Dettagli tecnici" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "Il motore OCR non è disponibile. Assicurati di aver installato il pacchetto richiesto:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Tempo scaduto per l'operazione OCR." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1384,19 +1573,19 @@ msgstr "Il file '{0}' non può essere importato." #: MiscResources.resx$ImportErrorNAPS2Pdf$Message msgid "The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported." -msgstr "Il file '{0}' non può essere importato. Solo i files PDF generati da NAPS2 possono essere importati." +msgstr "Il file '{0}' non può essere importato. Solo i file PDF generati da NAPS2 possono essere importati." #: MiscResources.resx$FileInUse$Message msgid "The file could not be overwritten because it is currently in use." -msgstr "Il file non può essere sovrascritto perchè è attualmente in uso." +msgstr "Il file non può essere sovrascritto perché è attualmente in uso." #: MiscResources.resx$ConfirmOverwriteFile$Message msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Il file {0} esiste già. Vuoi sovrascriverlo?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Il seguente file è criptato ed è richiesta una password per aprirlo: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Il seguente file è cifrato e richiede una password per essere aperto:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1406,17 +1595,17 @@ msgstr "Carta inceppata nello scanner." #: MiscResources.resx$DeviceWarmingUp$Message #: SdkResources.resx$DeviceWarmingUp$Message msgid "The scanner is warming up." -msgstr "Surriscaldamento scanner." +msgstr "Lo scanner si sta riscaldando." #: MiscResources.resx$DeviceCoverOpen$Message #: SdkResources.resx$DeviceCoverOpen$Message msgid "The scanner's cover is open." -msgstr "Coperchio dello scanner aperto." +msgstr "Il coperchio dello scanner è aperto." #: MiscResources.resx$DriverNotSupported$Message #: SdkResources.resx$DriverNotSupported$Message msgid "The selected driver is not supported on this system." -msgstr "Il driver selezionato non è supportato in questo sistema." +msgstr "Il driver selezionato non è supportato da questo sistema." #: MiscResources.resx$DeviceNotFound$Message #: SdkResources.resx$DeviceNotFound$Message @@ -1426,12 +1615,12 @@ msgstr "Lo scanner selezionato non è stato trovato." #: MiscResources.resx$NoFeederSupport$Message #: SdkResources.resx$NoFeederSupport$Message msgid "The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver." -msgstr "Lo scanner selezionato non supporta l'uso di un alimentatore. Se il tuo scanner ha un alimentatore, prova ad usare un driver differente." +msgstr "Lo scanner selezionato non supporta l'uso di un alimentatore automatico. Se lo scanner ha un alimentatore automatico, prova a usare un driver differente." #: MiscResources.resx$NoDuplexSupport$Message #: SdkResources.resx$NoDuplexSupport$Message msgid "The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver." -msgstr "Lo scanner selezionato non supporta l'uso del duplex. Se lo scanner ha un supporto duplex, prova ad usare un driver differente." +msgstr "Lo scanner selezionato non supporta la scansione fronte retro. Se lo scanner dovrebbe supportare la scansione fronte retro, prova a usare un driver differente." #: MiscResources.resx$DeviceBusy$Message #: SdkResources.resx$DeviceBusy$Message @@ -1441,31 +1630,68 @@ msgstr "Lo scanner selezionato è occupato." #: MiscResources.resx$DeviceOffline$Message #: SdkResources.resx$DeviceOffline$Message msgid "The selected scanner is offline." -msgstr "Lo scanner selezionato è disconnesso." +msgstr "Lo scanner selezionato è offline." + +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Il processo di elaborazione si è bloccato." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Il processo di elaborazione si è bloccato. \n" +"Controlla il visualizzatore eventi di Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Tema:" -#: FImageSettings.resx$groupTiff.Text$Message +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Opzioni Tiff" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Intervallo tra le scansioni (secondi):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Titolo:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Strumenti" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Implementazione Twain:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "Legale USA (8.5x14 in)" +msgstr "Legale USA (8.5x14 pollici)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" -msgstr "Lettera USA (8.5x11 in)" +msgstr "Lettera USA (8.5x11 pollici)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Rimuovi assegnazione" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Annulla operazione" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Annulla operazione {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Scanner sconosciuto" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" @@ -1473,11 +1699,11 @@ msgstr "Modifiche non salvate" #: MiscResources.resx$UpdateProgress$Message msgid "Update Progress" -msgstr "Avanzamento dell'aggiornamento" +msgstr "Progresso aggiornamento" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Il controllo degli aggiornamenti è disabilitato." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,24 +1713,21 @@ msgstr "Aggiornamento..." msgid "Uploading email..." msgstr "Caricamento email..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Usa UI nativa" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Usa impostazioni predefinite" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Password utente:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." -msgstr "L'uso dell'OCR richiede che venga scaricata ogni lingua che vuoi far riconoscere." +msgstr "L'uso dell'OCR richiede che venga scaricata ogni lingua che si vuole far riconoscere." #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message @@ -1515,16 +1738,15 @@ msgstr "Versione {0}" msgid "View" msgstr "Visualizza" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "Driver WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "In attesa di TWAIN per completare..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "In attesa di autorizzazione..." @@ -1532,19 +1754,19 @@ msgstr "In attesa di autorizzazione..." msgid "Waiting for scan {0}..." msgstr "In attesa per la scansione {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Soglia bianco" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Versione Wia:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Anno" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Anno (00-99)" @@ -1555,42 +1777,39 @@ msgstr "Non hai i permessi per copiare il contenuto dal file '{0}'." #: MiscResources.resx$DontHavePermission$Message msgid "You don't have permission to save files at this location." -msgstr "Non hai i permessi per salvare i files in questo percorso." +msgstr "Non hai i permessi per salvare i file in questo percorso." #: MiscResources.resx$ExitWithUnsavedChanges$Message msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" -msgstr "Ci sono modifiche non salvate. Sei sicuro di voler uscire ed abbandonare tali modifiche?" +msgstr "Ci sono modifiche non salvate. Sei sicuro di voler uscire e abbandonare tali modifiche?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" -msgstr "" +msgstr "Zoom" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Zoom attuale" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" -msgstr "Aumenta zoom" +msgstr "Zoom +" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" -msgstr "Riduci zoom" +msgstr "Zoom -" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "pollici" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,30 +1817,35 @@ msgstr "di {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" -msgstr "" +msgstr "{0} / {1} file" + +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} dispositivi trovati." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" -msgstr "{0} immagine(i) digitalizzata(e) su {1} a {2} potrebbero non essere state salvate e sono recuperabili. Vuoi recuperarle?" +msgstr "{0} immagine/i digitalizzata/e il {1} alle ore {2} potrebbero non essere state salvate e sono recuperabili. Vuoi recuperarle?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." diff --git a/NAPS2.Lib/Lang/po/ja.po b/NAPS2.Lib/Lang/po/ja.po index 48fa7e4d28..d0f2174672 100644 --- a/NAPS2.Lib/Lang/po/ja.po +++ b/NAPS2.Lib/Lang/po/ja.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Japanese\n" "Language: ja\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24ビットカラー" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "" @@ -76,12 +60,15 @@ msgstr "NAPS2について" msgid "Acquiring data..." msgstr "データを取得中..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "詳細設定" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "プロファイル設定(詳細)" @@ -93,35 +80,35 @@ msgstr "すべて({0})" msgid "All Files" msgstr "すべてのファイル" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "注釈を許可" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "コンテンツのコピーを許可" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "アクセシビリティのためにコンテンツのコピーを許可する" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "ドキュメントの結合を許可" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "ドキュメントの変更を許可" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "フォームへの書き込みを許可" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "オリジナル画質の印刷を許可する" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "印刷を許可する" @@ -133,17 +120,22 @@ msgstr "" msgid "Alternate Interleave" msgstr "デインターリーブの変更" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "PDFファイルをインポートするのに必要なファイルがあります。ダウンロードしますか?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "アップデートをインストールする際にエラーが発生しました." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "" #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "認証中にエラーが発生しました." msgid "An error occurred when trying to auto save." msgstr "自動セーブ中にエラーが発生しました." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "アップデートをインストールする際にエラーが発生しました." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "セーブ中にエラーが発生しました." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "作業が進行中です。それでも終了しますか?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "一括スキャン中に不明なエラーが発生しました." +msgid "An unknown error occurred during the batch scan." +msgstr "" #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -190,7 +186,15 @@ msgstr "新しいOCRのアップデートがあります." msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "スキャン後に明度とコントラストの処理を行う" @@ -222,19 +226,27 @@ msgstr "{0}個のアイテムを削除しますか?" msgid "Are you sure you want to delete {0} profiles?" msgstr "{0}個のプロファイルを本当に削除しますか?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "{0}個のイメージへの変更を取り消しますか?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "添付ファイルの名前:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "作成者:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "認証" @@ -242,29 +254,27 @@ msgstr "認証" msgid "Auto" msgstr "自動" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "自動保存に関する設定" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "連番の自動付与 (1 ケタ)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "連番の自動付与 (2 ケタ)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "連番の自動付与 (3 ケタ)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "連番の自動付与 (4 ケタ)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "スキャン後にOCRを自動適用する" @@ -277,8 +287,8 @@ msgstr "" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "一括スキャン" @@ -298,7 +308,6 @@ msgstr "エラーのために一括スキャンは異常終了しました." msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "ビット深度:" @@ -309,10 +318,10 @@ msgstr "ビットマップファイル(*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "白黒" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "空白のページ" @@ -320,7 +329,6 @@ msgstr "空白のページ" msgid "Brightness / Contrast" msgstr "明度/コントラスト" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "明度:" @@ -329,24 +337,11 @@ msgstr "明度:" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "キャンセル" @@ -363,7 +358,7 @@ msgstr "キャンセル...." msgid "Center" msgstr "中央揃え" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "変更" @@ -375,7 +370,7 @@ msgstr "更新のチェック" msgid "Checking..." msgstr "確認中です..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "メールプロバイダを選択" @@ -383,7 +378,6 @@ msgstr "メールプロバイダを選択" msgid "Choose Profile" msgstr "プロファイルを選択" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "デバイスを選択" @@ -397,7 +391,7 @@ msgstr "クリア" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "保存後にイメージをクリア" @@ -405,16 +399,30 @@ msgstr "保存後にイメージをクリア" msgid "Close" msgstr "閉じる" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "互換性" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "圧縮:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "コントラスト:" @@ -435,7 +443,7 @@ msgstr "コピー..." msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "閾値" @@ -443,7 +451,7 @@ msgstr "閾値" msgid "Crop" msgstr "切り抜き" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "ページサイズに切り抜く" @@ -451,10 +459,14 @@ msgstr "ページサイズに切り抜く" msgid "Custom ({0}x{1} {2})" msgstr "ユーザー設定 ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "ページサイズ設定" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "回転設定" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "カスタムSMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "カスタム..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "(01-31)日" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "デフォルト" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "デフォルトのパス:" @@ -487,7 +504,6 @@ msgstr "デフォルトのパス:" msgid "Deinterleave" msgstr "ディインターリーブ" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "傾き補正" msgid "Deskew Progress" msgstr "歪み補正の進捗" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "歪み補正をする" @@ -509,16 +525,14 @@ msgstr "歪み補正をする" msgid "Deskewing..." msgstr "歪み補正..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "デバイス:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "大きさ" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "名前を表示する:" @@ -532,12 +546,10 @@ msgstr "" msgid "Donate" msgstr "支援" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "完了しました" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "ダウンロード" @@ -550,19 +562,47 @@ msgstr "ダウンロードエラー" msgid "Download Needed" msgstr "ダウンロードが必要です" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "ダウンロードの進捗状況" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "両面" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "編集" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +616,32 @@ msgstr "PDFをEmailで送信" msgid "Email PDF Progress" msgstr "PDF送信の進捗" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "メールの設定" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "自動保存を許可" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "暗号化されたPDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "暗号化" @@ -602,12 +649,15 @@ msgstr "暗号化" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "エラー" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "推定ダウンロードサイズ: 約{0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "余白ページを除去" @@ -629,24 +679,31 @@ msgstr "" msgid "Feeder" msgstr "フィーダー" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "ファイル名" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "ファイルのパス:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "反転" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "両面のページを反転" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "80以上の高画質JPEGを得るためには、プロファイル内の画質設定をあげてください。." @@ -654,7 +711,6 @@ msgstr "80以上の高画質JPEGを得るためには、プロファイル内の msgid "GIF File (*.gif)" msgstr "" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "他の言語を追加" @@ -671,12 +727,11 @@ msgstr "" msgid "Grayscale" msgstr "グレースケール" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "水平方向揃え:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "(0-23)時間" @@ -684,6 +739,10 @@ msgstr "(0-23)時間" msgid "Hue / Saturation" msgstr "色相/彩度" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "" @@ -696,12 +755,12 @@ msgstr "イメージ" msgid "Image Files" msgstr "イメージファイル" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "画質" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "画像設定" @@ -745,6 +804,10 @@ msgstr "インストールが完了しました。NAPS2を再起動しますか? msgid "Installation failed." msgstr "インストールに失敗しました." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "インターリーブ" @@ -757,11 +820,20 @@ msgstr "" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "JPEG画像の品質" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "キーワード:" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "言語" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "左側" @@ -781,33 +857,48 @@ msgstr "左側" msgid "Legacy (native UI only)" msgstr "スキャナ付属のUIのみを利用" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "NAPS2にイメージを読み込む" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "OCRを利用して検索可能なPDFを作成" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "最大画質(ファイルサイズ大)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "メタデータ" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "(00-59)分" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "(01-12)月" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "詳細情報" @@ -819,11 +910,19 @@ msgstr "下へ" msgid "Move Up" msgstr "上へ" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "複数をスキャン(スキャン間に遅延を設ける)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "複数をスキャン(スキャン毎に通知する)" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2はフリーウェアです。気に入ったら寄付をお願いします。." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "名前 (オプション)" @@ -849,6 +948,10 @@ msgstr "名前 (オプション)" msgid "Name missing." msgstr "名前がありません." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "新規作成" @@ -861,7 +964,7 @@ msgstr "新規プロファイル" msgid "Next" msgstr "次へ" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "次のスキャン" @@ -870,12 +973,15 @@ msgstr "次のスキャン" msgid "No device selected." msgstr "デバイスが選択されていません." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "フィーダーが空です." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "プロバイダが選択されていません。." @@ -897,11 +1003,11 @@ msgstr "なし" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "後で" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "スキャンの数:" @@ -909,7 +1015,6 @@ msgstr "スキャンの数:" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCRのダウンロード" @@ -918,37 +1023,23 @@ msgstr "OCRのダウンロード" msgid "OCR Progress" msgstr "OCR適用中" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCRのセットアップ" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCRに用いる言語:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "OCRモード:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "ページごとに1つファイルを作成" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "スキャンごとに1つのファイルを作成" @@ -970,7 +1059,11 @@ msgstr "スキャンごとに1つのファイルを作成" msgid "One or more files could not be downloaded." msgstr "1つ以上のファイルをダウンロードできませんでした." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "フォルダーを開く" @@ -978,11 +1071,15 @@ msgstr "フォルダーを開く" msgid "Operation in Progress" msgstr "実行中" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "Outlookへのウェブアクセス" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "出力" @@ -990,7 +1087,7 @@ msgstr "出力" msgid "Overwrite File" msgstr "ファイルを上書きする" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "オーナーのパスワード:" @@ -998,8 +1095,8 @@ msgstr "オーナーのパスワード:" msgid "PDF Document (*.pdf)" msgstr "" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDFの設定" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "ページサイズ:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "給紙元:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "パスワード" @@ -1045,21 +1140,24 @@ msgstr "パスワード" msgid "Paste" msgstr "貼り付け" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "プレースホルダー" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "後処理" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "準備ができたらスタートを押してください." @@ -1067,7 +1165,7 @@ msgstr "準備ができたらスタートを押してください." msgid "Preview" msgstr "プレビュー" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "プレビュー:" @@ -1080,12 +1178,11 @@ msgstr "前へ" msgid "Print" msgstr "印刷" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "プロファイル設定" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "プロファイル:" @@ -1094,23 +1191,27 @@ msgstr "プロファイル:" msgid "Profiles" msgstr "プロファイル" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "プロバイダー" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "{0}個の準備が完了しました." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "回復" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "スキャンイメージを回復する" @@ -1122,9 +1223,15 @@ msgstr "回復中..." msgid "Recovery Progress" msgstr "回復作業の進捗" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "設定を保存する" @@ -1140,15 +1247,11 @@ msgstr "リセット" msgid "Reset Image" msgstr "画像のリセット" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "解像度:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "標準設定に戻す" @@ -1156,6 +1259,14 @@ msgstr "標準設定に戻す" msgid "Reverse" msgstr "反転させる" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "元に戻す" @@ -1176,7 +1287,6 @@ msgstr "左に回転させる" msgid "Rotate Right" msgstr "右に回転させる" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "バックグラウンドで実行" @@ -1185,7 +1295,6 @@ msgstr "バックグラウンドで実行" msgid "Running OCR..." msgstr "OCR適用中..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "SANE ドライバ" @@ -1194,6 +1303,11 @@ msgstr "SANE ドライバ" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "PDFを保存" msgid "Save PDF Progress" msgstr "PDF保存の進捗" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "1つのファイルに保存" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "複数のファイルに保存" @@ -1244,29 +1363,45 @@ msgstr "バッチの保存..." msgid "Saving {0}..." msgstr "{0} を保存中..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "ウインドウに合わせる" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "拡大・縮小:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "スキャン" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "スキャン設定" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "スキャンされた画像" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "ページ {0} をスキャン中" @@ -1280,11 +1415,14 @@ msgstr "ページ {0} をスキャン中 (スキャン {1})..." msgid "Scanning page {0}..." msgstr "ページ {0} をスキャン中..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "(00-59)秒" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "選択" @@ -1293,7 +1431,10 @@ msgstr "選択" msgid "Select All" msgstr "すべて選択" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "ソースの選択" @@ -1302,7 +1443,6 @@ msgstr "ソースの選択" msgid "Select a profile before clicking Scan." msgstr "プロファイルを選んでください." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "言語を選択してください:" @@ -1311,8 +1451,7 @@ msgstr "言語を選択してください:" msgid "Selected ({0})" msgstr "({0})個を選択中" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Patch-Tでファイル分割" @@ -1320,37 +1459,84 @@ msgstr "Patch-Tでファイル分割" msgid "Set Default" msgstr "既定の設定" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "シャープに" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "見せる" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "単一ページのファイル" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "1枚だけスキャン" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "保存確認をスキップ" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "開始" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "ページの大きさに合わせる" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "件名:" @@ -1358,12 +1544,11 @@ msgstr "件名:" msgid "TIFF File (*.tiff, *.tif)" msgstr "" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAINドライバ" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "技術的な詳細" @@ -1372,6 +1557,10 @@ msgstr "技術的な詳細" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "OCRエンジンが利用できません。言語パッケージをインストールしてください。:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "使用中ファイルのため、上書きできませんでした。." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "{0}は既存のファイルです。上書きしますか?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "暗号化されているためパスワードが必要です: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "選択されたスキャナは使用中です。." msgid "The selected scanner is offline." msgstr "選択されたスキャナは接続されていません。." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "TIFF形式の設定" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "スキャン間の時間 (seconds):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "タイトル:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Twainの補助機能:" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "US Legal (8.5x14 インチ)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "保存されていない変更" @@ -1487,21 +1712,18 @@ msgstr "更新中..." msgid "Uploading email..." msgstr "メールをアップロード中..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "元のUIを利用" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "以前の設定を利用" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "ユーザーのパスワード:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "OCRを使用するにはそれぞれの言語ファイルが必要です." @@ -1515,16 +1737,15 @@ msgstr "" msgid "View" msgstr "表示" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIAドライバー" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "TWAINの完了を待機中です..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "認証待ち..." @@ -1532,19 +1753,19 @@ msgstr "認証待ち..." msgid "Waiting for scan {0}..." msgstr "スキャン待機中 {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "白さのしきい値" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "年" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "(00-99) 年" @@ -1561,21 +1782,18 @@ msgstr "この場所に保存する権限がありません." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "未保存の変更があります。変更を破棄しますか?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "ズーム" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "実寸サイズ" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "ズームイン" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "ズームアウト" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "ファイル:{0} / {1}個 " -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{2} に {1} でスキャンされた {0} 個の画像は保存されませんでした。回復可能ですが、回復しますか?" diff --git a/NAPS2.Lib/Lang/po/ko.po b/NAPS2.Lib/Lang/po/ko.po index c24566b6a9..5fd566450a 100644 --- a/NAPS2.Lib/Lang/po/ko.po +++ b/NAPS2.Lib/Lang/po/ko.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Korean\n" "Language: ko\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "\"저장\"버튼 기본 작동:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "\"스캔\" 버튼 기본 작동:" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "\"스캔\" 메뉴로 기본 프로파일 변경하기" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 장치 발견됨." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-비트 컬러" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -74,14 +58,17 @@ msgstr "정보" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." -msgstr "수집 데이터..." +msgstr "데이터 수집 중..." + +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "동작" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "상세" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "상세 프로필 설정" @@ -93,35 +80,35 @@ msgstr "전체 ({0})" msgid "All Files" msgstr "모든 파일" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "주석 허용" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "내용 복사 허용" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "접근성을 위한 내용 복사 허용" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "문서 조합 허용" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "문서 변경 허용" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "서식 채우기 허용" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "고품질 인쇄 허용" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "인쇄 허용" @@ -133,33 +120,42 @@ msgstr "대체 디인터리브" msgid "Alternate Interleave" msgstr "대체 인터리브" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "대체 전송" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "항상 묻기" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "항상 묻기" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "이 PDF 파일을 불러오기 위해서는 추가적인 컴포넌트가 필요합니다. 지금 다운로드 하시겠습니까?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "" +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "OCR 작업 중 오류가 발생했습니다." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "인증을 시도하는 중 오류가 발생했습니다." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." msgstr "파일을 자동으로 저장하는 중 오류가 발생했습니다.." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "업데이트를 설치하는 중 오류가 발생했습니다." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "파일을 저장하는 중 오류가 발생했습니다.." #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "" +msgstr "이메일을 보내는 중 오류가 발생했습니다." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." @@ -172,25 +168,33 @@ msgstr "스캔 중 오류가 발생했습니다.." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "" +msgstr "작업이 진행 중입니다. 진행중인 작업을 취소하고 종료하시겠습니까?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "일괄 스캔 중 오류가 발생했습니다.." +msgid "An unknown error occurred during the batch scan." +msgstr "일괄 스캔 중 알 수 없는 오류가 발생했습니다." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "" +msgstr "업데이트가 있습니다." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "OCR 업데이트를 사용할 수 있습니다.." +msgstr "OCR 업데이트를 사용할 수 있습니다." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple 드라이버" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple 메일" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "어플리케이션" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "스캔 후 밝기/대비 적용" @@ -200,7 +204,7 @@ msgstr "선택된 {0} 개의 이미지에 적용" #: MiscResources.resx$ConfirmCancelBatch$Message msgid "Are you sure you want to cancel the batch scan?" -msgstr "" +msgstr "일괄 스캔을 취소하시겠습니까?" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" @@ -222,83 +226,88 @@ msgstr "{0} 개의 이미지를 삭제하시겠습니까?" msgid "Are you sure you want to delete {0} profiles?" msgstr "{0} 개의 프로파일을 삭제하시겠습니까?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "{0} 의 공유를 중단하시겠습니까?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "{0} 개의 이미지에 대한 변경사항을 취소하시겠습니까?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "할당" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "첨부명:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "작성자:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "인증" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "자동" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "자동 저장 설정" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "자동 증가 번호 (한자리)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "자동 증가 번호 (두자리)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "자동 증가 번호 (세자리)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "자동 증가 번호 (네자리)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "" +msgstr "스캔 후 자동으로 OCR 실행" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "일괄 스캔" #: MiscResources.resx$BatchStatusCancelled$Message msgid "Batch cancelled." -msgstr "일괄 작업이 취소되었습니다.." +msgstr "일괄 작업이 취소되었습니다." #: MiscResources.resx$BatchStatusComplete$Message msgid "Batch completed successfully." -msgstr "일괄 작업이 완료되었습니다.." +msgstr "일괄 작업이 완료되었습니다." #: MiscResources.resx$BatchStatusError$Message msgid "Batch scan stopped due to error." -msgstr "오류로 인해 일괄 작업이 중지되었습니다.." +msgstr "오류로 인해 일괄 작업이 중지되었습니다." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "최고 품질" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "비트 깊이:" @@ -309,10 +318,10 @@ msgstr "비트맵 파일 (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "흑백" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "빈 페이지" @@ -320,70 +329,55 @@ msgstr "빈 페이지" msgid "Brightness / Contrast" msgstr "밝기 / 명암" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "밝기:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "스캐너를 찾을 수 없습니까? NAPS2 Flatpak의 한계에 대해서 확인해보세요." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "취소" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" -msgstr "" +msgstr "일괄 작업 취소" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." -msgstr "취소하기...." +msgstr "취소하는 중..." #: SettingsResources.resx$HorizontalAlign_Center$Message msgid "Center" msgstr "중앙" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" -msgstr "" +msgstr "변경" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "" +msgstr "업데이트 확인" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "" +msgstr "확인 중..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "" +msgstr "이메일 제공자 선택" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" msgstr "프로파일 선택" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "장치 선택" @@ -391,30 +385,44 @@ msgstr "장치 선택" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message msgid "Clear" -msgstr "비우기" +msgstr "지우기" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "모두 지우기" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" -msgstr "자동저장 후 이미지 비우기" +msgstr "자동 저장 후 이미지 지우기" #: MiscResources.resx$Close$Message msgid "Close" msgstr "닫기" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "합치기" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "스캔 장비와 연결이 끊어졌습니다." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "호환성" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" -msgstr "" +msgstr "압축 방법:" + +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "연결" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "연결 오류." -#: FEditProfile.resx$label7.Text$Message #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "대비:" @@ -425,7 +433,7 @@ msgstr "복사" #: MiscResources.resx$CopyProgress$Message msgid "Copy Progress" -msgstr "복사 진행상태" +msgstr "복사 진행 중" #: MiscResources.resx$Copying$Message msgid "Copying..." @@ -433,17 +441,17 @@ msgstr "복사 중..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} NAPS2 기여자들" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "적용범위 임계값" #: UiStrings.resx$Crop$Message msgid "Crop" -msgstr "잘라내기" +msgstr "자르기" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "용지 사이즈에 맞춰 자르기" @@ -451,35 +459,44 @@ msgstr "용지 사이즈에 맞춰 자르기" msgid "Custom ({0}x{1} {2})" msgstr "사용자 설정 ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "페이지 크기 사용자 설정" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "사용자 정의 회전" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" -msgstr "" +msgstr "사용자 설정 SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "사용자 설정..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" -msgstr "일자 (01-31)" +msgstr "일 (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "기본값" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "기본 파일 경로:" @@ -487,7 +504,6 @@ msgstr "기본 파일 경로:" msgid "Deinterleave" msgstr "디인터리브" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -499,9 +515,9 @@ msgstr "기울기 보정" #: MiscResources.resx$AutoDeskewProgress$Message msgid "Deskew Progress" -msgstr "기울기 보정" +msgstr "기울기 보정 중" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "스캔 된 페이지 기울기 보정" @@ -509,35 +525,31 @@ msgstr "스캔 된 페이지 기울기 보정" msgid "Deskewing..." msgstr "기울기 보정..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "장치:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "크기" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "표시 이름:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "문서 교정" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "" +msgstr "후원" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "완료" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "다운로드" @@ -550,64 +562,102 @@ msgstr "다운로드 오류" msgid "Download Needed" msgstr "다운로드가 필요합니다" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" -msgstr "다운로드 진행상황" +msgstr "다운로드 진행 중" + +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "양방향" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL 드라이버" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL 네트워크 드라이버" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB 드라이버" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "편집" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "모두 이메일 보내기" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "모두 PDF로 이메일로 보내기" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "이메일 PDF" +msgstr "PDF로 이메일 보내기" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" -msgstr "이메일 PDF 진행상황" +msgstr "PDF로 이메일 보내기 진행 중" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "선택된 항목 이메일로 보내기" #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "선택된 항목을 PDF로 이메일 보내기" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "이메일 설정" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "자동 저장 켜기" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "디버그 로깅 활성화" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "PDF 암호화" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "암호화" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "확장 메타 파일 (*.emf)" +msgstr "확장 메타파일 (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "오류" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,46 +665,52 @@ msgstr "예상 다운로드 용량: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "교환이미지 파일 (*.exif)" +msgstr "교환 가능한 이미지 파일 (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "빈 페이지 제외" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "빠르게" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "공급기" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "파일명" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "파일 경로:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "화이트밸런스 조정 및 노이즈 제거" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "뒤집기" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "양면인쇄의 뒷면 회전하기" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "양면 인쇄 된 페이지 뒤집기" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "" +msgstr "높은 JPEG 품질(80 이상)을 선택한 경우 최상의 결과를 위해 프로파일에서 이미지 품질도 높히는 것이 좋습니다." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" msgstr "GIF 파일 (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "더 많은 언어 설치하기" @@ -665,18 +721,17 @@ msgstr "평판" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "회색조" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "수평 정렬:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "시간 (0-23)" @@ -684,6 +739,10 @@ msgstr "시간 (0-23)" msgid "Hue / Saturation" msgstr "색조 / 채도" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/호스트" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "아이콘 출처:" @@ -696,12 +755,12 @@ msgstr "이미지" msgid "Image Files" msgstr "이미지 파일" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "이미지 품질" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "이미지 설정" @@ -715,7 +774,7 @@ msgstr "가져오기" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" -msgstr "가져오기 진행 상태" +msgstr "가져오기 진행 중" #: MiscResources.resx$ImportingFormat$Message msgid "Importing {0}..." @@ -727,7 +786,7 @@ msgstr "가져오는 중입니다..." #: MiscResources.resx$Install$Message msgid "Install {0}" -msgstr "" +msgstr "{0} 설치" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" @@ -745,6 +804,10 @@ msgstr "설치가 완료되었습니다. NAPS2 를 재시작 하시겠습니까? msgid "Installation failed." msgstr "설치 실패." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "인터페이스" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "인터리브" @@ -755,24 +818,37 @@ msgstr "JPEG 파일 (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000 파일 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Jpeg 품질" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "세션간 이미지 유지하기" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "키보드 단축키" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "검색어:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "언어" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "왼쪽" @@ -781,33 +857,48 @@ msgstr "왼쪽" msgid "Legacy (native UI only)" msgstr "레거시 (네이티브 UI 만 가능)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "NAPS2 에 이미지 불러오기" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "OCR 을 이용하여 검색 가능한 PDF 만들기" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "IP 수동 설정" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "최대 품질 (용량 큼)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "메모리 전송" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "메타데이터" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "분 (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "월 (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "상세 정보" @@ -819,11 +910,19 @@ msgstr "아래로 이동" msgid "Move Up" msgstr "위로 이동" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "복수의 언어" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "복수의 언어..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "여러장 스캔하기 (매 장마다 정해진 시간 만큼 지연)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "여러장 스캔하기 (매 장마다 팝업 표시)" @@ -831,17 +930,17 @@ msgstr "여러장 스캔하기 (매 장마다 팝업 표시)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "" +msgstr "NAPS2는 완전 무료로 제공됩니다. 기부를 고려해 보십시오." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "이름 (선택사항)" @@ -849,6 +948,10 @@ msgstr "이름 (선택사항)" msgid "Name missing." msgstr "이름이 없습니다." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "네이티브 전송" + #: UiStrings.resx$New$Message msgid "New" msgstr "새로 만들기" @@ -861,7 +964,7 @@ msgstr "새 프로파일" msgid "Next" msgstr "다음" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "다음 스캔" @@ -870,85 +973,73 @@ msgstr "다음 스캔" msgid "No device selected." msgstr "선택된 장치가 없습니다.." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "발견된 장치 없음." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." -msgstr "공급장치에 용지가 없습니다.." +msgstr "공급장치에 용지가 없습니다." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "" +msgstr "제공자 선택되지 않음" #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message msgid "No scanning device was found." -msgstr "스캔 장치를 찾을 수 없습니다.." +msgstr "스캔 장치를 찾을 수 없습니다." #: MiscResources.resx$NoUpdates$Message msgid "No updates available." -msgstr "" +msgstr "사용 가능한 업데이트가 없습니다." #: SettingsResources.resx$TiffComp_None$Message msgid "None" -msgstr "" +msgstr "없음" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "나중에" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "스캔 장 수:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR 다운로드" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "" +msgstr "OCR 진행 중" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCR 설정" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR 언어:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "" +msgstr "OCR 모드:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "확인" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "정렬을 기반으로 한 오프셋 폭 (WIA)" @@ -956,33 +1047,39 @@ msgstr "정렬을 기반으로 한 오프셋 폭 (WIA)" msgid "Old DSM" msgstr "구 DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "페이지 당 한 개 파일" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "스캔 당 한 개 파일" #: MiscResources.resx$FilesCouldNotBeDownloaded$Message msgid "One or more files could not be downloaded." -msgstr "한개 또는 그 이상의 파일을 다운로드 할 수 없습니다.." +msgstr "하나 이상의 파일을 다운로드 할 수 없습니다." + +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "한 개의 NAPS2 창만 허용하기" -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "폴더 열기" #: MiscResources.resx$ActiveOperations$Message msgid "Operation in Progress" +msgstr "작업 진행 중" + +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" msgstr "" #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "웹용 Outlook" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "출력" @@ -990,7 +1087,7 @@ msgstr "출력" msgid "Overwrite File" msgstr "파일 덮어쓰기" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "소유자 암호:" @@ -998,8 +1095,8 @@ msgstr "소유자 암호:" msgid "PDF Document (*.pdf)" msgstr "PDF 문서 (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF 설정" @@ -1009,35 +1106,33 @@ msgstr "PDF 가 저장되었습니다." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "PNG 파일 (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "용지 크기:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "용지 공급:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "암호" @@ -1045,21 +1140,24 @@ msgstr "암호" msgid "Paste" msgstr "붙여넣기" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "파일명 형식" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "포트" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "후처리" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "스캔 후 자동으로 OCR 실행" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "준비가 되면 시작을 눌러주세요.." @@ -1067,7 +1165,7 @@ msgstr "준비가 되면 시작을 눌러주세요.." msgid "Preview" msgstr "미리보기" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "미리보기:" @@ -1080,12 +1178,11 @@ msgstr "이전" msgid "Print" msgstr "인쇄" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "프로파일 설정" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "프로파일:" @@ -1094,23 +1191,27 @@ msgstr "프로파일:" msgid "Profiles" msgstr "프로파일" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "선택한 경우 묻기" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "파일 경로창" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "제공자" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." -msgstr "{0} 개의 스캔 준비됨." +msgstr "스캔 {0} 준비됨." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "복구" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "스캔 된 이미지 복구" @@ -1120,13 +1221,19 @@ msgstr "복구 중..." #: MiscResources.resx$RecoveryProgress$Message msgid "Recovery Progress" -msgstr "복구 진행 상황" +msgstr "복구 진행 중" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "다시 실행" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "{0} 다시 실행" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" -msgstr "설정값 기억" +msgstr "설정 기억하기" #: UiStrings.resx$Reorder$Message msgid "Reorder" @@ -1140,25 +1247,29 @@ msgstr "초기화" msgid "Reset Image" msgstr "이미지 초기화" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "해상도:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" -msgstr "초기설정으로 복원" +msgstr "초기 설정으로 복원" #: UiStrings.resx$Reverse$Message msgid "Reverse" msgstr "뒤집기" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "모두 뒤집기" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "선택한 항목 뒤집기" + #: UiStrings.resx$Revert$Message msgid "Revert" -msgstr "되돌림" +msgstr "되돌리기" #: SettingsResources.resx$HorizontalAlign_Right$Message msgid "Right" @@ -1176,31 +1287,34 @@ msgstr "왼쪽으로 회전" msgid "Rotate Right" msgstr "오른쪽으로 회전" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "" +msgstr "백그라운드에서 실행" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "" +msgstr "OCR 실행 중..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "SANE 드라이버" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "저장" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "모두 저장" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "모두 이미지로 저장" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "모두 PDF로 저장" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1209,7 +1323,7 @@ msgstr "이미지 저장" #: MiscResources.resx$SaveImagesProgress$Message msgid "Save Images Progress" -msgstr "이미지 저장 상황" +msgstr "이미지 저장 중" #: MiscResources.resx$SavePdf$Message #: UiStrings.resx$SavePdf$Message @@ -1218,73 +1332,97 @@ msgstr "PDF 저장" #: MiscResources.resx$SavePdfProgress$Message msgid "Save PDF Progress" -msgstr "PDF 저장 상황" +msgstr "PDF 저장 중" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "선택된 항목 저장" #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "선택된 항목을 이미지로 저장" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "선택된 항목을 PDF로 저장" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "한 개의 파일로 저장하기" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "여러 파일로 저장하기" #: MiscResources.resx$BatchStatusSaving$Message msgid "Saving batch results..." -msgstr "일괄 작업 결과 저장하기..." +msgstr "일괄 작업 결과 저장 중..." #: MiscResources.resx$SavingFormat$Message msgid "Saving {0}..." msgstr "{0} 저장 중..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "창 크기와 함께 크기 변경" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "크기 조정:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "스캔" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "스캔 설정" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "기본 프로파일을 사용하여 스캔하기" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "새로운 프로파일을 사용하여 스캔하기" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "{0} 프로파일을 사용하여 스캔하기" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "스캔된 이미지" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "스캐너 공유" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "{0} 페이지 스캔 중" #: MiscResources.resx$BatchStatusScanPage$Message msgid "Scanning page {0} (scan {1})..." -msgstr "{0} 페이지 스캔 중 ({1} 스캔)..." +msgstr "{0} 페이지 스캔 중 (스캔 {1})..." #: MiscResources.resx$BatchStatusPage$Message #: MiscResources.resx$ScanProgressPage$Message msgid "Scanning page {0}..." msgstr "{0} 페이지 스캔 중..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "장치를 찾는 중..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "초 (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "선택" @@ -1293,26 +1431,27 @@ msgstr "선택" msgid "Select All" msgstr "전체 선택" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "장치 선택" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "원본 선택" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." -msgstr "스캔을 클릭하기 전에 프로파일을 선택 해 주세요.." +msgstr "스캔을 클릭하기 전에 프로파일을 선택 해 주세요." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" -msgstr "한 개 또는 그 이상의 언어를 선택 해 주세요.:" +msgstr "한 개 이상의 언어를 선택 해 주세요:" #: MiscResources.resx$SelectedCount$Message msgid "Selected ({0})" -msgstr "{0} 개 선택됨" +msgstr "선택된 항목 ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Patch-T 를 통한 파일 분할" @@ -1320,37 +1459,84 @@ msgstr "Patch-T 를 통한 파일 분할" msgid "Set Default" msgstr "기본값 설정" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "설정" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "공유" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "NAPS2를 닫아도 공" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "공유된 스캐너 설정" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "공유된 스캐너는 로컬 네트워크에 있는 다른 컴퓨터의 NAPS2 프로필 설정에서 \"ESCL 드라이버\"를 선택해 사용할 수 있습니다." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "샤픈" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "단축키" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "보기" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "\"프로파일\" 도구모음 보이기" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "기본 TWAIN 진행과정 보이기" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "페이지 번호 보이기" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "사이드바" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" -msgstr "" +msgstr "단일 페이지 파일" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "한 장 스캔" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "저장 확인창 건너뛰기" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "나누기" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "시작" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "용지 사이즈에 맞춰 늘리기" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "제목:" @@ -1358,33 +1544,36 @@ msgstr "제목:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF 파일 (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN 드라이버" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "기술 상세" #: MiscResources.resx$TesseractNotAvailable$Message #: SdkResources.resx$TesseractNotAvailable$Message msgid "The OCR engine is not available. Make sure to install the required package:" -msgstr "" +msgstr "OCR 엔진을 사용할 수 없습니다. 다음 패키지가 설치 되어 있는지 확인해 주십시오:" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "OCR 작업 시간이 초과되었습니다." #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" -msgstr "" +msgstr "SANE 드라이버를 사용할 수 없습니다. 다음 패키지가 설치 되어 있는지 확인해 주십시오:" #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message msgid "The file '{0}' could not be imported." -msgstr "'{0}' 파일을 불러올 수 없습니다.." +msgstr "'{0}' 파일을 불러올 수 없습니다." #: MiscResources.resx$ImportErrorNAPS2Pdf$Message msgid "The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported." -msgstr "'{0}' 파일을 불러올 수 없습니다. NAPS2 는 PDF 파일만 제작이 가능합니다.." +msgstr "'{0}' 파일을 불러올 수 없습니다. NAPS2에서 제작한 PDF 파일만 불러올 수 있습니다." #: MiscResources.resx$FileInUse$Message msgid "The file could not be overwritten because it is currently in use." @@ -1394,9 +1583,9 @@ msgstr "파일이 사용 중이므로 덮어쓰기 할 수 없습니다." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "{0} 파일이 이미 존재합니다. 덮어쓰시겠습니까?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "암호화 된 {0} 파일을 열기 위해서는 암호가 필요합니다." +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "다음 파일은 암호화 되어 있으며 열기 위해서는 암호가 필요합니다:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1416,55 +1605,91 @@ msgstr "스캐너 커버 열림." #: MiscResources.resx$DriverNotSupported$Message #: SdkResources.resx$DriverNotSupported$Message msgid "The selected driver is not supported on this system." -msgstr "" +msgstr "선택한 드라이버는 이 시스템에서 사용할 수 없습니다." #: MiscResources.resx$DeviceNotFound$Message #: SdkResources.resx$DeviceNotFound$Message msgid "The selected scanner could not be found." -msgstr "선택된 스캐너를 찾을 수 없습니다.." +msgstr "선택된 스캐너를 찾을 수 없습니다." #: MiscResources.resx$NoFeederSupport$Message #: SdkResources.resx$NoFeederSupport$Message msgid "The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver." -msgstr "선택된 스캐너에서는 지급기를 사용할 수 없습니다. 만약 지급기가 있는 스캐너라면, 다른 드라이버를 선택 해 주세요.." +msgstr "선택된 스캐너에서는 지급기를 사용할 수 없습니다. 만약 지급기가 있는 스캐너라면, 다른 드라이버를 선택 해 주세요." #: MiscResources.resx$NoDuplexSupport$Message #: SdkResources.resx$NoDuplexSupport$Message msgid "The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver." -msgstr "선택된 스캐너에서는 양면 기능을 사용할 수 없습니다. 만약 스캐너가 양면 기능을 지원한다면, 다른 드라이버를 선택 해 주세요.." +msgstr "선택된 스캐너에서는 양면 기능을 사용할 수 없습니다. 만약 스캐너가 양면 기능을 지원한다면, 다른 드라이버를 선택 해 주세요." #: MiscResources.resx$DeviceBusy$Message #: SdkResources.resx$DeviceBusy$Message msgid "The selected scanner is busy." -msgstr "선택된 스캐너가 사용 중 입니다.." +msgstr "선택된 스캐너가 사용 중 입니다." #: MiscResources.resx$DeviceOffline$Message #: SdkResources.resx$DeviceOffline$Message msgid "The selected scanner is offline." -msgstr "선택된 스캐너의 연결이 끊겼습니다. ." +msgstr "선택된 스캐너의 연결이 끊겼습니다." -#: FImageSettings.resx$groupTiff.Text$Message -msgid "Tiff Options" +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "작업 프로세스가 튕겼습니다." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "작업 프로세스가 튕겼습니다. Windows 이벤트 뷰어를 확인하십시오." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message +msgid "Tiff Options" +msgstr "TIFF 옵션" + +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "스캔 사이의 지연 시간 (초):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "제목:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "도구" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" -msgstr "Twain 구현:" +msgstr "TWAIN 구현:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 인치)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" +msgstr "US Letter (8.5x11 인치)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "할당 취소" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "실행취소" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "{0} 실행취소" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" msgstr "" #: MiscResources.resx$UnsavedChanges$Message @@ -1473,78 +1698,74 @@ msgstr "저장되지 않은 변경 사항" #: MiscResources.resx$UpdateProgress$Message msgid "Update Progress" -msgstr "" +msgstr "업데이트 진행 중" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "업데이트 확인이 비활성화 되어 있습니다." #: MiscResources.resx$Updating$Message msgid "Updating..." -msgstr "" +msgstr "업데이트 중..." #: MiscResources.resx$UploadingEmail$Message msgid "Uploading email..." -msgstr "" +msgstr "이메일 업로드 중..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "네이티브 UI 사용" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "사전 정의 설정 사용" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "사용자 암호:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." -msgstr "OCR 을 이용하여 스캔하고자 하는 각 언어를 선택 해 주세요.." +msgstr "OCR을 이용하려면 스캔하고자 하는 언어를 다운로드 해야 합니다." #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message msgid "Version {0}" -msgstr "{0} 버전" +msgstr "버전 {0}" #: UiStrings.resx$View$Message msgid "View" msgstr "보기" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA 드라이버" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "TWAIN 완료 대기중..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." -msgstr "" +msgstr "인증을 기다리는 중..." #: MiscResources.resx$BatchStatusWaitingForScan$Message msgid "Waiting for scan {0}..." -msgstr "{0} 스캔 대기중..." +msgstr "스캔 {0} 대기중..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "백색 임계값" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "WIA 버전:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "년도" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "년도 (00-99)" @@ -1561,67 +1782,69 @@ msgstr "이 위치에 파일을 쓸 수 있는 권한을 가지고 있지 않습 msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "저장되지 않은 변경사항이 있습니다. 변경사항을 무시하고 종료하시겠습니까?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "확대/축소" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "실제 크기로 확대" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "확대" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "축소" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "인치" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" -msgstr "" +msgstr "중 {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} 파일" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} 장치 발견됨." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" -msgstr "스캔된 {0} 이미지가 {2} 의 {1} 에 저장되지 않았을 수 있습니다. 복구 하시겠습니까?" +msgstr "{1} {2} 에 스캔된 {0} 개의 이미지를 복구 할 수 있습니다. 복구 하시겠습니까?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." diff --git a/NAPS2.Lib/Lang/po/lt.po b/NAPS2.Lib/Lang/po/lt.po index adb67ec12c..60daef7343 100644 --- a/NAPS2.Lib/Lang/po/lt.po +++ b/NAPS2.Lib/Lang/po/lt.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Lithuanian\n" "Language: lt\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-bitų spalva" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "" @@ -76,12 +60,15 @@ msgstr "Apie programą" msgid "Acquiring data..." msgstr "Duomenų gavimas..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Papildomi" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Išplėstiniai profilio nustatymai" @@ -93,35 +80,35 @@ msgstr "Viską ({0})" msgid "All Files" msgstr "Visos bylos" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Leisti anotacijas" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Leisti kopijuoti turinį" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Leisti kopijuoti turinį prieinamumui" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Leisti surinkti dokumentą" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Leisti keisti dokumentą" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Lesti pildyti formą" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Leisti spausdinti visiškos kokybės" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Leisti spausdinti" @@ -133,17 +120,22 @@ msgstr "Alternatyvus tarpų šalinimas" msgid "Alternate Interleave" msgstr "Alternatyvus tarpavimas" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternatyvus perdavimas" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Norint importuoti šį PDF failą reikalingas papildomas komponentas. Ar norėtumėte jį atsisiųsti?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Įvyko klaida bandant įdiegti atnaujinimus." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "" #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "" msgid "An error occurred when trying to auto save." msgstr "Įvyko klaida bandant automatiškai išsaugoti." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Įvyko klaida bandant įdiegti atnaujinimus." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Įvyko klaida bandant išsaugoti bylą." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Įvyko nežinoma paketinio skenavimo klaida." +msgid "An unknown error occurred during the batch scan." +msgstr "" #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -190,7 +186,15 @@ msgstr "" msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Pritaikyti ryškumą/kontrastą po skenavimo" @@ -222,19 +226,27 @@ msgstr "Ar tikrai norite pašalinti {0} objektą(-us)?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Ar tikrai norite ištrinti {0} profilius?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Ar tikrai norite atšaukti pakeitimus {0} vaizdui(-ams)?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Priedo pavadinimas:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autorius:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "" @@ -242,29 +254,27 @@ msgstr "" msgid "Auto" msgstr "" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Automatinio išsaugojimo nustatymai" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Automatinis numerio didinimas (1 skaičius)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Automatinis numerio didinimas (2 skaičiai)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Automatinis numerio didinimas (3 skaičiai)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Automatinis numerio didinimas (4 skaičiai)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "" @@ -277,8 +287,8 @@ msgstr "B4 (250 x 353 mm)" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Paketinis skenavimas" @@ -298,7 +308,6 @@ msgstr "Paketinis skenavimas sustabdytas dėl klaidos." msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bitų gylis:" @@ -309,10 +318,10 @@ msgstr "Bitmap bylos (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Nespalvotas" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Tušti puslapiai" @@ -320,7 +329,6 @@ msgstr "Tušti puslapiai" msgid "Brightness / Contrast" msgstr "Ryškumas / Kontrastas" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Šviesumas:" @@ -329,24 +337,11 @@ msgstr "Šviesumas:" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Atšaukti" @@ -363,7 +358,7 @@ msgstr "Atšaukiama...." msgid "Center" msgstr "Centrinė" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "" @@ -375,7 +370,7 @@ msgstr "" msgid "Checking..." msgstr "" -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "" @@ -383,7 +378,6 @@ msgstr "" msgid "Choose Profile" msgstr "Pasirinkti profilį" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Pasirinkti įrenginį" @@ -397,7 +391,7 @@ msgstr "Išvalyti" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Išvalyti vaizdus po išsaugojimo" @@ -405,16 +399,30 @@ msgstr "Išvalyti vaizdus po išsaugojimo" msgid "Close" msgstr "Užverti" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Suderinamumas" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrastas:" @@ -435,7 +443,7 @@ msgstr "Kopijuojama..." msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Padengimo slenkstis" @@ -443,7 +451,7 @@ msgstr "Padengimo slenkstis" msgid "Crop" msgstr "Apkirpti" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "" @@ -451,10 +459,14 @@ msgstr "" msgid "Custom ({0}x{1} {2})" msgstr "Pritaikytas ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Pasirinktinis puslapio dydis" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Pasirinktinis sukimas" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Pritaikytas..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Diena (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Numatytasis" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "" @@ -487,7 +504,6 @@ msgstr "" msgid "Deinterleave" msgstr "Tarpų šalinimas" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "" msgid "Deskew Progress" msgstr "" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" @@ -509,16 +525,14 @@ msgstr "" msgid "Deskewing..." msgstr "" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Įrenginys:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Rodomas pavadinimas:" @@ -532,12 +546,10 @@ msgstr "" msgid "Donate" msgstr "" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Atlikta" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Atsisiųsti" @@ -550,19 +562,47 @@ msgstr "Siuntimo klaida" msgid "Download Needed" msgstr "" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Siuntimo eiga" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Dvipusis skenavimas" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Redaguoti" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +616,32 @@ msgstr "Siųsti PDF" msgid "Email PDF Progress" msgstr "Vyksta PDF siuntimas el. paštu" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "El. pašto nustatymai" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Leisti automatinį išsaugojimą" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Užšifruoti PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Šifravimas" @@ -602,12 +649,15 @@ msgstr "Šifravimas" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Patobulintas metafailas (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Klaida" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "Numatomas atsiuntimo dydis: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "Keičiamo vaizdo byla (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Pašalinti tuščius puslapius" @@ -629,24 +679,31 @@ msgstr "" msgid "Feeder" msgstr "Padavimo mechanizmas" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Bylos pavadinimas" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Kelias iki bylos:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Veidrodinis vaizdas" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" @@ -654,7 +711,6 @@ msgstr "" msgid "GIF File (*.gif)" msgstr "GIF byla (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Įdiegti daugiau kalbų" @@ -671,12 +727,11 @@ msgstr "" msgid "Grayscale" msgstr "Pilkumo atspalviai" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Horizontali lygiuotė:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Valanda (0-23)" @@ -684,6 +739,10 @@ msgstr "Valanda (0-23)" msgid "Hue / Saturation" msgstr "" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Piktogramos iš:" @@ -696,12 +755,12 @@ msgstr "Vaizdas" msgid "Image Files" msgstr "Vaizdo bylos" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Vaizdo kokybė" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Vaizdo parametrai" @@ -745,6 +804,10 @@ msgstr "Diegimas baigtas. Ar norite paleisti NAPS2 dabar?" msgid "Installation failed." msgstr "Diegimas nepavyko." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Tarpuoti" @@ -757,11 +820,20 @@ msgstr "JPEG byla (*.jpg, *.jpeg)" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "JPEG kokybė" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Raktažodžiai:" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "Kalba" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Kairinė" @@ -781,33 +857,48 @@ msgstr "Kairinė" msgid "Legacy (native UI only)" msgstr "Paveldėjimas (tik skenerio sąsajai)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Įkrauti vaizdus į NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Padaryti PDF su paieškos galimybe naudojant OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Maksimali kokybė (didelės bylos)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Meta-duomenys" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minutė (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Mėnuo (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Daugiau informacijos" @@ -819,11 +910,19 @@ msgstr "Perkelti žemyn" msgid "Move Up" msgstr "Perkelti aukštyn" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Kelių puslapių skenavimas (fiksuotas uždelsimas tarp skenavimų)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Kelių puslapių skenavimas (pranešimas tarp skenavimų)" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "" @@ -849,6 +948,10 @@ msgstr "" msgid "Name missing." msgstr "Nenurodytas pavadinimas." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "Naujas" @@ -861,7 +964,7 @@ msgstr "Naujas profilis" msgid "Next" msgstr "Kitas" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Kitas skenavimas" @@ -870,12 +973,15 @@ msgstr "Kitas skenavimas" msgid "No device selected." msgstr "Įrenginys nepasirinktas." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Padavimo mechanizme nėra lapų." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "" @@ -897,11 +1003,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Ne dabar" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Skenavimų kiekis:" @@ -909,7 +1015,6 @@ msgstr "Skenavimų kiekis:" msgid "OCR" msgstr "Teksto atpažinimas" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Atsisiųsti bylas tekstui atpažinti" @@ -918,37 +1023,23 @@ msgstr "Atsisiųsti bylas tekstui atpažinti" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Teksto atpažinimo nustatymai" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Teksto atpažinimo kalba:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "Gerai" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Viena byla puslapiui" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Viena byla skenavimui" @@ -970,7 +1059,11 @@ msgstr "Viena byla skenavimui" msgid "One or more files could not be downloaded." msgstr "Viena ar kelios bylos negali būti atsisiųstos." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "" @@ -978,11 +1071,15 @@ msgstr "" msgid "Operation in Progress" msgstr "" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Išvestis" @@ -990,7 +1087,7 @@ msgstr "Išvestis" msgid "Overwrite File" msgstr "Perrašyti bylą" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Savininko slaptažodis:" @@ -998,8 +1095,8 @@ msgstr "Savininko slaptažodis:" msgid "PDF Document (*.pdf)" msgstr "PDF dokumentas (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF nustatymai" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "PNG byla (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Puslapio dydis:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Popieriaus šaltinis:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Slaptažodis" @@ -1045,21 +1140,24 @@ msgstr "Slaptažodis" msgid "Paste" msgstr "Įterpti" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Vietaženkliai" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Paspauskite Pradėti, kai būsite pasiruošę." @@ -1067,7 +1165,7 @@ msgstr "Paspauskite Pradėti, kai būsite pasiruošę." msgid "Preview" msgstr "Peržiūra" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Peržiūra:" @@ -1080,12 +1178,11 @@ msgstr "Ankstesnis" msgid "Print" msgstr "Spausdinti" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Profilio nustatymai" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profilis:" @@ -1094,23 +1191,27 @@ msgstr "Profilis:" msgid "Profiles" msgstr "Profiliai" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Paruoštas skenavimui {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Atkurti" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Atkurti nuskenuotus vaizdus" @@ -1122,9 +1223,15 @@ msgstr "Atstatymas..." msgid "Recovery Progress" msgstr "Vyksta atstatymas" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Atsiminti šiuos nustatymus" @@ -1140,15 +1247,11 @@ msgstr "Atstatyti" msgid "Reset Image" msgstr "Atstatyti vaizdą" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Skiriamoji geba:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Atstatyti numatytas parinktis" @@ -1156,6 +1259,14 @@ msgstr "Atstatyti numatytas parinktis" msgid "Reverse" msgstr "Atvirkščiai" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Grąžinti" @@ -1176,7 +1287,6 @@ msgstr "Pasukti į kairę" msgid "Rotate Right" msgstr "Pasukti į dešinę" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1185,7 +1295,6 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1303,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "Išsaugoti PDF" msgid "Save PDF Progress" msgstr "PDF išsaugojimo progresas" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Išsaugoti į vieną bylą" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Išsaugoti į kelias bylas" @@ -1244,29 +1363,45 @@ msgstr "Paketo rezultatų išsaugojimas..." msgid "Saving {0}..." msgstr "Įrašoma {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Pagal langą" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Mastelis:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skenuoti" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Skenavimo nustatymai" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Nuskenuotas vaizdas" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Skenuojamas puslapis {0}" @@ -1280,11 +1415,14 @@ msgstr "Skenuojamas puslapis {0} ({1})..." msgid "Scanning page {0}..." msgstr "Skenuojamas puslapis {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekundė (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Pažymėti" @@ -1293,7 +1431,10 @@ msgstr "Pažymėti" msgid "Select All" msgstr "Pažymėti viską" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Pasirinkti šaltinį" @@ -1302,7 +1443,6 @@ msgstr "Pasirinkti šaltinį" msgid "Select a profile before clicking Scan." msgstr "Prieš pradėdami skenavimą pasirinkite profilį." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Pasirinkite vieną ar daugiau kalbų:" @@ -1311,8 +1451,7 @@ msgstr "Pasirinkite vieną ar daugiau kalbų:" msgid "Selected ({0})" msgstr "Pasirinkta ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,37 +1459,84 @@ msgstr "" msgid "Set Default" msgstr "Nustatyti kaip numatytąjį" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Rodyti" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Viengubas skenavimas" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Pradėti" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Tema:" @@ -1358,12 +1544,11 @@ msgstr "Tema:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF byla (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN tvarkyklė" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "" @@ -1372,6 +1557,10 @@ msgstr "" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Byla {0} jau yra. Ar norite ją perrašyti?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Byla yra užšifruota ir reikalauja slaptažodžio atidarymui: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "" msgid "The selected scanner is offline." msgstr "Pasirinktas skeneris neprisijungęs." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Laikas tarp skenavimų (sekundės):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Antraštė:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "TWAIN realizacija:" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Neišsaugoti pakeitimai" @@ -1487,21 +1712,18 @@ msgstr "" msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Skenerio sąsaja" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Naudoti nustatytas parinktis" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Naudotojo slaptažodis:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Teksto atpažinimo funkcijai reikia atsisiųsti visų reikalingų kalbų paketus." @@ -1515,16 +1737,15 @@ msgstr "Versija {0}" msgid "View" msgstr "Peržiūrėti" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "Tvarkyklė WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Laukiama TWAIN užbaigimo..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1532,19 +1753,19 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "Laukiama skenavimo {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Baltumo slenkstis" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Metai" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Metai (00-99)" @@ -1561,21 +1782,18 @@ msgstr "Neturite leidimo įrašyti bylų šioje vietoje." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Turite neišsaugotų pakeitimų. Ar tikrai norite išeiti ir atmesti pakeitimus?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Mastelis" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Tikrasis mastelis" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Padidinti" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Sumažinti" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} bylų" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} vaizdas(-ai) nuskenuotas(-i) nuo {1} iki {2} nebuvo išsaugoti ir gali būti atstatyti. Ar norite juos atstatyti?" diff --git a/NAPS2.Lib/Lang/po/lv.po b/NAPS2.Lib/Lang/po/lv.po index 9fb0425c00..959268a267 100644 --- a/NAPS2.Lib/Lang/po/lv.po +++ b/NAPS2.Lib/Lang/po/lv.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Latvian\n" "Language: lv\n" @@ -12,60 +12,44 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Translate Toolkit 1.13.0\n" "X-Poedit-SourceCharset: iso-8859-1\n" -"Plural-Forms: nplurals=3; plural=(n==0 ? 0 : n%10==1 && n%100!=11 ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;\n" "X-Crowdin-Project: naps2\n" "X-Crowdin-Project-ID: 531762\n" "X-Crowdin-Language: lv\n" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Pogas \"Saglabāt\" noklusējuma darbība:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Pogas \"Skenēt\" noklusējuma darbība:" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "\"Skenēt\" komandkarte maina noklusējuma profilu" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "Atrasta 1 ierīce." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-bitu krāsa" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -76,12 +60,15 @@ msgstr "Par" msgid "Acquiring data..." msgstr "Iegūst datus..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Papildu" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Papildu profila iestatījumi" @@ -93,35 +80,35 @@ msgstr "Visi ({0})" msgid "All Files" msgstr "Visas datnes" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Atļaut anotācijas" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Atļaut satura kopēšanu" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Atļaut pieejamības satura kopēšanu" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Atļaut dokumenta montāžu" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Atļaut dokumenta modificēšanu" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Atļaut veidlapas aizpildīšanu" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Atļaut augstākās kvalitātes drukāšanu" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Atļaut drukāšanu" @@ -133,17 +120,22 @@ msgstr "Noņemt pretēju kārtojumu" msgid "Alternate Interleave" msgstr "Pārkārtot pretēji" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternatīvā pārnešana" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Vienmēr prasīt" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Vienmēr atgādināt" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Lai importētu šo PDF datni, vajadzīga papildus komponente. Vai vēlaties to lejuplādēt tūlīt?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Notikusi kļūda mēģinot instalēt jauninājumu." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Pie teksta atpazīšanas notika kļūda." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Notikusi kļūda mēģinot autorizēt." msgid "An error occurred when trying to auto save." msgstr "Notika kļūda mēģinot veikt automātisko saglabāšanu." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Notikusi kļūda mēģinot instalēt jauninājumu." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Notika kļūda mēģinot saglabāt datni." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Notiek darbība. Vai esat pārliecināti, ka vēlaties iziet un pārtraukt darbību?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Notika nezināma kļūda partijas skenēšanas laikā." +msgid "An unknown error occurred during the batch scan." +msgstr "Pie partijas skenēšanas notika nezināma kļūda." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -188,9 +184,17 @@ msgstr "Pieejams OCR atjauninājums." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple draiveris" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Lietotne" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Pēc skenēšanas koriģēt spilgtumu/kontrastu" @@ -222,63 +226,69 @@ msgstr "Vai tiešām vēlaties dzēst {0} vienumu(s)?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Vai tiešām vēlaties dzēst {0} profilus?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Vai tiešām vēlaties atcelt {0} kopīgošanu?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Vai tiešām vēlaties atsaukt jūsu veiktās izmaiņas {0} attēlam(iem)?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Pielikuma nosaukums:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autors:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autorizēt" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "Automātiski" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Automātiski saglabāt iestatījumus" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Automātiski palielināt skaitli (1 cipars)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Automātiski palielināt skaitli (1 numurs)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Automātiski palielināt skaitli (2 cipari)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Automātiski palielināt skaitli (3 cipari)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Automātiski palielināt skaitli (4 cipari)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Automātiski palaist OCR (teksta atpazīšanu) pēc skenēšanas" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Partijas skenēšana" @@ -296,9 +306,8 @@ msgstr "Partijas skenēšana apstājās kļūdas dēļ." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Vislabākais" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bitu dziļums:" @@ -309,10 +318,10 @@ msgstr "Bitmap datnes (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Melns & balts" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Tukšās lapas" @@ -320,33 +329,19 @@ msgstr "Tukšās lapas" msgid "Brightness / Contrast" msgstr "Spilgtums / Kontrasts" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Spilgtums:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Atcelt" @@ -363,7 +358,7 @@ msgstr "Atceļ...." msgid "Center" msgstr "Centrēt" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Mainīt" @@ -375,7 +370,7 @@ msgstr "Pārbaudīt atjauninājumus" msgid "Checking..." msgstr "Pārbauda..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Izvēlēties e-pasta provaideri" @@ -383,7 +378,6 @@ msgstr "Izvēlēties e-pasta provaideri" msgid "Choose Profile" msgstr "Izvēlies profilu" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Izvēlies ierīci" @@ -395,9 +389,9 @@ msgstr "Notīrīt" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Notīrīt visu" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Notīrīt attēlus pēc saglabāšanas" @@ -405,16 +399,30 @@ msgstr "Notīrīt attēlus pēc saglabāšanas" msgid "Close" msgstr "Aizvērt" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Apvienot" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Pārtrūka sakari ar skenēšanas ierīci." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Savietojamība" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Kompresija:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Pieslēgties" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Pieslēguma kļūda." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrasts:" @@ -433,9 +441,9 @@ msgstr "Kopē..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "{0} NAPS2 Contributors autortiesības" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Pārklājuma slieksnis" @@ -443,7 +451,7 @@ msgstr "Pārklājuma slieksnis" msgid "Crop" msgstr "Apgriezt" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Apgriezt līdz lappuses izmēram" @@ -451,10 +459,14 @@ msgstr "Apgriezt līdz lappuses izmēram" msgid "Custom ({0}x{1} {2})" msgstr "Pielāgots ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Pielāgots lapas izmērs" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Pielāgota pagriešana" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "Izvēles SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Pielāgots..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Diena (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Noklusētais" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Noklusētais datnes ceļš:" @@ -487,7 +504,6 @@ msgstr "Noklusētais datnes ceļš:" msgid "Deinterleave" msgstr "Noņemt kārtojumu" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Izlīdzināt" msgid "Deskew Progress" msgstr "Izlīdzināšanas progress" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Izlīdzināt skenētās lapas" @@ -509,35 +525,31 @@ msgstr "Izlīdzināt skenētās lapas" msgid "Deskewing..." msgstr "Izlīdzina..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Ierīce:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Izmēri" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Parādāmais nosaukums:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Koriģēt dokumentu" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Ziedot" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Pabeigts" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Lejupielādēt" @@ -550,22 +562,50 @@ msgstr "Lejupielādes kļūda" msgid "Download Needed" msgstr "Vajadzīga lejupielāde" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Lejupielādes progress" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Duplekss" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL draiveris" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL tīkla draiveris" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB draiveris" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Rediģēt" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Visu nosūtīt uz e-pastu" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Visu nosūtīt uz e-pastu kā PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,38 +616,48 @@ msgstr "PDF uz e-pastu" msgid "Email PDF Progress" msgstr "PDF uz e-pastu progress" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Nosūtīt uz e-pastu izvēlēto" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Nosūtīt uz e-pastu izvēlēto kā PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "E-pasta iestatījumi" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Iespējot automātisku saglabāšanu" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Ieslēgt atkļūdošanu" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Šifrēt PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Šifrēšana" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "" +msgstr "Paplašinātais metafails (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Kļūda" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,38 +665,45 @@ msgstr "Paredzamais lejupielādes izmērs: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Paplašinātais attēlu fails (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Izlaist tukšās lapas" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Ātrais" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Padevējs" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Datnes nosaukums" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Datnes ceļš:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Fiksēt baltā balansu un dzēst trokšņus" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Apvērst" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Apgriezt puses divpusējām lapām" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Apvērst dupleksās lapas" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Ja iestatīta augsta JPEG kvalitāte (80+), paaugstini iestatījumu Attēla kvalitāte arī profilā, lai iegūtu labāko rezultātu." @@ -654,7 +711,6 @@ msgstr "Ja iestatīta augsta JPEG kvalitāte (80+), paaugstini iestatījumu Att msgid "GIF File (*.gif)" msgstr "GIF datne (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Iegūt vairāk valodu" @@ -665,18 +721,17 @@ msgstr "Stikls" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Pelēktoņu" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Līdzināt horizontāli:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Stunda (0-23)" @@ -684,6 +739,10 @@ msgstr "Stunda (0-23)" msgid "Hue / Saturation" msgstr "Tonis / Piesātinājums" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Host adrese" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Ikonas no:" @@ -696,12 +755,12 @@ msgstr "Attēls" msgid "Image Files" msgstr "Attēlu datnes" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Attēla kvalitāte" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Attēla iestatījumi" @@ -727,7 +786,7 @@ msgstr "Importē..." #: MiscResources.resx$Install$Message msgid "Install {0}" -msgstr "" +msgstr "Instalēt {0}" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" @@ -745,6 +804,10 @@ msgstr "Instalēšana pabeigta. Vai vēlaties tagad pārstartēt NAPS2?" msgid "Installation failed." msgstr "Instalēšana neizdevās." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Saskarne" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Pārkārtot" @@ -755,24 +818,37 @@ msgstr "JPEG datne (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000 fails (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "JPEG kvalitāte" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Saglabāt attēlus starp seansiem" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Atslēgvārdi:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Valoda" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Pa kreisi" @@ -781,33 +857,48 @@ msgstr "Pa kreisi" msgid "Legacy (native UI only)" msgstr "Savietojams (tikai standarta saskarne)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Ielādēt attēlus NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Padarīt PDF meklējamus, izmantojot OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Manuālā IP adrese" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Maksimālā kvalitāte (lielas datnes)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Nosūtīšana izmantojot atmiņu" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metadati" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minūte (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Mēnesis (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Vairāk informācijas" @@ -819,11 +910,19 @@ msgstr "Pārvietot uz leju" msgid "Move Up" msgstr "Pārvietot uz augšu" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Vairākas valodas" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Vairākas valodas..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Vairākas skenēšanas (fiksēta aizture starp skenējumiem)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Vairākas skenēšanas (jautāt starp skenējumiem)" @@ -831,17 +930,17 @@ msgstr "Vairākas skenēšanas (jautāt starp skenējumiem)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 ir pilnīgi bez maksas. Apsveriet iespēju ziedot." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Nosaukums (neobligāts)" @@ -849,6 +948,10 @@ msgstr "Nosaukums (neobligāts)" msgid "Name missing." msgstr "Nav nosaukuma." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Vecā tipa nosūtīšana" + #: UiStrings.resx$New$Message msgid "New" msgstr "Jauns" @@ -861,7 +964,7 @@ msgstr "Jauns profils" msgid "Next" msgstr "Nākamais" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Nākamais skenējums" @@ -870,12 +973,15 @@ msgstr "Nākamais skenējums" msgid "No device selected." msgstr "Nav izvēlēta ierīce." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Neviena ierīce nav atrasta." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Padevē nav lapu." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Nav izvēlēts provaideris." @@ -895,21 +1001,20 @@ msgstr "Nav" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Ne tagad" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Skenējumu skaits:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR lejupielāde" @@ -918,37 +1023,23 @@ msgstr "OCR lejupielāde" msgid "OCR Progress" msgstr "OCR progress" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCR uzstādīšana" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR valoda:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "" +msgstr "OCR režīms:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "Apstiprināt" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Platuma nobīde pēc pielīdzināšanas (WIA)" @@ -956,13 +1047,11 @@ msgstr "Platuma nobīde pēc pielīdzināšanas (WIA)" msgid "Old DSM" msgstr "Vecais DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Lappuse vienā datnē" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Skenējums vienā datnē" @@ -970,7 +1059,11 @@ msgstr "Skenējums vienā datnē" msgid "One or more files could not be downloaded." msgstr "Vienu vai vairākas datnes nevarēja lejupielādēt." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Atļaut tikai vienu NAPS2 eksemplāru" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Atvērt mapi" @@ -978,11 +1071,15 @@ msgstr "Atvērt mapi" msgid "Operation in Progress" msgstr "Notiek darbība" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "Outllok WEB pieeja" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Izvade" @@ -990,7 +1087,7 @@ msgstr "Izvade" msgid "Overwrite File" msgstr "Pārrakstīt datni" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Īpašnieka parole:" @@ -998,8 +1095,8 @@ msgstr "Īpašnieka parole:" msgid "PDF Document (*.pdf)" msgstr "PDF dokuments (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF iestatījumi" @@ -1009,35 +1106,33 @@ msgstr "PDF saglabāts." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "PNG datne (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Lappuses izmērs:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Papīra avots:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Parole" @@ -1045,21 +1140,24 @@ msgstr "Parole" msgid "Paste" msgstr "Ielīmēt" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Vietrāži" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Ports" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Pēcapstrāde" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Pēc skanēšanas sākt teksta atpazīšanu" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Nospied Sākt, kad gatavs." @@ -1067,7 +1165,7 @@ msgstr "Nospied Sākt, kad gatavs." msgid "Preview" msgstr "Priekšskatījums" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Priekšskatījums:" @@ -1080,12 +1178,11 @@ msgstr "Iepriekšējais" msgid "Print" msgstr "Drukāt" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Profila iestatījumi" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profils:" @@ -1094,23 +1191,27 @@ msgstr "Profils:" msgid "Profiles" msgstr "Profili" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Prasīt, ja izvēlēts" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Datnes ceļa dialogs" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Provaideris" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Gatavs skenēšanai {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Atgūt" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Atgūt skenētos attēlus" @@ -1122,9 +1223,15 @@ msgstr "Atgūst..." msgid "Recovery Progress" msgstr "Atgūšanas progress" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Atkārtot" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Atkārtot {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Atcerēties šos iestatījumus" @@ -1140,15 +1247,11 @@ msgstr "Atiestatīt" msgid "Reset Image" msgstr "Atiestatīt attēlu" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Izšķirtspēja:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Atiestatīt uz noklusēto" @@ -1156,6 +1259,14 @@ msgstr "Atiestatīt uz noklusēto" msgid "Reverse" msgstr "Apvērst" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Apriezt visu" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Apgriezt izvēlēto" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Atgriezt" @@ -1176,7 +1287,6 @@ msgstr "Pagriezt pa kreisi" msgid "Rotate Right" msgstr "Pagriezt pa labi" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Darbojas fonā" @@ -1185,22 +1295,26 @@ msgstr "Darbojas fonā" msgid "Running OCR..." msgstr "Notiek OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "SANE draiveris" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Saglabāt" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Saglabāt visu" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Saglabāt visu kā attēlus" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Saglabāt visu kā PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,6 +1334,11 @@ msgstr "Saglabāt PDF" msgid "Save PDF Progress" msgstr "PDF saglabāšanas progress" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Saglabāt izvēlēto" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Saglabāt vienā datnē" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Saglabāt vairākās datnēs" @@ -1244,29 +1363,45 @@ msgstr "Saglabā partijas rezultātus..." msgid "Saving {0}..." msgstr "Saglabā {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Mērogot ar logu" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Mērogs:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skenēt" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Skenēšanas konfigurācija" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Skenēt ar noklusējuma profilu" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Skenētais attēls" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Skenera kopīgošana" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Skenē {0}. lappusi" @@ -1280,11 +1415,14 @@ msgstr "Skenē {0}. lappusi (skenējums {1})..." msgid "Scanning page {0}..." msgstr "Skenē {0}. lappusi..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Tiek meklētas ierīces..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekunde (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Izvēlēties" @@ -1293,7 +1431,10 @@ msgstr "Izvēlēties" msgid "Select All" msgstr "Izvēlēties visu" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Izvēlieties ierīci" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Izvēlēties avotu" @@ -1302,7 +1443,6 @@ msgstr "Izvēlēties avotu" msgid "Select a profile before clicking Scan." msgstr "Izvēlieties profilu pirms spiest uz Skenēt." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Izvēlieties vienu vai vairākas valodas:" @@ -1311,8 +1451,7 @@ msgstr "Izvēlieties vienu vai vairākas valodas:" msgid "Selected ({0})" msgstr "Izvēlēti ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Sadalīt datnēs pēc Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Sadalīt datnēs pēc Patch-T" msgid "Set Default" msgstr "Uzstādīt noklusējumu" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Uzstādījumi" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Kopīgot" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Asināt" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Rādīt" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Vienas lappuses datnes" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Viens skenējums" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Izlaist saglabāšanas dialogu" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Sākt" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Izstiept līdz lappuses izmēram" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Temats:" @@ -1358,12 +1544,11 @@ msgstr "Temats:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF datne (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN draiveris" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Tehniskās detaļas" @@ -1372,6 +1557,10 @@ msgstr "Tehniskās detaļas" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "OCR (teksta atpazīšanas) dzinis nav pieejams. Instalējiet nepieciešamo spraudni:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Datni nevar pārrakstīt jo tā pašlaik tiek lietota." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Datne {0} jau pastāv. Vai vēlaties to pārrakstīt?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Sekojošā datne ir šifrēta un atvēršanai pieprasa paroli: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "Izvēlētais skeneris ir aizņemts." msgid "The selected scanner is offline." msgstr "Izvēlētais skeneris ir bezsaistē." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Tiff iestatījumi" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Laiks starp skenēšanām (sekundes):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Virsraksts:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Rīki" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "TWAIN realizācija:" @@ -1467,6 +1676,22 @@ msgstr "US Legal (8.5x14 collas)" msgid "US Letter (8.5x11 in)" msgstr "US Letter (8.5x11 collas)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Atcelt" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Atcelt {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Nesaglabātās izmaiņas" @@ -1477,7 +1702,7 @@ msgstr "Atjauninājuma progress" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Jauninājumu pārbaude ir atslēgta." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Atjaunina..." msgid "Uploading email..." msgstr "Sūta e-pastu..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Lietot noklusējuma saskarni" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Lietot iepriekšdefinētos iestatījumus" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Lietotāja parole:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Lai lietotu OCR, jums jālejupielādē katra valoda, kuru vēlaties skenēt." @@ -1515,16 +1737,15 @@ msgstr "Versija {0}" msgid "View" msgstr "Skatīt" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA draiveris" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Gaida TWAIN, lai pabeigtu..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Gaida autorizāciju..." @@ -1532,19 +1753,19 @@ msgstr "Gaida autorizāciju..." msgid "Waiting for scan {0}..." msgstr "Gaida skenēšanu {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Baltā slieksnis" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Wia versija:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Gads" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Gads (00-99)" @@ -1561,28 +1782,25 @@ msgstr "Jums nav atļaujas saglabāt datnes šajā vietā." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Jums ir nesaglabātas izmaiņas. Vai jūs tiešām gribat iziet un atmest šīs izmaiņas?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Mērogs" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Oriģinālais izmērs" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Palielināt" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Samazināt" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" @@ -1590,7 +1808,7 @@ msgstr "collas" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,28 +1816,33 @@ msgstr "no {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} datnes" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "Atrasta(-s) {0} ierīce(-s)." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} attēls(i), kas skenēti {1} {2}, iespējams nav saglabāti un ir atgūstami. Vai vēlaties tos atgūt?" diff --git a/NAPS2.Lib/Lang/po/nb.po b/NAPS2.Lib/Lang/po/nb.po index a30a747082..475d3d9ec0 100644 --- a/NAPS2.Lib/Lang/po/nb.po +++ b/NAPS2.Lib/Lang/po/nb.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Norwegian Bokmal\n" "Language: nb_NO\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "100 ppt" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "1200 ppt" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "150 ppt" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "200 ppt" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-bit farger" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "300 ppt" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "400 ppt" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "600 ppt" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "800 ppt" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "" @@ -76,12 +60,15 @@ msgstr "Om" msgid "Acquiring data..." msgstr "Henter data..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Avansert" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Avanserte profilinnstillinger" @@ -93,35 +80,35 @@ msgstr "Alle ({0})" msgid "All Files" msgstr "Alle filer" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Tillat merknader" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Tillat innholdskopiering" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Tillat innholdskopiering for tilgjengelighet" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Tillat sammenstilling av dokument" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Tillat dokumentmodifikasjon" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Tillat skjemautfylling" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Tillat fullkvalitets utskrift" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Tillat utskrift" @@ -133,17 +120,22 @@ msgstr "Vekslende ikke-mellomlegg" msgid "Alternate Interleave" msgstr "Vekslende mellomlegg" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternativ overføring" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "En tilleggskomponent trengs for import av denne PDF-filen. Vil du laste den ned nå?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Det oppsto en feil ved installasjon av oppdateringen." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "" #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "En feil oppsto under autorisasjonsforsøket." msgid "An error occurred when trying to auto save." msgstr "Feil ved autolagring." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Det oppsto en feil ved installasjon av oppdateringen." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "En feil oppsto under fillagring." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "En ukjent feil oppstod under serieskanningen." +msgid "An unknown error occurred during the batch scan." +msgstr "" #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -190,7 +186,15 @@ msgstr "En oppdatering for OCR er tilgjengelig." msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Tilfør lyshet/kontrast etter skanning" @@ -222,19 +226,27 @@ msgstr "Er du sikker på at du vil slette {0} element(er)?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Er du sikker på at du vil slette {0} profiler?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Er du sikker på at du vil angre endringer på {0} bilde(r)?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Vedleggsnavn:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Forfatter:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autoriser" @@ -242,29 +254,27 @@ msgstr "Autoriser" msgid "Auto" msgstr "" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Innstillinger for Automatisk Lagring" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Auto-økningstall (1 siffer)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Auto-økningstall (2 siffer)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Auto-økningstall (3 siffer)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Auto-økningstall (4 siffer)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Kjør OCR automatisk etter skanning" @@ -277,8 +287,8 @@ msgstr "" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Serieskanning" @@ -298,7 +308,6 @@ msgstr "Serieskanning avbrutt på grunn av feil." msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bitdybde:" @@ -309,10 +318,10 @@ msgstr "Bitmap Filer (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Svart-hvitt" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Blanke sider" @@ -320,7 +329,6 @@ msgstr "Blanke sider" msgid "Brightness / Contrast" msgstr "" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Lysstyrke:" @@ -329,24 +337,11 @@ msgstr "Lysstyrke:" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Avbryt" @@ -363,7 +358,7 @@ msgstr "Avbryter...." msgid "Center" msgstr "Senter" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "" @@ -375,7 +370,7 @@ msgstr "Se etter oppdateringer" msgid "Checking..." msgstr "Sjekker..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Velg eposttilbyder" @@ -383,7 +378,6 @@ msgstr "Velg eposttilbyder" msgid "Choose Profile" msgstr "Velg profil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Velg enhet" @@ -397,7 +391,7 @@ msgstr "Tøm" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Tøm bilder etter lagring" @@ -405,16 +399,30 @@ msgstr "Tøm bilder etter lagring" msgid "Close" msgstr "Lukk" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Kompatibilitet" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Komprimering:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrast:" @@ -435,7 +443,7 @@ msgstr "Kopierer..." msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Dekningsterskel" @@ -443,7 +451,7 @@ msgstr "Dekningsterskel" msgid "Crop" msgstr "Beskjær" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Beskjær til sidestørrelse" @@ -451,10 +459,14 @@ msgstr "Beskjær til sidestørrelse" msgid "Custom ({0}x{1} {2})" msgstr "Egendefinert ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Egendefinert Sidestørrelse" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Egendefinert rotasjon" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Egendefinert..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Dag (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Standard" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Standard filsti:" @@ -487,7 +504,6 @@ msgstr "Standard filsti:" msgid "Deinterleave" msgstr "" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "" msgid "Deskew Progress" msgstr "" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" @@ -509,16 +525,14 @@ msgstr "" msgid "Deskewing..." msgstr "" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Enhet:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Størrelser" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Visningsnavn:" @@ -532,12 +546,10 @@ msgstr "" msgid "Donate" msgstr "" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Ferdig" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Nedlasting" @@ -550,19 +562,47 @@ msgstr "Nedlastingsfeil" msgid "Download Needed" msgstr "" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Framgang for nedlasting" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Tosidig" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Rediger" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +616,32 @@ msgstr "Send PDF i epost" msgid "Email PDF Progress" msgstr "Epost PDF Fremdrift" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "E-postinnstillinger" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Aktiver Autolagring" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Krypter PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Kryptering" @@ -602,12 +649,15 @@ msgstr "Kryptering" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Feil" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "Anslått størrelse for nedlasting: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Utelat blanke sider" @@ -629,24 +679,31 @@ msgstr "" msgid "Feeder" msgstr "Mater" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Filnavn" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Filsti:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Speilvend" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Speilvend dupleks-sider" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" @@ -654,7 +711,6 @@ msgstr "" msgid "GIF File (*.gif)" msgstr "" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Installer flere språk" @@ -671,12 +727,11 @@ msgstr "" msgid "Grayscale" msgstr "Gråtoner" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Horisontal justering:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Time (0-23)" @@ -684,6 +739,10 @@ msgstr "Time (0-23)" msgid "Hue / Saturation" msgstr "" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Ikoner fra:" @@ -696,12 +755,12 @@ msgstr "Bilde" msgid "Image Files" msgstr "Bildefiler" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Bildekvalitet" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Innstillinger for bilde" @@ -745,6 +804,10 @@ msgstr "Installasjon ferdig. Ønsker du å restarte NAPS2 nå?" msgid "Installation failed." msgstr "Installasjon mislyktes." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "" @@ -757,11 +820,20 @@ msgstr "" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "Språk" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "" @@ -781,33 +857,48 @@ msgstr "" msgid "Legacy (native UI only)" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Gjør PDF søkbar gjennom OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Høyeste kvalitet (store filer)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "" @@ -819,11 +910,19 @@ msgstr "" msgid "Move Up" msgstr "" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "" @@ -849,6 +948,10 @@ msgstr "" msgid "Name missing." msgstr "Navn mangler." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "" @@ -861,7 +964,7 @@ msgstr "" msgid "Next" msgstr "" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "" @@ -870,12 +973,15 @@ msgstr "" msgid "No device selected." msgstr "Ingen enhet er valgt." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "" -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "" @@ -897,11 +1003,11 @@ msgstr "Ingen" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Ikke nå" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "" @@ -909,7 +1015,6 @@ msgstr "" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR nedlastning" @@ -918,37 +1023,23 @@ msgstr "OCR nedlastning" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCR oppsett" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR språk:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "" @@ -970,7 +1059,11 @@ msgstr "" msgid "One or more files could not be downloaded." msgstr "En eller flere filer kunne ikke lastes ned." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "" @@ -978,11 +1071,15 @@ msgstr "" msgid "Operation in Progress" msgstr "" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "" @@ -990,7 +1087,7 @@ msgstr "" msgid "Overwrite File" msgstr "OVerskrive fil" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "" @@ -998,8 +1095,8 @@ msgstr "" msgid "PDF Document (*.pdf)" msgstr "PDF-dokument (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "PNG fil (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Sidestørrelse:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Papirkilde:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "" @@ -1045,21 +1140,24 @@ msgstr "" msgid "Paste" msgstr "" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "" @@ -1067,7 +1165,7 @@ msgstr "" msgid "Preview" msgstr "Forhåndsvisning" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Forhåndsvisning:" @@ -1080,12 +1178,11 @@ msgstr "" msgid "Print" msgstr "" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Profilinnstillinger" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profil:" @@ -1094,23 +1191,27 @@ msgstr "Profil:" msgid "Profiles" msgstr "Profiler" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "" -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Gjenopprett" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Gjenopprett slettede bilder" @@ -1122,9 +1223,15 @@ msgstr "" msgid "Recovery Progress" msgstr "Gjenopprettingsfremdrift" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "" @@ -1140,15 +1247,11 @@ msgstr "" msgid "Reset Image" msgstr "" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Oppløsning:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "" @@ -1156,6 +1259,14 @@ msgstr "" msgid "Reverse" msgstr "Omvendt" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "" @@ -1176,7 +1287,6 @@ msgstr "Roter til venstre" msgid "Rotate Right" msgstr "Roter til høyre" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1185,7 +1295,6 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1303,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "Lagre PDF" msgid "Save PDF Progress" msgstr "Lagre PDF Fremdrift" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Lagre til en enkelt fil" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Lagre til flere filer" @@ -1244,29 +1363,45 @@ msgstr "Lagrer Batch resultat..." msgid "Saving {0}..." msgstr "Lagrer {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Skaler med Vindu" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Skalering:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skann" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Skanneoppsett" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Skannet bilde" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Skanner side {0}" @@ -1280,11 +1415,14 @@ msgstr "Skanner side {0} (skanning {1})..." msgid "Scanning page {0}..." msgstr "Skanner side {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekund (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Velg" @@ -1293,7 +1431,10 @@ msgstr "Velg" msgid "Select All" msgstr "Velg alle" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Velg kilde" @@ -1302,7 +1443,6 @@ msgstr "Velg kilde" msgid "Select a profile before clicking Scan." msgstr "Velg en profil før du klikker Skann." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Velg ett eller flere språk:" @@ -1311,8 +1451,7 @@ msgstr "Velg ett eller flere språk:" msgid "Selected ({0})" msgstr "Valgt ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,37 +1459,84 @@ msgstr "" msgid "Set Default" msgstr "Angi standard" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Vis" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Enkeltskanning" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "" @@ -1358,12 +1544,11 @@ msgstr "" msgid "TIFF File (*.tiff, *.tif)" msgstr "" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "" @@ -1372,6 +1557,10 @@ msgstr "" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,8 +1583,8 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" msgstr "" #: MiscResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "" msgid "The selected scanner is offline." msgstr "" -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "" @@ -1487,21 +1712,18 @@ msgstr "" msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "" @@ -1515,16 +1737,15 @@ msgstr "" msgid "View" msgstr "" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA-driver" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "" -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1532,19 +1753,19 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "" -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "" @@ -1561,21 +1782,18 @@ msgstr "" msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} ppt" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "" diff --git a/NAPS2.Lib/Lang/po/nl.po b/NAPS2.Lib/Lang/po/nl.po index 1f7b8e0cfc..d8fda43028 100644 --- a/NAPS2.Lib/Lang/po/nl.po +++ b/NAPS2.Lib/Lang/po/nl.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Dutch\n" "Language: nl\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Standaardactie van de knop \"Opslaan\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Standaardactie van de knop \"Scannen\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Menu \"Scan\" wijzigt het standaardprofiel" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 apparaat gevonden." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-bits kleuren" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -74,14 +58,17 @@ msgstr "Over" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." -msgstr "Data wordt opgehaald..." +msgstr "Gegevens ophalen..." + +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Actie" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Geavanceerd" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Geavanceerde profielinstellingen" @@ -93,35 +80,35 @@ msgstr "Alle ({0})" msgid "All Files" msgstr "Alle bestanden" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Annotaties toestaan" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Inhoud kopiëren toestaan" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Inhoud kopiëren voor toegankelijkheid toestaan" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Documentassemblage toestaan" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Document bewerken toestaan" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Formulier invullen toestaan" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Afdrukken met volledige kwaliteit toestaan" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Afdrukken toestaan" @@ -133,17 +120,22 @@ msgstr "Alternatief terug tussenvoegen" msgid "Alternate Interleave" msgstr "Alternatief afwisselend tussenvoegen" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternatieve overdracht" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Altijd vragen" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Altijd vragen" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Er is een additioneel component nodig om deze PDF te importeren. Nu downloaden?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Er was een fout bij het installeren van de update." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Er is een fout opgetreden bij het uitvoeren van OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Er was een fout bij het autoriseren." msgid "An error occurred when trying to auto save." msgstr "Er was een fout bij het automatisch opslaan." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Er was een fout bij het installeren van de update." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Er was een fout bij het opslaan van het bestand." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Er is een bewerking bezig. Weet u zeker dat u wilt stoppen en de bewerking annuleren?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Er is een onbekende fout opgetreden tijdens het batch scannen." +msgid "An unknown error occurred during the batch scan." +msgstr "Er is een onbekende fout opgetreden tijdens de batch scan." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -184,13 +180,21 @@ msgstr "Een update is beschikbaar." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "Er is een OCR update beschikbaar." +msgstr "Er is een update voor OCR beschikbaar." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple driver" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Toepassing" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Helderheid/contrast toepassen na scannen" @@ -222,63 +226,69 @@ msgstr "Weet u zeker dat u {0} item(s) wilt verwijderen?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Weet u zeker dat u {0} profielen wilt verwijderen?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Weet u zeker dat u wilt stoppen met het delen van {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Weet u zeker dat u wijzigingen ongedaan wilt maken aan {0} afbeelding(en)?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Toewijzen" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Naam bijlage:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Auteur:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "Autoriseer" +msgstr "Autoriseren" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "Automatisch" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Instellingen voor automatisch opslaan" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Automatisch ophogend nummer (1 cijfer)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Automatisch toenemend nummer (1 cijfer)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Automatisch ophogend nummer (2 cijfers)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Automatisch ophogend nummer (3 cijfers)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Automatisch ophogend nummer (4 cijfers)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Start OCR automatisch na het scannen" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Automatisch scannen" @@ -296,9 +306,8 @@ msgstr "Batch scan gestopt door fout." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Beste" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Kleurdiepte:" @@ -309,10 +318,10 @@ msgstr "Bitmapbestanden (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Zwart-wit" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Lege pagina's" @@ -320,33 +329,19 @@ msgstr "Lege pagina's" msgid "Brightness / Contrast" msgstr "Helderheid / contrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Helderheid:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Kun je je scanner niet vinden? Lees dan over de beperkingen van NAPS2 Flatpak." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Annuleren" @@ -357,13 +352,13 @@ msgstr "Annuleer batch" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." -msgstr "Bezig met annuleren...." +msgstr "Annuleren...." #: SettingsResources.resx$HorizontalAlign_Center$Message msgid "Center" msgstr "Centreren" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Wijzig" @@ -373,9 +368,9 @@ msgstr "Controleer op updates" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "Controleert..." +msgstr "Controleren..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Kies e-mail provider" @@ -383,7 +378,6 @@ msgstr "Kies e-mail provider" msgid "Choose Profile" msgstr "Profiel kiezen" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Apparaat kiezen" @@ -395,9 +389,9 @@ msgstr "Wissen" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Alles wissen" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Afbeeldingen verwijderen na opslaan" @@ -405,19 +399,33 @@ msgstr "Afbeeldingen verwijderen na opslaan" msgid "Close" msgstr "Sluiten" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Samenvoegen" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Communicatie met het scanapparaat is onderbroken." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Compatibiliteit" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Compressie:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Verbinden" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Verbindingsfout." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" -msgstr "" +msgstr "Contrast:" #: UiStrings.resx$Copy$Message msgid "Copy" @@ -429,13 +437,13 @@ msgstr "Voortgang kopiëren" #: MiscResources.resx$Copying$Message msgid "Copying..." -msgstr "Bezig met kopiëren..." +msgstr "Kopiëren..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} NAPS2 bijdragers" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Dekkingsdrempel" @@ -443,7 +451,7 @@ msgstr "Dekkingsdrempel" msgid "Crop" msgstr "Bijsnijden" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Bijsnijden naar paginagrootte" @@ -451,35 +459,44 @@ msgstr "Bijsnijden naar paginagrootte" msgid "Custom ({0}x{1} {2})" msgstr "Aangepast ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Aangepaste paginagrootte" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Aangepaste resolutie" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" -msgstr "Aangepast roteren" +msgstr "Aangepast draaien" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" msgstr "Aangepaste SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Aangepast..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Donker" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Dag (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Standaard" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Standaard bestandspad:" @@ -487,7 +504,6 @@ msgstr "Standaard bestandspad:" msgid "Deinterleave" msgstr "Terug tussenvoegen" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,43 +517,39 @@ msgstr "Rechtzetten" msgid "Deskew Progress" msgstr "Voortgang rechtzetten" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Gescande pagina's rechtzetten" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." -msgstr "Aan het rechtzetten..." +msgstr "Rechtzetten..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Apparaat:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Afmetingen" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Schermnaam:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Documentcorrectie" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "Doneer" +msgstr "Doneren" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Gereed" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Downloaden" @@ -550,51 +562,86 @@ msgstr "Fout bij downloaden" msgid "Download Needed" msgstr "Download vereist" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" -msgstr "Downloadvoortgang" +msgstr "Voortgang downloaden" + +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "Dpi" #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Dubbelzijdig" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL Driver" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL Netwerk Driver" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB Driver" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Bewerken" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Bewerken met {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Bewerken met..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Alles e-mailen" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Alles als PDF e-mailen" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "PDF mailen" +msgstr "PDF e-mailen" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" -msgstr "E-mail PDF voortgang" +msgstr "Voortgang PDF e-mailen" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Selectie e-mailen" #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Selectie als PDF e-mailen" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "E-mailinstellingen" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Automatisch opslaan inschakelen" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Debug logging toestaan" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "PDF versleutelen" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Versleuteling" @@ -602,12 +649,15 @@ msgstr "Versleuteling" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Enhanced Windows Metafile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Fout" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Fout bij starten van toepassing {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,46 +665,52 @@ msgstr "Geschatte omvang: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Lege pagina's uitsluiten" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Snel" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Automatische documentinvoer" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Bestandsnaam" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Bestandspad:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Witbalans verbeteren en ruis verminderen" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Spiegelen" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Achterzijde van dubbelzijdige pagina's omdraaien" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Dubbelzijdige pagina's omdraaien" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Voor hoge-kwaliteit JPEG (80+), verhoog ook de afbeeldingskwaliteit in uw profiel voor het beste resultaat." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" -msgstr "GIF bestand (*.gif)" +msgstr "GIF-bestand (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Meer talen installeren" @@ -665,18 +721,17 @@ msgstr "Glasplaat" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Grijswaarden" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Horizontaal uitlijnen:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Uur (0-23)" @@ -684,6 +739,10 @@ msgstr "Uur (0-23)" msgid "Hue / Saturation" msgstr "Tint / verzadiging" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Host" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Pictogrammen van:" @@ -696,14 +755,14 @@ msgstr "Afbeelding" msgid "Image Files" msgstr "Afbeeldingbestanden" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Kwaliteit afbeelding" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" -msgstr "Instellingen afbeeldingen" +msgstr "Afbeeldingsinstellingen" #: MiscResources.resx$ImageSaved$Message msgid "Image saved." @@ -723,7 +782,7 @@ msgstr "Importeren {0}..." #: MiscResources.resx$Importing$Message msgid "Importing..." -msgstr "Bezig met importeren..." +msgstr "Importeren..." #: MiscResources.resx$Install$Message msgid "Install {0}" @@ -745,34 +804,51 @@ msgstr "Installatie voltooid. Wilt u NAPS2 nu herstarten?" msgid "Installation failed." msgstr "Installatie mislukt." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Uiterlijk" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Afwisselend tussenvoegen" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" -msgstr "JPEG bestand (*.jpg, *.jpeg)" +msgstr "JPEG-bestand (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000-bestand(*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "JPEG kwaliteit" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Afbeeldingen behouden tussen sessies" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Sneltoetsen" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Sleutelwoorden:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Taal" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Schrijf een recensie" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Links" @@ -781,33 +857,48 @@ msgstr "Links" msgid "Legacy (native UI only)" msgstr "Legacy (alleen eigen interface)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Licht" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Vind je NAPS2 leuk?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Afbeeldingen laden in NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" -msgstr "PDFs doorzoekbaar maken middels OCR" +msgstr "Maak PDF's doorzoekbaar met OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Handmatig IP-adres" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Maximale kwaliteit (grote bestanden)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Geheugen overdracht" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" -msgstr "" +msgstr "Metadata" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minuut (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Maand (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Meer informatie" @@ -819,11 +910,19 @@ msgstr "Omlaag" msgid "Move Up" msgstr "Omhoog" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Meerdere talen" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Meerdere talen..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Meerdere scans (vaste pauze tussen scans)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Meerdere scans (vraag tussen 2 scans)" @@ -831,17 +930,17 @@ msgstr "Meerdere scans (vraag tussen 2 scans)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 is helemaal gratis. Overweeg een donatie." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Naam (optioneel)" @@ -849,6 +948,10 @@ msgstr "Naam (optioneel)" msgid "Name missing." msgstr "Naam ontbreekt." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Oorspronkelijke overdracht" + #: UiStrings.resx$New$Message msgid "New" msgstr "Nieuw" @@ -861,7 +964,7 @@ msgstr "Nieuw profiel" msgid "Next" msgstr "Volgende" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Volgende scan" @@ -870,12 +973,15 @@ msgstr "Volgende scan" msgid "No device selected." msgstr "Geen apparaat geselecteerd." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Geen apparaten gevonden." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Geen pagina's in de invoerlade." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Geen provider gekozen." @@ -895,60 +1001,45 @@ msgstr "Geen" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Niet nu" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Aantal scans:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR downloaden" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "OCR voortgang" +msgstr "Voortgang van OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" -msgstr "OCR instellingen" +msgstr "OCR-instellingen" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" -msgstr "OCR taal:" +msgstr "OCR-taal:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "OCR modus:" - -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message +msgstr "OCR-modus:" + #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Breedte aanpassen op basis van uitlijning (WIA)" @@ -956,13 +1047,11 @@ msgstr "Breedte aanpassen op basis van uitlijning (WIA)" msgid "Old DSM" msgstr "Oude DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Eén bestand per pagina" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Eén bestand per scan" @@ -970,19 +1059,27 @@ msgstr "Eén bestand per scan" msgid "One or more files could not be downloaded." msgstr "Download mislukt voor 1 of meerdere bestanden." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Slechts één NAPS2-instantie toestaan" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Map openen" #: MiscResources.resx$ActiveOperations$Message msgid "Operation in Progress" -msgstr "Bewerking is bezig" +msgstr "Bewerking wordt uitgevoerd" + +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (nieuw)" #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Web Access" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Uitvoer" @@ -990,18 +1087,18 @@ msgstr "Uitvoer" msgid "Overwrite File" msgstr "Bestand overschrijven" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Wachtwoord eigenaar:" #: MiscResources.resx$FileTypePdf$Message msgid "PDF Document (*.pdf)" -msgstr "PDF bestand (*.pdf)" +msgstr "PDF-document (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" -msgstr "PDF instellingen" +msgstr "PDF-instellingen" #: MiscResources.resx$PdfSaved$Message msgid "PDF saved." @@ -1009,35 +1106,33 @@ msgstr "PDF opgeslagen." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" -msgstr "PNG bestand (*.png)" +msgstr "PNG-bestand (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Paginaformaat:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Papierbron:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Wachtwoord" @@ -1045,21 +1140,24 @@ msgstr "Wachtwoord" msgid "Paste" msgstr "Plakken" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" -msgstr "" +msgstr "Placeholders" + +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Poort" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Nabewerking" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Start OCR automatisch na het scannen" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Klik 'Beginnen' om te starten." @@ -1067,7 +1165,7 @@ msgstr "Klik 'Beginnen' om te starten." msgid "Preview" msgstr "Voorbeeld" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Voorbeeld:" @@ -1080,12 +1178,11 @@ msgstr "Vorige" msgid "Print" msgstr "Afdrukken" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Profielinstellingen" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profiel:" @@ -1094,37 +1191,47 @@ msgstr "Profiel:" msgid "Profiles" msgstr "Profielen" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Vragen indien geselecteerd" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Vraag om bestandspad" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "Provider" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Klaar voor scannen {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Herstellen" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Herstel gescande afbeeldingen" #: MiscResources.resx$Recovering$Message msgid "Recovering..." -msgstr "Bezig met herstellen..." +msgstr "Herstellen..." #: MiscResources.resx$RecoveryProgress$Message msgid "Recovery Progress" -msgstr "Herstelvoortgang" +msgstr "Voortgang herstellen" + +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Opnieuw" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Opnieuw {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Onthoud deze instellingen" @@ -1134,21 +1241,17 @@ msgstr "Hersorteren" #: UiStrings.resx$Reset$Message msgid "Reset" -msgstr "" +msgstr "Reset" #: MiscResources.resx$ResetImage$Message msgid "Reset Image" msgstr "Afbeeldingen resetten" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Resolutie:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Standaardinstellingen herstellen" @@ -1156,9 +1259,17 @@ msgstr "Standaardinstellingen herstellen" msgid "Reverse" msgstr "Omdraaien" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Alles ongedaan maken" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Selectie omkeren" + #: UiStrings.resx$Revert$Message msgid "Revert" -msgstr "Herstel" +msgstr "Herstellen" #: SettingsResources.resx$HorizontalAlign_Right$Message msgid "Right" @@ -1176,31 +1287,34 @@ msgstr "Linksom draaien" msgid "Rotate Right" msgstr "Rechtsom draaien" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "In de achtergrond draaien" +msgstr "Op de achtergrond uitvoeren" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "OCR draait..." +msgstr "OCR uitvoeren..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "SANE driver" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Opslaan" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Alles opslaan" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Alles opslaan als afbeeldingen" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Alles opslaan als PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1218,21 +1332,26 @@ msgstr "PDF opslaan" #: MiscResources.resx$SavePdfProgress$Message msgid "Save PDF Progress" -msgstr "PDF voortgang bewaren" +msgstr "Voortgang PDF opslaan" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Selectie bewaren" #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Selectie opslaan als afbeeldingen" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Selectie opslaan als PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Opslaan naar één bestand" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Opslaan naar meerdere bestanden" @@ -1242,58 +1361,80 @@ msgstr "Batchresultaten opslaan..." #: MiscResources.resx$SavingFormat$Message msgid "Saving {0}..." -msgstr "Bezig met opslaan {0}..." +msgstr "Opslaan {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Schalen" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Schalen:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Scannen" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Scaninstellingen" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Scannen met standaardprofiel" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Scannen met nieuw profiel" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Scannen met Profiel {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Gescande afbeelding" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Scanner delen" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" -msgstr "Scannen pagina {0}" +msgstr "Pagina {0} aan het scannen" #: MiscResources.resx$BatchStatusScanPage$Message msgid "Scanning page {0} (scan {1})..." -msgstr "Scant pagina {0} (scan {1})..." +msgstr "Pagina {0} (scan {1}) aan het scannen..." #: MiscResources.resx$BatchStatusPage$Message #: MiscResources.resx$ScanProgressPage$Message msgid "Scanning page {0}..." -msgstr "Scannen pagina {0}..." +msgstr "Pagina {0} aan het scannen..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Zoeken naar apparaten..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Seconde (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" -msgstr "Selecteer" +msgstr "Selecteren" #: UiStrings.resx$SelectAll$Message msgid "Select All" msgstr "Alles selecteren" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Apparaat selecteren" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Bron selecteren" @@ -1302,17 +1443,15 @@ msgstr "Bron selecteren" msgid "Select a profile before clicking Scan." msgstr "Kies een profiel voor het scannen." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" -msgstr "Kies een of meer talen:" +msgstr "Kies één of meer talen:" #: MiscResources.resx$SelectedCount$Message msgid "Selected ({0})" msgstr "Geselecteerd ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Bestanden scheiden met Patch-T" @@ -1320,57 +1459,107 @@ msgstr "Bestanden scheiden met Patch-T" msgid "Set Default" msgstr "Als standaard instellen" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Instellingen" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Delen" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Delen zelfs als NAPS2 gesloten is" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Instellingen gedeelde scanner" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Gedeelde scanners kunnen worden gebruikt vanaf andere computers op het lokale netwerk door \"ESCL Driver\" te selecteren in de NAPS2-profielinstellingen van de andere computer." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Verscherpen" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Sneltoets" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Tonen" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Werkbalk \"Profielen\" weergeven" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "TWAIN-voortgang weergegeven" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Paginanummers weergeven" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Zijbalk" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Enkele-pagina bestanden" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Enkele scan" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Opslaanvenster overslaan" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Splitsen" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Beginnen" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Scanner delen stoppen" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Uitrekken naar paginagrootte" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Onderwerp:" #: MiscResources.resx$FileTypeTiff$Message msgid "TIFF File (*.tiff, *.tif)" -msgstr "TIFF bestand (*.tiff, *.tif)" +msgstr "TIFF-bestand (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN driver" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Technische details" #: MiscResources.resx$TesseractNotAvailable$Message #: SdkResources.resx$TesseractNotAvailable$Message msgid "The OCR engine is not available. Make sure to install the required package:" -msgstr "De OCR engine is niet beschikbaar. Installeer het vereiste pakket:" +msgstr "De OCR-engine is niet beschikbaar. Installeer het vereiste pakket:" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "De OCR-bewerking is verlopen." #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message @@ -1394,9 +1583,9 @@ msgstr "Overschrijven bestand mislukt want het is in gebruik." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Bestand {0} bestaat al. Wilt u het bestand overschrijven?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Het volgende bestand is versleuteld en vereist een wachtwoord voor openen: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Het volgende bestand is versleuteld en heeft een wachtwoord nodig om te openen:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,29 +1632,65 @@ msgstr "De geselecteerde scanner is bezig." msgid "The selected scanner is offline." msgstr "De geselecteerde scanner is offline." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Het werkproces is gecrasht." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Het werkproces is gecrasht. Controleer het Windows Gebeurtenis-logboek." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Thema:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Tiff opties" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Tijd tussen scans (seconden):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Titel:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Hulpmiddelen" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Twain-implementatie:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 in)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" -msgstr "" +msgstr "US Letter (8.5x11 in)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Toewijzing verwijderen" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Ongedaan maken" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "{0} ongedaan maken" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Onbekende scanner" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" @@ -1477,7 +1702,7 @@ msgstr "Voortgang bijwerken" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Controleren van updates is uitgeschakeld." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Bijwerken..." msgid "Uploading email..." msgstr "E-mail uploaden..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Gebruik native interface" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Gebruik voorgedefinieerd" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Wachtwoord gebruiker:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Voor OCR moet u elke taal downloaden die u wilt laten herkennen." @@ -1515,16 +1737,15 @@ msgstr "Versie {0}" msgid "View" msgstr "Beeld" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA driver" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Wachten op TWAIN scan..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Wacht op autorisatie..." @@ -1532,19 +1753,19 @@ msgstr "Wacht op autorisatie..." msgid "Waiting for scan {0}..." msgstr "Wacht op scan {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Witdrempel" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Wia versie:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Jaar" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Jaar (00-99)" @@ -1561,36 +1782,33 @@ msgstr "U heeft geen rechten om hier bestanden op te slaan." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "U heeft niet-opgeslagen wijzigingen. Weet u zeker dat u wilt afsluiten en wijzigingen annuleren?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Zoomen" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Zoom 100%" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Inzoomen" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Uitzoomen" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "in" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,28 +1816,33 @@ msgstr "van {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} bestanden" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} apparaten gevonden." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} afbeelding(en) gescand op {1} om {2} zijn mogelijk niet opgeslagen en zijn te herstellen. Wilt u herstellen?" diff --git a/NAPS2.Lib/Lang/po/nn.po b/NAPS2.Lib/Lang/po/nn.po index f3d3ddf7c5..47061b6208 100644 --- a/NAPS2.Lib/Lang/po/nn.po +++ b/NAPS2.Lib/Lang/po/nn.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Norwegian Nynorsk\n" "Language: nn_NO\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 eining funnen." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-bit Fargar" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210 x 297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148 x 210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -76,12 +60,15 @@ msgstr "Om" msgid "Acquiring data..." msgstr "Henter data..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Handling" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Avansert" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Avanserte profilsettingar" @@ -93,35 +80,35 @@ msgstr "Alle ({0})" msgid "All Files" msgstr "Alle filer" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Tillat merknader" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Tillat innhaldskopiering" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Tillat innhaldskopiering for tilgjengelighet" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Tillat dokumentsamansetting" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Tillat dokumentmodifikasjon" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Tillat formfylling" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Tillat full kvalitet utskrift" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Tillat utskrift" @@ -133,17 +120,22 @@ msgstr "Vekslande defletting" msgid "Alternate Interleave" msgstr "Vekslande fletting" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternativ overføring" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Spør alltid" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Ein tilleggskomponent trengs for import av denne PDF-fila. Vil du lasta den ned nå?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Det vart ein feil ved installasjon av oppdatering." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "" #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Feil ved autorisering." msgid "An error occurred when trying to auto save." msgstr "Feil ved autolagring." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Det vart ein feil ved installasjon av oppdatering." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Feil ved lagring av fila." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Ein operasjon er i gang. Er du sikker på at du vil avbryta operasjonen og avslutta?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Ukjend feil ved gruppeskanning." +msgid "An unknown error occurred during the batch scan." +msgstr "" #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -188,9 +184,17 @@ msgstr "Oppdatering til OCR er tilgjengeleg." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" +msgstr "Apple driver" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Juster lysstyrke/kontrast etter skanning" @@ -222,63 +226,69 @@ msgstr "Er du sikker på at du vil sletta {0} element?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Er du sikker på at du vil sletta {0} profilar?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Er du sikker på at du vil gjera om endringane til {0} bilde?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Tilknytt" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Vedleggsnamn:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Forfattar:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autoriser" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "Auto" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Settingar for autolagring" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Auto-inkrementer nummer (1 siffer)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Auto-inkrementer nummer (2 siffer)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Auto-inkrementer nummer (3 siffer)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Auto-inkrementer nummer (4 siffer)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Kjør OCR automatisk etter skanning" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250 x 353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176 x 250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Multiskann" @@ -296,9 +306,8 @@ msgstr "Multiskann stoppa på grunn av feil." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Beste" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bit-djupne:" @@ -309,10 +318,10 @@ msgstr "Bitmap-filer (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Svart & kvitt" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Tome sider" @@ -320,33 +329,19 @@ msgstr "Tome sider" msgid "Brightness / Contrast" msgstr "Lysstyrke / kontrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Lysstyrke:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Avbryt" @@ -363,7 +358,7 @@ msgstr "Avbryter...." msgid "Center" msgstr "Senter" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Endre" @@ -375,7 +370,7 @@ msgstr "Sjekk oppdatering" msgid "Checking..." msgstr "Sjekker..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Velg epost-leverandør" @@ -383,7 +378,6 @@ msgstr "Velg epost-leverandør" msgid "Choose Profile" msgstr "Vel profil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Vel skannar" @@ -395,9 +389,9 @@ msgstr "Fjern" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Fjern alle" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Fjern foto etter lagring" @@ -405,16 +399,30 @@ msgstr "Fjern foto etter lagring" msgid "Close" msgstr "Lukk" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Slå saman" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Kommunikasjon med skannaren vart avbroten." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Kompatibilitet" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Komprimering:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Kopla til" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Tilkoplingsfeil." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrast:" @@ -433,9 +441,9 @@ msgstr "Kopierer..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Opphavsrett {0} NAPS2 bidragsytarar" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Dekningsterskel" @@ -443,7 +451,7 @@ msgstr "Dekningsterskel" msgid "Crop" msgstr "Beskjær" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Beskjær til sidestorleik" @@ -451,10 +459,14 @@ msgstr "Beskjær til sidestorleik" msgid "Custom ({0}x{1} {2})" msgstr "Eigendefinert ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Eigendefinert sidestorleik" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Eigendefinert rotasjon" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "Eigen SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Eigendefinert..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Mørkt" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Dag (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Standard" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Standard fil-sti:" @@ -487,7 +504,6 @@ msgstr "Standard fil-sti:" msgid "Deinterleave" msgstr "De-fletting" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Rett opp" msgid "Deskew Progress" msgstr "Oppretting framdrift" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Rett opp skanna sider" @@ -509,35 +525,31 @@ msgstr "Rett opp skanna sider" msgid "Deskewing..." msgstr "Oppretting..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Skannar:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Dimensjonar" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Displaynavn:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Dokumentretting" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Doner" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Utført" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Last ned" @@ -550,22 +562,50 @@ msgstr "Nedlastingsfeil" msgid "Download Needed" msgstr "Nedlasting nødvendig" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Nedlasting framdrift" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" +msgstr "Duplex" + +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" msgstr "" #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Rediger" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Endre med {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Endre med..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Send alle som PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,25 +616,32 @@ msgstr "Send PDF som epost" msgid "Email PDF Progress" msgstr "Epost PDF framdrift" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Send valde som PDF i e-post" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Epost-settingar" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Slå på autolagring" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Krypter PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Kryptering" @@ -602,12 +649,15 @@ msgstr "Kryptering" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Forbetra Windows MetaFil (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Feil" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Feil under oppstart av programmet {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,38 +665,45 @@ msgstr "Berekna nedlastingsvolum: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Ikkje bruk tome sider" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Kjapp" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Matar" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Filnamn" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Fil-sti:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Fiks kvitbalanse og fjern støy" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Vend" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Vend baksida av dupleks-sider" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Vend tosidige sider" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "For høg JPEG kvalitet (80+), auk også bildekvalitet i profilen for å få best resultat." @@ -654,7 +711,6 @@ msgstr "For høg JPEG kvalitet (80+), auk også bildekvalitet i profilen for å msgid "GIF File (*.gif)" msgstr "GIF-fil (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Hent fleire språk" @@ -665,18 +721,17 @@ msgstr "Glas" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Gråskala" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Horisontal justering:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Time (0-23)" @@ -684,6 +739,10 @@ msgstr "Time (0-23)" msgid "Hue / Saturation" msgstr "Farge / metning" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Vert" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Ikonar frå:" @@ -696,12 +755,12 @@ msgstr "Bilde" msgid "Image Files" msgstr "Bildefiler" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Bildekvalitet" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Bildesettingar" @@ -711,7 +770,7 @@ msgstr "Bilde lagra." #: UiStrings.resx$Import$Message msgid "Import" -msgstr "" +msgstr "Importer" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" @@ -745,6 +804,10 @@ msgstr "Innstallasjon fullført. Vil du restarta NAPS2 nå?" msgid "Installation failed." msgstr "Installasjon feila." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Utfør fletting" @@ -755,24 +818,37 @@ msgstr "JPEG-fil (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000 fil (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Jpeg-kvalitet" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Tastatursnarvegar" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Stikkord:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Språk" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Legg igjen en omtale" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Venstre" @@ -781,33 +857,48 @@ msgstr "Venstre" msgid "Legacy (native UI only)" msgstr "Gammal (bare maskinen sitt oppsettvindu)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Lyst" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Liker du: NAPS2?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Last inn bilde i NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Gjer PDF-ar søkbare med OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Manuell IP" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Maksimum kvalitet (blir stor fil)" -#: FPdfSettings.resx$groupMetadata.Text$Message -msgid "Metadata" +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" msgstr "" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Metadata$Message +msgid "Metadata" +msgstr "Metadata" + +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minutt (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Månad (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Meir informasjon" @@ -819,11 +910,19 @@ msgstr "Flytt ned" msgid "Move Up" msgstr "Flytt opp" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Fleire språk" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Fleire språk..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Fleire skann (fast opphald mellom skann)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Fleire skann (spør mellom skann)" @@ -831,17 +930,17 @@ msgstr "Fleire skann (spør mellom skann)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 – {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 er heilt gratis. Vurder å gje ein donasjon." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Namn (valfri)" @@ -849,6 +948,10 @@ msgstr "Namn (valfri)" msgid "Name missing." msgstr "Namn manglar." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "Ny" @@ -861,7 +964,7 @@ msgstr "Ny profil" msgid "Next" msgstr "Neste" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Neste skann" @@ -870,12 +973,15 @@ msgstr "Neste skann" msgid "No device selected." msgstr "Ingen skannar vald." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Ingen eining funnen." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Der er ingen ark i mataren." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Ingen leverandør vald." @@ -895,21 +1001,20 @@ msgstr "Ingen" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Ikkje nå" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Tal på skann:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR nedlasting" @@ -918,37 +1023,23 @@ msgstr "OCR nedlasting" msgid "OCR Progress" msgstr "OCR framgang" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCR oppsett" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR språk:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "OCR modus:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Offsett bredde basert på justering (WIA)" @@ -956,13 +1047,11 @@ msgstr "Offsett bredde basert på justering (WIA)" msgid "Old DSM" msgstr "Gammal DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Ei fil per side" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Ei fil per skann" @@ -970,7 +1059,11 @@ msgstr "Ei fil per skann" msgid "One or more files could not be downloaded." msgstr "Ei eller fleire filer vart ikkje lasta ned." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Opne folder" @@ -978,11 +1071,15 @@ msgstr "Opne folder" msgid "Operation in Progress" msgstr "Operasjon under gjennomføring" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Web Access" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Data frå skannar" @@ -990,7 +1087,7 @@ msgstr "Data frå skannar" msgid "Overwrite File" msgstr "Overskriv fil" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Eigarpassord:" @@ -998,8 +1095,8 @@ msgstr "Eigarpassord:" msgid "PDF Document (*.pdf)" msgstr "PDF-dokument (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF settingar" @@ -1009,35 +1106,33 @@ msgstr "PDF lagra." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "PNG-fil (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Papirstorleik:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Papirkjelde:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Passord" @@ -1045,21 +1140,24 @@ msgstr "Passord" msgid "Paste" msgstr "Lim inn" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Plassholdar" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Post-prosessering" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Kjør OCR automatisk etter skanning" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Trykk start når du er klar." @@ -1067,7 +1165,7 @@ msgstr "Trykk start når du er klar." msgid "Preview" msgstr "Førehandsvising" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Førehandsvising:" @@ -1080,12 +1178,11 @@ msgstr "Forrige" msgid "Print" msgstr "Skriv ut" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Profilsettingar" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profil:" @@ -1094,23 +1191,27 @@ msgstr "Profil:" msgid "Profiles" msgstr "Profilar" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Spør etter fil-sti" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Leverandør" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Klar for skann {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Hent tilbake" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Hent tilbake skanna bilde" @@ -1122,9 +1223,15 @@ msgstr "Henter tilbake..." msgid "Recovery Progress" msgstr "Tilbakehenting framgang" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Gjenta" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Gjenta {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Hugs desse settingane" @@ -1140,15 +1247,11 @@ msgstr "Resett" msgid "Reset Image" msgstr "Resett bilde" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Oppløysing:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Hent standard" @@ -1156,6 +1259,14 @@ msgstr "Hent standard" msgid "Reverse" msgstr "Reverser" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Tilbakestill" @@ -1176,7 +1287,6 @@ msgstr "Roter venstre" msgid "Rotate Right" msgstr "Roter høgre" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Kjør i bakgrunnen" @@ -1185,22 +1295,26 @@ msgstr "Kjør i bakgrunnen" msgid "Running OCR..." msgstr "Kjører OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "SANE driver" #: UiStrings.resx$Save$Message msgid "Save" +msgstr "Lagre" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" msgstr "" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Lagre alle som bilder" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Lagre alle som PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Lagre PDF" msgid "Save PDF Progress" msgstr "Lagre PDF framdrift" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Lagre valde som bilde" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Lagre valde som PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Lagre til ei enkel fil" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Lagre til fleire filer" @@ -1244,29 +1363,45 @@ msgstr "Lagrer multiskann resultat..." msgid "Saving {0}..." msgstr "Lagrer {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Skaler med vindu" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Skala:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skann" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Skann-konfigurasjon" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Skann med ny profil" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Skann med profilen {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Skanna bilde" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Skanner side {0}" @@ -1280,11 +1415,14 @@ msgstr "Skanner side {0} (skann {1})..." msgid "Scanning page {0}..." msgstr "Skanner side {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Søker etter einingar..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekund (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Vel" @@ -1293,7 +1431,10 @@ msgstr "Vel" msgid "Select All" msgstr "Vel alle" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Vel eining" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Vel kjelde" @@ -1302,7 +1443,6 @@ msgstr "Vel kjelde" msgid "Select a profile before clicking Scan." msgstr "Vel ein profil før du klikker Skann." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Vel eit eller fleire språk:" @@ -1311,8 +1451,7 @@ msgstr "Vel eit eller fleire språk:" msgid "Selected ({0})" msgstr "Vald ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Separate filer av Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Separate filer av Patch-T" msgid "Set Default" msgstr "Sett standard" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Gjer skarpare" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Snarveg" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Vis" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Vis innebygd TWAIN framgang" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Enkelside filer" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Enkelt skann" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Hopp over lagre-spørsmål" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Splitt" + +#: UiStrings.resx$Start$Message msgid "Start" -msgstr "" +msgstr "Start" + +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Stopp skannerdeling" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Strekk til sidestorleik" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Emne:" @@ -1358,12 +1544,11 @@ msgstr "Emne:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF fil (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN driver" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Tekniske detaljar" @@ -1372,6 +1557,10 @@ msgstr "Tekniske detaljar" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "OCR-motoren er ikkje tilgjengeleg. Installer den nødvendige pakken:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Fila kunne ikkje overskrivast fordi den er i bruk." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Fila {0} eksisterer. Vil du overskriva den?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Fila {0} er kryptert og treng eit passord for å kunna opnast" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,29 +1632,65 @@ msgstr "Den valde skannaren er opptatt." msgid "The selected scanner is offline." msgstr "Den valde skannaren er fråkopla." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Arbeidsprosessen krasja." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Arbeidsprosessen krasja Sjekk Windows-hendingsvisaren." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Tema:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Innstillingar for Tiff" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Tid mellom skann (sekund):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Tittel:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Twain implementering:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8,5x14 in)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" -msgstr "" +msgstr "US Letter (8,5x11 in)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Opphev tilordning" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Angre" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Angre {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Ukjent skanner" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" @@ -1477,7 +1702,7 @@ msgstr "Oppdatering framgang" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Oppdateringskontroll er deaktivert." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Oppdaterer..." msgid "Uploading email..." msgstr "Laster opp epost..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Bruk maskinen sitt vindu" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Bruk førehandsdefinert oppsett" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Brukarpassord:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Bruk av OCR krev at du lastar ned kvart språk du ynskjer å skanna." @@ -1515,16 +1737,15 @@ msgstr "Versjon {0}" msgid "View" msgstr "Vis" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA driver" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Venter på at TWAIN blir ferdig..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Venter på autorisasjon..." @@ -1532,19 +1753,19 @@ msgstr "Venter på autorisasjon..." msgid "Waiting for scan {0}..." msgstr "Venter på skann {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Kvit-terskel" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Wia versjon:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "År" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "År (00-99)" @@ -1561,36 +1782,33 @@ msgstr "Du har ikkje lov til å lagra filer til denne staden." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Du har ulagra endringar. Er du sikker på at du vil avslutta og forkasta desse endringane?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" -msgstr "" +msgstr "Forstørre" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Zoom faktisk" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Zoom inn" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Zoom ut" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "tomme" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,28 +1816,33 @@ msgstr "av {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "X64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} filer" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} einingar var funnen." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} bilde skanna på {1} ved {2} er kanskje ikkje lagra, men kan gjenopprettast. Vil du gjenoppretta dei?" diff --git a/NAPS2.Lib/Lang/po/pl.po b/NAPS2.Lib/Lang/po/pl.po index 9f21ecb9b1..e08db602e4 100644 --- a/NAPS2.Lib/Lang/po/pl.po +++ b/NAPS2.Lib/Lang/po/pl.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Polish\n" "Language: pl\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Domyślna czynność przycisku „Zapisz”:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Domyślna czynność przycisku „Skanuj”:" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Menu „Skanuj” zmienia profil domyślny" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "Znaleziono 1 urządzenie." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "Kolor 24-bitowy" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "300 dpi" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "400 dpi" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "600 dpi" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "800 dpi" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "A3 (297 x 420 mm)" @@ -76,12 +60,15 @@ msgstr "O programie" msgid "Acquiring data..." msgstr "Pozyskiwanie danych..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Czynność" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Zaawansowane" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Zaawansowane ustawienia profilu" @@ -93,35 +80,35 @@ msgstr "Wszystkie ({0})" msgid "All Files" msgstr "Wszystkie pliki" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Zezwalaj na adnotacje" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Zezwalaj na kopiowanie zawartości" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" -msgstr "Zezwalaj na kopiowanie zawartości dla dostępności" +msgstr "Zezwalaj na kopiowanie zawartości w celu ułatwienia dostępu" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Zezwalaj na składanie dokumentu" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Zezwalaj na modyfikowanie dokumentu" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Zezwalaj na wypełnianie formularzy" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Zezwalaj na pełną jakość wydruku" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Zezwalaj na drukowanie" @@ -133,17 +120,22 @@ msgstr "Alternatywne usuwanie przeplatania" msgid "Alternate Interleave" msgstr "Alternatywne przeplatanie" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternatywny transfer" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Zawsze pytaj" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Zawsze pytaj" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Do zaimportowania tego pliku PDF potrzebny jest dodatkowy składnik. Czy chcesz go teraz pobrać?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Wystąpił błąd podczas próby zainstalowania aktualizacji." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Wystąpił błąd podczas uruchamiania OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Wystąpił błąd podczas próby autoryzacji." msgid "An error occurred when trying to auto save." msgstr "Wystąpił błąd podczas próby automatycznego zapisania." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Wystąpił błąd podczas próby zainstalowania aktualizacji." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Wystąpił błąd podczas próby zapisania pliku." @@ -172,11 +168,11 @@ msgstr "Wystąpił błąd w sterowniku skanowania." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "Operacja jest w toku. Czy na pewno chcesz zakończyć i anulować operację?" +msgstr "Operacja jest w toku. Czy na pewno chcesz zakończyć i anulować operację?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Nieznany błąd wystąpił podczas skanowania wsadowego." +msgid "An unknown error occurred during the batch scan." +msgstr "Wystąpił nieznany błąd podczas skanowania wsadowego." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -190,7 +186,15 @@ msgstr "Dostępna jest aktualizacja OCR." msgid "Apple Driver" msgstr "Sterownik Apple" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Aplikacja" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Zastosuj jasność/kontrast po skanowaniu" @@ -222,19 +226,27 @@ msgstr "Czy na pewno chcesz usunąć {0} element(y/ów)?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Czy na pewno chcesz usunąć {0} profile/profilów?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Czy na pewno chcesz zatrzymać udostępnianie {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Czy na pewno chcesz cofnąć zmiany w pliku/plikach {0}?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Przypisz" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Nazwa załącznika:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autor:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autoryzuj" @@ -242,29 +254,27 @@ msgstr "Autoryzuj" msgid "Auto" msgstr "Automatyczna" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Ustawienia automatycznego zapisywania" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "Numer autoinkrementacji (1 cyfra)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Numer autoinkrementacji (2 cyfry)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Numer autoinkrementacji (3 cyfry)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Numer autoinkrementacji (4 cyfry)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Automatycznie uruchom OCR po skanowaniu" @@ -277,8 +287,8 @@ msgstr "B4 (250 x 353 mm)" msgid "B5 (176x250 mm)" msgstr "B5 (176 x 250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Skanowanie wsadowe" @@ -298,7 +308,6 @@ msgstr "Skanowanie wsadowe zatrzymane z powodu błędu." msgid "Best" msgstr "Najlepsza" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Głębia bitowa:" @@ -309,10 +318,10 @@ msgstr "Pliki bitmapowe (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Czarno-biały" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Puste strony" @@ -320,7 +329,6 @@ msgstr "Puste strony" msgid "Brightness / Contrast" msgstr "Jasność / kontrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Jasność:" @@ -329,24 +337,11 @@ msgstr "Jasność:" msgid "CCITT4" msgstr "CCITT4" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Nie możesz znaleźć swojego skanera? Przeczytaj o ograniczeniach Flatpaka NAPS2." + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Anuluj" @@ -363,7 +358,7 @@ msgstr "Anulowanie...." msgid "Center" msgstr "Wyśrodkowanie" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Zmień" @@ -375,7 +370,7 @@ msgstr "Sprawdź aktualizacje" msgid "Checking..." msgstr "Sprawdzanie..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Wybierz dostawcę poczty e-mail" @@ -383,7 +378,6 @@ msgstr "Wybierz dostawcę poczty e-mail" msgid "Choose Profile" msgstr "Wybierz profil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Wybierz urządzenie" @@ -397,7 +391,7 @@ msgstr "Wyczyść" msgid "Clear All" msgstr "Wyczyść wszystkie" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Wyczyść obrazy po zapisaniu" @@ -405,16 +399,30 @@ msgstr "Wyczyść obrazy po zapisaniu" msgid "Close" msgstr "Zamknij" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Połącz" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Komunikacja z urządzeniem skanującym została przerwana." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Zgodność" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Kompresja:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Połącz" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Błąd połączenia." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrast:" @@ -435,7 +443,7 @@ msgstr "Kopiowanie..." msgid "Copyright {0} NAPS2 Contributors" msgstr "Prawa autorskie {0} współtwórcy NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Próg pokrycia" @@ -443,7 +451,7 @@ msgstr "Próg pokrycia" msgid "Crop" msgstr "Przycięcie" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Przytnij do rozmiaru strony" @@ -451,10 +459,14 @@ msgstr "Przytnij do rozmiaru strony" msgid "Custom ({0}x{1} {2})" msgstr "Niestandardowy ({0} x {1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Niestandardowy rozmiar strony" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Rozdzielczość niestandardowa" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Własne obracanie" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "Niestandardowy SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Niestandardowy..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Ciemny" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Dzień (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Domyślna" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Domyślna ścieżka pliku:" @@ -487,7 +504,6 @@ msgstr "Domyślna ścieżka pliku:" msgid "Deinterleave" msgstr "Usuń przeplatanie" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Prostuj" msgid "Deskew Progress" msgstr "Postęp prostowania" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Prostuj zeskanowane strony" @@ -509,16 +525,14 @@ msgstr "Prostuj zeskanowane strony" msgid "Deskewing..." msgstr "Prostowanie..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Urządzenie:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Wymiary" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Wyświetlana nazwa:" @@ -532,12 +546,10 @@ msgstr "Korekta dokumentu" msgid "Donate" msgstr "Wspomóż" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Gotowe" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Pobierz" @@ -550,19 +562,47 @@ msgstr "Błąd pobierania" msgid "Download Needed" msgstr "Potrzebne pobranie" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Postęp pobierania" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "dpi" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Dupleks" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Sterownik ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "Sterownik sieciowy ESCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "Sterownik USB ESCL" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Modyfikuj" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Edytuj w {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Edytuj w..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Wyślij wszystkie" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "Wyślij wszystkie jako PDF" @@ -576,25 +616,32 @@ msgstr "Wyślij PDF" msgid "Email PDF Progress" msgstr "Postęp wysyłania PDF" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Wyślij zaznaczone" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "Wyślij zaznaczone jako PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Ustawienia poczty" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Włącz automatyczne zapisywanie" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Włącz rejestrowanie debugowania" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Zaszyfruj PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Szyfrowanie" @@ -602,12 +649,15 @@ msgstr "Szyfrowanie" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Plik Enhanced Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Błąd" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Błąd podczas uruchamiania aplikacji {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "Przewidywany rozmiar do pobrania: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "Plik Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Wyklucz puste strony" @@ -629,32 +679,38 @@ msgstr "Szybka" msgid "Feeder" msgstr "Podajnik" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Nazwa pliku" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Ścieżka pliku:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Napraw balans bieli i usuń szum" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Odwróć" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Odwróć tyły stron dwustronnych" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Odwróć strony dwustronne" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "Ponadto zwiększ Jakość obrazu w swoim profilu, dla wysokiej jakości JPEG (80+), aby uzyskać najlepsze rezultaty." +msgstr "Aby uzyskać wysoką jakość JPEG (80+) i najlepsze wyniki, również zwiększ w swoim profilu jakość obrazu." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" msgstr "Plik GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Uzyskaj więcej języków" @@ -671,12 +727,11 @@ msgstr "Gmail" msgid "Grayscale" msgstr "Odcienie szarości" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Wyrównanie poziome:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Godzina (0-23)" @@ -684,6 +739,10 @@ msgstr "Godzina (0-23)" msgid "Hue / Saturation" msgstr "Barwa / nasycenie" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "Adres IP / Host" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Ikony z:" @@ -696,12 +755,12 @@ msgstr "Obraz" msgid "Image Files" msgstr "Pliki obrazów" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Jakość obrazu" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Ustawienia obrazu" @@ -745,6 +804,10 @@ msgstr "Instalacja zakończona. Czy chcesz teraz uruchomić ponownie NAPS2?" msgid "Installation failed." msgstr "Instalacja zakończona niepowodzeniem." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Interfejs" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Przeplataj" @@ -757,11 +820,20 @@ msgstr "Plik JPEG (*.jpg, *.jpeg)" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "Plik JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Jakość JPEG" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Zachowaj obrazy między sesjami" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Skróty klawiszowe" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Słowa kluczowe:" @@ -773,6 +845,10 @@ msgstr "LZW" msgid "Language" msgstr "Wybór języka" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Napisz recenzję" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Do lewej" @@ -781,33 +857,48 @@ msgstr "Do lewej" msgid "Legacy (native UI only)" msgstr "Starsza (tylko natywny interfejs)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Jasny" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Lubisz NAPS2?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Załaduj obrazy do NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Stwórz PDF-y zdatne do wyszukiwania za pomocą OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Ręczny adres IP" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Maksymalna jakość (duże pliki)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Transfer pamięciowy" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metadane" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minuta (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Miesiąc (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Więcej informacji" @@ -819,11 +910,19 @@ msgstr "Przesuń w dół" msgid "Move Up" msgstr "Przesuń w górę" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Wiele języków" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Wiele języków..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Wielokrotne skanowanie (stały odstęp pomiędzy skanowaniami)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Wielokrotne skanowanie (monit pomiędzy skanowaniami)" @@ -841,7 +940,7 @@ msgstr "NAPS2 - {0}" msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 jest całkowicie bezpłatny. Rozważ przekazanie darowizny." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Nazwa (opcjonalnie)" @@ -849,6 +948,10 @@ msgstr "Nazwa (opcjonalnie)" msgid "Name missing." msgstr "Brak nazwy." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Transfer natywny" + #: UiStrings.resx$New$Message msgid "New" msgstr "Nowy" @@ -861,7 +964,7 @@ msgstr "Nowy profil" msgid "Next" msgstr "Następny" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Następne skanowanie" @@ -870,12 +973,15 @@ msgstr "Następne skanowanie" msgid "No device selected." msgstr "Nie wybrano urządzenia." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Nie znaleziono urządzeń." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Nie ma stron w podajniku." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Nie wybrano dostawcy." @@ -897,11 +1003,11 @@ msgstr "Brak" msgid "Not Another PDF Scanner" msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Nie teraz" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Liczba skanowań:" @@ -909,7 +1015,6 @@ msgstr "Liczba skanowań:" msgid "OCR" msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Pobieranie OCR" @@ -918,37 +1023,23 @@ msgstr "Pobieranie OCR" msgid "OCR Progress" msgstr "Postęp OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Instalacja OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Język OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "Tryb OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Szerokość przesunięcia na podstawie wyrównania (WIA)" @@ -956,13 +1047,11 @@ msgstr "Szerokość przesunięcia na podstawie wyrównania (WIA)" msgid "Old DSM" msgstr "Stare DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Jeden plik na stronę" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Jeden plik na skanowanie" @@ -970,7 +1059,11 @@ msgstr "Jeden plik na skanowanie" msgid "One or more files could not be downloaded." msgstr "Nie można pobrać jednego lub więcej plików." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Zezwól tylko na pojedynczą instancję NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Otwórz folder" @@ -978,11 +1071,15 @@ msgstr "Otwórz folder" msgid "Operation in Progress" msgstr "Operacja w toku" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (nowy)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "Outlook Web Access" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Wyjście" @@ -990,7 +1087,7 @@ msgstr "Wyjście" msgid "Overwrite File" msgstr "Nadpisz plik" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Hasło właściciela:" @@ -998,8 +1095,8 @@ msgstr "Hasło właściciela:" msgid "PDF Document (*.pdf)" msgstr "Dokument PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Ustawienia PDF" @@ -1027,17 +1124,15 @@ msgstr "PDF/A-3u" msgid "PNG File (*.png)" msgstr "Plik PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Rozmiar strony:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Źródło papieru:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Hasło" @@ -1045,21 +1140,24 @@ msgstr "Hasło" msgid "Paste" msgstr "Wklej" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Symbole zastępcze" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Przetwarzanie końcowe" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Zapobiegawczo uruchom OCR po skanowaniu" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Kliknij Rozpocznij, gdy gotowe." @@ -1067,7 +1165,7 @@ msgstr "Kliknij Rozpocznij, gdy gotowe." msgid "Preview" msgstr "Podgląd" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Podgląd:" @@ -1080,12 +1178,11 @@ msgstr "Poprzedni" msgid "Print" msgstr "Drukuj" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Ustawienia profilu" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profil:" @@ -1094,23 +1191,27 @@ msgstr "Profil:" msgid "Profiles" msgstr "Profile" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Pytaj, jeśli zaznaczone" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Pytaj o ścieżkę pliku" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Dostawca" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Gotowy na skanowanie {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Odzyskaj" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Odzyskaj zeskanowane obrazy" @@ -1122,9 +1223,15 @@ msgstr "Odzyskiwanie..." msgid "Recovery Progress" msgstr "Postęp odzyskiwania" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Ponów" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Ponów {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Zapamiętaj te ustawienia" @@ -1140,15 +1247,11 @@ msgstr "Zresetuj" msgid "Reset Image" msgstr "Zresetuj obraz" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Rozdzielczość:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Przywróć domyślne" @@ -1156,6 +1259,14 @@ msgstr "Przywróć domyślne" msgid "Reverse" msgstr "Оdwróć" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Odwróć wszystkie" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Odwróć zaznaczone" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Przywróć" @@ -1176,7 +1287,6 @@ msgstr "Obróć w lewo" msgid "Rotate Right" msgstr "Obróć w prawo" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Uruchom w tle" @@ -1185,7 +1295,6 @@ msgstr "Uruchom w tle" msgid "Running OCR..." msgstr "Uruchamianie OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "Sterownik SANE" @@ -1194,6 +1303,11 @@ msgstr "Sterownik SANE" msgid "Save" msgstr "Zapisz" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Zapisz wszystkie" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "Zapisz wszystkie jako obrazy" @@ -1220,6 +1334,11 @@ msgstr "Zapisz PDF" msgid "Save PDF Progress" msgstr "Zapisz postęp PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Zapisz zaznaczone" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "Zapisz zaznaczone jako obrazy" @@ -1228,11 +1347,11 @@ msgstr "Zapisz zaznaczone jako obrazy" msgid "Save Selected as PDF" msgstr "Zapisz zaznaczone jako PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Zapisz do pojedynczego pliku" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Zapisz do wielu plików" @@ -1244,29 +1363,45 @@ msgstr "Zapisywanie wyników przetwarzania..." msgid "Saving {0}..." msgstr "Zapisywanie {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Przeskaluj do okna" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Skalowanie:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skanuj" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Konfiguracja skanowania" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Skanuj za pomocą domyślnego profilu" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Skanuj za pomocą nowego profilu" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Skanuj za pomocą profilu {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Zeskanowane obrazy" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Udostępnianie skanera" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Skanowanie strony {0}" @@ -1280,11 +1415,14 @@ msgstr "Skanowana strona {0} (skanowanie {1})..." msgid "Scanning page {0}..." msgstr "Skanowanie strony {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Wyszukiwanie urządzeń..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekunda (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Wybierz" @@ -1293,7 +1431,10 @@ msgstr "Wybierz" msgid "Select All" msgstr "Zaznacz wszystko" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Wybierz urządzenie" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Wybierz źródło" @@ -1302,7 +1443,6 @@ msgstr "Wybierz źródło" msgid "Select a profile before clicking Scan." msgstr "Wybierz profil przed naciśnięciem przycisku Skanuj." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Zaznacz jeden lub więcej języków:" @@ -1311,8 +1451,7 @@ msgstr "Zaznacz jeden lub więcej języków:" msgid "Selected ({0})" msgstr "Zaznaczone ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Oddzielne pliki według Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Oddzielne pliki według Patch-T" msgid "Set Default" msgstr "Ustaw domyślne" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Ustawienia" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Udostępnij" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Udostępnij, nawet jeśli NAPS2 jest zamknięty" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Ustawienia udostępnionego skanera" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Udostępnionych skanerów można używać z innych komputerów w sieci lokalnej, wybierając opcję „Sterownik ESCL” w ustawieniach profilu NAPS2 drugiego komputera." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Wyostrzanie" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Skrót" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Wyświetl" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Pokaż pasek narzędzi „Profile”" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Pokaż postęp natywnego TWAIN" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Pokaż numery stron" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Pasek boczny" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" -msgstr "Pliki pojedynczej strony" +msgstr "Pliki jednostronicowe" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Pojedyncze skanowanie" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Pomiń monit zapisu" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Podziel" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Rozpocznij" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Zatrzymaj udostępnianie skanera" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Rozciągnij do rozmiaru strony" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Temat:" @@ -1358,12 +1544,11 @@ msgstr "Temat:" msgid "TIFF File (*.tiff, *.tif)" msgstr "Plik TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "Sterownik TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Szczegóły techniczne" @@ -1372,6 +1557,10 @@ msgstr "Szczegóły techniczne" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "Silnik OCR nie jest dostępny. Upewnij się, że zainstalowałeś wymagany pakiet:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Upłynął limit czasu operacji OCR." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Plik nie mógł zostać zastąpiony, ponieważ jest obecnie w użyciu." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Plik {0} już istnieje. Czy chcesz go zastąpić?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Następujący plik jest zaszyfrowany i potrzebuje hasła do otworzenia: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Następujący plik jest zaszyfrowany i wymaga hasła do otwarcia:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "Wybrany skaner jest zajęty." msgid "The selected scanner is offline." msgstr "Wybrany skaner jest w trybie offline." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Proces roboczy uległ awarii." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Proces roboczy uległ awarii. Sprawdź podgląd zdarzeń systemu Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Motyw:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Opcje TIFF" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Czas pomiędzy skanowaniami (sekundy):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Tytuł:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Narzędzia" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Implementacja TWAIN:" @@ -1467,6 +1676,22 @@ msgstr "US Legal (8.5 x 14 in)" msgid "US Letter (8.5x11 in)" msgstr "US Letter (8.5 x 11 in)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Usuń przypisanie" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Cofnij" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Cofnij {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Skaner nieznany" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Niezapisane zmiany" @@ -1487,21 +1712,18 @@ msgstr "Aktualizacja..." msgid "Uploading email..." msgstr "Przesyłanie wiadomości e-mail..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Użyj natywnego interfejsu" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Predefiniowane ustawienia" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Hasło użytkownika:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Używanie OCR wymaga pobrania każdego języka, który chcesz przeskanować." @@ -1515,16 +1737,15 @@ msgstr "Wersja {0}" msgid "View" msgstr "Widok" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "Sterownik WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Oczekiwanie na zakończenie zadania TWAIN..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Oczekiwanie na autoryzację..." @@ -1532,19 +1753,19 @@ msgstr "Oczekiwanie na autoryzację..." msgid "Waiting for scan {0}..." msgstr "Oczekiwanie na skanowanie {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Próg bieli" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "Wersja WIA:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Rok" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Rok (00-99)" @@ -1559,23 +1780,20 @@ msgstr "Nie masz uprawnień do zapisywania plików we wskazanym położeniu." #: MiscResources.resx$ExitWithUnsavedChanges$Message msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" -msgstr "Masz niezapisane zmiany. Czy na pewno chcesz wyjść i odrzucić te zmiany?" +msgstr "Masz niezapisane zmiany. Czy na pewno chcesz wyjść i odrzucić te zmiany?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Powiększenie" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Powiększenie rzeczywiste" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Przybliż" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Oddal" @@ -1604,24 +1822,29 @@ msgstr "x64" msgid "{0} ({1}x{2} {3})" msgstr "{0} ({1} x {2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} plików" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "Znalezione urządzenia: {0}." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" -msgstr "{0} plik(i) przeskanowane w {1} na {2} mogły nie zostać zapisane, ale są do odzyskania. Czy chcesz je odzyskać?" +msgstr "{0} plik(i) przeskanowane dnia {1} o {2} mogły nie zostać zapisane, ale są do odzyskania. Czy chcesz je odzyskać?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." diff --git a/NAPS2.Lib/Lang/po/pt-BR.po b/NAPS2.Lib/Lang/po/pt-BR.po index d48a2020a9..b27d733ec2 100644 --- a/NAPS2.Lib/Lang/po/pt-BR.po +++ b/NAPS2.Lib/Lang/po/pt-BR.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Portuguese, Brazilian\n" "Language: pt\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Ação padrão do botão \"Salvar\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Ação padrão do botão \"Digitalizar\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "O menu do botão \"Digitalizar\" altera o perfil padrão" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 dispositivo encontrado." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "Colorido 24 bits" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -76,12 +60,15 @@ msgstr "Sobre" msgid "Acquiring data..." msgstr "Obtendo dados..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Ação" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Avançado" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Configurações Avançadas do Perfil" @@ -93,35 +80,35 @@ msgstr "Todos ({0})" msgid "All Files" msgstr "Todos os Arquivos" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Permitir anotações" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Permitir Cópia do Conteúdo" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Permitir Cópia de Conteúdo para Acessibilidade" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Permitir União do Documento" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Permitir Modificação do Documento" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Permitir Preenchimento de Formulário" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Permitir Qualidade Máxima da Impressão" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Permitir Impressão" @@ -133,17 +120,22 @@ msgstr "Desintercalar Alternado" msgid "Alternate Interleave" msgstr "Intercalar Alternado" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Transferência Alternativa" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Sempre Perguntar" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Sempre Perguntar" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "É necessário um componente adicional para importar este arquivo PDF. Gostaria de baixá-lo agora?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Ocorreu um erro ao tentar instalar a atualização." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Ocorreu um erro ao executar o OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Ocorreu um erro ao tentar autorizar." msgid "An error occurred when trying to auto save." msgstr "Ocorreu um erro ao tentar salvar automaticamente." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Ocorreu um erro ao tentar instalar a atualização." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Ocorreu um erro ao tentar salvar o arquivo." @@ -175,7 +171,7 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Uma operação está em andamento. Tem certeza de que deseja sair e cancelar a operação?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "Ocorreu um erro desconhecido durante a digitalização em lote." #: MiscResources.resx$UpdateAvailable$Message @@ -188,9 +184,17 @@ msgstr "Uma atualização para OCR está disponível." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple Driver" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Aplicativo" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Aplicar Brilho/Contraste após digitalizar" @@ -222,19 +226,27 @@ msgstr "Confirma a remoção de {0} item(s)?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Confirma remoção de {0} perfis?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Tem certeza que deseja interromper o compartilhamento {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Tem certeza de que deseja desfazer as alterações para {0} imagem(s)?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Associar" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Nome do anexo:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autor:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autorizar" @@ -242,43 +254,41 @@ msgstr "Autorizar" msgid "Auto" msgstr "Automático" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Configurações de salvamento automático" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "Auto-incrementar número (1 dígito)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Auto-incrementar número (2 dígitos)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Auto-incrementar número (3 dígitos)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Auto-incrementar número (4 dígitos)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Executar automaticamente o OCR após a digitalização" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Digitalização em lote" @@ -296,9 +306,8 @@ msgstr "Digitalização em lote interrompida devido a erro." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Melhor" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Qualidade:" @@ -309,10 +318,10 @@ msgstr "Arquivos Bitmap (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Preto e Branco" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Páginas em Branco" @@ -320,33 +329,19 @@ msgstr "Páginas em Branco" msgid "Brightness / Contrast" msgstr "Brilho / Contraste" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Brilho:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Não consegue encontrar seu scanner? Leia sobre as limitações do NAPS2 Flatpak." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Cancelar" @@ -363,7 +358,7 @@ msgstr "Cancelando...." msgid "Center" msgstr "Centralizado" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Alterar" @@ -375,7 +370,7 @@ msgstr "Verificar atualizações" msgid "Checking..." msgstr "Verificando..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Escolha o provedor de e-mail" @@ -383,7 +378,6 @@ msgstr "Escolha o provedor de e-mail" msgid "Choose Profile" msgstr "Escolha o perfil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Escolha o dispositivo" @@ -395,9 +389,9 @@ msgstr "Limpar" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Limpar Tudo" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Limpar imagens após salvar" @@ -405,16 +399,30 @@ msgstr "Limpar imagens após salvar" msgid "Close" msgstr "Fechar" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Combinar" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "A comunicação com scanner foi interrompida." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Compatibilidade" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Compressão:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Conectar" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Erro de conexão." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Contraste:" @@ -433,9 +441,9 @@ msgstr "Copiando..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} NAPS2 Contributors" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Limiar de Cobertura" @@ -443,7 +451,7 @@ msgstr "Limiar de Cobertura" msgid "Crop" msgstr "Cortar" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Cortar para o tamanho da página" @@ -451,10 +459,14 @@ msgstr "Cortar para o tamanho da página" msgid "Custom ({0}x{1} {2})" msgstr "Personalizado ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Tamanho de página personalizada" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Resolução Personalizada" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Rotação personalizada" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "SMTP personalizado" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Personalizado..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Escuro" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Dia (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Padrão" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Caminho padrão do arquivo:" @@ -487,7 +504,6 @@ msgstr "Caminho padrão do arquivo:" msgid "Deinterleave" msgstr "Desintercalar" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Endireitar" msgid "Deskew Progress" msgstr "Progresso da Correção" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Endireitar páginas digitalizadas" @@ -509,35 +525,31 @@ msgstr "Endireitar páginas digitalizadas" msgid "Deskewing..." msgstr "Alinhando..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Dispositivo:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Dimensões" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Nome para exibição:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Correção de Documento" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Doar" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Concluído" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Baixar" @@ -550,51 +562,86 @@ msgstr "Erro no download" msgid "Download Needed" msgstr "Download Necessário" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Progresso do download" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "Dpi" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" -msgstr "" +msgstr "Duplex" + +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Driver ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL Network Driver" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB Driver" #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Editar" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Editar com {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Editar com..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Enviar tudo" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Enviar tudo como PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "" +msgstr "Email PDF" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" msgstr "Progresso Email PDF" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Enviar Selecionados" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "E-mail Selecionado como PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Configurações de e-mail" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Ativar Auto Salvamento" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Habilitar log de depuração" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Proteger PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Criptografia" @@ -602,12 +649,15 @@ msgstr "Criptografia" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Arquivo EMF (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Erro" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Erro ao iniciar o aplicativo {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,38 +665,45 @@ msgstr "Tamanho estimado do download: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Excluir páginas em branco" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Rápido" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Alimentador" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Nome do Arquivo" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Caminho do arquivo:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Corrigir equilíbrio branco e remover ruído" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Inverter" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Virar o verso das páginas em duplex" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Virar páginas frente e verso" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Para altas qualidades de JPEG (80+), aumente também a Qualidade da Imagem no seu Perfil para obter melhores resultados." @@ -654,7 +711,6 @@ msgstr "Para altas qualidades de JPEG (80+), aumente também a Qualidade da Imag msgid "GIF File (*.gif)" msgstr "Arquivo GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Instalar mais idiomas" @@ -665,18 +721,17 @@ msgstr "Vidro" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Escala de cinza" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Alinhamento horizontal:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Hora (0-23)" @@ -684,6 +739,10 @@ msgstr "Hora (0-23)" msgid "Hue / Saturation" msgstr "Tom / Saturação" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Host" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Ícones de:" @@ -696,12 +755,12 @@ msgstr "Imagem" msgid "Image Files" msgstr "Arquivos de imagens" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Qualidade da imagem" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Configurações de imagem" @@ -745,6 +804,10 @@ msgstr "Instalação completada. Deseja reiniciar o NAPS2 agora?" msgid "Installation failed." msgstr "Falha na instalação." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Interface" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Intercalar" @@ -755,24 +818,37 @@ msgstr "Arquivo JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000 File (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Qualidade JPEG" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Manter imagens entre as sessões" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Atalhos de Teclado" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Palavras-chave:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Idioma" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Deixe uma avaliação" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Esquerda" @@ -781,33 +857,48 @@ msgstr "Esquerda" msgid "Legacy (native UI only)" msgstr "Legado (UI nativa apenas)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Claro" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Gosta do NAPS2?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Carregar imagens em NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Criar PDF pesquisável usando OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "IP manual" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Alta qualidade (arquivos grandes)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Transferência de Memória" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metadados" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minutos (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Mês (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Mais informações" @@ -819,11 +910,19 @@ msgstr "Mover abaixo" msgid "Move Up" msgstr "Mover acima" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Múltiplos idiomas" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Múltiplos idiomas..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Múltiplas digitalizações (Fixar tempo entre cada uma)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Múltiplas digitalizações (Solicitar confirmação entre cada uma)" @@ -831,17 +930,17 @@ msgstr "Múltiplas digitalizações (Solicitar confirmação entre cada uma)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 é totalmente gratuito. Considere fazer uma doação." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Nome (opcional)" @@ -849,6 +948,10 @@ msgstr "Nome (opcional)" msgid "Name missing." msgstr "Nome está faltando." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Transferência Nativa" + #: UiStrings.resx$New$Message msgid "New" msgstr "Novo" @@ -861,7 +964,7 @@ msgstr "Novo perfil" msgid "Next" msgstr "Próximo" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Próxima Digitalização" @@ -870,12 +973,15 @@ msgstr "Próxima Digitalização" msgid "No device selected." msgstr "Nenhum scanner escolhido." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Nenhum dispositivo encontrado." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Sem papel no alimentador." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Nenhum provedor selecionado." @@ -895,21 +1001,20 @@ msgstr "Nenhuma" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Não agora" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Número de digitalizações:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Baixar OCR" @@ -918,37 +1023,23 @@ msgstr "Baixar OCR" msgid "OCR Progress" msgstr "Progresso do OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Configuração do OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Idioma do OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "Modo do OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Compensar largura com base no alinhamento (WIA)" @@ -956,13 +1047,11 @@ msgstr "Compensar largura com base no alinhamento (WIA)" msgid "Old DSM" msgstr "DSM Antigo" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Um arquivo por página" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Um arquivo por digitalização" @@ -970,7 +1059,11 @@ msgstr "Um arquivo por digitalização" msgid "One or more files could not be downloaded." msgstr "Um ou mais arquivos podem não ter sido baixados." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Permitir apenas uma instância do NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Abrir pasta" @@ -978,11 +1071,15 @@ msgstr "Abrir pasta" msgid "Operation in Progress" msgstr "Operação em Progresso" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (novo)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Web Access" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Saída" @@ -990,7 +1087,7 @@ msgstr "Saída" msgid "Overwrite File" msgstr "Sobrescrever arquivo" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Senha do Proprietário:" @@ -998,8 +1095,8 @@ msgstr "Senha do Proprietário:" msgid "PDF Document (*.pdf)" msgstr "Documento PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Configurações de PDF" @@ -1009,35 +1106,33 @@ msgstr "PDF salvo." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "Arquivo PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Tamanho da folha:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Origem do papel:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Senha" @@ -1045,21 +1140,24 @@ msgstr "Senha" msgid "Paste" msgstr "Colar" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Substituições" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Porta" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Pós-processamento" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Antecipar execução do OCR durante a digitalização" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Pressione Iniciar quando estiver pronto." @@ -1067,7 +1165,7 @@ msgstr "Pressione Iniciar quando estiver pronto." msgid "Preview" msgstr "Visualizar" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Visualizar:" @@ -1080,12 +1178,11 @@ msgstr "Anterior" msgid "Print" msgstr "Imprimir" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Configurar Perfil" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Perfil:" @@ -1094,23 +1191,27 @@ msgstr "Perfil:" msgid "Profiles" msgstr "Perfis" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Perguntar se selecionado" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Solicitar o caminho do arquivo" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Provedor" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Pronto para digitalizar {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Recuperar" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Recuperar imagens digitalizadas" @@ -1122,9 +1223,15 @@ msgstr "Recuperando..." msgid "Recovery Progress" msgstr "Progresso da Recuperação" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Refazer" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Refazer {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Lembrar essas configurações" @@ -1140,15 +1247,11 @@ msgstr "Restaurar" msgid "Reset Image" msgstr "Restaurar Imagem" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Resolução:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Restaurar padrões" @@ -1156,6 +1259,14 @@ msgstr "Restaurar padrões" msgid "Reverse" msgstr "Inverter" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Inverter tudo" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Inverter selecionados" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Reverter" @@ -1176,7 +1287,6 @@ msgstr "Girar para esquerda" msgid "Rotate Right" msgstr "Girar para direita" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Executar em segundo plano" @@ -1185,22 +1295,26 @@ msgstr "Executar em segundo plano" msgid "Running OCR..." msgstr "Executando OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "Driver SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Salvar" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Salvar Tudo" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Salvar tudo como Imagens" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Salvar Tudo como PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Salvar PDF" msgid "Save PDF Progress" msgstr "Progresso de Salvamento do PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Salvar Selecionados" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Salvar seleção como Imagens" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Salvar seleção como PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Salve em um único arquivo" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Salvar em múltiplos arquivos" @@ -1244,29 +1363,45 @@ msgstr "Salvando resultados de lote..." msgid "Saving {0}..." msgstr "Salvando {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Ajustar à Janela" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Escala:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Digitalizar" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Configuração da digitalização" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Digitalizar com o perfil padrão" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Digitalizar com novo perfil" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Digitalizar com o Perfil {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Imagem digitalizada" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Compartilhar Scanner" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Digitalizando página {0}" @@ -1280,29 +1415,34 @@ msgstr "Digitalizando página {0} (Digitalizado {1})..." msgid "Scanning page {0}..." msgstr "Digitalizando página {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Procurando dispositivos..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Segundos (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Selecionar" #: UiStrings.resx$SelectAll$Message msgid "Select All" -msgstr "Seleciona tudo" +msgstr "Selecionar tudo" + +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Selecionar dispositivo" -#: FSelectDevice.resx$$this.Text$Message #: UiStrings.resx$SelectSource$Message msgid "Select Source" -msgstr "Selecionar origem" +msgstr "Selecionar Origem" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." msgstr "Escolha um perfil antes de digitalizar." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Selecione uma ou mais linguagens:" @@ -1311,8 +1451,7 @@ msgstr "Selecione uma ou mais linguagens:" msgid "Selected ({0})" msgstr "Selecionado ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Separar Arquivos por Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Separar Arquivos por Patch-T" msgid "Set Default" msgstr "Definir Padrão" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Configurações" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Compartilhar" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Compartilhar mesmo quando o NAPS2 estiver fechado" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Configurações do Scanner Compartilhado" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Scanners compartilhados podem ser usados a partir de outros computadores na rede local selecionando \"Driver ESCL\" nas configurações de perfil NAPS2 do outro computador." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Nitidez" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Atalho" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Mostrar" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Exibir barra de ferramentas \"Perfis\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Mostrar progresso nativo do TWAIN" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Mostrar números de páginas" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Barra lateral" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Arquivos de página única" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Única digitalização" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Pular pedido de salvar" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Separar" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Iniciar" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Parar Compartilhamento de Scanner" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Estender para o tamanho da página" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Assunto:" @@ -1358,12 +1544,11 @@ msgstr "Assunto:" msgid "TIFF File (*.tiff, *.tif)" msgstr "Arquivo TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "Driver TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Detalhes Técnicos" @@ -1372,6 +1557,10 @@ msgstr "Detalhes Técnicos" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "O mecanismo de OCR não está disponível. Certifique-se de instalar o pacote necessário:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "O tempo limite da operação OCR expirou." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "O Arquivo não pode ser sobrescrito porque está atualmente em uso." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "O arquivo {0} já existe. Deseja sobrescrever?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "O seguinte arquivo é criptografado e requer uma senha para abrir: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "O seguinte arquivo é criptografado e requer uma senha para abrir:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "O scanner escolhido está ocupado." msgid "The selected scanner is offline." msgstr "O scanner escolhido está offline." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "O processo de trabalho travou." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "O processo de trabalho travou. Verifique o Visualizador de Eventos do Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Tema:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Opções Tiff" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Tempo entre digitalizações (segundos):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Título:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Ferramentas" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Implementação Twain:" @@ -1467,6 +1676,22 @@ msgstr "Ofício (8.5x14 pol)" msgid "US Letter (8.5x11 in)" msgstr "Ofício (8.5x11 pol)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Desassociar" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Desfazer" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Desfazer {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Scanner Desconhecido" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Alterações não salvas" @@ -1477,7 +1702,7 @@ msgstr "Progresso da Atualização" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Verificação de atualizações está desativada." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Atualizando..." msgid "Uploading email..." msgstr "Enviando e-mail..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Usar WIA UI nativo" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Usar configurações padrões" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Senha do Usuário:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "O uso do OCR requer que você baixe cada linguagem que deseja escanear." @@ -1515,16 +1737,15 @@ msgstr "Versão {0}" msgid "View" msgstr "Visualizar" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "Driver WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Aguardando driver TWAIN..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Aguardando autorização..." @@ -1532,19 +1753,19 @@ msgstr "Aguardando autorização..." msgid "Waiting for scan {0}..." msgstr "Aguardando para digitalizar {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Limiar de branco" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Versão Wia:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Ano" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Ano (00-99)" @@ -1561,28 +1782,25 @@ msgstr "Você não tem permissão para salvar arquivos neste local." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Você tem alterações não salvas. Tem certeza de que deseja sair e descartar essas alterações?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" -msgstr "" +msgstr "Zoom" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Tamanho Original" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Ampliar" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Diminuir" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" @@ -1590,7 +1808,7 @@ msgstr "pol" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,28 +1816,33 @@ msgstr "de {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0}/{1} arquivos" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} dispositivos encontrados." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} imagem(s) digitalizadas em {1} a {2} pode(m) não ter sido salva(s) mas é(são) recuperável(is). Deseja recuperá-las?" diff --git a/NAPS2.Lib/Lang/po/pt-PT.po b/NAPS2.Lib/Lang/po/pt-PT.po index bd15091445..63ba56eb7e 100644 --- a/NAPS2.Lib/Lang/po/pt-PT.po +++ b/NAPS2.Lib/Lang/po/pt-PT.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:34\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Portuguese\n" "Language: pt\n" @@ -19,71 +19,58 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "100 ppp" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Ação padrão do botão \"Guardar\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "1200 ppp" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Ação padrão do botão \"Digitalizar\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "150 ppp" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "O menu \"Digitalizar\" altera o perfil padrão" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "200 ppp" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 dispositivo encontrado." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" -msgstr "cor de 24 bits" - -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "300 ppp" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "400 ppp" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "600 ppp" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "800 ppp" +msgstr "Cor de 24 bits" #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297 × 420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210 × 297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148 × 210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message msgid "About" -msgstr "Sobre" +msgstr "Acerca" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." msgstr "A obter dados..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Ação" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Avançado" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" -msgstr "Configuração Avançada do Perfil" +msgstr "Definições avançadas do perfil" #: MiscResources.resx$AllCount$Message msgid "All ({0})" @@ -93,215 +80,237 @@ msgstr "Todos ({0})" msgid "All Files" msgstr "Todos os ficheiros" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" -msgstr "Permitir Anotações" +msgstr "Permitir anotações" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" -msgstr "Permitir Cópia de Conteúdo" +msgstr "Permitir cópia de conteúdo" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" -msgstr "Permitir Cópia de Conteúdo para Acessibilidade" +msgstr "Permitir cópia de conteúdo para acessibilidade" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" -msgstr "Permitir Montagem do Documento" +msgstr "Permitir organização do documento" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" -msgstr "Permitir Modificação Do Documento" +msgstr "Permitir alteração do documento" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" -msgstr "Permitir Preenchimento de Formulário" +msgstr "Permitir preenchimento de formulários" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" -msgstr "Permitir Impressão de Alta Qualidade" +msgstr "Permitir impressão de alta qualidade" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" -msgstr "Permitir Impressão" +msgstr "Permitir impressão" #: UiStrings.resx$AltDeinterleave$Message msgid "Alternate Deinterleave" -msgstr "desintercalar alternativo" +msgstr "Desintercalar alternado" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" -msgstr "" +msgstr "Intercalar alternado" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Perguntar sempre" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Perguntar sempre" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" -msgstr "" +msgstr "Um componente adicional é necessário para importar este ficheiro PDF. Descarregar agora?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "" +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Ocorreu um erro ao executar OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "Ocorreu um erro ao tentar autorizar." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." -msgstr "" +msgstr "Ocorreu um erro ao tentar guardar automaticamente." + +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Ocorreu um erro ao tentar instalar a atualização." #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." -msgstr "Ocorreu um erro ao tentar gravar o ficheiro." +msgstr "Ocorreu um erro ao tentar guardar o ficheiro." #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "" +msgstr "Ocorreu um erro ao tentar enviar o e-mail." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." -msgstr "Ocorreu um erro enquanto tentava enviar um email." +msgstr "Ocorreu um erro durante o envio do e-mail." #: MiscResources.resx$UnknownDriverError$Message #: SdkResources.resx$UnknownDriverError$Message msgid "An error occurred with the scanning driver." -msgstr "Ocorreu um erro com o driver do scanner." +msgstr "Ocorreu um erro com o controlador de digitalização." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "" +msgstr "Está em curso uma operação. Tem a certeza de que pretende sair e cancelar a operação?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "Ocorreu um erro desconhecido durante a digitalização em lote." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "" +msgstr "Existe uma atualização." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "Uma atualização para OCR está disponível." +msgstr "Está disponível uma atualização para OCR." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Controlador Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Aplicação" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" -msgstr "" +msgstr "Aplicar brilho/contraste após digitalização" #: UiStrings.resx$ApplyToSelected$Message msgid "Apply to all {0} selected images" -msgstr "" +msgstr "Aplicar às {0} imagens selecionadas" #: MiscResources.resx$ConfirmCancelBatch$Message msgid "Are you sure you want to cancel the batch scan?" -msgstr "" +msgstr "Tem a certeza de que pretende cancelar a digitalização em lote?" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" -msgstr "Confirma a limpeza de {0} iten(s)?" +msgstr "Tem a certeza de que pretende remover {0} itens?" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "" +msgstr "Tem a certeza de que pretende eliminar \"{0}\"?" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" -msgstr "Confirma que quer eliminar o perfil {0}?" +msgstr "Tem a certeza de que pretende eliminar o perfil \"{0}\"?" #: MiscResources.resx$ConfirmDeleteItems$Message msgid "Are you sure you want to delete {0} item(s)?" -msgstr "Confirma que quer eliminar {0} itens?" +msgstr "Tem a certeza de que pretende eliminar {0} itens?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "Confirma que quer eliminar {0} perfis? " +msgstr "Tem a certeza de que pretende eliminar {0} perfis?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Tem a certeza que pretende parar de partilhar {0}?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" -msgstr "Tem a certeza que quer desfazer as alterações a {0} imagens?" +msgstr "Tem a certeza de que pretende desfazer as alterações a {0} imagens?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Atribuir" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Nome do anexo:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autor:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "Autorizar" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "Automático" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" -msgstr "" +msgstr "Definições de gravação automática" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Número de auto-incremento (1 dígito)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Incrementar número automaticamente (1 dígito)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" -msgstr "Número de auto-incremento (2 dígitos)" +msgstr "Incrementar número automaticamente (2 dígitos)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" -msgstr "Número de auto-incremento (3 dígitos)" +msgstr "Incrementar número automaticamente (3 dígitos)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" -msgstr "Número de auto-incremento (4 dígitos)" +msgstr "Incrementar número automaticamente (4 dígitos)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "" +msgstr "Executar OCR após a digitalização" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250 × 353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176 × 250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" -msgstr "" +msgstr "Digitalização em lote" #: MiscResources.resx$BatchStatusCancelled$Message msgid "Batch cancelled." -msgstr "" +msgstr "Lote cancelado." #: MiscResources.resx$BatchStatusComplete$Message msgid "Batch completed successfully." -msgstr "" +msgstr "Lote concluído com sucesso." #: MiscResources.resx$BatchStatusError$Message msgid "Batch scan stopped due to error." -msgstr "" +msgstr "Digitalização em lote parada devido a um erro." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Melhor" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" -msgstr "Qualidade:" +msgstr "Profundidade de cor:" #: MiscResources.resx$FileTypeBmp$Message msgid "Bitmap Files (*.bmp)" @@ -309,81 +318,66 @@ msgstr "Ficheiro Bitmap (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" -msgstr "Preto e Branco" +msgid "Black and White" +msgstr "Preto e branco" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" -msgstr "" +msgstr "Páginas vazias" #: UiStrings.resx$BrightnessContrast$Message msgid "Brightness / Contrast" -msgstr "" +msgstr "Brilho/Contraste" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Brilho:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Não consegue encontrar o seu digitalizador? Leia sobre as limitações do NAPS2 Flatpak." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Cancelar" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" -msgstr "" +msgstr "Cancelar lote" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." -msgstr "" +msgstr "A cancelar..." #: SettingsResources.resx$HorizontalAlign_Center$Message msgid "Center" -msgstr "Centrar" +msgstr "Centro" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" -msgstr "" +msgstr "Alterar" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "" +msgstr "Procurar atualizações" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "" +msgstr "A verificar..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "" +msgstr "Escolha um serviço de e-mail" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" msgstr "Escolha o perfil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Escolha o dispositivo" @@ -395,26 +389,40 @@ msgstr "Limpar" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Limpar tudo" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" -msgstr "" +msgstr "Limpar imagens após guardar" #: MiscResources.resx$Close$Message msgid "Close" -msgstr "" +msgstr "Fechar" + +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Combinar" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "A comunicação com o digitalizador foi interrompida." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" -msgstr "" +msgstr "Compatibilidade" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" -msgstr "" +msgstr "Compressão:" + +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Conectar" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Erro de ligação." -#: FEditProfile.resx$label7.Text$Message #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Contraste:" @@ -425,69 +433,77 @@ msgstr "Copiar" #: MiscResources.resx$CopyProgress$Message msgid "Copy Progress" -msgstr "" +msgstr "Progresso da cópia" #: MiscResources.resx$Copying$Message msgid "Copying..." -msgstr "" +msgstr "A copiar…" #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Direitos de autor {0}, Colaboradores NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" -msgstr "" +msgstr "Limite de cobertura" #: UiStrings.resx$Crop$Message msgid "Crop" msgstr "Recortar" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" -msgstr "" +msgstr "Recortar ao tamanho da página" #: MiscResources.resx$CustomPageSizeFormat$Message msgid "Custom ({0}x{1} {2})" -msgstr "Personalizado ({0}x{1} {2})" +msgstr "Personalizado ({0} × {1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Tamanho de página personalizado" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Resolução personalizada" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Rotação personalizada" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" -msgstr "" +msgstr "SMTP personalizado" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Personalizar..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Escuro" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" -msgstr "Dia (01-31)" +msgstr "Dias (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" -msgstr "" +msgstr "Padrão" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" -msgstr "" +msgstr "Caminho padrão do ficheiro:" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" msgstr "Desintercalar" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -495,119 +511,153 @@ msgstr "Eliminar" #: UiStrings.resx$Deskew$Message msgid "Deskew" -msgstr "" +msgstr "Endireitar" #: MiscResources.resx$AutoDeskewProgress$Message msgid "Deskew Progress" -msgstr "" +msgstr "Progresso da ação de endireitar" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" -msgstr "" +msgstr "Endireitar páginas digitalizadas" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." -msgstr "" +msgstr "A endireitar..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Dispositivo:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" -msgstr "" +msgstr "Dimensões" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Nome a exibir:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Correção do documento" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "" +msgstr "Doar" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Terminado" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" -msgstr "Transferir" +msgstr "Descarregar" #: MiscResources.resx$DownloadError$Message msgid "Download Error" -msgstr "Erro na Transferência" +msgstr "Erro ao descarregar" #: MiscResources.resx$DownloadNeeded$Message msgid "Download Needed" -msgstr "" +msgstr "Descarga necessária" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" -msgstr "Evolução da Transferência" +msgstr "Progresso da descarga" + +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "PPP" #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" -msgstr "" +msgstr "Frente e verso" + +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Controlador ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "Controlador de rede ESCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "Controlador ESCL USB" #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Editar" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Editar com {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Editar com..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Enviar tudo por e-mail" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Enviar tudo por e-mail como PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "Enviar PDF por Email" +msgstr "Enviar por e-mail" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" -msgstr "" +msgstr "Progresso da ação de enviar PDF" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "E-mail selecionado" #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Enviar seleção por e-mail como PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" -msgstr "Definições de Email" +msgstr "Definições de e-mail" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" -msgstr "" +msgstr "Ativar guardar automático" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Ativar registos de depuração" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" -msgstr "Encriptar PDF" +msgstr "Cifrar PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" -msgstr "Encriptação" +msgstr "Cifra" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "Ficheiro EMF (*.emf)" +msgstr "Ficheiro Enhanced Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Erro" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Erro ao iniciar aplicação {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,49 +665,55 @@ msgstr "Tamanho estimado da transferência: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "Ficheiro EXIF" +msgstr "Ficheiro Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" -msgstr "" +msgstr "Excluir páginas vazias" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Rápido" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Alimentador" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "Nome do ficheiro" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Nome do ficheiro:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" -msgstr "" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" +msgstr "Caminho do ficheiro:" + +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Corrigir balanço de brancos e remover ruído" #: UiStrings.resx$Flip$Message msgid "Flip" -msgstr "Rodar" +msgstr "Inverter" + +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Inverter lados das páginas duplas" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" -msgstr "" +msgstr "Inverter páginas digitalizadas em frente e verso" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "" +msgstr "Para qualidade JPEG alta (80+), aumente a qualidade da imagem no seu perfil para obter melhores resultados." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" msgstr "Ficheiro GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" -msgstr "Instalar mais idiomas" +msgstr "Obter mais idiomas" #: SettingsResources.resx$Source_Glass$Message msgid "Glass" @@ -665,24 +721,27 @@ msgstr "Vidro" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Escala de cinza" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Alinhamento horizontal:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" -msgstr "Hora (0-23)" +msgstr "Horas (0-23)" #: UiStrings.resx$HueSaturation$Message msgid "Hue / Saturation" -msgstr "" +msgstr "Tom/Saturação" + +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Anfitrião" #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" @@ -694,20 +753,20 @@ msgstr "Imagem" #: MiscResources.resx$FileTypeImageFiles$Message msgid "Image Files" -msgstr "" +msgstr "Ficheiros de imagem" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" -msgstr "" +msgstr "Qualidade da imagem" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" -msgstr "Definições da Imagem" +msgstr "Definições de imagem" #: MiscResources.resx$ImageSaved$Message msgid "Image saved." -msgstr "" +msgstr "Imagem guardada." #: UiStrings.resx$Import$Message msgid "Import" @@ -715,23 +774,23 @@ msgstr "Importar" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" -msgstr "" +msgstr "Progresso da importação" #: MiscResources.resx$ImportingFormat$Message msgid "Importing {0}..." -msgstr "" +msgstr "A importar {0}..." #: MiscResources.resx$Importing$Message msgid "Importing..." -msgstr "" +msgstr "A importar..." #: MiscResources.resx$Install$Message msgid "Install {0}" -msgstr "" +msgstr "Instalar {0}" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" -msgstr "Instalação completa" +msgstr "Instalação concluída" #: MiscResources.resx$InstallFailedTitle$Message msgid "Installation Failed" @@ -739,15 +798,19 @@ msgstr "Falha na instalação" #: MiscResources.resx$InstallCompletePromptRestart$Message msgid "Installation complete. Do you want to restart NAPS2 now?" -msgstr "Instalação completa. Quer reiniciar o NAPS2 agora?" +msgstr "Instalação concluída. Reiniciar NAPS2 agora?" #: MiscResources.resx$InstallFailed$Message msgid "Installation failed." msgstr "Falha na instalação." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Interface" + #: UiStrings.resx$Interleave$Message msgid "Interleave" -msgstr "Intercalamento" +msgstr "Intercalar" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" @@ -755,61 +818,89 @@ msgstr "Ficheiro JPG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "Ficheiro JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Qualidade JPEG" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Manter imagens entre sessões" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Teclas de atalho" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" -msgstr "Palavras chave:" +msgstr "Palavras-chave:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Idioma" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Avaliar" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Esquerda" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" -msgstr "" +msgstr "Legado (interface nativa)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Claro" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Gosta de NAPS2?" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" -msgstr "" +msgstr "Carregar imagens em NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" -msgstr "" +msgstr "Tornar PDF pesquisáveis via OCR" + +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "IP manual" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Alta qualidade (ficheiros grandes)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Transferência de memória" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metadados" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" -msgstr "" +msgstr "Minutos (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Mês (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" -msgstr "" +msgstr "Mais informação" #: UiStrings.resx$MoveDown$Message msgid "Move Down" @@ -819,35 +910,47 @@ msgstr "Mover para baixo" msgid "Move Up" msgstr "Mover para cima" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Vários idiomas" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Vários idiomas..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" -msgstr "" +msgstr "Múltiplas digitalizações (intervalo fixo entre digitalizações)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" -msgstr "" +msgstr "Múltiplas digitalizações (perguntar entre digitalizações)" #: MiscResources.resx$NAPS2$Message #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "" +msgstr "NAPS2 é totalmente gratuito. Considere fazer um donativo." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" -msgstr "" +msgstr "Nome (opcional)" #: MiscResources.resx$NameMissing$Message msgid "Name missing." -msgstr "Falta o nome." +msgstr "Nome em falta." + +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Transferência nativa" #: UiStrings.resx$New$Message msgid "New" @@ -855,219 +958,214 @@ msgstr "Novo" #: UiStrings.resx$NewProfile$Message msgid "New Profile" -msgstr "Novo Perfil" +msgstr "Novo perfil" #: UiStrings.resx$Next$Message msgid "Next" msgstr "Seguinte" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" -msgstr "" +msgstr "Digitalização seguinte" #: MiscResources.resx$NoDeviceSelected$Message #: SdkResources.resx$NoDeviceSelected$Message msgid "No device selected." -msgstr "Nenhum dispositivo escolhido." +msgstr "Nenhum dispositivo selecionado." + +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Não foram encontrados dispositivos." #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Não existem folhas no alimentador." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "" +msgstr "Serviço não selecionado." #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message msgid "No scanning device was found." -msgstr "Nenhum scanner foi encontrado." +msgstr "Não foi encontrado um digitalizador." #: MiscResources.resx$NoUpdates$Message msgid "No updates available." -msgstr "" +msgstr "Não existem atualizações." #: SettingsResources.resx$TiffComp_None$Message msgid "None" -msgstr "" +msgstr "Nenhuma" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Agora não" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" -msgstr "" +msgstr "Número de digitalizações:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" -msgstr "Transferir OCR" +msgstr "Descarregar OCR" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "" +msgstr "Progresso de OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Configurar OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Idioma OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "" +msgstr "Modo OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" -msgstr "" +msgstr "Compensar largura em função do alinhamento (WIA)" #: SettingsResources.resx$TwainImpl_OldDsm$Message msgid "Old DSM" -msgstr "" +msgstr "DSM antigo" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" -msgstr "" +msgstr "Um ficheiro por página" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" -msgstr "" +msgstr "Um ficheiro por digitalização" #: MiscResources.resx$FilesCouldNotBeDownloaded$Message msgid "One or more files could not be downloaded." -msgstr "Não foi possível transferir um ou mais ficheiros." +msgstr "Não foi possível descarregar um ou mais ficheiros." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Permitir apenas uma instância NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" -msgstr "" +msgstr "Abrir pasta" #: MiscResources.resx$ActiveOperations$Message msgid "Operation in Progress" -msgstr "" +msgstr "Operação em curso" + +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (novo)" #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook na Web" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" -msgstr "" +msgstr "Resultado" #: MiscResources.resx$OverwriteFile$Message msgid "Overwrite File" -msgstr "O ficheiro já existe. Deseja substituir?" +msgstr "Substituir ficheiro" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" -msgstr "" +msgstr "Palavra-passe de proprietário:" #: MiscResources.resx$FileTypePdf$Message msgid "PDF Document (*.pdf)" -msgstr "" +msgstr "Documento PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Definições PDF" #: MiscResources.resx$PdfSaved$Message msgid "PDF saved." -msgstr "" +msgstr "PDF guardado." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "Ficheiro PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" -msgstr "Tamanho da folha:" +msgstr "Tamanho da página:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Origem do papel:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Palavra-passe" #: UiStrings.resx$Paste$Message msgid "Paste" -msgstr "" +msgstr "Colar" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" -msgstr "" +msgstr "Marcadores" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Porta" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" -msgstr "" +msgstr "Pós-processamento" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Antecipar execução OCR após a digitalização" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." -msgstr "" +msgstr "Prima Iniciar quando quiser." #: UiStrings.resx$PreviewFormTitle$Message msgid "Preview" msgstr "Pré-visualizar" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Pré-visualizar:" @@ -1080,85 +1178,98 @@ msgstr "Anterior" msgid "Print" msgstr "Imprimir" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" -msgstr "Configurar Perfil" +msgstr "Definições de perfil" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" -msgstr "" +msgstr "Perfil:" #: UiStrings.resx$Profiles$Message #: UiStrings.resx$ProfilesFormTitle$Message msgid "Profiles" msgstr "Perfis" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Perguntar se selecionado" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" -msgstr "" +msgstr "Pedir caminho do ficheiro" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "Serviço" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." -msgstr "" +msgstr "Pronto para a {0} digitalização." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Recuperar" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" -msgstr "Recuperar Imagens Digitalizadas" +msgstr "Recuperar imagens digitalizadas" #: MiscResources.resx$Recovering$Message msgid "Recovering..." -msgstr "" +msgstr "A recuperar..." #: MiscResources.resx$RecoveryProgress$Message msgid "Recovery Progress" -msgstr "" +msgstr "Progresso da recuperação" + +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Refazer" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Refazer {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" -msgstr "Lembrar estas configurações" +msgstr "Memorizar definições" #: UiStrings.resx$Reorder$Message msgid "Reorder" -msgstr "" +msgstr "Reordenar" #: UiStrings.resx$Reset$Message msgid "Reset" -msgstr "" +msgstr "Repor" #: MiscResources.resx$ResetImage$Message msgid "Reset Image" -msgstr "" +msgstr "Repor imagem" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Resolução:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" -msgstr "" +msgstr "Restaurar padrões" #: UiStrings.resx$Reverse$Message msgid "Reverse" -msgstr "" +msgstr "Inverter" + +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Inverter tudo" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Inverter seleção" #: UiStrings.resx$Revert$Message msgid "Revert" -msgstr "" +msgstr "Reverter" #: SettingsResources.resx$HorizontalAlign_Right$Message msgid "Right" @@ -1166,345 +1277,456 @@ msgstr "Direita" #: UiStrings.resx$Rotate$Message msgid "Rotate" -msgstr "" +msgstr "Rodar" #: UiStrings.resx$RotateLeft$Message msgid "Rotate Left" -msgstr "Rodar para a esquerda" +msgstr "Rodar à esquerda" #: UiStrings.resx$RotateRight$Message msgid "Rotate Right" -msgstr "Rodar para a direita" +msgstr "Rodar à direita" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "" +msgstr "Enviar para segundo plano" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "" +msgstr "A executar OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "Controlador SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Guardar" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Guardar tudo" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Guardar tudo como imagens" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Guardar tudo como PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message msgid "Save Images" -msgstr "Salvar imagens" +msgstr "Guardar imagens" #: MiscResources.resx$SaveImagesProgress$Message msgid "Save Images Progress" -msgstr "" +msgstr "Progresso da gravação de imagens" #: MiscResources.resx$SavePdf$Message #: UiStrings.resx$SavePdf$Message msgid "Save PDF" -msgstr "Salvar PDF" +msgstr "Guardar PDF" #: MiscResources.resx$SavePdfProgress$Message msgid "Save PDF Progress" -msgstr "" +msgstr "Progresso da gravação PDF" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Guardar seleção" #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Guardar seleção como imagem" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Guardar seleção como PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" -msgstr "" +msgstr "Guardar para um ficheiro" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" -msgstr "" +msgstr "Guardar para vários ficheiros" #: MiscResources.resx$BatchStatusSaving$Message msgid "Saving batch results..." -msgstr "" +msgstr "A guardar lote..." #: MiscResources.resx$SavingFormat$Message msgid "Saving {0}..." -msgstr "" +msgstr "A guardar {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" -msgstr "" +msgstr "Ajustar à janela" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Escala:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Digitalizar" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" -msgstr "" +msgstr "Configurações de digitalização" + +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Digitalizar com o perfil padrão" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Digitalizar com o novo perfil" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Digitalizar com o perfil {0}" #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Imagem digitalizada" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Partilha de digitalizador" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" -msgstr "" +msgstr "A digitalizar página {0}" #: MiscResources.resx$BatchStatusScanPage$Message msgid "Scanning page {0} (scan {1})..." -msgstr "" +msgstr "A digitalizar página {0} (digitalização {1})..." #: MiscResources.resx$BatchStatusPage$Message #: MiscResources.resx$ScanProgressPage$Message msgid "Scanning page {0}..." -msgstr "" +msgstr "A digitalizar página {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "A procurar dispositivos..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" -msgstr "" +msgstr "Segundos (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" -msgstr "" +msgstr "Selecionar" #: UiStrings.resx$SelectAll$Message msgid "Select All" -msgstr "" +msgstr "Selecionar tudo" + +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Selecionar dispositivo" -#: FSelectDevice.resx$$this.Text$Message #: UiStrings.resx$SelectSource$Message msgid "Select Source" -msgstr "" +msgstr "Selecionar origem" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." -msgstr "Escolha um perfil antes de clicar Scan." +msgstr "Escolha um perfil antes de Digitalizar." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" -msgstr "" +msgstr "Escolha um ou mais idiomas:" #: MiscResources.resx$SelectedCount$Message msgid "Selected ({0})" -msgstr "" +msgstr "Selecionadas ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" -msgstr "" +msgstr "Separar ficheiros com Patch-T" #: UiStrings.resx$SetDefault$Message msgid "Set Default" -msgstr "" +msgstr "Definir como padrão" + +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Definições" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Partilhar" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Partilhar mesmo se NAPS2 estiver fechado" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Definições do digitalizador partilhado" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Os digitalizadores partilhados podem ser usados a partir de outros computadores na rede local, selecionando \"Controlador ESCL\" nas definições do perfil NAPS2 do outro computador." #: UiStrings.resx$Sharpen$Message msgid "Sharpen" -msgstr "" +msgstr "Nitidez" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Atalho" + +#: UiStrings.resx$Show$Message msgid "Show" -msgstr "" +msgstr "Mostrar" + +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Mostrar barra de ferramentas \"Perfis\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Mostrar progresso TWAIN nativo" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Mostrar número de páginas" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Barra lateral" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" -msgstr "" +msgstr "Ficheiros de uma página" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" -msgstr "" +msgstr "Digitalização individual" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" -msgstr "" +msgstr "Ignorar questão para guardar" + +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Separar" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Start$Message msgid "Start" -msgstr "" +msgstr "Iniciar" + +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Parar partilha de digitalizador" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" -msgstr "" +msgstr "Ajustar ao tamanho da página" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" -msgstr "" +msgstr "Assunto:" #: MiscResources.resx$FileTypeTiff$Message msgid "TIFF File (*.tiff, *.tif)" msgstr "Ficheiro TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" -msgstr "Driver TWAIN" +msgstr "Controlador TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" -msgstr "" +msgstr "Detalhes técnicos" #: MiscResources.resx$TesseractNotAvailable$Message #: SdkResources.resx$TesseractNotAvailable$Message msgid "The OCR engine is not available. Make sure to install the required package:" -msgstr "" +msgstr "O motor OCR não está disponível. Certifique-se que instalou o pacote necessário:" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "A operação OCR caducou." #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" -msgstr "" +msgstr "O controlador SANE não está disponível. Certifique-se que instala os pacotes necessários:" #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message msgid "The file '{0}' could not be imported." -msgstr "" +msgstr "Não foi possível importar o ficheiro \"{0}\"." #: MiscResources.resx$ImportErrorNAPS2Pdf$Message msgid "The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported." -msgstr "" +msgstr "Não foi possível importar o ficheiro \"{0}\". Apenas pode importar ficheiros PDF gerados por NAPS2." #: MiscResources.resx$FileInUse$Message msgid "The file could not be overwritten because it is currently in use." -msgstr "" +msgstr "O ficheiro está a ser utilizado e não pode ser substituído." #: MiscResources.resx$ConfirmOverwriteFile$Message msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "O ficheiro {0} já existe. Deseja substituir?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "O seguinte ficheiro está cifrado e requer uma palavra-passe para ser aberto:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message msgid "The scanner has a paper jam." -msgstr "" +msgstr "Existe papel encravado no digitalizador." #: MiscResources.resx$DeviceWarmingUp$Message #: SdkResources.resx$DeviceWarmingUp$Message msgid "The scanner is warming up." -msgstr "" +msgstr "O digitalizador está a aquecer." #: MiscResources.resx$DeviceCoverOpen$Message #: SdkResources.resx$DeviceCoverOpen$Message msgid "The scanner's cover is open." -msgstr "" +msgstr "A tampa do digitalizador está aberta." #: MiscResources.resx$DriverNotSupported$Message #: SdkResources.resx$DriverNotSupported$Message msgid "The selected driver is not supported on this system." -msgstr "" +msgstr "O controlador escolhido não é suportado neste sistema." #: MiscResources.resx$DeviceNotFound$Message #: SdkResources.resx$DeviceNotFound$Message msgid "The selected scanner could not be found." -msgstr "O scanner escolhido não foi encontrado." +msgstr "O digitalizador escolhido não foi encontrado." #: MiscResources.resx$NoFeederSupport$Message #: SdkResources.resx$NoFeederSupport$Message msgid "The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver." -msgstr "" +msgstr "Aparentemente, o digitalizador escolhido não possui um alimentador. Se o seu digitalizador possuir um alimentador, tente usar um controlador diferente." #: MiscResources.resx$NoDuplexSupport$Message #: SdkResources.resx$NoDuplexSupport$Message msgid "The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver." -msgstr "" +msgstr "Aparentemente, o digitalizador escolhido não permite digitalização em frente e verso. Se é suposto que o digitalizador o permita, tente usar um controlador diferente." #: MiscResources.resx$DeviceBusy$Message #: SdkResources.resx$DeviceBusy$Message msgid "The selected scanner is busy." -msgstr "" +msgstr "O digitalizador escolhido está ocupado." #: MiscResources.resx$DeviceOffline$Message #: SdkResources.resx$DeviceOffline$Message msgid "The selected scanner is offline." -msgstr "O scanner escolhido está offline." +msgstr "O digitalizador escolhido está desligado." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "O processo de trabalho falhou." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "O processo de trabalho falhou. Analise o visualizador de eventos Windows," + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Tema:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" -msgstr "" +msgstr "Opções TIFF" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" -msgstr "" +msgstr "Tempo entre digitalizações (segundos):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" -msgstr "" +msgstr "Título:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Ferramentas" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" -msgstr "" +msgstr "Implementação TWAIN:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "US Legal (8.5x14 pol)" +msgstr "US Legal (8,5 × 14 pol.)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" -msgstr "US Letter (8.5x11 pol)" +msgstr "US Letter (8,5 × 11 pol.)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Remover atribuição" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Desfazer" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Desfazer {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Digitalizador desconhecido" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" -msgstr "" +msgstr "Alterações não guardadas" #: MiscResources.resx$UpdateProgress$Message msgid "Update Progress" -msgstr "" +msgstr "Progresso da atualização" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "A verificação de atualizações está desativada." #: MiscResources.resx$Updating$Message msgid "Updating..." -msgstr "" +msgstr "A atualizar..." #: MiscResources.resx$UploadingEmail$Message msgid "Uploading email..." -msgstr "" +msgstr "A enviar e-mail..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" -msgstr "" +msgstr "Usar interface nativa" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" -msgstr "Usar configuração pré-definida" +msgstr "Usar definições predefinidas" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" -msgstr "" +msgstr "Palavra-passe do utilizador:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." -msgstr "" +msgstr "Para usar OCR, é necessário descarregar os idiomas que pretende digitalizar." #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message @@ -1513,119 +1735,120 @@ msgstr "Versão {0}" #: UiStrings.resx$View$Message msgid "View" -msgstr "" +msgstr "Ver" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" -msgstr "Driver WIA" +msgstr "Controlador WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Aguardar que o TWAIN termine..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." -msgstr "" +msgstr "A aguardar autorização..." #: MiscResources.resx$BatchStatusWaitingForScan$Message msgid "Waiting for scan {0}..." -msgstr "" +msgstr "A aguardar digitalização {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" -msgstr "" +msgstr "Limite de brancos" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Versão WIA:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" -msgstr "" +msgstr "Ano" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" -msgstr "" +msgstr "Anos (00-99)" #: MiscResources.resx$PdfNoPermissionToExtractContent$Message #: SdkResources.resx$PdfNoPermissionToExtractContent$Message msgid "You do not have permission to copy content from the file '{0}'." -msgstr "" +msgstr "Não tem permissão para copiar conteúdo do ficheiro \"{0}\"." #: MiscResources.resx$DontHavePermission$Message msgid "You don't have permission to save files at this location." -msgstr "Não tem permissão para salvar o ficheiro neste local." +msgstr "Não tem permissão para guardar ficheiros neste local." #: MiscResources.resx$ExitWithUnsavedChanges$Message msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" -msgstr "" +msgstr "Existem alterações não guardadas. Tem a certeza de que pretende sair e rejeitar as alterações?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Ampliar" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" -msgstr "" +msgstr "Tamanho real" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" -msgstr "" +msgstr "Ampliar" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" -msgstr "" +msgstr "Reduzir" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "pol" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" -msgstr "" +msgstr "de {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1} × {2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" -msgstr "" +msgstr "{0} / {1} ficheiros" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} dispositivo(s) encontrado(s)." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} ppp" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" -msgstr "" +msgstr "{0} digitalizações realizadas em {1} às {2} podem não ter sido guardadas e são recuperáveis. Recuperar agora?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." -msgstr "" +msgstr "{0} imagens guardadas." #: MiscResources.resx$PdfStatus$Message #: UiStrings.resx$XOfY$Message diff --git a/NAPS2.Lib/Lang/po/ro.po b/NAPS2.Lib/Lang/po/ro.po index ed240686c5..e3c58a8550 100644 --- a/NAPS2.Lib/Lang/po/ro.po +++ b/NAPS2.Lib/Lang/po/ro.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Romanian\n" "Language: ro\n" @@ -19,41 +19,25 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Acțiune implicită buton \"Salvează:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Acțiune implicit buton \"Scanează\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Meniul \"Scanare\" schimbă profilul implicit" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "Un dispozitiv găsit." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" -msgstr "Color pe 24 biți" - -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" +msgstr "Culoare 24 biți" #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" @@ -76,12 +60,15 @@ msgstr "Despre" msgid "Acquiring data..." msgstr "Colectează datele..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Acţiune" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Avansat" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Setări avansate profil" @@ -93,35 +80,35 @@ msgstr "Toate ({0})" msgid "All Files" msgstr "Toate fișierele" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Permite note explicative" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Permite copierea conținutului" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Permite copierea conținutului pentru Accesibilitate" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Permite asamblarea documentului" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Permite modificarea documentului" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Permite completarea formularului" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Permite printarea la calitate maxima" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Permite imprimarea" @@ -131,28 +118,37 @@ msgstr "Deintercalare alternativă" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" -msgstr "" +msgstr "Intercalare alternativă" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Transfer alternativ" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Întreabă întotdeauna" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Solicită întotdeauna" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Este necesară o nouă componentă pentru a putea importa acest fișier PDF. O adăugăm acum?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "A intervenit o eroare la instalarea actualizării." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "A apărut o eroare la rularea OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "A survenit o eroare la încercarea de autorizare." +msgstr "A apărut o eroare la încercarea de autorizare." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." msgstr "A apărut o eroare în timpul salvării automate." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "A intervenit o eroare la instalarea actualizării." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "A apărut o eroare la salvarea fișierului." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "O operație este în curs. Sigur doriți să ieșiți și să anulați operația?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "A apărut o eroare necunoscută la scanarea multiplă." +msgid "An unknown error occurred during the batch scan." +msgstr "A apărut o eroare necunoscută în timpul scanării lotului." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -188,9 +184,17 @@ msgstr "Este disponibilă o actualizare a componentei OCR." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Driver Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Aplicație" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Aplică luminozitate/contrast după scanare" @@ -222,49 +226,55 @@ msgstr "Sigur doriți să ștergeți {0} element(e)?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Sigur doriți să ștergeți {0} profile?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Ești sigur că vrei să oprești distribuirea {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Sunteți sigur că vreți să anulați modificările pentru {0} imagini?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Asociază" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Numele ataşamentului:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autor:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autorizează" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "Auto" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" -msgstr "Setări de salvare automată" +msgstr "Setări salvare automată" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "Incrementează automat numărul (o cifră)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Incrementează automat numărul (două cifre)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Incrementează automat numarul (trei cifre)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Incrementează automat numărul (patru cifre)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Rulează automat modulul OCR după scanare" @@ -275,10 +285,10 @@ msgstr "B4 (250 x 353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Scanare multiplă" @@ -296,9 +306,8 @@ msgstr "Scanarea multiplă oprită din cauza unei erori." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Cel mai bun" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Adâncime biți:" @@ -309,10 +318,10 @@ msgstr "Fișiere bitmap (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Alb-negru" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Pagini goale" @@ -320,33 +329,19 @@ msgstr "Pagini goale" msgid "Brightness / Contrast" msgstr "Luminozitate - Contrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Luminozitate:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Nu îți găsești scanerul? Citește despre limitările NAPS2 Flatpak." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Renunță" @@ -363,7 +358,7 @@ msgstr "Se anulează...." msgid "Center" msgstr "Centrează" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Schimbă" @@ -375,7 +370,7 @@ msgstr "Verificare actualizări" msgid "Checking..." msgstr "Verificare..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Alege serviciul de email" @@ -383,7 +378,6 @@ msgstr "Alege serviciul de email" msgid "Choose Profile" msgstr "Alegeți profilul" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Alegeți dispozitivul" @@ -395,9 +389,9 @@ msgstr "Şterge" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Ștergere totală" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Șterge imaginile după salvare" @@ -405,19 +399,33 @@ msgstr "Șterge imaginile după salvare" msgid "Close" msgstr "Închide" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Combină" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "A fost întreruptă comunicarea cu dispozitivul de scanare." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Compatibilitate" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Comprimare:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Conectare" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Eroare de conexiune." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" -msgstr "" +msgstr "Contrast:" #: UiStrings.resx$Copy$Message msgid "Copy" @@ -433,9 +441,9 @@ msgstr "Copiere..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Drepturi de autor {0} Colaboratori NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Pragul de acoperire" @@ -443,7 +451,7 @@ msgstr "Pragul de acoperire" msgid "Crop" msgstr "Decupează" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Decupează la mărimea paginii" @@ -451,10 +459,14 @@ msgstr "Decupează la mărimea paginii" msgid "Custom ({0}x{1} {2})" msgstr "Personalizat ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Mărime personalizată a paginii" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Rotație personalizată" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "Adresă server SMTP personalizată" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Personalizat..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Ziua (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Implicit" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Cale implicită fișier:" @@ -487,7 +504,6 @@ msgstr "Cale implicită fișier:" msgid "Deinterleave" msgstr "Deîntrețesere" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Îndreptare" msgid "Deskew Progress" msgstr "Progres îndreptare" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Îndreaptă paginile scanate" @@ -509,35 +525,31 @@ msgstr "Îndreaptă paginile scanate" msgid "Deskewing..." msgstr "Îndreptare..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Dispozitiv:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Dimensiuni" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Nume afișat:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Corecție Document" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "" +msgstr "Donează" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Finalizat" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Descarcă" @@ -550,22 +562,50 @@ msgstr "Eroare la descărcare" msgid "Download Needed" msgstr "Descărcarea este necesară" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Progresul descărcării" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Fată-verso" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Driver-ul ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "Driver de rețea ESCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "Driver USB ESCL" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Editare" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Trimiteți prin e-mail tuturor" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Trimiteți prin e-mail tuturor ca PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,25 +616,32 @@ msgstr "Trimite PDF prin email" msgid "Email PDF Progress" msgstr "Progres trimitere PDF prin email" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Trimiteți prin e-mail partea selectată" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Trimiteți prin e-mail partea selectată ca PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Configurări email" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Activează salvarea automată" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Activează jurnalul de depanare" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Criptează PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Criptare" @@ -602,12 +649,15 @@ msgstr "Criptare" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Enhanced Windows Metafile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Eroare" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,46 +665,52 @@ msgstr "Mărime estimată de download: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Fișier imagine interschimbabil (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Elimină paginile goale" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Rapid" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Alimentator" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Nume fişier" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Cale fișier:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Repară balanța de alb și elimină zgomotul" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Întoarce" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Întoarcere laturi anterioare ale paginilor duplex" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Întoarce paginile de pe verso" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "" +msgstr "Pentru calități JPEG înalte (80+), creșteți calitatea imaginii în profilul dvs. pentru cele mai bune rezultate." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" msgstr "Fișier GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Instalează mai multe limbi" @@ -665,18 +721,17 @@ msgstr "Sticlă" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Tonuri de gri" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Aliniere pe orizontală:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Ora (0-23)" @@ -684,6 +739,10 @@ msgstr "Ora (0-23)" msgid "Hue / Saturation" msgstr "Nuanță / Saturație" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Host" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Icoane din:" @@ -696,12 +755,12 @@ msgstr "Imagine" msgid "Image Files" msgstr "Fișiere Imagine" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Calitatea Imaginii" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Preferințe imagine" @@ -745,6 +804,10 @@ msgstr "Instalare completă. Doriți să reporniți NAPS2 acum?" msgid "Installation failed." msgstr "Instalarea a eșuat." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Interfață" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Întrețesere" @@ -755,59 +818,87 @@ msgstr "Fișier JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "Fișier JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Calitate JPEG" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Păstrează imaginile pe durata sesiunilor" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Comenzi rapide tastatură" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Cuvinte cheie:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Limbă" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Stânga" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" +msgstr "Vechi (numai interfața de utilizare nativă)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Încarcă imagini în NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Crează fișier PDF cu posibilitatea căutării în text" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "IP manual" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Calitate maximă (fișiere mari)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Transfer de memorie" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metadate" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" -msgstr "" +msgstr "Minute (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Luna (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Mai multe informații" @@ -819,11 +910,19 @@ msgstr "Mai jos" msgid "Move Up" msgstr "Mai sus" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Limbi multiple" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Limbi multiple..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Scanare multiplă (cu o pauză între scanări)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Scanare multiplă (cu confirmare între scanări)" @@ -831,17 +930,17 @@ msgstr "Scanare multiplă (cu confirmare între scanări)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "" +msgstr "NAPS2 este complet gratuit. Luați în considerare să faceți o donație." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Nume (opțional)" @@ -849,6 +948,10 @@ msgstr "Nume (opțional)" msgid "Name missing." msgstr "Lipsește numele." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Transfer nativ" + #: UiStrings.resx$New$Message msgid "New" msgstr "Nou" @@ -861,7 +964,7 @@ msgstr "Profil Nou" msgid "Next" msgstr "Înainte" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Următoarea scanare" @@ -870,15 +973,18 @@ msgstr "Următoarea scanare" msgid "No device selected." msgstr "Nici un dispozitiv selectat." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Niciun dispozitiv găsit." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Nici o foaie in alimentator." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "" +msgstr "Niciun furnizor selectat." #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message @@ -895,60 +1001,45 @@ msgstr "Nimic" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Nici alt scaner PDF" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Nu acum" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Numarul de scanări:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Descarcă OCR" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "" +msgstr "OCR în lucru" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Configurează OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Limba OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "" +msgstr "Modul OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "Ok" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Offset bazat pe aliniere (WIA)" @@ -956,13 +1047,11 @@ msgstr "Offset bazat pe aliniere (WIA)" msgid "Old DSM" msgstr "DSM vechi" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" -msgstr "" +msgstr "Un fișier pe pagină" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Fișiere individuale per scanare" @@ -970,7 +1059,11 @@ msgstr "Fișiere individuale per scanare" msgid "One or more files could not be downloaded." msgstr "Unul sau mai multe fișiere nu s-au putut descărca." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Permite doar o singură instanță NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Deschide folder-ul" @@ -978,11 +1071,15 @@ msgstr "Deschide folder-ul" msgid "Operation in Progress" msgstr "Operaţiune în desfăşurare" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Acces Outlook Web" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Rezultat" @@ -990,16 +1087,16 @@ msgstr "Rezultat" msgid "Overwrite File" msgstr "Suprascrie fișierul" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Parolă Autor:" #: MiscResources.resx$FileTypePdf$Message msgid "PDF Document (*.pdf)" -msgstr "" +msgstr "Document PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Setări PDF" @@ -1009,35 +1106,33 @@ msgstr "PDF salvat." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "Fișier PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Dimensiune pagină:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Sursă hârtie:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Parolă" @@ -1045,21 +1140,24 @@ msgstr "Parolă" msgid "Paste" msgstr "Lipire" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Înlocuitori" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Postprocesare" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Rulează automat modulul OCR după scanare" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Apasă Start când ești pregătit." @@ -1067,7 +1165,7 @@ msgstr "Apasă Start când ești pregătit." msgid "Preview" msgstr "Previzualizare" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Previzualizare:" @@ -1080,12 +1178,11 @@ msgstr "Precedent" msgid "Print" msgstr "Tipărire" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Configurări profil" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profil:" @@ -1094,23 +1191,27 @@ msgstr "Profil:" msgid "Profiles" msgstr "Profiluri" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Solicită dacă este selectat" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Cere calea fișierului" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Furnizor" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Gata de scanare {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Recuperează" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Recuperează imaginile scanate" @@ -1122,9 +1223,15 @@ msgstr "Recuperare..." msgid "Recovery Progress" msgstr "Progres recuperare" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Refacere" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Refacere {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Păstrează aceste setări" @@ -1134,21 +1241,17 @@ msgstr "Reordonare" #: UiStrings.resx$Reset$Message msgid "Reset" -msgstr "" +msgstr "Resetare" #: MiscResources.resx$ResetImage$Message msgid "Reset Image" msgstr "Reinițializează imaginea" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Rezoluție:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Restaurare valori implicite" @@ -1156,6 +1259,14 @@ msgstr "Restaurare valori implicite" msgid "Reverse" msgstr "Inversează" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Inversează Tot" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Inversează ce este selectat" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Revenire" @@ -1176,31 +1287,34 @@ msgstr "Rotește la stânga" msgid "Rotate Right" msgstr "Rotește la dreapta" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Rulează în fundal" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "" +msgstr "Se rulează OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "Driver SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Salvează" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Salvează tot" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Salvează toate ca Imagini" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Salvează toate ca PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Salvează PDF" msgid "Save PDF Progress" msgstr "Progres salvare PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Salvează selecția" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Salvați cele selectate ca imagini" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Salvați cele selectate ca PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Salvează într-un singur fișier" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Salvează în fișiere separate" @@ -1244,29 +1363,45 @@ msgstr "Salvare rezultate procesare multiplă..." msgid "Saving {0}..." msgstr "Salvare {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Scalează odată cu fereastra" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Scalare:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Scanează" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Configurare scanare" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Scanare cu profilul implicit" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Scanare cu profil nou" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Scanează cu profilul {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Imagine scanată" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Distribuire scaner" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Scanare pagina {0}" @@ -1280,11 +1415,14 @@ msgstr "Scanare pagina {0} (scan {1})..." msgid "Scanning page {0}..." msgstr "Scanare pagina {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Căutare dispozitive..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Secunde (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Selectează" @@ -1293,7 +1431,10 @@ msgstr "Selectează" msgid "Select All" msgstr "Selectează tot" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Selectați dispozitivul" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Alegeți sursa" @@ -1302,7 +1443,6 @@ msgstr "Alegeți sursa" msgid "Select a profile before clicking Scan." msgstr "Selectați un profil înainte de a apăsa Scanare." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Alegeți una sau mai multe limbi:" @@ -1311,8 +1451,7 @@ msgstr "Alegeți una sau mai multe limbi:" msgid "Selected ({0})" msgstr "Selectate ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Separarea fișierelor pe baza codurilor Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Separarea fișierelor pe baza codurilor Patch-T" msgid "Set Default" msgstr "Stabilește ca implicit" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Setări" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Distribuie" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Distribuie chiar și atunci când NAPS2 este închis" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Setări scaner partajat" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Scanerele partajate pot fi utilizate de alte calculatoare din rețeaua locală selectând \"ESCL Driver\" din setările de profil ale celorlalte calculatoare NAPS2." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Claritate" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Comandă rapidă" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Arată" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Afișează bara de instrumente \"Profiluri\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Arată progresul nativ TWAIN" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Arată numerele paginii" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Bară laterală" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Fișier cu o singură pagină." -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "O singură scanare" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Omite notificarea de salvare" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Împarte" + +#: UiStrings.resx$Start$Message msgid "Start" +msgstr "Începe" + +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Redimensionează la mărimea paginii" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Subiect:" @@ -1358,12 +1544,11 @@ msgstr "Subiect:" msgid "TIFF File (*.tiff, *.tif)" msgstr "Fișier TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "Driver TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Detalii tehnice" @@ -1372,10 +1557,14 @@ msgstr "Detalii tehnice" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "Motorul OCR nu este disponibil. Asigură-te că ai instalat pachetul necesar.:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Operațiunea OCR a expirat." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" -msgstr "" +msgstr "Driverul SANE nu este disponibil. Asigurați-vă că instalați pachetele necesare:" #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message @@ -1394,9 +1583,9 @@ msgstr "Fișierul nu poate fi suprascris deoarece este în uz." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Fișierul {0} deja există. Doriți să-l suprascrieți?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Fișierul este criptat și este nevoie de parolă pentru a-l deschide: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Următorul fișier este criptat și necesită o parolă pentru a se deschide:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,28 +1632,64 @@ msgstr "Scanerul selectat este ocupat." msgid "The selected scanner is offline." msgstr "Scanerul selectat este deconectat." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Procesul worker s-a oprit neașteptat." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Procesul worker s-a oprit neașteptat. Verificați Windows event viewer." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Pasăre tunet" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Opțiuni Tiff" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Durată între scanări (secunde):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Titlu:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Unelte" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Implementare Twain:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 in)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" +msgstr "US Letter (8.5x11 in)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Anulează atribuirea" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Anulare" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Anulează {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" msgstr "" #: MiscResources.resx$UnsavedChanges$Message @@ -1477,7 +1702,7 @@ msgstr "Progres Actualizare" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Verificarea actualizării este dezactivată." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Se actualizează..." msgid "Uploading email..." msgstr "Se încarcă e-mailul ..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Folosește interfața nativă" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Folosește configurările prestabilite" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Parolă utilizator:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Folosirea funcției OCR necesită descărcarea fiecărei limbi pe care dorești să o folosești." @@ -1515,16 +1737,15 @@ msgstr "Versiune {0}" msgid "View" msgstr "Vizualizare" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "Driver WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Se așteaptă ca scanerul TWAIN să termine..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Se așteaptă autorizarea...." @@ -1532,19 +1753,19 @@ msgstr "Se așteaptă autorizarea...." msgid "Waiting for scan {0}..." msgstr "Așteptați scanarea {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Limita albă" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Versiunea Wia:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Anul" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Anul (00-99)" @@ -1561,65 +1782,67 @@ msgstr "Nu aveţi permisiune de salvare în această locație." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Există modificări care nu sunt salvate. Sigur vreți să ieșiți și să renunțați la aceste modificări?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" -msgstr "" +msgstr "Zoom" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Zoom actual" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Mărește" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Micșorează" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "in" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" -msgstr "din {0}" +msgstr "Din {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" -msgstr "0} / {1} files" +msgstr "{0} / {1} fișiere" + +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} dispozitive găsite." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} imagini scanate în {1} la {2} nu au fost salvate și pot fi recuperate. Doriți să le recuperăm?" diff --git a/NAPS2.Lib/Lang/po/ru.po b/NAPS2.Lib/Lang/po/ru.po index eaedda6c59..49bac16f15 100644 --- a/NAPS2.Lib/Lang/po/ru.po +++ b/NAPS2.Lib/Lang/po/ru.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Russian\n" "Language: ru\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Действие кнопки «Сохранить»:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "1200 точек/дюйм" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Действие кнопки \"Сканировать\" по умолчанию:" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "150 точек/дюйм" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Меню \"Сканировать\" меняет профиль по умолчанию" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "200 точек/дюйм" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 устройство найдено." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "Цветной, 24 бит/пикс." -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "300 точек/дюйм" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "400 точек/дюйм" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "600 точек/дюйм" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "800 точек/дюйм" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "A3 (297x420 мм)" @@ -76,12 +60,15 @@ msgstr "О программе" msgid "Acquiring data..." msgstr "Получение данных..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Действие" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Расширенные" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Расширенные настройки профиля" @@ -93,35 +80,35 @@ msgstr "Все ({0})" msgid "All Files" msgstr "Все файлы" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Разрешить примечания" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Разрешить копирование содержимого" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" -msgstr "Разрешить копирование содержимого для доступа" +msgstr "Разрешить копирование содержимого средствами для людей с ограниченными возможностями" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Разрешить сборку документа" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Разрешить изменение документа" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Разрешить заполнение форм" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" -msgstr "Разрешить полнокачественную печать" +msgstr "Разрешить печать в полном качестве" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Разрешить печать" @@ -133,26 +120,35 @@ msgstr "Альтернативное устранение чередования msgid "Alternate Interleave" msgstr "Альтернативное чередование" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Альтернативная передача" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Всегда спрашивать" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Всегда спрашивать" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Для импорта этого PDF файла необходим доп. компонент. Загрузить его сейчас?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Произошла ошибка во время установки обновления." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "При распознавании текста произошла ошибка." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "Произошла ошибка во время авторизации." +msgstr "При попытке авторизации произошла ошибка." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." msgstr "Ошибка при попытке автосохранения файла." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Произошла ошибка во время установки обновления." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Ошибка при сохранении файла." @@ -172,10 +168,10 @@ msgstr "Ошибка драйвера сканирования." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "Выполняется операция. Вы уверены, что хотите выйти и отменить операцию?" +msgstr "Уже выполняется операция. Вы уверены, что хотите выйти и отменить операцию?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "Произошла неизвестная ошибка при пакетном сканировании." #: MiscResources.resx$UpdateAvailable$Message @@ -184,13 +180,21 @@ msgstr "Доступно обновление." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "Обновить распознавание." +msgstr "Доступно обновление средства распознавания текста (OCR)." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Драйвер Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Приложение" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Применить яркость/контраст после сканирования" @@ -222,49 +226,55 @@ msgstr "Вы действительно хотите удалить эти об msgid "Are you sure you want to delete {0} profiles?" msgstr "Вы действительно хотите удалить выбранные профили ({0} шт.)?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Вы уверены, что хотите отключить общий доступ к {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Вы действительно хотите отменить изменения в изображениях ({0} шт.)?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Назначить" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Имя вложения:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Автор:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "Авторизовать" +msgstr "Авторизация" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" msgstr "Автоопределение" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Параметры автосохранения" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "Автоувеличение номера (1 цифра)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Автоувеличение номера (2 цифры)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Автоувеличение номера (3 цифры)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Автоувеличение номера (4 цифры)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Автоматически запустить распознавание текста (OCR) после сканирования" @@ -277,8 +287,8 @@ msgstr "B4 (250x353 мм)" msgid "B5 (176x250 mm)" msgstr "B5 (176x250 мм)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Пакетное сканирование" @@ -296,9 +306,8 @@ msgstr "Пакетное сканирование остановлено всл #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Лучшее" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Глубина цвета:" @@ -309,10 +318,10 @@ msgstr "Bitmap-файлы (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Чёрно-белое" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Чистые страницы" @@ -320,33 +329,19 @@ msgstr "Чистые страницы" msgid "Brightness / Contrast" msgstr "Яркость / Контрастность" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Яркость:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Не удается найти сканер? Читайте об ограничениях NAPS2 Flatpak." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Отмена" @@ -363,27 +358,26 @@ msgstr "Отмена…...." msgid "Center" msgstr "Центрировать" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Изменить" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "Проверить наличие обновлений" +msgstr "Проверять наличие обновлений" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." msgstr "Проверка..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "Выбрать email провайдера " +msgstr "Выбрать поставщика эл. почты" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" msgstr "Выбор профиля" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Выбор сканера" @@ -395,9 +389,9 @@ msgstr "Очистить" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Очистить всё" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Очистить картинки после сохранения" @@ -405,16 +399,30 @@ msgstr "Очистить картинки после сохранения" msgid "Close" msgstr "Закрыть" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Объединить" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Связь с устройством сканирования была прервана." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Совместимость" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Сжатие:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Подключиться" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Ошибка подключения." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Контраст:" @@ -433,9 +441,9 @@ msgstr "Копирование..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Авторские права защищены {0} Контрибьюторы NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Порог покрытия" @@ -443,7 +451,7 @@ msgstr "Порог покрытия" msgid "Crop" msgstr "Обрезать" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Обрезать до размера страницы" @@ -451,10 +459,14 @@ msgstr "Обрезать до размера страницы" msgid "Custom ({0}x{1} {2})" msgstr "Другой ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Другой размер страницы" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Пользовательское разрешение" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Другой поворот" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "Свой SMTP сервер" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Другой..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Темная" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "День (0-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "По умолчанию" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Путь к файлу:" @@ -487,7 +504,6 @@ msgstr "Путь к файлу:" msgid "Deinterleave" msgstr "Дечередование" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Устранение перекоса" msgid "Deskew Progress" msgstr "Выравниваем..." -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Выровнять отсканированные страницы" @@ -509,35 +525,31 @@ msgstr "Выровнять отсканированные страницы" msgid "Deskewing..." msgstr "Выравнивание..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Устройство:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Размеры" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Отображаемое имя:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Коррекция документа" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Пожертвовать" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Готово" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Загрузка" @@ -550,22 +562,50 @@ msgstr "Ошибка загрузки" msgid "Download Needed" msgstr "Необходимо скачать" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Загрузка..." +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "Dpi" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Двустороннее сканирование" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Драйвер ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "Сетевой драйвер ESCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "USB-драйвер ESCL" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Правка" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Редактировать с помощью {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Редактировать с помощью..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Отправить всё" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Отправить всё как PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,38 +616,48 @@ msgstr "Отправить PDF почтой" msgid "Email PDF Progress" msgstr "Процесс сохранения в PDF и отправки e-mail" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Отправить отмеченные" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Отправить отмеченные как PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Параметры эл. почты" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Включить автосохранение" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Включить ведение журнала отладки" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" -msgstr "Шифрование PDF" +msgstr "Зашифровать PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Шифрование" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "Улучшенный метафайл (*.emf)" +msgstr "Расширенный метафайл (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Ошибка" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Ошибка при запуске приложения {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,36 +667,43 @@ msgstr "Примерный размер загрузки: {0} МБ" msgid "Exchangeable Image File (*.exif)" msgstr "Файл метаданных (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Исключить пустые страницы" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Быстрое" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Устройство подачи" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "Имя файла" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Имя файла:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Путь к файлу:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Исправить баланс белого и убрать шум" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Отразить" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Перевернуть обратные стороны дуплексных листов" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Развернуть дуплексные страницы" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "При высоком качестве JPEG (80+) рекомендуется увеличить параметр \"Максимальное качество\" в настройках профиля для получения наилучших результатов сканирования.." @@ -654,7 +711,6 @@ msgstr "При высоком качестве JPEG (80+) рекомендует msgid "GIF File (*.gif)" msgstr "Файл GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Установить другие языки" @@ -665,18 +721,17 @@ msgstr "Со стекла" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Оттенки серого" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Горизонтальное выравнивание:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Час (0-23)" @@ -684,6 +739,10 @@ msgstr "Час (0-23)" msgid "Hue / Saturation" msgstr "Оттенок / Насыщенность" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Хост" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Значки из:" @@ -696,12 +755,12 @@ msgstr "Изображение" msgid "Image Files" msgstr "Изображения" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Качество изображения" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Настройки изображения" @@ -723,7 +782,7 @@ msgstr "Импортируется \"{0}\"..." #: MiscResources.resx$Importing$Message msgid "Importing..." -msgstr "Импорт......" +msgstr "Импортирование..." #: MiscResources.resx$Install$Message msgid "Install {0}" @@ -735,7 +794,7 @@ msgstr "Установка завершена" #: MiscResources.resx$InstallFailedTitle$Message msgid "Installation Failed" -msgstr "Ошибка установки" +msgstr "Ошибка при установке" #: MiscResources.resx$InstallCompletePromptRestart$Message msgid "Installation complete. Do you want to restart NAPS2 now?" @@ -743,7 +802,11 @@ msgstr "Установка завершена. Перезапустить NAPS2? #: MiscResources.resx$InstallFailed$Message msgid "Installation failed." -msgstr "Ошибка установки." +msgstr "Ошибка при установке." + +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Интерфейс" #: UiStrings.resx$Interleave$Message msgid "Interleave" @@ -755,93 +818,129 @@ msgstr "Файл JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "Файл JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Качество JPEG" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Сохранять изображения между сеансами" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Горячие клавиши" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Ключевые слова:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Язык" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Оставить отзыв" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Повернуть влево" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" -msgstr "Наследие (только пользовательский интерфейс)" +msgstr "Устаревшая (только нативный UI)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Светлая" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Нравится NAPS2?" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Загрузить изображения в NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Создать PDF с возможностью поиска текста" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Ручной IP" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Максимальное качество (большой размер)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Передача через память" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Метаданные" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Минута (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Месяц (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Дополнительно" #: UiStrings.resx$MoveDown$Message msgid "Move Down" -msgstr "Ниже" +msgstr "Переместить ниже" #: UiStrings.resx$MoveUp$Message msgid "Move Up" -msgstr "Выше" +msgstr "Переместить выше" + +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Несколько языков" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Несколько языков..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" -msgstr "Многостраничное сканирование (задержка по времени)" +msgstr "Многостраничное сканирование (с задержкой по времени)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" -msgstr "Многостраничное сканирование (запрос между сканированием)" +msgstr "Многостраничное сканирование (запрос перед каждым сканированием)" #: MiscResources.resx$NAPS2$Message #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "NAPS2 - бесплатная программа,однако Вы можете пожертвовать деньги на развитие проекта." +msgstr "NAPS2 - бесплатная программа, однако Вы можете пожертвовать деньги на развитие проекта." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Имя (опционально)" @@ -849,6 +948,10 @@ msgstr "Имя (опционально)" msgid "Name missing." msgstr "Не указано имя." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Нативная передача" + #: UiStrings.resx$New$Message msgid "New" msgstr "Новый" @@ -861,7 +964,7 @@ msgstr "Новый профиль" msgid "Next" msgstr "Далее" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Сканировать" @@ -870,15 +973,18 @@ msgstr "Сканировать" msgid "No device selected." msgstr "Не выбран сканер." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Устройств не найдено." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "В податчике нет листов." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "Поставщик не выбран." +msgstr "Поставщик эл. почты не выбран." #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message @@ -895,13 +1001,13 @@ msgstr "Нет" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Не сейчас" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Количество страниц:" @@ -909,7 +1015,6 @@ msgstr "Количество страниц:" msgid "OCR" msgstr "Распознавание" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Загрузка файлов для распознавания текста" @@ -918,37 +1023,23 @@ msgstr "Загрузка файлов для распознавания текс msgid "OCR Progress" msgstr "Идет распознавание текста (OCR)" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Настройка распознавания" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Язык распознавания:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "Режим распознавания текста (OCR):" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "ОК" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Смещение ширины при выравнивании (WIA)" @@ -956,13 +1047,11 @@ msgstr "Смещение ширины при выравнивании (WIA)" msgid "Old DSM" msgstr "Старый DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Один файл на страницу" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Один файл на сканирование" @@ -970,7 +1059,11 @@ msgstr "Один файл на сканирование" msgid "One or more files could not be downloaded." msgstr "Не удалось загрузить некоторые файлы." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Разрешать только один экземпляр NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Открыть папку" @@ -978,19 +1071,23 @@ msgstr "Открыть папку" msgid "Operation in Progress" msgstr "Операция в процессе" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (новый)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Web Access" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Вывод" #: MiscResources.resx$OverwriteFile$Message msgid "Overwrite File" -msgstr "Заменить файл" +msgstr "Перезаписать файл" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Пароль владельца:" @@ -998,8 +1095,8 @@ msgstr "Пароль владельца:" msgid "PDF Document (*.pdf)" msgstr "Документ PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Настройки PDF" @@ -1009,35 +1106,33 @@ msgstr "PDF сохранен." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "Файл PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Размер страницы:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Источник бумаги:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Пароль" @@ -1045,47 +1140,49 @@ msgstr "Пароль" msgid "Paste" msgstr "Вставить" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" -msgstr "Подстановка" +msgstr "Поля для заполнения" + +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Порт" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Постобработка" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Превентивно запускать распознавание после сканирования" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." -msgstr "Нажмите «Старт», для запуска.." +msgstr "Нажмите «Запустить», чтобы начать." #: UiStrings.resx$PreviewFormTitle$Message msgid "Preview" msgstr "Предпросмотр" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Предпросмотр:" #: UiStrings.resx$Previous$Message msgid "Previous" -msgstr "Назад" +msgstr "Предыдущие" #: MiscResources.resx$Print$Message #: UiStrings.resx$Print$Message msgid "Print" msgstr "Печать" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Настройки профиля" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Профили:" @@ -1094,23 +1191,27 @@ msgstr "Профили:" msgid "Profiles" msgstr "Профили" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Спрашивать при выборе" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Запрашивать путь сохранения" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Провайдер" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Готов сканировать {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" -msgstr "Востановление" +msgstr "Восстановить" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Восстановить отсканированные изображения" @@ -1122,9 +1223,15 @@ msgstr "Восстановление..." msgid "Recovery Progress" msgstr "Процесс восстановления" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Повторить" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Повторить {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Запомнить эти настройки" @@ -1140,21 +1247,25 @@ msgstr "Сброс" msgid "Reset Image" msgstr "Сброс изображения" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Разрешение:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" -msgstr "Восстановить стандартные" +msgstr "Восстановить по умолчанию" #: UiStrings.resx$Reverse$Message msgid "Reverse" -msgstr "Обратить" +msgstr "В обратном порядке" + +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Обратить всё" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Обратить выделенное" #: UiStrings.resx$Revert$Message msgid "Revert" @@ -1176,7 +1287,6 @@ msgstr "Повернуть влево" msgid "Rotate Right" msgstr "Повернуть вправо" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Запустить в фоновом режиме" @@ -1185,22 +1295,26 @@ msgstr "Запустить в фоновом режиме" msgid "Running OCR..." msgstr "Идет распознавание текста (OCR)..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "Драйвер SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Сохранить" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Сохранить всё" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Сохранить все как изображения" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Сохранить все как PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,21 +1334,26 @@ msgstr "Сохранить PDF" msgid "Save PDF Progress" msgstr "Процесс сохранения в PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Сохранить выбранные" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Сохранить выбранные как изображения" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Сохранить выбранные как PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Сохранить в один файл" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" -msgstr "Сохранить в множество файлов" +msgstr "Сохранить как множество файлов" #: MiscResources.resx$BatchStatusSaving$Message msgid "Saving batch results..." @@ -1244,29 +1363,45 @@ msgstr "Сохранение результатов..." msgid "Saving {0}..." msgstr "Сохранение {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Масштабировать с окном" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Масштаб:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Сканировать" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Настройка сканирования" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Сканировать с профилем по умолчанию" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Сканировать с новым профилем" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Сканировать с профилем {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Отсканированное изображение" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Общий доступ к сканеру" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Сканирование страницы {0}" @@ -1280,11 +1415,14 @@ msgstr "Сканирование страницы {0} (скан {1})..." msgid "Scanning page {0}..." msgstr "Сканирование страницы {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Поиск устройств..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Секунда (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Выбрать" @@ -1293,7 +1431,10 @@ msgstr "Выбрать" msgid "Select All" msgstr "Выбрать все" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Выбор устройства" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Выбрать источник" @@ -1302,7 +1443,6 @@ msgstr "Выбрать источник" msgid "Select a profile before clicking Scan." msgstr "Выберите профиль перед сканированием." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Выберите один или несколько языков:" @@ -1311,8 +1451,7 @@ msgstr "Выберите один или несколько языков:" msgid "Selected ({0})" msgstr "Выбранные ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Разделить файлы по Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Разделить файлы по Patch-T" msgid "Set Default" msgstr "Установить по умолчанию" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Настройки" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Поделиться" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Сохранять общий доступ даже при закрытом NAPS2" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Настройки общего сканера" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Общие сканеры можно использовать с других компьютеров в локальной сети, выбрав \"ESCL Driver\" в настройках профиля NAPS2 на другом компьютере." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Резкость" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Горячая клавиша" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Показать" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Показывать панель \"Профили\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Показать \"нативный\" прогресс TWAIN" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Показывать номера страниц" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Боковая панель" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Одностраничные файлы" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Одиночное сканирование" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Пропустить запрос на сохранение" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Разделить" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Запустить" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Отключить общий доступ к сканеру" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Растянуть до размера страницы" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Тема:" @@ -1358,12 +1544,11 @@ msgstr "Тема:" msgid "TIFF File (*.tiff, *.tif)" msgstr "Файл TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "Драйвер TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Технические сведения" @@ -1372,6 +1557,10 @@ msgstr "Технические сведения" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "Инструменты распознавания текста (OCR) недоступны. Убедитесь, что необходимые пакеты установлены:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Время выполнения операции распознавания текста истекло." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Файл не может быть перезаписан, так как msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Файл {0} уже существует. Перезаписать его?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Этот файл зашифрован, для открытия требуется пароль: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Этот файл зашифрован, и для открытия требуется пароль:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1426,7 +1615,7 @@ msgstr "Не найден выбранный сканер." #: MiscResources.resx$NoFeederSupport$Message #: SdkResources.resx$NoFeederSupport$Message msgid "The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver." -msgstr "Выбранный сканер не поддерживает использование автоподатчика. Если ваш сканер имеет АПД, попробуйте другой драйвер.." +msgstr "Выбранный сканер не поддерживает использование автоподатчика. Если ваш сканер имеет АПД, попробуйте другой драйвер." #: MiscResources.resx$NoDuplexSupport$Message #: SdkResources.resx$NoDuplexSupport$Message @@ -1443,30 +1632,66 @@ msgstr "Выбранный сканер занят." msgid "The selected scanner is offline." msgstr "Выбранный сканер отключён." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Рабочий процесс завершился сбоем." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Рабочий процесс завершился сбоем. Проверьте средство просмотра событий Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Тема:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Настройки TIFF" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Время между сканированием (в секундах):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Заголовок:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Инструменты" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Реализация TWAIN:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "US Legal (8.5x14 дюймов)" +msgstr "US Letter (8.5x11 дюймов)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" msgstr "US Letter (8.5x11 дюймов)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Сбросить" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Отменить" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Отменить {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Не найден сканер" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Несохранённые изменения" @@ -1477,7 +1702,7 @@ msgstr "Ход обновления" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Проверка обновлений отключена." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Обновление..." msgid "Uploading email..." msgstr "Загрузка электронного сообщения..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Собственный интерфейс" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Предустановки" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Пароль пользователя:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Для распознавания требуется загрузка соответствующих языковых файлов." @@ -1515,16 +1737,15 @@ msgstr "Версия {0}" msgid "View" msgstr "Вид" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "Драйвер WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Ожидание завершения работы TWAIN..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Ожидание авторизации..." @@ -1532,26 +1753,26 @@ msgstr "Ожидание авторизации..." msgid "Waiting for scan {0}..." msgstr "Ожидание сканирования {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" -msgstr "Белый порог" +msgstr "Порог белого" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Версия WIA:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Год" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Год (00-99)" #: MiscResources.resx$PdfNoPermissionToExtractContent$Message #: SdkResources.resx$PdfNoPermissionToExtractContent$Message msgid "You do not have permission to copy content from the file '{0}'." -msgstr "У вас нет разрешения копировать содержимое из файла '{0}'.." +msgstr "У вас нет разрешения копировать содержимое из файла '{0}'." #: MiscResources.resx$DontHavePermission$Message msgid "You don't have permission to save files at this location." @@ -1561,21 +1782,18 @@ msgstr "У вас нет прав на запись в это место." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Есть несохранённые изменения. Вы уверены, что хотите выйти и отказаться от этих изменений?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Масштаб" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" -msgstr "Настоящий размер" +msgstr "Исходный размер" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Увеличить" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Уменьшить" @@ -1598,30 +1816,35 @@ msgstr "из {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "{0} / {1} МБ" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} файлов" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "Найдено устройств: {0}." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} точек/дюйм" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" -msgstr "Изображения ({0} шт.), сканированные на {1} в {2}, возможно, не были сохранены и могут быть восстановлены. Хотите восстановить их?" +msgstr "Изображения ({0} шт.), сканированные {1} в {2}, возможно, не были сохранены и могут быть восстановлены. Хотите восстановить их?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." diff --git a/NAPS2.Lib/Lang/po/si.po b/NAPS2.Lib/Lang/po/si.po index 90ca6dacb6..1c6184d4aa 100644 --- a/NAPS2.Lib/Lang/po/si.po +++ b/NAPS2.Lib/Lang/po/si.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:35\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Sinhala\n" "Language: si\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "අඟලට තිත් 100" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "අඟලට තිත් 1200" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "අඟලට තිත් 150" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "අඟලට තිත් 200" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "බිටු 24 වර්ණ " -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "අඟලට තිත් 300" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "අඟලට තිත් 400" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "අඟලට තිත් 600" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "අඟලට තිත් 800" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "" @@ -76,12 +60,15 @@ msgstr "මෘදුකාංගය පිලිබඳ තොරතුරු " msgid "Acquiring data..." msgstr "දත්ත ලබාගනිමින් පවතී..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "ප්‍රගත " -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "ප්‍රගත පැතිකඩ සැකසුම් " @@ -93,35 +80,35 @@ msgstr "සියල්ල ({0})" msgid "All Files" msgstr "සියලුම ගොනු" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "අනුසටහන් සඳහා ඉඩ ලබාදේ" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "අන්තර්ගතය පිටපත් කිරීමට ඉඩ ලබාදේ" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "ප්‍රවේශය සඳහා අන්තර්ගතය පිටපත් කිරීමට ඉඩ ලබාදේ" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "ලේඛන එකලස් කිරීමට ඉඩ ලබාදේ" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "ලේඛන සංශෝධනය කිරීමට ඉඩ ලබාදේ" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "ෆෝරම පිරවීමට ඉඩ ලබාදේ" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "පුර්ණ ගුණාත්මයෙන් යුතුව මුද්‍රණය අනුමත කරයි " -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "මුද්‍රණයට ඉඩදේ " @@ -133,33 +120,42 @@ msgstr "" msgid "Alternate Interleave" msgstr "මාරුවෙන් මාරුවට කොළ එකතු කිරීම " -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "විකල්ප මාරුව " +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "සෑමවිටම පෙන්වන්න" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "මෙම PDF ගොනුව ලබාගැනීමට අමතර උපාංගයක් අවශ්‍යය. එය දැන්ම බාගත කරන්න ඔබ කැමතිද?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "" +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "OCR ධාවනය කිරීමේදී දෝෂයක් ඇති විය." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "අවසර දීමට උත්සාහ කිරීමේදී දෝෂයක් ඇති විය." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." msgstr "නිතැන් සුරැකීමට උත්සාහ කරද්දී දෝෂයක් පැනනැගිනි." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "යාවත්කාලීන ස්ථාපනය කිරීමට උත්සාහ කිරීමේදී දෝෂයක් ඇති විය." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "ගොනුව සුරැකීමට උත්සාහ කරද්දී දෝෂයක් පැනනැගිනි." #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "" +msgstr "ඊමේල් යැවීමට උත්සාහ කිරීමේදී දෝෂයක් ඇති විය." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." @@ -172,11 +168,11 @@ msgstr "පරිලෝකකයෙහි උපක්‍රම ධාවකය #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "" +msgstr "මෙහෙයුමක් ක්‍රියාත්මකයි. මෙහෙයුම අවලංගු කර පිටවීමට අවශ්‍ය ද?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "කාණ්ඩ පරිලෝකනයේදී නාදුනන දෝෂයක් හටගැනින." +msgid "An unknown error occurred during the batch scan." +msgstr "" #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -190,7 +186,15 @@ msgstr "ප්‍රකාශ අක්ෂර කියවනය සඳහා msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "" @@ -222,19 +226,27 @@ msgstr "" msgid "Are you sure you want to delete {0} profiles?" msgstr "" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "{0} චායාරූපයේ/රූපයන්ගේ සිදුකල වෙනස්කම් ඔබ නිසැකයෙන්ම අස්කර ගන්නවාද?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "ඇමුණුමෙහි නම :" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "රචකයා :" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "" @@ -242,29 +254,27 @@ msgstr "" msgid "Auto" msgstr "" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "" @@ -277,8 +287,8 @@ msgstr "" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "" @@ -298,7 +308,6 @@ msgstr "" msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "" @@ -309,10 +318,10 @@ msgstr "" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "" @@ -320,7 +329,6 @@ msgstr "" msgid "Brightness / Contrast" msgstr "" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "" @@ -329,27 +337,14 @@ msgstr "" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" -msgstr "" +msgstr "අවලංගු කරන්න" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" @@ -363,7 +358,7 @@ msgstr "" msgid "Center" msgstr "" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "" @@ -375,18 +370,17 @@ msgstr "" msgid "Checking..." msgstr "" -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" -msgstr "" +msgstr "පැතිකඩ තෝරන්න" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" -msgstr "" +msgstr "උපාංගය තෝරන්න" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message @@ -397,7 +391,7 @@ msgstr "" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "" @@ -405,16 +399,30 @@ msgstr "" msgid "Close" msgstr "" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "" @@ -435,7 +443,7 @@ msgstr "" msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "" @@ -443,7 +451,7 @@ msgstr "" msgid "Crop" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "" @@ -451,10 +459,14 @@ msgstr "" msgid "Custom ({0}x{1} {2})" msgstr "" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "" -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "" @@ -487,7 +504,6 @@ msgstr "" msgid "Deinterleave" msgstr "" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "" msgid "Deskew Progress" msgstr "" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" @@ -509,16 +525,14 @@ msgstr "" msgid "Deskewing..." msgstr "" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "" @@ -532,12 +546,10 @@ msgstr "" msgid "Donate" msgstr "" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "" @@ -550,19 +562,47 @@ msgstr "" msgid "Download Needed" msgstr "" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +616,32 @@ msgstr "" msgid "Email PDF Progress" msgstr "" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "" @@ -602,12 +649,15 @@ msgstr "" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "" msgid "Exchangeable Image File (*.exif)" msgstr "" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "" @@ -629,24 +679,31 @@ msgstr "" msgid "Feeder" msgstr "" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "" + +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" msgstr "" #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" @@ -654,7 +711,6 @@ msgstr "" msgid "GIF File (*.gif)" msgstr "" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "" @@ -671,12 +727,11 @@ msgstr "" msgid "Grayscale" msgstr "" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "" @@ -684,6 +739,10 @@ msgstr "" msgid "Hue / Saturation" msgstr "" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "" @@ -696,12 +755,12 @@ msgstr "" msgid "Image Files" msgstr "" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "" @@ -745,6 +804,10 @@ msgstr "" msgid "Installation failed." msgstr "" +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "" @@ -757,11 +820,20 @@ msgstr "" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "" @@ -781,33 +857,48 @@ msgstr "" msgid "Legacy (native UI only)" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "" @@ -819,11 +910,19 @@ msgstr "" msgid "Move Up" msgstr "" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "" @@ -849,6 +948,10 @@ msgstr "" msgid "Name missing." msgstr "" +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "" @@ -861,7 +964,7 @@ msgstr "" msgid "Next" msgstr "" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "" @@ -870,12 +973,15 @@ msgstr "" msgid "No device selected." msgstr "" +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "" -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "" @@ -897,11 +1003,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "" @@ -909,7 +1015,6 @@ msgstr "" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "" @@ -918,37 +1023,23 @@ msgstr "" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "" @@ -970,7 +1059,11 @@ msgstr "" msgid "One or more files could not be downloaded." msgstr "" -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "" @@ -978,11 +1071,15 @@ msgstr "" msgid "Operation in Progress" msgstr "" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "" @@ -990,7 +1087,7 @@ msgstr "" msgid "Overwrite File" msgstr "" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "" @@ -998,8 +1095,8 @@ msgstr "" msgid "PDF Document (*.pdf)" msgstr "" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "" @@ -1045,21 +1140,24 @@ msgstr "" msgid "Paste" msgstr "" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "" @@ -1067,7 +1165,7 @@ msgstr "" msgid "Preview" msgstr "" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "" @@ -1080,12 +1178,11 @@ msgstr "" msgid "Print" msgstr "" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "" @@ -1094,23 +1191,27 @@ msgstr "" msgid "Profiles" msgstr "" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "" -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "" @@ -1122,9 +1223,15 @@ msgstr "" msgid "Recovery Progress" msgstr "" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "" @@ -1140,15 +1247,11 @@ msgstr "" msgid "Reset Image" msgstr "" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "" @@ -1156,6 +1259,14 @@ msgstr "" msgid "Reverse" msgstr "" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "" @@ -1176,7 +1287,6 @@ msgstr "" msgid "Rotate Right" msgstr "" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1185,7 +1295,6 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1303,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "" msgid "Save PDF Progress" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "" @@ -1244,29 +1363,45 @@ msgstr "" msgid "Saving {0}..." msgstr "" -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "" @@ -1280,11 +1415,14 @@ msgstr "" msgid "Scanning page {0}..." msgstr "" -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "" @@ -1293,7 +1431,10 @@ msgstr "" msgid "Select All" msgstr "" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "" @@ -1302,7 +1443,6 @@ msgstr "" msgid "Select a profile before clicking Scan." msgstr "" -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "" @@ -1311,8 +1451,7 @@ msgstr "" msgid "Selected ({0})" msgstr "" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,37 +1459,84 @@ msgstr "" msgid "Set Default" msgstr "" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "" @@ -1358,12 +1544,11 @@ msgstr "" msgid "TIFF File (*.tiff, *.tif)" msgstr "" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "" @@ -1372,6 +1557,10 @@ msgstr "" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,8 +1583,8 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" msgstr "" #: MiscResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "" msgid "The selected scanner is offline." msgstr "" -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "" @@ -1487,21 +1712,18 @@ msgstr "" msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "" @@ -1515,16 +1737,15 @@ msgstr "" msgid "View" msgstr "" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "" -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1532,19 +1753,19 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "" -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "" @@ -1561,21 +1782,18 @@ msgstr "" msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "අඟලට තිත් {0}" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "" diff --git a/NAPS2.Lib/Lang/po/sk.po b/NAPS2.Lib/Lang/po/sk.po index 597c4caed3..23b3d68199 100644 --- a/NAPS2.Lib/Lang/po/sk.po +++ b/NAPS2.Lib/Lang/po/sk.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Slovak\n" "Language: sk\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Predvolená akcia tlačidla \"Uložiť\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Predvolená akcia tlačidla \"Skenovať\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Ponuka \"Skenovať\" zmení predvolený profil" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "Nájdené 1 zariadenie." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-bitová farba" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -76,12 +60,15 @@ msgstr "O programe" msgid "Acquiring data..." msgstr "Získať údaje..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Rozšírené" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Rozšírené nastavenia profilu" @@ -93,35 +80,35 @@ msgstr "Všetko ({0})" msgid "All Files" msgstr "Všetky súbory" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Povoliť vysvetlivky" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Povoliť kopírovanie obsahu" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Povoliť dostupnosť kopírovania obsahu" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Povoliť zostavovanie dokumentu" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Povoliť úpravy dokumentu" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Povoliť vyplňovanie formulárov" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Povoliť tlač v plnej kvalite" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Povoliť tlač" @@ -133,17 +120,22 @@ msgstr "Alternatívne prekladanie" msgid "Alternate Interleave" msgstr "Alternatívne vkladanie" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternatívny prenos" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Vždy sa pýtať" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Vždy sa spýtať" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" -msgstr "Na import tohto súboru PDF je potrebný ďalší komponent. Chcete ho stiahnuť teraz?" +msgstr "Na import tohto súboru PDF je potrebný ďalší komponent. Chcete ho teraz stiahnuť?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Pri pokuse o inštaláciu aktualizácie sa vyskytla chyba." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Pri spustení OCR došlo k chybe." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Pri pokuse o autorizáciu sa vyskytla chyba." msgid "An error occurred when trying to auto save." msgstr "Pri pokuse o automatické uloženie sa vyskytla chyba." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Pri pokuse o inštaláciu aktualizácie sa vyskytla chyba." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Pri pokuse o uloženie súboru sa vyskytla chyba." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Prebieha operácia. Naozaj chcete operáciu ukončiť a zrušiť?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Počas dávkového skenovania sa vyskytla neznáma chyba." +msgid "An unknown error occurred during the batch scan." +msgstr "Počas dávkového skenovania došlo k neznámej chybe." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -188,9 +184,17 @@ msgstr "Je dostupná aktualizácia OCR." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Ovládač Apple" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Pošta Apple" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Aplikácia" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Po skenovaní použite jas/kontrast" @@ -222,19 +226,27 @@ msgstr "Naozaj chcete odstrániť {0} položky?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Naozaj chcete odstrániť {0} profilov?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Naozaj chcete prestať zdieľať {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Naozaj chcete vrátiť späť svoje zmeny v {0} obrázkoch?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Názov prílohy:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autor:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autorizovať" @@ -242,43 +254,41 @@ msgstr "Autorizovať" msgid "Auto" msgstr "Automaticky" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Nastavenia automatického ukladania" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Automatické zvyšovanie čísla (1 číslica)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Automaticky sa zvyšujúce číslo (1 číslica)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Automatické zvyšovanie čísla (2 číslice)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Automatické zvyšovanie čísla (3 číslice)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Automatické zvyšovanie čísla (4 číslice)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Po skenovaní automaticky spustiť OCR" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Dávkové skenovanie" @@ -296,9 +306,8 @@ msgstr "Dávkové skenovanie sa zastavilo kvôli chybe." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Najlepšie" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bitová hĺbka:" @@ -309,10 +318,10 @@ msgstr "Súbor bitmapy (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Čiernobiela" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Prázdne stránky" @@ -320,33 +329,19 @@ msgstr "Prázdne stránky" msgid "Brightness / Contrast" msgstr "Jas / kontrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Jas:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Zrušiť" @@ -363,7 +358,7 @@ msgstr "Ruší sa...." msgid "Center" msgstr "Na stred" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Zmeniť" @@ -375,7 +370,7 @@ msgstr "Kontrola aktualizácií" msgid "Checking..." msgstr "Kontrola..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Vyberte poskytovateľa e-mailu" @@ -383,7 +378,6 @@ msgstr "Vyberte poskytovateľa e-mailu" msgid "Choose Profile" msgstr "Vyberte profil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Vyberte zariadenie" @@ -395,9 +389,9 @@ msgstr "Zmazať" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Zmazať všetko" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Po uložení obrázky vymažte" @@ -405,16 +399,30 @@ msgstr "Po uložení obrázky vymažte" msgid "Close" msgstr "Zavrieť" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Kombinovať" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Komunikácia so skenovacím zariadením bola prerušená." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Kompatibilta" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Kompresia:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Pripojiť" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Chyba pripojenia." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrast:" @@ -433,9 +441,9 @@ msgstr "Kopírovanie..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} prispievatelia NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Prahová hodnota pokrytia" @@ -443,7 +451,7 @@ msgstr "Prahová hodnota pokrytia" msgid "Crop" msgstr "Orezať" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Orezať na veľkosť strany" @@ -451,10 +459,14 @@ msgstr "Orezať na veľkosť strany" msgid "Custom ({0}x{1} {2})" msgstr "Vlastné ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Vlastná veľkosť stránky" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Vlastné rozlíšenie" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Vlastné natočenie" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "Vlastné SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Vlastné..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Deň (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Pôvodné" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Pôvodná cesta k súborom:" @@ -487,7 +504,6 @@ msgstr "Pôvodná cesta k súborom:" msgid "Deinterleave" msgstr "Rozklad" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Vyrovnanie sklonu" msgid "Deskew Progress" msgstr "Priebeh vyrovnávania sklonu" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Vyrovnanie sklonu skenovaných stránok" @@ -509,35 +525,31 @@ msgstr "Vyrovnanie sklonu skenovaných stránok" msgid "Deskewing..." msgstr "Vyrovnávanie sklonu..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Zariadenie:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Rozmery" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Zobrazovaný názov:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Oprava dokumentu" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "Obdarovať" +msgstr "Darovať" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Hotovo" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Stiahnuť" @@ -550,22 +562,50 @@ msgstr "Chyba sťahovania" msgid "Download Needed" msgstr "Potrebné stiahnutie" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Priebeh sťahovania" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "Dpi" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Obojsmerne" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Ovládač ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "Sieťový ovládač ESCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "Ovládač ESCL USB" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Úprava" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Poslať e-mail všetkým" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Poslať e-mail všetkým ako PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,38 +616,48 @@ msgstr "PDF e-mailom" msgid "Email PDF Progress" msgstr "Priebeh PDF e-mailom" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Poslať e-mail vybraným" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Poslať e-mail vybraným ako PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Nastavenie e-mailu" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Povoliť automatické ukladanie" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Povoliť záznam ladenia" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Zašifrovať PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Šifrovanie" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "" +msgstr "Enhanced Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Chyba" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,38 +665,45 @@ msgstr "Odhadovaná veľkosť sťahovaného súboru: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Vylúčiť prázdne stránky" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Rýchlo" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Podávač" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" -msgstr "Názov súboru" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "Názov súboru:" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Cesta súboru:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Oprava vyváženia bielej a odstránenie šumu" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Preklopiť" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Preklopenie zadných strán obojstranných strán" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" -msgstr "Preklopiť obojsmerné strany" +msgstr "Preklopiť obojstranný scan" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Pre najlepšie výsledky vysokej kvality obrázkov JPEG (80+), zvýšte tiež Kvalitu obrázku vo svojom profile." @@ -654,7 +711,6 @@ msgstr "Pre najlepšie výsledky vysokej kvality obrázkov JPEG (80+), zvýšte msgid "GIF File (*.gif)" msgstr "Súbor GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Získať viac jazykov" @@ -665,18 +721,17 @@ msgstr "Sklo" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Odtiene sivej" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Zarovnať horizontálne:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Hodina (0-23)" @@ -684,6 +739,10 @@ msgstr "Hodina (0-23)" msgid "Hue / Saturation" msgstr "Odtieň / nasýtenie" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/hostiteľ" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Ikony z:" @@ -696,12 +755,12 @@ msgstr "Obrázok" msgid "Image Files" msgstr "Súbory obrázkov" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Kvalita obrázku" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Nastavenia obrázkov" @@ -711,7 +770,7 @@ msgstr "Obrázok bol uložený." #: UiStrings.resx$Import$Message msgid "Import" -msgstr "" +msgstr "Import" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" @@ -745,6 +804,10 @@ msgstr "Inštalácia dokončená. Chcete teraz reštartovať NAPS2?" msgid "Installation failed." msgstr "Inštalácia zlyhala." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Rozhranie" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Prekladanie" @@ -755,24 +818,37 @@ msgstr "Súbor JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "Súbor JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" -msgstr "Kvalita Jpeg" +msgstr "Kvalita JPEG" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Zachovať obrázky naprieč reláciami" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Kľúčové slová:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Jazyk" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Zanechať recenziu" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Vľavo" @@ -781,33 +857,48 @@ msgstr "Vľavo" msgid "Legacy (native UI only)" msgstr "Staršie (len natívne užívateľské rozhranie)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Ako NAPS2?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Načítanie obrázkov do NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Umožnite vyhľadávanie PDF pomocou OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Manuálne IP" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Maximálna kvalita (veľké súbory)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Presun pamäte" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metaúdaje" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minúta (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Mesiac (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Viac informácií" @@ -819,11 +910,19 @@ msgstr "Posunúť nadol" msgid "Move Up" msgstr "Posunúť nahor" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Viacero jazykov" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Viacero jazykov..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Viacnásobné skenovanie (s pevným oneskorením medzi skenovaniami)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Viacnásobné skenovanie (výzva medzi skenovaniami)" @@ -831,17 +930,17 @@ msgstr "Viacnásobné skenovanie (výzva medzi skenovaniami)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 je úplne zadarmo. Zvážte obdarovanie." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Názov (nepovinné)" @@ -849,6 +948,10 @@ msgstr "Názov (nepovinné)" msgid "Name missing." msgstr "Chýba názov." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Natívny prenos" + #: UiStrings.resx$New$Message msgid "New" msgstr "Nový" @@ -861,7 +964,7 @@ msgstr "Nový profil" msgid "Next" msgstr "Ďalej" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Ďalšie skenovanie" @@ -870,12 +973,15 @@ msgstr "Ďalšie skenovanie" msgid "No device selected." msgstr "Nebolo vybrané žiadne zariadenie." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Nenájdené žiadne zariadenia." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "V podávači nie sú žiadne listy." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Nie je vybratý žiadny poskytovateľ." @@ -895,21 +1001,20 @@ msgstr "Žiadny" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Nie je to ďalší skener PDF" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Teraz nie" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Počet skenov:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Stiahnuť OCR" @@ -918,37 +1023,23 @@ msgstr "Stiahnuť OCR" msgid "OCR Progress" msgstr "Priebeh OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Nastavenie OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Jazyk OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "Režim OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Šírka posunu na základe zarovnania (WIA)" @@ -956,13 +1047,11 @@ msgstr "Šírka posunu na základe zarovnania (WIA)" msgid "Old DSM" msgstr "Staršie DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Jeden súbor na stránku" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Jeden súbor na skenovanie" @@ -970,7 +1059,11 @@ msgstr "Jeden súbor na skenovanie" msgid "One or more files could not be downloaded." msgstr "Jeden alebo viac súborov sa nepodarilo stiahnuť." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Povoliť len jednu inštanciu NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Otvoriť priečinok" @@ -978,11 +1071,15 @@ msgstr "Otvoriť priečinok" msgid "Operation in Progress" msgstr "Priebeh operácie" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (nové)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "Služba Outlook Web Access" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Výstup" @@ -990,7 +1087,7 @@ msgstr "Výstup" msgid "Overwrite File" msgstr "Prepísať súbor" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Heslo vlastníka:" @@ -998,10 +1095,10 @@ msgstr "Heslo vlastníka:" msgid "PDF Document (*.pdf)" msgstr "Dokument PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" -msgstr "Nastavenie PDF" +msgstr "Nastavenia PDF" #: MiscResources.resx$PdfSaved$Message msgid "PDF saved." @@ -1009,35 +1106,33 @@ msgstr "PDF bolo uložené." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "Súbor PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Veľkosť stránky:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Zdroj papiera:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Heslo" @@ -1045,21 +1140,24 @@ msgstr "Heslo" msgid "Paste" msgstr "Prilepiť" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Zástupné symboly" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Následné spracovanie" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Predbežné spustenie OCR po skenovaní" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Ak budete pripravení, stlačte Štart." @@ -1067,7 +1165,7 @@ msgstr "Ak budete pripravení, stlačte Štart." msgid "Preview" msgstr "Náhľad" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Náhľad:" @@ -1080,12 +1178,11 @@ msgstr "Predošlý" msgid "Print" msgstr "Tlačiť" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Nastavenia profilu" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profil:" @@ -1094,23 +1191,27 @@ msgstr "Profil:" msgid "Profiles" msgstr "Profily" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Spýtať sa, ak je vybrané" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Výzva na zadanie cesty k súboru" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Poskytovateľ" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Pripravené na skenovanie {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Obnoviť" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Obnoviť naskenované obrázky" @@ -1122,9 +1223,15 @@ msgstr "Obnovovanie..." msgid "Recovery Progress" msgstr "Priebeh obnovovania" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Vpred" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Vpred {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Zapamätať si tieto nastavenia" @@ -1140,15 +1247,11 @@ msgstr "Vynulovať" msgid "Reset Image" msgstr "Vynulovať obrázok" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Rozlíšenie:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Obnoviť pôvodné" @@ -1156,6 +1259,14 @@ msgstr "Obnoviť pôvodné" msgid "Reverse" msgstr "Opačné poradie" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Obrátiť všetko" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Obrátiť vybrané" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Prevrátiť" @@ -1176,7 +1287,6 @@ msgstr "Natočiť vľavo" msgid "Rotate Right" msgstr "Natočiť vpravo" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Spustiť na pozadí" @@ -1185,22 +1295,26 @@ msgstr "Spustiť na pozadí" msgid "Running OCR..." msgstr "Spustené OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "Ovládač SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Uložiť" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Uložiť všetko" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Uložiť všetko ako obrázky" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Uložiť všetko ako PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Uložiť PDF" msgid "Save PDF Progress" msgstr "Priebeh ukladania PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Uložiť vybrané" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Uložiť vybrané ako obrázky" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Uložiť vybrané ako PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Uložiť do jedného súboru" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Uložiť do viacerých súborov" @@ -1244,29 +1363,45 @@ msgstr "Výsledok uloženia dávky..." msgid "Saving {0}..." msgstr "Ukladanie {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Mierka podľa okna" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Mierka:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skenovať" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Konfigurácia skenovania" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Skenovať pomocou predvoleného profilu" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Naskenovaný obrázok" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Zdieľanie skenera" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Skenovanie strany {0}" @@ -1280,11 +1415,14 @@ msgstr "Skenovanie strany {0} (skenovanie {1})..." msgid "Scanning page {0}..." msgstr "Skenovanie strany {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Vyhľadávanie zariadení..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekunda (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Vybrať" @@ -1293,16 +1431,18 @@ msgstr "Vybrať" msgid "Select All" msgstr "Vybrať všetko" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Výber zariadenia" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Vybrať zdroj" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." -msgstr "Pred kliknutím na Skenovať najskôr vyberte profil." +msgstr "Najskôr vyberte profil pred kliknutím na Skenovať." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Vybrať jeden alebo viac jazykov:" @@ -1311,8 +1451,7 @@ msgstr "Vybrať jeden alebo viac jazykov:" msgid "Selected ({0})" msgstr "Vybrané ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Oddeľte súbory podľa Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Oddeľte súbory podľa Patch-T" msgid "Set Default" msgstr "Nastaviť pôvodné" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Nastavenia" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Zdieľať" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Zdieľať aj keď je NAPS2 zavretý" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Nastavenia zdieľaného skeneru" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Zdieľané skenery môžu byť použité z iných počítačov v miestnej sieti zvolením možnosti \"Ovládač ESCL\" v nastaveniach profilu NAPS2 iného počítača." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Zaostriť" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Ukázať" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Zobraziť panel nástrojov \"Profily\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Zobraziť prirodzený priebeh TWAIN" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Zobraziť čísla stránok" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Jednostránkové súbory" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Jedno skenovanie" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Preskočiť výzvu na uloženie" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Rozdeliť" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Štart" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Roztiahnuť na veľkosť stránky" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Predmet:" @@ -1358,12 +1544,11 @@ msgstr "Predmet:" msgid "TIFF File (*.tiff, *.tif)" msgstr "Súbor TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "Ovládač TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Technické detaily" @@ -1372,6 +1557,10 @@ msgstr "Technické detaily" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "Mechanizmus OCR nie je k dispozícii. Určite preverte požadovaný balík pre inštaláciu:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Čas OCR operácie vypršal." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1380,7 +1569,7 @@ msgstr "Ovládač SANE nie je k dispozícii. Preverte požadované balíky pre i #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message msgid "The file '{0}' could not be imported." -msgstr "Súbor '{0}' sa nemožno importovať." +msgstr "Súbor '{0}' nemožno importovať." #: MiscResources.resx$ImportErrorNAPS2Pdf$Message msgid "The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported." @@ -1394,9 +1583,9 @@ msgstr "Súbor nemožno prepísať, pretože sa momentálne používa." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Súbor {0} už existuje. Chcete ho prepísať?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Tento súbor je šifrovaný a je potrebné heslo na otvorenie: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Nasledujúci súbor je zašifrovaný a na jeho otvorenie je potrebné heslo:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,28 +1632,64 @@ msgstr "Vybraný skener je zaneprázdnený." msgid "The selected scanner is offline." msgstr "Vybraný skener je offline." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Pracovný proces spadol." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Pracovný proces spadol. Skontrolujte prehliadač udalostí systému Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" -msgstr "Možnosti Tiff" +msgstr "Možnosti TIFF" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Čas medzi skenovaniami (v sekundách):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Titulok:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Nástroje" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" -msgstr "Implementácia Twain:" +msgstr "Implementácia TWAIN:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 in)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" +msgstr "US Letter (8.5x11 in)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Späť" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Späť {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" msgstr "" #: MiscResources.resx$UnsavedChanges$Message @@ -1477,7 +1702,7 @@ msgstr "Priebeh aktualizácie" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Kontrola aktualizácií je vypnutá." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Aktualizácia..." msgid "Uploading email..." msgstr "Povýšenie e-mailom..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Použiť natívne užívateľské rozhranie" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Použite preddefinované nastavenia" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Heslo užívateľa:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Používanie OCR vyžaduje, aby ste si stiahli každý jazyk, ktorý chcete skenovať." @@ -1515,16 +1737,15 @@ msgstr "Verzia {0}" msgid "View" msgstr "Zobrazenie" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "Ovládač WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Čaká sa na dokončenie TWAIN..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Čaká sa na autorizáciu..." @@ -1532,19 +1753,19 @@ msgstr "Čaká sa na autorizáciu..." msgid "Waiting for scan {0}..." msgstr "Čaká sa na skenovanie {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Prah bielej" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Verzia WIA:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Rok" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Rok (00-99)" @@ -1561,28 +1782,25 @@ msgstr "Na tomto mieste nemáte povolenie na ukladanie súborov." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Máte neuložené zmeny. Naozaj chcete skončiť a tieto zmeny zahodiť?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Priblíženie" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Aktuálne priblíženie" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Priblíženie" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Oddialenie" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" @@ -1590,7 +1808,7 @@ msgstr "palec" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,28 +1816,33 @@ msgstr "z {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} súborov" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "Nájdených {0} zariadení." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} skenovaných obrázkov na {1} v {2} zostalo neuložených a sú obnoviteľné. Chcete ich obnoviť?" diff --git a/NAPS2.Lib/Lang/po/sl.po b/NAPS2.Lib/Lang/po/sl.po index 1ca7c13daa..90f0dc065a 100644 --- a/NAPS2.Lib/Lang/po/sl.po +++ b/NAPS2.Lib/Lang/po/sl.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Slovenian\n" "Language: sl\n" @@ -12,60 +12,44 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Translate Toolkit 1.13.0\n" "X-Poedit-SourceCharset: iso-8859-1\n" -"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);\n" +"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n" "X-Crowdin-Project: naps2\n" "X-Crowdin-Project-ID: 531762\n" "X-Crowdin-Language: sl\n" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Privzeto dejanje gumba \"Shrani\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Privzeto dejanje gumba \"Skeniraj\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Meni \"Skeniraj\" spremeni privzeti profil" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "Najdena je bila 1 naprava." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" -msgstr "24-bit Barvno" - -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" +msgstr "24-bit barvno" #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -76,12 +60,15 @@ msgstr "Vizitka" msgid "Acquiring data..." msgstr "Zajemam podatke..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Napredno" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Napredne nastavitve profila" @@ -93,57 +80,62 @@ msgstr "Vse ({0})" msgid "All Files" msgstr "Vse datoteke" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" -msgstr "Dovoli Opombe" +msgstr "Dovolite opombe" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" -msgstr "Dovoli Kopiranje Vsebine" +msgstr "Dovolite kopiranje vsebine" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" -msgstr "Dovoli Kopiranje Vsebine za Dostopnost" +msgstr "Dovolite kopiranje vsebine za dostopnost" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" -msgstr "Dovoli Sestavo Dokumenta" +msgstr "Dovolite sestavo dokumenta" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" -msgstr "Dovoli Spreminjanje Dokumenta" +msgstr "Dovolite spreminjanje dokumenta" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" -msgstr "Dovoli Izpolnjevanje Obrazca" +msgstr "Dovolite izpolnjevanje obrazca" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" -msgstr "Dovoli Tiskanje v Polni Kvaliteti" +msgstr "Dovolite tiskanje v najboljši kvaliteti" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" -msgstr "Dovoli Tiskanje" +msgstr "Dovolite tiskanje" #: UiStrings.resx$AltDeinterleave$Message msgid "Alternate Deinterleave" -msgstr "Drugačno Razpletanje" +msgstr "Drugačno razpletanje" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" -msgstr "Drugačno Prepletanje" +msgstr "Drugačno prepletanje" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternativni Prenos" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Vedno vprašaj" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Vedno vprašaj" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Za uvoz te PDF datoteke je potrebna dodatna komponenta. Jo želite prenesti sedaj?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Med nameščanjem posodobitve je prišlo do napake." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Pri zagonu OCR je prišlo do napake." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Napaka pri avtorizaciji." msgid "An error occurred when trying to auto save." msgstr "Pri samodejnem shranjevanju je prišlo do napake." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Med nameščanjem posodobitve je prišlo do napake." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Pri shranjevanju datoteke je prišlo do napake." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Operacija je v teku. Želite prekiniti operacijo in zapustiti aplikacijo?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Med zaporednim skeniranjem je prišlo do napake." +msgid "An unknown error occurred during the batch scan." +msgstr "Med zaporednim skeniranjem je prišlo do neznane napake." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -188,19 +184,27 @@ msgstr "Na voljo je posodobitev za OCR." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Gonilnik Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Aplikacija" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" -msgstr "Uveljavi svetlost/kontrast po skeniranju" +msgstr "Uveljavite svetlost/kontrast po skeniranju" #: UiStrings.resx$ApplyToSelected$Message msgid "Apply to all {0} selected images" -msgstr "Uveljavi na vseh {0} izbranih slikah" +msgstr "Uveljavite na vseh {0} izbranih slikah" #: MiscResources.resx$ConfirmCancelBatch$Message msgid "Are you sure you want to cancel the batch scan?" -msgstr "Prekini zaporedno skeniranje?" +msgstr "Ali želite prekiniti zaporedno skeniranje?" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" @@ -222,19 +226,27 @@ msgstr "Ali ste prepričani, da želite izbrisati {0} elementov?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Ali ste prepričani, da želite izbrisati {0} profilov?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Ali si prepričan, da želiš ustaviti deljenje {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Ali ste prepričani, da želite razveljaviti spremembe na {0} slikah?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Ime Priponke:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Avtor:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Odobri" @@ -242,43 +254,41 @@ msgstr "Odobri" msgid "Auto" msgstr "Samodejno" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Nastavitve Samodejnega Shranjevanja" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "Naraščajoče število (1 cifra)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Naraščajoče število (2 cifri)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Naraščajoče število (3 cifre)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Naraščajoče število (4 cifre)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Avtomatsko zaženi OCR po skeniranju" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Zaporedno Skeniranje" @@ -296,9 +306,8 @@ msgstr "Zaporedno skeniranje se je ustavilo zaradi napake." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Najboljše" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bitna globina:" @@ -309,10 +318,10 @@ msgstr "Bitmap datoteke (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Črno-belo" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Prazne Strani" @@ -320,33 +329,19 @@ msgstr "Prazne Strani" msgid "Brightness / Contrast" msgstr "Svetlost / Kontrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Svetlost:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Prekliči" @@ -363,7 +358,7 @@ msgstr "Prekinitev...." msgid "Center" msgstr "Sredina" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Spremeni" @@ -375,7 +370,7 @@ msgstr "Preveri za posodobitve" msgid "Checking..." msgstr "Preverjanje..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Izberite e-poštnega ponudnika" @@ -383,7 +378,6 @@ msgstr "Izberite e-poštnega ponudnika" msgid "Choose Profile" msgstr "Izberi Profil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Izberi Napravo" @@ -395,9 +389,9 @@ msgstr "Počisti" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Počisti vse" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Po shranjevanju počisti slike" @@ -405,16 +399,30 @@ msgstr "Po shranjevanju počisti slike" msgid "Close" msgstr "Zapri" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Združi" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Komunikacija s skenerjem je bila prekinjena." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Združljivost" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Stiskanje:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Poveži" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Napaka v povezavi." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrast:" @@ -433,9 +441,9 @@ msgstr "Kopiranje..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} NAPS2 Contributors" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Meja pokritja" @@ -443,7 +451,7 @@ msgstr "Meja pokritja" msgid "Crop" msgstr "Obreži" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Obreži na velikost strani" @@ -451,10 +459,14 @@ msgstr "Obreži na velikost strani" msgid "Custom ({0}x{1} {2})" msgstr "Po meri ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Velikost strani po meri" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Zasuk po meri" @@ -464,30 +476,34 @@ msgid "Custom SMTP" msgstr "SMTP po meri" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Po meri..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Dan (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Privzeto" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" -msgstr "Privzeta Pot Datotek:" +msgstr "Privzeta pot datotek:" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" msgstr "Razpletanje" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Poravnaj" msgid "Deskew Progress" msgstr "Potek Poravnave" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Poravnaj skenirane strani" @@ -509,35 +525,31 @@ msgstr "Poravnaj skenirane strani" msgid "Deskewing..." msgstr "Poravnavanje..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Naprava:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Velikosti" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Ime profila:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Popravek dokumenta" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Donacije" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Končaj" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Prenesi" @@ -550,22 +562,50 @@ msgstr "Napaka med prenosom" msgid "Download Needed" msgstr "Potreben prenos" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Prenos v teku" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Obojestransko" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL gonilnik" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL omrežni gonilnik" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB gonilnik" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Uredi" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Pošlji vse" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Pošlji vse kot PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,38 +616,48 @@ msgstr "Pošlji PDF" msgid "Email PDF Progress" msgstr "Pošiljanje PDF v teku" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Shrani izbrano" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "E-pošta izbrana kot PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Nastavitve e-pošte" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Omogoči Samodejno Shranjevanje" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Omogoči beleženje iskanja in odstranjevanja napak (debug)" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Šifriraj PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Šifriranje" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "" +msgstr "Enhanced Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Napaka" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,46 +665,52 @@ msgstr "Ocenjena velikost prenosa: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Izpusti prazne strani" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Hitro" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Podajalec" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Ime Datoteke" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Pot datoteke:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Popravi ravnovesje beline in odstrani šum" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Preslikaj" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Obrni zadnje strani dvostranskih listov" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Preslikaj obojestranske strani" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Za najboljši rezultat pri visoki JPEG kakovosti (80+), povišajte tudi kakovost slike v vašem profilu." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" -msgstr "" +msgstr "GIF File (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Pridobi več jezikov" @@ -665,18 +721,17 @@ msgstr "Steklo" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Sivine" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Horizontalna poravnava:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Ura (0-23)" @@ -684,6 +739,10 @@ msgstr "Ura (0-23)" msgid "Hue / Saturation" msgstr "Odtenek / Nasičenje" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/gostitelj" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Ikone od:" @@ -696,12 +755,12 @@ msgstr "Slika" msgid "Image Files" msgstr "Slikovne Datoteke" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Kvaliteta Slike" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Nastavitve Slike" @@ -745,34 +804,51 @@ msgstr "Namestitev zaključena. Želite ponovno zagnati NAPS2?" msgid "Installation failed." msgstr "Namestitev spodletela." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Vmesnik" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Prepletanje" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" -msgstr "" +msgstr "Datoteka JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "Datoteka JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Jpeg Kvaliteta" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Ohrani slike med sejami" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Ključne besede:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Jezik" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Levo" @@ -781,33 +857,48 @@ msgstr "Levo" msgid "Legacy (native UI only)" msgstr "Opuščeno (samo osnovni UI)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Naloži slike v NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Naredi PDF z možnostjo iskanja z uporabo OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Ročni IP naslov" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Maximum quality (velike datoteke)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Prenos pomnilnika" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metapodatki" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" -msgstr "" +msgstr "Minuta (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Mesec (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Več informacij" @@ -819,11 +910,19 @@ msgstr "Premakni Dol" msgid "Move Up" msgstr "Premakni Gor" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Večjezično" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Večjezično..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Več strani (časovni zamik med posameznimi branji)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Več strani (poziv med posameznimi branji)" @@ -831,17 +930,17 @@ msgstr "Več strani (poziv med posameznimi branji)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 je popolnoma brezplačen. Razmislite o donaciji." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Ime (neobvezno)" @@ -849,6 +948,10 @@ msgstr "Ime (neobvezno)" msgid "Name missing." msgstr "Manjka ime." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Izvorni prenos" + #: UiStrings.resx$New$Message msgid "New" msgstr "Novo" @@ -861,7 +964,7 @@ msgstr "Nov Profil" msgid "Next" msgstr "Naslednji" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Naslednje Skeniranje" @@ -870,12 +973,15 @@ msgstr "Naslednje Skeniranje" msgid "No device selected." msgstr "Naprava ni izbrana." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Ne najdem naprav." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "V podajalniku ni listov." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Ponudnik ni izbran." @@ -895,21 +1001,20 @@ msgstr "Brez" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Ne Sedaj" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Število strani:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR Prenos" @@ -918,37 +1023,23 @@ msgstr "OCR Prenos" msgid "OCR Progress" msgstr "Potek OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCR Namestitev" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR Jezik:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "Način OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "V redu" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Zamik glede na poravnavo (WIA)" @@ -956,13 +1047,11 @@ msgstr "Zamik glede na poravnavo (WIA)" msgid "Old DSM" msgstr "Star DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Ena datoteka na stran" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Ena datoteka na sken" @@ -970,7 +1059,11 @@ msgstr "Ena datoteka na sken" msgid "One or more files could not be downloaded." msgstr "Ene ali več datotek ni bilo mogoče prenesti." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Dovoli samo eno aktivno okno NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Odpri Mapo" @@ -978,11 +1071,15 @@ msgstr "Odpri Mapo" msgid "Operation in Progress" msgstr "Opravilo v teku" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "Outlook Splet" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Izhod" @@ -990,7 +1087,7 @@ msgstr "Izhod" msgid "Overwrite File" msgstr "Prepiši Datoteko" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Lastniško Geslo:" @@ -998,8 +1095,8 @@ msgstr "Lastniško Geslo:" msgid "PDF Document (*.pdf)" msgstr "PDF Dokument (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF Nastavitve" @@ -1009,35 +1106,33 @@ msgstr "PDF shranjen." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "PNG Datoteka (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Velikost strani:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Izvor papirja:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Geslo" @@ -1045,21 +1140,24 @@ msgstr "Geslo" msgid "Paste" msgstr "Prilepi" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Mesta" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Vrata" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Post-procesiranje" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Izvedi OCR po skeniranju" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Pritisnite Začni, ko boste pripravljeni." @@ -1067,7 +1165,7 @@ msgstr "Pritisnite Začni, ko boste pripravljeni." msgid "Preview" msgstr "Predogled" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Predogled:" @@ -1080,12 +1178,11 @@ msgstr "Prejšnji" msgid "Print" msgstr "Natisni" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Nastavitve Profila" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profil:" @@ -1094,23 +1191,27 @@ msgstr "Profil:" msgid "Profiles" msgstr "Profili" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Vprašaj, če je izbrano" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Zahtevaj vnos poti datoteke" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Ponudnik" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Pripravljen na skeniranje {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Reši" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Reši Skenirane Slike" @@ -1122,9 +1223,15 @@ msgstr "Rešujem..." msgid "Recovery Progress" msgstr "Potek Reševanja" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Uveljavi" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Uveljjavi {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Zapomni si te nastavitve" @@ -1140,22 +1247,26 @@ msgstr "Ponastavi" msgid "Reset Image" msgstr "Ponastavi Sliko" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Ločljivost:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" -msgstr "Povrni Privzete Nastavitve" +msgstr "Povrni privzete nastavitve" #: UiStrings.resx$Reverse$Message msgid "Reverse" msgstr "Obrni Vrstni Red" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Obrnite vse" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Obratno izbrano" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Povrni" @@ -1176,31 +1287,34 @@ msgstr "Zasukaj v levo" msgid "Rotate Right" msgstr "Zasukaj v desno" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "Poženi v ozadju" +msgstr "Izvajaj v ozadju" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." msgstr "Poteka OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "SANE gonilnik" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Shrani" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Shrani vse" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Shrani vse kot sliko" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Shrani vse kot PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Shrani PDF" msgid "Save PDF Progress" msgstr "Potek Shranjevanja PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Shrani izbrano" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Shrani izbrano kot slike" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Shrani izbrano kot PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Shrani v eno datoteko" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Shrani v več datotek" @@ -1244,29 +1363,45 @@ msgstr "Shranjujem rezultate skeniranja..." msgid "Saving {0}..." msgstr "Shranjujem {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Povečava Glede Na Okno" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Merilo:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skeniraj" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Nastavitev Skeniranja" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Skeniraj s privzetim profilom" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Skenirana Slika" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Skupna raba skenerja" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Skeniram stran {0}" @@ -1280,11 +1415,14 @@ msgstr "Skeniram stran {0} (sken {1})..." msgid "Scanning page {0}..." msgstr "Skeniram stran {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Iščem naprave..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekunda (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Izberi" @@ -1293,7 +1431,10 @@ msgstr "Izberi" msgid "Select All" msgstr "Izberi Vse" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Izberi napravo" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Izberi Izvor" @@ -1302,7 +1443,6 @@ msgstr "Izberi Izvor" msgid "Select a profile before clicking Scan." msgstr "Izberite profil preden pritisnete Skeniraj." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Izberite enega ali več jezikov:" @@ -1311,46 +1451,92 @@ msgstr "Izberite enega ali več jezikov:" msgid "Selected ({0})" msgstr "Izbranih ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Loči datoteke po Patch-T" #: UiStrings.resx$SetDefault$Message msgid "Set Default" -msgstr "Nastavi Privzeto" +msgstr "Nastavi privzeto" + +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Nastavitve" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Souporaba" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Skupna raba, četudi je NAPS2 zaprt" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Skupna raba nastavitev skenerja" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Skenerje v skupni rabi lahko uporabljajo drugi računalniki v lokalnem omrežju z izbiro \"ESCL gonilnik\" v nastavitvah profila NAPS2 na drugem računalniku." #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Izostritev" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Pokaži" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Prikaži vrstico \"Profili\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Prikaži nativni TWAIN napredek" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Prikaži številko strani" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Datoteke z eno stranjo" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Skeniraj eno" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Preskoči okno shranjevanja" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Razdeli" + +#: UiStrings.resx$Start$Message msgid "Start" +msgstr "Začetek" + +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Raztegni na velikost strani" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Zadeva:" @@ -1358,12 +1544,11 @@ msgstr "Zadeva:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF Datoteka (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN Gonilnik" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Tehnične Podrobnosti" @@ -1372,6 +1557,10 @@ msgstr "Tehnične Podrobnosti" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "OCR ni na voljo. Preverite namestitev potrebnega dodatka:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Časovna omejitev operacije OCR je potekla." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Datoteke ni bilo mogoče prepisati, ker je odprta v drugem programu." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Datoteka {0} že obstaja. Ali jo želite prepisati?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Ta datoteka je kriptirana in za odprtje potrebuje geslo: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Naslednja datoteka je šifrirana in za odpiranje zahteva geslo:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,28 +1632,64 @@ msgstr "Izbran skener je zaseden." msgid "The selected scanner is offline." msgstr "Izbran skener je izklopljen." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Delovni proces se je zrušil." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Delovni proces se je zrušil. Preveri pregledovalnik dogodkov v sistemu Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Tiff možnosti" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Čas med posameznimi skeni (v sekundah):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Naslov:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Orodja" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Twain Implementacija:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 in)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" +msgstr "US Letter (8.5x11 in)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Razveljavi" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Razveljevi {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" msgstr "" #: MiscResources.resx$UnsavedChanges$Message @@ -1477,7 +1702,7 @@ msgstr "Potek posodobitve" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Preverjanje posodobitev je onemogočeno." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Posodabljanje..." msgid "Uploading email..." msgstr "Nalaganje elektronske pošte..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Uporabi privzeti uporabniški vmesnik" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Uporabi privzete nastavitve" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Uporabniško Geslo:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Uporaba OCR zahteva prenos jezikov, ki jih boste uporabljali pri skeniranju." @@ -1515,16 +1737,15 @@ msgstr "Različica {0}" msgid "View" msgstr "Pogled" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA Gonilnik" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Čakam na TWAIN, da dokonča..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Čakanje na odobritev..." @@ -1532,19 +1753,19 @@ msgstr "Čakanje na odobritev..." msgid "Waiting for scan {0}..." msgstr "Čakam na skeniranje {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Meja Beline" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Različica Wia:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Leto" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Leto (00-99)" @@ -1561,36 +1782,33 @@ msgstr "Nimate pravic za shanjevanje datotek na tem mestu." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Imate neshranjene spremembe. Ali ste prepričani, da želite zavreči spremembe in zapreti program?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Povečaj" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Naravna Velikost" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Približaj" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Oddalji" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "in" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,28 +1816,33 @@ msgstr "od {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} datotek" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} naprav najdenih." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} slik skeniranih na {1} ob {2} morda ni bilo shranjenih in jih je mogoče povrniti. Ali jih želite povrniti?" diff --git a/NAPS2.Lib/Lang/po/sq.po b/NAPS2.Lib/Lang/po/sq.po index bec090c143..7e4b9cb26f 100644 --- a/NAPS2.Lib/Lang/po/sq.po +++ b/NAPS2.Lib/Lang/po/sq.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Albanian\n" "Language: sq\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Veprimi i paracaktuar i butonit \"Ruaj\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Veprimi i paracaktuar i butonit \"Skano\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Меню \"Скан\" меняет профиль по умолчанию" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "U gjet 1 pajisje." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "Ngjyra me 24 bit" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "A3 (297 x 420 mm)" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "A4 (210 x 297 mm)" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "A5 (148 x 210 mm)" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -76,12 +60,15 @@ msgstr "Rreth" msgid "Acquiring data..." msgstr "Po merr të dhënat..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "I avancuar" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Cilësimet e avancuara të profilit" @@ -93,35 +80,35 @@ msgstr "Të gjitha ({0})" msgid "All Files" msgstr "Të gjithë skedarët" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Lejo Shënimet" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Lejo kopjimin e përmbajtjes" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Lejo kopjimin e përmbajtjes për mundësi hyrjeje" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Lejo montimin e dokumentit" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Lejo modifikimin e dokumentit" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Lejo plotësimin e formularit" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Lejo printimin me cilësi të plotë" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Lejo printimin" @@ -133,17 +120,22 @@ msgstr "Heqja e shtresave alternative" msgid "Alternate Interleave" msgstr "Krijimi i shtresave alternative" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Transfertë alternative" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Pyet gjithmonë" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Всегда спрашивать" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Nevojitet një komponent shtesë për të importuar këtë skedar PDF. A dëshiron ta shkarkosh tani?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Ndodhi një gabim gjatë instalimit të përditësimit." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Ndodhi një gabim gjatë ekzekutimit të OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Ndodhi një gabim gjatë provës për të autorizuar." msgid "An error occurred when trying to auto save." msgstr "Ndodhi një gabim gjatë provës për ruajtjen automatike." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Ndodhi një gabim gjatë instalimit të përditësimit." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Ndodhi një gabim gjatë provës për ruajtjen e skedarit." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Një operacion është në progres. Je i sigurt se dëshiron të dalësh dhe të anulosh operacionin?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Ndodhi një gabim i panjohur gjatë skanimit në grup." +msgid "An unknown error occurred during the batch scan." +msgstr "Ndodhi një gabim i panjohur gjatë ekzekutimit të skanimit në grup." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -188,9 +184,17 @@ msgstr "Një përditësim i OCR është i disponueshëm." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Drejtuesi i Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Приложение" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Zbato shkëlqimin/kontrastin pas skanimit" @@ -222,19 +226,27 @@ msgstr "A je i sigurt se dëshiron të fshish {0} element(e)?" msgid "Are you sure you want to delete {0} profiles?" msgstr "A je i sigurt se dëshiron të fshish {0} profile?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Je i sigurt se dëshiron të ndalosh disponueshmërinë e {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "A je i sigurt se dëshiron të zhbësh ndryshimet e tua te {0} imazh(e)?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Cakto" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Emri i bashkëlidhjes:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autori:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autorizo" @@ -242,43 +254,41 @@ msgstr "Autorizo" msgid "Auto" msgstr "Automatike" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Cilësimet e ruajtjes automatike" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Po rrit automatikisht numrin (1 shifër)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Duke rritur automatikisht numrin (1 shifër)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Po rrit automatikisht numrin (2 shifra)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Po rrit automatikisht numrin (3 shifra)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Po rrit automatikisht numrin (4 shifra)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Ekzekuto OCR automatikisht pas skanimit" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Skanim në grup" @@ -296,9 +306,8 @@ msgstr "Skanimi në grup ndaloi për shkak gabimi." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Më e mira" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Thellësia e bitit:" @@ -309,10 +318,10 @@ msgstr "Skedarët Bitmap (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Bardhë e zi" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Faqet bosh" @@ -320,33 +329,19 @@ msgstr "Faqet bosh" msgid "Brightness / Contrast" msgstr "Ndriçimi/Kontrasti" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Shkëlqimi:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Anulo" @@ -363,7 +358,7 @@ msgstr "Po anulon...." msgid "Center" msgstr "Në qendër" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Ndrysho" @@ -375,7 +370,7 @@ msgstr "Kontrollo për përditësime" msgid "Checking..." msgstr "Po kontrollon..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Zgjidh siguriuesin e shërbimit të emailit" @@ -383,7 +378,6 @@ msgstr "Zgjidh siguriuesin e shërbimit të emailit" msgid "Choose Profile" msgstr "Zgjidh Profilin" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Zgjidh pajisjen" @@ -395,9 +389,9 @@ msgstr "Pastro" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Pastro të gjitha" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Pastro imazhet pas ruajtjes" @@ -405,16 +399,30 @@ msgstr "Pastro imazhet pas ruajtjes" msgid "Close" msgstr "Mbyll" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Kombinoje" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Komunikimi me pajisjen e skanimit u ndërpre." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Pajtueshmëria" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Ngjeshja:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Lidhu" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Gabim në lidhje." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrasti:" @@ -433,9 +441,9 @@ msgstr "Po kopjon..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Të drejtat e autorit {0} Kontribuesit e NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Limiti i mbulimit" @@ -443,7 +451,7 @@ msgstr "Limiti i mbulimit" msgid "Crop" msgstr "Preje" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Prit te madhësia e faqes" @@ -451,10 +459,14 @@ msgstr "Prit te madhësia e faqes" msgid "Custom ({0}x{1} {2})" msgstr "E personalizuar ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Madhësi e personalizuar e faqes" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Rezolucion i personalizuar" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Rrotullim i personalizuar" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "SMTP e personalizuar" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "E personalizuar..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "E errët" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Dita (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "E paracaktuar" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Shtegu i paracaktuar i skedarit:" @@ -487,7 +504,6 @@ msgstr "Shtegu i paracaktuar i skedarit:" msgid "Deinterleave" msgstr "Heq përzierjen" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Drejto" msgid "Deskew Progress" msgstr "Progresi i drejtimit" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Drejto faqet e skanuara" @@ -509,35 +525,31 @@ msgstr "Drejto faqet e skanuara" msgid "Deskewing..." msgstr "Po drejton..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Pajisja:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Dimensionet" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Emri i afishimit:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Korrigjimi i dokumentit" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Dhuro" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "U krye" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Shkarko" @@ -550,22 +562,50 @@ msgstr "Gabim gjatë shkarkimit" msgid "Download Needed" msgstr "Nevojitet shkarkim" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Progresi i shkarkimit" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "Dpi" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Dupleks" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Drejtuesi ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "Drejtuesi i rrjetit të ESCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "Drejtuesi USB i ESCL" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Modifiko" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Dërgoje të gjithë me e-mail" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Dërgoje të gjithë me e-mail si PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,25 +616,32 @@ msgstr "Dërgo PDF me email" msgid "Email PDF Progress" msgstr "Progresi i email të PDF" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Dërgo me e-mail pjesën e zgjedhur" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Dërgo me e-mail pjesë e zgjedhur si PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Cilësimet e emailit" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Aktivizo ruajtjen automatike" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Aktivizo ruajtjen e logeve të debug" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Shifro PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Shifrimi" @@ -602,12 +649,15 @@ msgstr "Shifrimi" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Metaskedar i përmirësuar i Windows (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Gabim" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Gabim në hapjen e aplikacionit {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,36 +667,43 @@ msgstr "Madhësia e vlerësuar e shkarkimit: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "Skedar imazhi i shkëmbyeshëm (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Përjashto faqet bosh" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "I shpejtë" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Furnizuesi" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Emri i skedarit" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Shtegu i skedarit:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Rregullo balancimin e ngjyrës së bardhë dhe hiq zhurmën" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Përmbys" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Kalo mbrapa anët e faqeve duplekse" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Kthe përmbys faqet duplekse" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Për cilësi të larta të JPEG (80+), gjithashtu rrisni Cilësinë e imazhit te profili juaj për rezultatet më të mira." @@ -654,7 +711,6 @@ msgstr "Për cilësi të larta të JPEG (80+), gjithashtu rrisni Cilësinë e im msgid "GIF File (*.gif)" msgstr "Skedar GIF (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Merr më shumë gjuhë" @@ -665,18 +721,17 @@ msgstr "Xham" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "E hirtë" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Drejto horizontalisht:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Ora (0-23)" @@ -684,6 +739,10 @@ msgstr "Ora (0-23)" msgid "Hue / Saturation" msgstr "Ngjyrimi/Ngopja" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Makina" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Ikonat nga:" @@ -696,12 +755,12 @@ msgstr "Imazhi" msgid "Image Files" msgstr "Skedarët e imazhit" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Cilësia e imazhit" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Cilësimet e imazhit" @@ -745,6 +804,10 @@ msgstr "Instalimi përfundoi. A dëshiron ta rihapësh NAPS2 tani?" msgid "Installation failed." msgstr "Instalimi dështoi." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Ndërfaqja" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Përziej" @@ -755,24 +818,37 @@ msgstr "Skedar JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "Skedar JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Cilësi Jpeg" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Mbaji imazhet ndërmjet sesioneve" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Shkurtimet nga tastiera" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Fjalët kyçe:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Gjuha" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Majtas" @@ -781,33 +857,48 @@ msgstr "Majtas" msgid "Legacy (native UI only)" msgstr "E vjetër (vetëm ndërfaqja e përdoruesit origjinale)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "E zbardhur" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Ngarko imazhet në NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Bëji PDF-të të kërkueshme duke përdorur OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "IP statike" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Cilësi maksimale (skedarë të mëdhenj)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Transferim në kujtesë" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Të dhëna mbi të dhënat" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minuta (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Muaji (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Më shumë informacion" @@ -819,11 +910,19 @@ msgstr "Lëviz poshtë" msgid "Move Up" msgstr "Lëviz lart" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Shumë gjuhë" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Shumë gjuhë..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Shumë skanime (vonesë fikse ndërmjet skanimeve)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Shumë skanime (pyet ndërmjet skanimeve)" @@ -831,17 +930,17 @@ msgstr "Shumë skanime (pyet ndërmjet skanimeve)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 is tërësisht falas. Mendoni të dhuroni." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Emri (jo i detyrueshëm)" @@ -849,6 +948,10 @@ msgstr "Emri (jo i detyrueshëm)" msgid "Name missing." msgstr "Emri mungon." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Transferim nativ" + #: UiStrings.resx$New$Message msgid "New" msgstr "I ri" @@ -861,7 +964,7 @@ msgstr "Profil i ri" msgid "Next" msgstr "Pasardhëse" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Skanimi tjetër" @@ -870,12 +973,15 @@ msgstr "Skanimi tjetër" msgid "No device selected." msgstr "Nuk është zgjedhur asnjë pajisje." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Nuk u gjet asnjë pajisje." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Nuk ka faqe te furnizuesi." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Nuk u zgjodh asnjë ofrues." @@ -895,21 +1001,20 @@ msgstr "Asnjë" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Jo tani" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Numri i skanimeve:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Shkarkim i OCR" @@ -918,37 +1023,23 @@ msgstr "Shkarkim i OCR" msgid "OCR Progress" msgstr "Progresi i OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Konfigurimi i OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Gjuha e OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "Regjimi OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Gjerësia e zhvendosjes bazuar në drejtimin (WIA)" @@ -956,13 +1047,11 @@ msgstr "Gjerësia e zhvendosjes bazuar në drejtimin (WIA)" msgid "Old DSM" msgstr "DSM e vjetër" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Një skedar për faqe" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Një skedar për skanim" @@ -970,7 +1059,11 @@ msgstr "Një skedar për skanim" msgid "One or more files could not be downloaded." msgstr "Një ose më shumë skedarë s'mund të shkarkoheshin." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Lejo vetëm një instancë të NAPS2" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Hap Dosjen" @@ -978,11 +1071,15 @@ msgstr "Hap Dosjen" msgid "Operation in Progress" msgstr "Veprimi në progres" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (i ri)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "Hyrja nga Uebi në Outlook" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Dalja" @@ -990,7 +1087,7 @@ msgstr "Dalja" msgid "Overwrite File" msgstr "Mbishkruaj skedarin" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Fjalëkalimi i pronarit:" @@ -998,8 +1095,8 @@ msgstr "Fjalëkalimi i pronarit:" msgid "PDF Document (*.pdf)" msgstr "Dokument PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Cilësimet e PDF" @@ -1009,35 +1106,33 @@ msgstr "PDF u ruajt." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "Skedar PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Madhësia e faqes:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Burimi i letrës:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Fjalëkalimi" @@ -1045,21 +1140,24 @@ msgstr "Fjalëkalimi" msgid "Paste" msgstr "Ngjit" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Vendmbajtëset" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Porta" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "Pas përpunimit" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Ekzekuto në mënyrë paraprake OCR mbas skanimit" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Shtyp Fillo kur të jetë gati." @@ -1067,7 +1165,7 @@ msgstr "Shtyp Fillo kur të jetë gati." msgid "Preview" msgstr "Shiko paraprakisht" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Shiko paraprakisht:" @@ -1080,12 +1178,11 @@ msgstr "Paraardhës" msgid "Print" msgstr "Printo" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Cilësimet e profilit" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profili:" @@ -1094,23 +1191,27 @@ msgstr "Profili:" msgid "Profiles" msgstr "Profilet" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Kërkoje nëse është e zgjedhur" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Pyet për shtegun e skedarit" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Siguruesi i shërbimit" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Gati për skanim {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Rikupero" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Rikupero imazhet e skanuara" @@ -1122,9 +1223,15 @@ msgstr "Po rikuperohet..." msgid "Recovery Progress" msgstr "Progresi i rekuperimit" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Ribëje" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Ribëje {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Mbaj mend këto cilësime" @@ -1140,15 +1247,11 @@ msgstr "Rivendos" msgid "Reset Image" msgstr "Rivendos imazhin" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Rezolucioni:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Rikthe të paracaktuarat" @@ -1156,6 +1259,14 @@ msgstr "Rikthe të paracaktuarat" msgid "Reverse" msgstr "Kthe mbrapsht" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Ktheji të gjitha së prapthi" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Kthe së prapthi pjesën e zgjedhur" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Rikthe" @@ -1176,7 +1287,6 @@ msgstr "Rrotullo majtas" msgid "Rotate Right" msgstr "Rrotullo djathtas" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Ekzekuto në sfond" @@ -1185,22 +1295,26 @@ msgstr "Ekzekuto në sfond" msgid "Running OCR..." msgstr "Po ekzekuton OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "Drejtuesi SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Ruaj" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Ruaji të gjitha" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Ruaji të gjitha si Imazhe" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Ruaji të gjitha si PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Ruaj PDF" msgid "Save PDF Progress" msgstr "Progresi i ruajtjes së PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Ruaj pjesën e zgjedhur" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Ruaj pjesën e zgjedhur si Imazhe" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Ruaj pjesën e zgjedhur si Imazhe PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Ruaj në skedar të vetëm" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Ruaj në shumë skedarë" @@ -1244,29 +1363,45 @@ msgstr "Po ruan rezultatet në grup..." msgid "Saving {0}..." msgstr "Po ruan {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Shkallëzo me dritaren" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Shkalla:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skano" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Konfigurimi i skanimit" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Skano me profilin e paracaktuar" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Skano me profilin e ri" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Skano me profilin {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Imazhi i skanuar" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Disponueshmëria e skanerit" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Po skanon faqen {0}" @@ -1280,11 +1415,14 @@ msgstr "Po skanon faqen {0} (skanimi {1})..." msgid "Scanning page {0}..." msgstr "Po skanon faqen {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Po kërkon pajisjet..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekonda (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Zgjidh" @@ -1293,7 +1431,10 @@ msgstr "Zgjidh" msgid "Select All" msgstr "Zgjidhi të gjitha" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Zgjidh pajisjen" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Zgjidh burimin" @@ -1302,7 +1443,6 @@ msgstr "Zgjidh burimin" msgid "Select a profile before clicking Scan." msgstr "Zgjidh një profil para se të klikosh Skano." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Zgjidh një ose më shumë gjuhë:" @@ -1311,8 +1451,7 @@ msgstr "Zgjidh një ose më shumë gjuhë:" msgid "Selected ({0})" msgstr "E zgjedhur ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Ndaj skedarët me Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Ndaj skedarët me Patch-T" msgid "Set Default" msgstr "Vendos të paracaktuar" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Cilësimet" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Bëje të disponueshme" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Bëje të disponueshme edhe kur NAPS2 është i mbyllur" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Cilësimet e skanerit të disponueshëm" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Skanerat e disponueshëm në rrjet mund të përdoren nga kompjuterat e tjerë në rrjetin lokal duke zgjedhur \"Drajveri i ESCL\" në cilësimet e profilit të NAPS2." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Thekso" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Shfaq" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Shfaqe rreshtin e mjeteve \"Profilet\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Shfaqe progresin nativ të TWAIN" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Shfaqi numrat e faqeve" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Skedarët me një faqe" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Skanim i vetëm" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Kapërce shenjën e ruajtjes" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Ndaje" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Fillo" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Ndalo ndarjen e skanerit" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Zgjero te madhësia e faqes" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Subjekti:" @@ -1358,12 +1544,11 @@ msgstr "Subjekti:" msgid "TIFF File (*.tiff, *.tif)" msgstr "Skedar TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "Drejtuesi TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Detajet teknike" @@ -1372,6 +1557,10 @@ msgstr "Detajet teknike" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "Motori i OCR nuk është i disponueshëm. Sigurohu të instalosh paketën e kërkuar:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Afati i veprimit të OCR skadoi." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Skedari nuk mund të mbishkruhej sepse aktualisht është në përdorim. msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Skedari {0} tashmë ekziston. A dëshironi ta mbishkruani?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Skedari i mëposhtëm është i shifruar dhe kërkon një fjalëkalim për t'u hapur: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Skedari i mëposhtëm është i shifruar dhe kërkon një fjalëkaim për t'u hapur:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "Skaneri i zgjedhur është i zënë." msgid "The selected scanner is offline." msgstr "Skaneri i zgjedhur është jashtë linje." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Procesi i punës u ndërpre." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Procesi i punës u ndërpre. Kontrolloni shikuesin e ngjarjeve të Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Motivi:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Opsionet Tiff" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Koha ndërmjet skanimeve (sekonda):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Titulli:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Mjetet" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Implementimi i Twain:" @@ -1467,6 +1676,22 @@ msgstr "Ligjore SHBA (8.5 x 14 inç)" msgid "US Letter (8.5x11 in)" msgstr "Letër SHBA (8.5 x 11 inç)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Mos cakto" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Zhbëje" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Zhbëje {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Skcaner i panjohur" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Ndryshimet e pa ruajtura" @@ -1477,7 +1702,7 @@ msgstr "Progresi i përditësimit" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Kontrolli i përditësimit është çaktivizuar." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Po përditëson..." msgid "Uploading email..." msgstr "Po ngarkon emailin..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Përdor UI origjinal" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Përdor cilësimet e paracaktuara" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Fjalëkalimi i përdoruesit:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Përdorimi i OCR ju kërkon të shkarkoni çdo gjuhë që dëshironi të skanoni." @@ -1515,16 +1737,15 @@ msgstr "Versioni {0}" msgid "View" msgstr "Shfaq" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "Drejtuesi WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Po pret që të përfundojë TWAIN..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Po pret për autorizim..." @@ -1532,19 +1753,19 @@ msgstr "Po pret për autorizim..." msgid "Waiting for scan {0}..." msgstr "Po pret për skanimin {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Kufiri i bardhë" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Versioni Wia:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Viti" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Viti (00-99)" @@ -1561,28 +1782,25 @@ msgstr "Nuk ke të drejta për të ruajtur skedarët në këtë vendndodhje." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Ke ndryshime të paruajtura. A je i sigurt se dëshiron të dalësh dhe të mos i ruash ato ndryshime?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Shkalla e zmadhimit" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Zmadho aktualen" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Zmadho" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Zvogëlo" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" @@ -1590,7 +1808,7 @@ msgstr "inç" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,28 +1816,33 @@ msgstr "nga {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} skedarë" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "U gjetën {0} pajisje." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} imazh(e) i(të) skanuar në {1} në {2} mund të mos jenë ruajtur, dhe janë të rikuperueshme. A dëshironi t'i rikuperoni ato?" diff --git a/NAPS2.Lib/Lang/po/sr-CS.po b/NAPS2.Lib/Lang/po/sr-CS.po index 8b16c1240f..5662f00c6c 100644 --- a/NAPS2.Lib/Lang/po/sr-CS.po +++ b/NAPS2.Lib/Lang/po/sr-CS.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Serbian (Latin)\n" "Language: sr\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-bitna Boja" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "" @@ -76,12 +60,15 @@ msgstr "O programu" msgid "Acquiring data..." msgstr "Prikupljam podatke..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Napredne opcije" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Napredne opcije profila" @@ -93,35 +80,35 @@ msgstr "Sve ({0})" msgid "All Files" msgstr "Sve datoteke" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Dozvoli anotacije" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Dozvoli kopiranje sadržaja" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Dozvoli kopiranje sadržaja radi dostupnosti" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Dozvoli sastavljanje dokumenta (Assembly)" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Dozvoli izmene u dokumentu" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Dozvoli popunjavanje formi" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Dozvoli štampu najvišeg kvaliteta" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Dozvoli štampu" @@ -133,17 +120,22 @@ msgstr "Izmeni neispreplitanost" msgid "Alternate Interleave" msgstr "Izmeni ispreplitanost" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternativni Prenos" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Potrebna je dodatna komponenta za uvoz ovog PDF fajla. Da li želite da je sada preuzmete?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Nastala je greška prilikom pokušaja instalacije nadogradnje." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "" #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "" msgid "An error occurred when trying to auto save." msgstr "Došlo je do greške prilikom automatskog snimanja." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Nastala je greška prilikom pokušaja instalacije nadogradnje." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Došlo je do greške u pokušaju snimanja datoteke." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Došlo je do nepoznate greške u toku paketnog skeniranja." +msgid "An unknown error occurred during the batch scan." +msgstr "" #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -190,7 +186,15 @@ msgstr "Dostupno je ažuriranje za OCR.." msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Primeni osvetljenje/kontrast nakon skeniranja" @@ -222,19 +226,27 @@ msgstr "Da li ste sigurni da želite da obrišete sledeći broj datoteka: {0}?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Da li ste sigurni da želite da obrišete {0} profila?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Da li zaista želite da poništite promene na sledećem broju slika: {0}" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Naziv priloga:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Autor:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "" @@ -242,29 +254,27 @@ msgstr "" msgid "Auto" msgstr "Automatsko" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Podešavanja automatskog snimanja" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Automatsko uvećavanje broja (1 cifra)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Automatsko uvećavanje broja (2 cifre)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Automatsko uvećavanje broja (3 cifre)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Automatsko uvećavanje broja (4 cifre)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "" @@ -277,8 +287,8 @@ msgstr "" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Paketno skeniranje" @@ -298,7 +308,6 @@ msgstr "Paketno skeniranje zaustavljeno usled greške." msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Dubina bitova:" @@ -309,10 +318,10 @@ msgstr "Bitmap datoteka (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Crno/belo" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Prazne strane" @@ -320,7 +329,6 @@ msgstr "Prazne strane" msgid "Brightness / Contrast" msgstr "Osvetljenje / Kontrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Osvetljenost:" @@ -329,24 +337,11 @@ msgstr "Osvetljenost:" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Prekini" @@ -363,7 +358,7 @@ msgstr "Prekid u toku...." msgid "Center" msgstr "Centrirano" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "" @@ -375,7 +370,7 @@ msgstr "" msgid "Checking..." msgstr "" -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "" @@ -383,7 +378,6 @@ msgstr "" msgid "Choose Profile" msgstr "Odaberite profil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Odaberite uređaj" @@ -397,7 +391,7 @@ msgstr "Očisti" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Očisti slike nakon snimanja" @@ -405,16 +399,30 @@ msgstr "Očisti slike nakon snimanja" msgid "Close" msgstr "Zatvori" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Kompatibilnost" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Kompresija:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrast:" @@ -435,7 +443,7 @@ msgstr "Kopiranje..." msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Granična vrednost pokrivenosti" @@ -443,7 +451,7 @@ msgstr "Granična vrednost pokrivenosti" msgid "Crop" msgstr "Iseci" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Smanjiti do veličine stranice" @@ -451,10 +459,14 @@ msgstr "Smanjiti do veličine stranice" msgid "Custom ({0}x{1} {2})" msgstr "Proizvoljno ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Proizvoljna veličina strane" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Proizvoljna rotacija" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Proizvoljno..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Dan (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Podrazumevano" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Podrazumevana putanja:" @@ -487,7 +504,6 @@ msgstr "Podrazumevana putanja:" msgid "Deinterleave" msgstr "Neukršteno" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "" msgid "Deskew Progress" msgstr "" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" @@ -509,16 +525,14 @@ msgstr "" msgid "Deskewing..." msgstr "" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Uređaj:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Ime za prikaz:" @@ -532,12 +546,10 @@ msgstr "" msgid "Donate" msgstr "" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Gotovo" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Preuzmi" @@ -550,19 +562,47 @@ msgstr "Greška u preuzimanju" msgid "Download Needed" msgstr "" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Tok preuzimanja" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Dvostrano" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Uredi" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +616,32 @@ msgstr "Pošalji PDF e-mailom" msgid "Email PDF Progress" msgstr "Tok slanja PDF e-mailom" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "E-mail podešavanja" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Omogući automatsko snimanje" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Enkriptuj PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Enkripcija" @@ -602,12 +649,15 @@ msgstr "Enkripcija" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Poboljšani Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Greška" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "Procenjena veličina preuzimanja: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "Razmenjiva slika (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Izostavi prazne strane" @@ -629,24 +679,31 @@ msgstr "" msgid "Feeder" msgstr "Umetač" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Naziv datoteke" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Putanja datoteke:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Okreni" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" @@ -654,7 +711,6 @@ msgstr "" msgid "GIF File (*.gif)" msgstr "GIF datoteka (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Instaliraj više jezika" @@ -671,12 +727,11 @@ msgstr "" msgid "Grayscale" msgstr "Nijanse sive" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Vodoravno poravnanje:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Sat (0-23)" @@ -684,6 +739,10 @@ msgstr "Sat (0-23)" msgid "Hue / Saturation" msgstr "" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Ikonice iz:" @@ -696,12 +755,12 @@ msgstr "Slika" msgid "Image Files" msgstr "Slikovne datoteke" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Kvalitet slike" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Podešavanje slike" @@ -745,6 +804,10 @@ msgstr "Instalacija uspešna. Da li želite restart aplikacije?" msgid "Installation failed." msgstr "Instalacija nije uspela." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Ukrštanje" @@ -757,11 +820,20 @@ msgstr "JPEG datoteka (*.jpg, *.jpeg)" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Jpeg kvalitet" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Ključne reči:" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "Jezik (language)" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Levo" @@ -781,33 +857,48 @@ msgstr "Levo" msgid "Legacy (native UI only)" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Učitaj slike u aplikaciju" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Učinite PDF-ove pretraživim koristeći OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Maksimalni kvalitet (velike datoteke)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Metapodaci" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minut (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Mesec (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Više informacija" @@ -819,11 +910,19 @@ msgstr "Pomeri dole" msgid "Move Up" msgstr "Pomeri gore" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Višestruko skeniranje (fiksno kašnjenje između skeniranja)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Višestruko skeniranje (pitaj između skeniranja)" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "" @@ -849,6 +948,10 @@ msgstr "" msgid "Name missing." msgstr "Nedostaje ime." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "Novi" @@ -861,7 +964,7 @@ msgstr "Novi profil" msgid "Next" msgstr "Sledeće" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Sledeći sken" @@ -870,12 +973,15 @@ msgstr "Sledeći sken" msgid "No device selected." msgstr "Uređaj nije odabran." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Nema listova u uvlakaču." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "" @@ -897,11 +1003,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Ne sada" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Broj skenova:" @@ -909,7 +1015,6 @@ msgstr "Broj skenova:" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Preuzmi OCR" @@ -918,37 +1023,23 @@ msgstr "Preuzmi OCR" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Podesi OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR jezik:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "U redu" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Svaka strana je zasebna datoteka" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Svaki sken je zasebna datoteka" @@ -970,7 +1059,11 @@ msgstr "Svaki sken je zasebna datoteka" msgid "One or more files could not be downloaded." msgstr "Jedna ili više datoteka nije moglo biti preuzeto." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "" @@ -978,11 +1071,15 @@ msgstr "" msgid "Operation in Progress" msgstr "" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Izlazni rezultat" @@ -990,7 +1087,7 @@ msgstr "Izlazni rezultat" msgid "Overwrite File" msgstr "Presnimiti datoteku?" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Lozinka vlasnika:" @@ -998,8 +1095,8 @@ msgstr "Lozinka vlasnika:" msgid "PDF Document (*.pdf)" msgstr "" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF podešavanja" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "PNG datoteka (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Veličina stranice:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Lozinka" @@ -1045,21 +1140,24 @@ msgstr "Lozinka" msgid "Paste" msgstr "Nalepi" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Pritisni Start kada si spreman." @@ -1067,7 +1165,7 @@ msgstr "Pritisni Start kada si spreman." msgid "Preview" msgstr "Pregled" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Pregled:" @@ -1080,12 +1178,11 @@ msgstr "Prethodni" msgid "Print" msgstr "Štampaj" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Postavke profila" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "" @@ -1094,23 +1191,27 @@ msgstr "" msgid "Profiles" msgstr "Profil" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Spreman za skeniranje {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Oporavi" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Oporavi skenirane slike" @@ -1122,9 +1223,15 @@ msgstr "Oporavljanje..." msgid "Recovery Progress" msgstr "Tok oporavka" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Zapamti ova podešavanja" @@ -1140,15 +1247,11 @@ msgstr "" msgid "Reset Image" msgstr "Reset slike" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Rezolucija:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Vrati na podrazumevana podešavanja" @@ -1156,6 +1259,14 @@ msgstr "Vrati na podrazumevana podešavanja" msgid "Reverse" msgstr "Unazad" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Vrati" @@ -1176,7 +1287,6 @@ msgstr "Rotiraj levo" msgid "Rotate Right" msgstr "Rotiraj desno" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1185,7 +1295,6 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1303,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "Snimi PDF" msgid "Save PDF Progress" msgstr "Tok snimanja PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Snimi kao jednu datoteku" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Snimi kao više datoteka" @@ -1244,29 +1363,45 @@ msgstr "" msgid "Saving {0}..." msgstr "Snimanje {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Skaliraj sa veličinom prozora" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Razmer:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skeniraj" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Konfiguracija skeniranja" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Skenirana slika" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Skeniranje strane {0}" @@ -1280,11 +1415,14 @@ msgstr "Skeniranje strane {0} (scan {1})..." msgid "Scanning page {0}..." msgstr "Skeniranje strane {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekunde (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Izaberi" @@ -1293,7 +1431,10 @@ msgstr "Izaberi" msgid "Select All" msgstr "Izaberi sve sve" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Odaberi izvor" @@ -1302,7 +1443,6 @@ msgstr "Odaberi izvor" msgid "Select a profile before clicking Scan." msgstr "Izaberite profil pre nego kliknete dugme za skeniranje." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Izaberite jedan ili više jezika:" @@ -1311,8 +1451,7 @@ msgstr "Izaberite jedan ili više jezika:" msgid "Selected ({0})" msgstr "Odabrano ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,37 +1459,84 @@ msgstr "" msgid "Set Default" msgstr "Postavi kao podrazumevano" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Pokaži" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Tema:" @@ -1358,12 +1544,11 @@ msgstr "Tema:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF datoteka (*.tiff, *tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "" @@ -1372,6 +1557,10 @@ msgstr "" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Datoteka {0} već postoji. Da li želite da je presnimite?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Ova datoteka je zaštićena i zahteva lozinku da je otvorite: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "" msgid "The selected scanner is offline." msgstr "Odabrani skener nije uključen." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Vreme između skenova (sekunde):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Naslov:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Uvođenje TWAIN-a:" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Nesnimljene promene" @@ -1487,21 +1712,18 @@ msgstr "" msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Koristi izvorni korisnički interfejs" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Koristi predefinisane postavke" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Korisnička lozinka:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Korišćenje OCR-a zahteva da preuzmete svaki jezik koji želite da skenirate." @@ -1515,16 +1737,15 @@ msgstr "Verzija {0}" msgid "View" msgstr "Pogled" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Čekam da TWAIN završi..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1532,19 +1753,19 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "Čekam skeniranje {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Granična vrednost belog" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Godina" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Godina (00-99)" @@ -1561,21 +1782,18 @@ msgstr "Nemate dopuštenja da snimite datoteke na ovu lokaciju." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Promene nisu snimljene. Da li ste sigurni da želite da napustite program i odbacite promene?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Uvećaj/Umanji" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Uvećaj na pravu veličinu" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} datoteka" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} slika skeniranih na {1} na {2} možda nije sačuvano, i mogu ponovo da se kreiraju. Da li želite da ih kreirate ponovo?" diff --git a/NAPS2.Lib/Lang/po/sr.po b/NAPS2.Lib/Lang/po/sr.po index db2c2fefaf..7938312325 100644 --- a/NAPS2.Lib/Lang/po/sr.po +++ b/NAPS2.Lib/Lang/po/sr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Serbian (Cyrillic)\n" "Language: sr\n" @@ -19,49 +19,33 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 уређај је пронађен" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-битна Боја" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" @@ -76,12 +60,15 @@ msgstr "О програму" msgid "Acquiring data..." msgstr "Прикупљам податке..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Напредне опције" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Напредне опције профила" @@ -93,35 +80,35 @@ msgstr "Све ({0})" msgid "All Files" msgstr "Све датотеке" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Дозволи анотације" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Дозволи копирање садржаја" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Дозволи копирање садржаја ради доступности" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Дозволи састављање документа (Assembly)" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Дозволи измене у документу" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Дозволи попуњавање форми" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Дозволи штампу највишег квалитета" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Дозволи штампу" @@ -133,16 +120,21 @@ msgstr "Измени неиспреплитаност" msgid "Alternate Interleave" msgstr "Измени испреплитаност" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Увек питај" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." msgstr "" #: MiscResources.resx$AuthError$Message @@ -153,6 +145,10 @@ msgstr "" msgid "An error occurred when trying to auto save." msgstr "Дошло је до грешке приликом аутоматског снимања." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "" + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Дошло је до грешке у покушају снимања датотеке." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Дошло је до непознате грешке у току пакетног скенирања." +msgid "An unknown error occurred during the batch scan." +msgstr "" #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -190,7 +186,15 @@ msgstr "ажурирање софтвера за препознавање тек msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Примени осветљење/контраст након скенирања" @@ -222,19 +226,27 @@ msgstr "Да ли сте сигурни да желите да обришете msgid "Are you sure you want to delete {0} profiles?" msgstr "Да ли сте сигурни да желите да обришете {0} профила?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Да ли заиста желите да поништите промене на следећем броју слика: {0}" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Назив прилога:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Аутор:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "" @@ -242,29 +254,27 @@ msgstr "" msgid "Auto" msgstr "" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Подешавања аутоматског снимања" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Аутоматско увећавање броја (1 цифра)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Аутоматско увећавање броја (2 цифре)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Аутоматско увећавање броја (3 цифре)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Аутоматско увећавање броја (4 цифре)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "" @@ -277,8 +287,8 @@ msgstr "" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Пакетно скенирање" @@ -298,7 +308,6 @@ msgstr "Пакетно скенирање заустављено услед гр msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Дубина битова:" @@ -309,10 +318,10 @@ msgstr "Bitmap датотека (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Црно/бело" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Празне стране" @@ -320,7 +329,6 @@ msgstr "Празне стране" msgid "Brightness / Contrast" msgstr "" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Осветљеност:" @@ -329,24 +337,11 @@ msgstr "Осветљеност:" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Прекини" @@ -363,7 +358,7 @@ msgstr "Прекид у току...." msgid "Center" msgstr "Центрирано" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "" @@ -373,9 +368,9 @@ msgstr "" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "" +msgstr "Проверавам..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "" @@ -383,7 +378,6 @@ msgstr "" msgid "Choose Profile" msgstr "Одаберите профил" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Одаберите уређај" @@ -397,7 +391,7 @@ msgstr "Очисти" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Очисти слике након снимања" @@ -405,16 +399,30 @@ msgstr "Очисти слике након снимања" msgid "Close" msgstr "Затвори" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Компатибилност" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Контраст:" @@ -435,7 +443,7 @@ msgstr "Копирање..." msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Гранична вредност покривености" @@ -443,7 +451,7 @@ msgstr "Гранична вредност покривености" msgid "Crop" msgstr "Исеци" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "" @@ -451,10 +459,14 @@ msgstr "" msgid "Custom ({0}x{1} {2})" msgstr "Произвољно ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Произвољна величина стране" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Произвољна ротација" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Произвољно..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Дан (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Подразумевано" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "подразумевани фајл:" @@ -487,7 +504,6 @@ msgstr "подразумевани фајл:" msgid "Deinterleave" msgstr "Неукрштено" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "" msgid "Deskew Progress" msgstr "" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" @@ -509,16 +525,14 @@ msgstr "" msgid "Deskewing..." msgstr "" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Уређај:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "димензије" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Име за приказ:" @@ -532,12 +546,10 @@ msgstr "" msgid "Donate" msgstr "" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Готово" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Преузми" @@ -550,19 +562,47 @@ msgstr "Грешка у преузимању" msgid "Download Needed" msgstr "" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Ток преузимања" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Двострано" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Уреди" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +616,32 @@ msgstr "Пошаљи мејлом ПДФ" msgid "Email PDF Progress" msgstr "Ток слања PDF е-маилом" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Е-маил подешавања" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Омогући аутоматско снимање" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Енкриптуј PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Енкрипција" @@ -602,12 +649,15 @@ msgstr "Енкрипција" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Побољшани Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Грешка" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "Процењена величина преузимања: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "Размењива слика (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Изостави празне стране" @@ -629,24 +679,31 @@ msgstr "" msgid "Feeder" msgstr "Увлакач (Уметач)" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Назив датотеке" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Путања датотеке:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Окрени" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "окрени дуплирану страну" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" @@ -654,7 +711,6 @@ msgstr "" msgid "GIF File (*.gif)" msgstr "GIF датотека (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Инсталирај више језика" @@ -671,12 +727,11 @@ msgstr "" msgid "Grayscale" msgstr "Нијансе сиве" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Водоравно поравнање:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Сат (0-23)" @@ -684,6 +739,10 @@ msgstr "Сат (0-23)" msgid "Hue / Saturation" msgstr "" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Иконице из:" @@ -696,12 +755,12 @@ msgstr "Слика" msgid "Image Files" msgstr "Сликовне датотеке" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Квалитет слике" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Подешавање слике" @@ -745,6 +804,10 @@ msgstr "Инсталација успешна. Да ли желите реста msgid "Installation failed." msgstr "Инсталација није успела." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Укрштање" @@ -757,11 +820,20 @@ msgstr "JPEG датотека (*.jpg, *.jpeg)" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Jpeg квалитет" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Кључне речи:" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "Језик (language)" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Лево" @@ -781,33 +857,48 @@ msgstr "Лево" msgid "Legacy (native UI only)" msgstr "Легат (изворни кориснички интерфејс)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Учитај слике у апликацију" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Учините PDF-ове претраживим користећи OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Максимални квалитет (велике датотеке)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Метаподаци" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Минут (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Месец (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Више информација" @@ -819,11 +910,19 @@ msgstr "Помери доле" msgid "Move Up" msgstr "Помери горе" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Вишеструко скенирање (фиксно кашњење између скенирања)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Вишеструко скенирање (питај између скенирања)" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Име (опционално)" @@ -849,6 +948,10 @@ msgstr "Име (опционално)" msgid "Name missing." msgstr "Недостаје име." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "Нови" @@ -861,7 +964,7 @@ msgstr "Нови профил" msgid "Next" msgstr "Следеће" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Следећи скен" @@ -870,12 +973,15 @@ msgstr "Следећи скен" msgid "No device selected." msgstr "Уређај није одабран." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Нема листова у увлакачу." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "" @@ -897,11 +1003,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Не сада" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Број скенова:" @@ -909,7 +1015,6 @@ msgstr "Број скенова:" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Преузми OCR" @@ -918,37 +1023,23 @@ msgstr "Преузми OCR" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Подеси OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR језик:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "У реду" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Свака страна је засебна датотека" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Сваки скен је засебна датотека" @@ -970,7 +1059,11 @@ msgstr "Сваки скен је засебна датотека" msgid "One or more files could not be downloaded." msgstr "Једна или више датотека није могло бити преузето." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Отвори фолдер" @@ -978,11 +1071,15 @@ msgstr "Отвори фолдер" msgid "Operation in Progress" msgstr "" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Излазни резултат" @@ -990,7 +1087,7 @@ msgstr "Излазни резултат" msgid "Overwrite File" msgstr "Преснимити датотеку?" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Лозинка власника:" @@ -998,8 +1095,8 @@ msgstr "Лозинка власника:" msgid "PDF Document (*.pdf)" msgstr "ПДФ документ" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF подешавања" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "PNG датотека (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Величина странице:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "извор папира:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Лозинка" @@ -1045,21 +1140,24 @@ msgstr "Лозинка" msgid "Paste" msgstr "Налепи" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Алокатор дужине и формата датотеке" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Притисни Старт када си спреман." @@ -1067,7 +1165,7 @@ msgstr "Притисни Старт када си спреман." msgid "Preview" msgstr "Преглед" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Преглед:" @@ -1080,12 +1178,11 @@ msgstr "Претходни" msgid "Print" msgstr "Штампај" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Поставке профила" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "" @@ -1094,23 +1191,27 @@ msgstr "" msgid "Profiles" msgstr "Профили" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Спреман за скенирање {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Опорави" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Опорави скениране слике" @@ -1122,9 +1223,15 @@ msgstr "Опорављање..." msgid "Recovery Progress" msgstr "Ток опоравка" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Запамти ова подешавања" @@ -1140,15 +1247,11 @@ msgstr "Ресет" msgid "Reset Image" msgstr "Ресет слике" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Резолуција:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Врати на подразумевана подешавања" @@ -1156,6 +1259,14 @@ msgstr "Врати на подразумевана подешавања" msgid "Reverse" msgstr "Уназад" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Врати" @@ -1176,7 +1287,6 @@ msgstr "Ротирај лево" msgid "Rotate Right" msgstr "Ротирај десно" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1185,7 +1295,6 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1303,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "Сними PDF" msgid "Save PDF Progress" msgstr "Ток снимања PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Сними као једну датотеку" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Сними као више датотека" @@ -1244,29 +1363,45 @@ msgstr "Снимање резултата пакетног скенирања... msgid "Saving {0}..." msgstr "Снимање {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Скалирај са величином прозора" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Размер:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Скенирај" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Конфигурација скенирања" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Скенирана слика" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Скенирање стране {0}" @@ -1280,11 +1415,14 @@ msgstr "Скенирање стране {0} (скен {1})..." msgid "Scanning page {0}..." msgstr "Скенирање стране {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Секунде (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Изабери" @@ -1293,7 +1431,10 @@ msgstr "Изабери" msgid "Select All" msgstr "Изабери све" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Одабери извор" @@ -1302,7 +1443,6 @@ msgstr "Одабери извор" msgid "Select a profile before clicking Scan." msgstr "Изаберите профил пре него кликнете дугме за скенирање." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Изаберите један или више језика:" @@ -1311,8 +1451,7 @@ msgstr "Изаберите један или више језика:" msgid "Selected ({0})" msgstr "Одабрано ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,37 +1459,84 @@ msgstr "" msgid "Set Default" msgstr "Постави као подразумевано" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Покажи" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Појединачни скен" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Старт" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Тема:" @@ -1358,12 +1544,11 @@ msgstr "Тема:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF датотека (*.tiff, *tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "ТWАИН Драјвер" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "технички детаљи" @@ -1372,6 +1557,10 @@ msgstr "технички детаљи" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Фајл не може бити преписан, јер је трену msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Датотека {0} већ постоји. Да ли желите да је преснимите?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Ова датотека је заштићена и захтева лозинку да је отворите: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "" msgid "The selected scanner is offline." msgstr "Одабрани скенер није укључен." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Време између скенова (секунде):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Наслов:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Увођење TWAIN-a:" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Неснимљене промене" @@ -1487,21 +1712,18 @@ msgstr "" msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Користи изворни кориснички интерфејс" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Користи предефинисане поставке" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Корисничка лозинка:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Коришћење ОЦР-а захтева да преузмете сваки језик који желите да скенирате." @@ -1515,16 +1737,15 @@ msgstr "Верзија {0}" msgid "View" msgstr "Поглед" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA Драјвер" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Чекам да TWAIN заврши..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1532,19 +1753,19 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "Чекам скенирање {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Гранична вредност белог" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Година" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Година (00-99)" @@ -1561,21 +1782,18 @@ msgstr "Немате допуштења да снимите датотеке н msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Промене нису снимљене. Да ли сте сигурни да желите да напустите програм и одбаците промене?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Uvećaj/Umanji" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Увећај на праву величину" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Увеличај" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Смањи" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} датотека" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} слика скенираних на {1} на {2} можда није сачувано, и могу поново да се креирају. Да ли желите да их креирате поново?" diff --git a/NAPS2.Lib/Lang/po/sv.po b/NAPS2.Lib/Lang/po/sv.po index 97a3c2e870..55835b775a 100644 --- a/NAPS2.Lib/Lang/po/sv.po +++ b/NAPS2.Lib/Lang/po/sv.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Swedish\n" "Language: sv\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Standardåtgärd för knappen \"Spara\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Standardåtgärd för knappen \"Skanna\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Menyn \"Skanna\" ändrar standardprofil" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 enhet hittades." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-bitars färg" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -76,12 +60,15 @@ msgstr "Om" msgid "Acquiring data..." msgstr "Hämtar data..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Åtgärd" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Avancerat" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Avancerade profilinställningar" @@ -93,35 +80,35 @@ msgstr "Alla ({0})" msgid "All Files" msgstr "Alla filer" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" -msgstr "Tillåt notiser" +msgstr "Tillåt annoteringar" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Tillåt innehållskopiering" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Tillåt innehållskopiering för tillgänglighet" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Tillåt dokumentsammanslagning" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Tillåt dokumentändring" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Tillåt formulärifyllning" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Tillåt full utskriftskvalitet" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Tillåt utskrift" @@ -133,33 +120,42 @@ msgstr "Alternativ avinterfoliering" msgid "Alternate Interleave" msgstr "Alternativ interfoliering" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Annan överföring" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Fråga alltid" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Fråga alltid" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "En extra komponent behövs för att importera denna PDF-fil. Vill du ladda ner den nu?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Ett fel inträffade när uppdateringen försökte installeras." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Ett fel uppstod vid körning av OCR." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "Ett fel uppstod vid försök att auktorisera." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." msgstr "Ett fel inträffade vid försök att spara automatiskt." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Ett fel inträffade när uppdateringen försökte installeras." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Ett fel inträffade vid försök att spara filen." #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "Ett fel uppstod när man försökte skicka till E-Post." +msgstr "Ett fel uppstod vid försök att skicka e-post." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "En tjänst är aktiv. Är du säker på du vill avbryta eller avsluta?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Ett okänt fel inträffade under mängdskanning." +msgid "An unknown error occurred during the batch scan." +msgstr "Ett okänt fel uppstod under mängdskanningen." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -188,9 +184,17 @@ msgstr "En uppdatering till OCR finns tillänglig." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple-drivrutin" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Applikation" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Tillämpa ljus/kontrast efter skanning" @@ -222,19 +226,27 @@ msgstr "Vill du verkligen ta bort {0} objekt?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Vill du verkligen ta bort {0} profiler?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Är du säker på att du vill sluta dela {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Vill du verkligen ångra dina ändringar i {0} bild(er)?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Tilldela" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Bilagenamn:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Upphovsman:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Autentisera" @@ -242,43 +254,41 @@ msgstr "Autentisera" msgid "Auto" msgstr "Automatiskt" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Spara inställningar automatiskt" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Automatiskt uppräkningnummer (1 siffra)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Auto-uppräknande nummer (1 siffra)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Automatiskt uppräkningnummer (2 siffror)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Automatiskt uppräkningnummer (3 siffror)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Automatiskt uppräkningnummer (4 siffror)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "Automatiskt köra ORC scanning" +msgstr "Kör automatiskt OCR efter skanning" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Mängdskanning" @@ -296,9 +306,8 @@ msgstr "Mängdskanning stoppad på grund av fel." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Bäst" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bitdjup:" @@ -309,44 +318,30 @@ msgstr "Bitmap-filer (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Svartvitt" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" -msgstr "Blanka sidor" +msgstr "Tomma sidor" #: UiStrings.resx$BrightnessContrast$Message msgid "Brightness / Contrast" msgstr "Ljusstyrka / Kontrast" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Ljus:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Hittar du inte din skanner? Läs om begränsningar i NAPS2 som Flatpak." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Avbryt" @@ -363,19 +358,19 @@ msgstr "Avbryter...." msgid "Center" msgstr "Centrera" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Ändra" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "Kontrollera för uppdatering" +msgstr "Sök efter uppdateringar" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." msgstr "Kontrollerar..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Välj e-post leverantör" @@ -383,7 +378,6 @@ msgstr "Välj e-post leverantör" msgid "Choose Profile" msgstr "Välj profil" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Välj enhet" @@ -395,9 +389,9 @@ msgstr "Rensa" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Rensa alla" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Rensa bort bilder efter att de sparats" @@ -405,16 +399,30 @@ msgstr "Rensa bort bilder efter att de sparats" msgid "Close" msgstr "Stäng" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Sammanfoga" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Kommunikationen med skanningsenheten avbröts." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Kompatibilitet" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Komprimera:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Anslut" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Anslutningsfel." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Kontrast:" @@ -433,9 +441,9 @@ msgstr "Kopierar..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Copyright {0} bidragsgivare till NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Tröskelvärde" @@ -443,18 +451,22 @@ msgstr "Tröskelvärde" msgid "Crop" msgstr "Beskär" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" -msgstr "Ändra till sidstorlek " +msgstr "Beskär till sidstorlek" #: MiscResources.resx$CustomPageSizeFormat$Message msgid "Custom ({0}x{1} {2})" msgstr "Anpassad ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Anpassad sidstorlek" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Anpassad upplösning" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Anpassad rotation" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "Modifierad SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Anpassad..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Mörkt" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Dag (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Standard" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Standard filsökväg:" @@ -487,7 +504,6 @@ msgstr "Standard filsökväg:" msgid "Deinterleave" msgstr "Interfoliera inte" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,43 +517,39 @@ msgstr "Rätta upp" msgid "Deskew Progress" msgstr "Rättar upp framsteg" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" -msgstr "Rättar scannade sidor" +msgstr "Räta upp skannade sidor" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." msgstr "Rättar..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Enhet:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Dimensioner" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Visningsnamn:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Dokumentkorrektion" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Donerar" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Klart" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Ladda ner" @@ -550,22 +562,50 @@ msgstr "Nedladdningsfel" msgid "Download Needed" msgstr "Nerladdning behövs" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Nedladdningsförlopp" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "DPI" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" -msgstr "" +msgstr "Duplex" + +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL-drivrutin" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL-nätverksdrivrutin" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL-USB-drivrutin" #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Redigera" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Redigera med {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Redigera med..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "E-posta alla" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "E-posta alla som PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,38 +616,48 @@ msgstr "E-posta PDF" msgid "Email PDF Progress" msgstr "E-postförlopp" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "E-posta markerade" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "E-posta markerade som PDF" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "E-postinställningar" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Spara automatiskt" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Aktivera felsökningsregistrering" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Kryptera PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Kryptering" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" -msgstr "" +msgstr "Enhanced Windows MetaFile (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Fel" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Fel vid applikationsstart {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -615,38 +665,45 @@ msgstr "Beräknad nedladdningsstorlek: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" -msgstr "" +msgstr "Exchangeable Image File (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" -msgstr "Undanta blanka sidor" +msgstr "Utelämna tomma sidor" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Snabb" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Matare" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Filnamn" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Filsökväg:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Åtgärda vitbalans och ta bort brus" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Vänd" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Vänd duplexsidors baksidor" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" -msgstr "Vänd duplex-sidor" +msgstr "Vänd duplexsidor" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "För hög JPEG kvalitet (80+), öka bild kvaliteten i din profil för bästa resultat." @@ -654,7 +711,6 @@ msgstr "För hög JPEG kvalitet (80+), öka bild kvaliteten i din profil för b msgid "GIF File (*.gif)" msgstr "GIF-fil (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Hämta fler språk" @@ -665,18 +721,17 @@ msgstr "Glas" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "Gråskala" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Horisontell justering:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Timma (0-23)" @@ -684,6 +739,10 @@ msgstr "Timma (0-23)" msgid "Hue / Saturation" msgstr "Ljus / Mättnad" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Värd" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Ikoner från:" @@ -696,12 +755,12 @@ msgstr "Bild" msgid "Image Files" msgstr "Bildfiler" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Bildkvalitet" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Bildinställningar" @@ -745,6 +804,10 @@ msgstr "Installationen slutförd. Vill du starta om NAPS2 nu?" msgid "Installation failed." msgstr "Installationen misslyckades." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Gränssnitt" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Interfoliera" @@ -755,59 +818,87 @@ msgstr "JPEG-fil (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000-fil (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "JPEG-kvalitet" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Behåll bilder mellan sessioner" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Kortkommandon" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Nyckelord:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Språk" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Skriv en recension" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Vänster" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" -msgstr "Inbyggd (endast inbyggt användargränssnitt)" +msgstr "Äldre (endast inbyggt gränssnitt)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Ljust" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Gillar du NAPS2?" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Läs in bilder i NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Gör PDF:er sökbara med hjälp av OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Manuell IP" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Maximal kvalitet (stora filer)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Minnesöverföring" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" -msgstr "" +msgstr "Metadata" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Minut (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Månad (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Mer info" @@ -819,11 +910,19 @@ msgstr "Flytta ner" msgid "Move Up" msgstr "Flytta upp" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Flera språk" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Flera språk..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Flera skanningar (fast fördröjning mellan skanningar)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Flera skanningar (fråga mellan varje skanning)" @@ -831,17 +930,17 @@ msgstr "Flera skanningar (fråga mellan varje skanning)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 − {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "NAPS2 är helt gratis. Tänk gärna på att donera.." +msgstr "NAPS2 är helt gratis. Överväg gärna att donera." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Namn (tillägg)" @@ -849,6 +948,10 @@ msgstr "Namn (tillägg)" msgid "Name missing." msgstr "Namn saknas." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Inbyggd överföring" + #: UiStrings.resx$New$Message msgid "New" msgstr "Nytt" @@ -861,7 +964,7 @@ msgstr "Ny profil" msgid "Next" msgstr "Nästa" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Nästa skanning" @@ -870,12 +973,15 @@ msgstr "Nästa skanning" msgid "No device selected." msgstr "Ingen enhet vald." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Inga enheter hittades." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Det finns inga sidor i mataren." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Ingen leverantör vald." @@ -895,21 +1001,20 @@ msgstr "Ingen" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Inte nu" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Antal skanningar:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OCR" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR-nedladdning" @@ -918,51 +1023,35 @@ msgstr "OCR-nedladdning" msgid "OCR Progress" msgstr "OCR Framgång" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCR-installation" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR-språk:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "OCR läge:" - -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message +msgstr "OCR-läge:" + #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "OK" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" -msgstr "Offset breddbaserad vid justering (WIA)" +msgstr "Förskjutningsbredd baserad på justering (WIA)" #: SettingsResources.resx$TwainImpl_OldDsm$Message msgid "Old DSM" msgstr "Gammal DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "En fil per sida" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "En fil per skanning" @@ -970,7 +1059,11 @@ msgstr "En fil per skanning" msgid "One or more files could not be downloaded." msgstr "En eller flera filer kunde inte laddas ner." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Tillåt endast en NAPS2-instans" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Öppna mapp" @@ -978,11 +1071,15 @@ msgstr "Öppna mapp" msgid "Operation in Progress" msgstr "Tjänsten körs" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (ny)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" -msgstr "" +msgstr "Outlook Web Access" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Utdata" @@ -990,7 +1087,7 @@ msgstr "Utdata" msgid "Overwrite File" msgstr "Skriv över filen" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Ägarlösenord:" @@ -998,8 +1095,8 @@ msgstr "Ägarlösenord:" msgid "PDF Document (*.pdf)" msgstr "PDF-dokument (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF-inställningar" @@ -1009,35 +1106,33 @@ msgstr "PDF sparad." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "PNG-fil (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Sidstorlek:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Papperskälla:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Lösenord" @@ -1045,21 +1140,24 @@ msgstr "Lösenord" msgid "Paste" msgstr "Klistra in" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Platshållare" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" -msgstr "För behandling" +msgstr "Efterbehandling" + +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Kör OCR efter skanning i förebyggande syfte" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Tryck start när det är klart." @@ -1067,7 +1165,7 @@ msgstr "Tryck start när det är klart." msgid "Preview" msgstr "Förhandsgranskning" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Förhandsgranskning:" @@ -1080,12 +1178,11 @@ msgstr "Föregående" msgid "Print" msgstr "Skriv ut" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Profilinställningar" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profil:" @@ -1094,23 +1191,27 @@ msgstr "Profil:" msgid "Profiles" msgstr "Profiler" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Fråga vid markering" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Ange fil sökväg" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Leverantör" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Klar att skanna {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Återställ" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Återställ skannade bilder" @@ -1122,9 +1223,15 @@ msgstr "Återställer..." msgid "Recovery Progress" msgstr "Återställningsförlopp" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Gör om" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Gör om {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Kom ihåg dessa inställningar" @@ -1140,21 +1247,25 @@ msgstr "Återställ" msgid "Reset Image" msgstr "Återställ bild" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Upplösning:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Återställ standard" #: UiStrings.resx$Reverse$Message msgid "Reverse" -msgstr "Omvänt" +msgstr "Vänd om" + +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Vänd om alla" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Vänd om markerade" #: UiStrings.resx$Revert$Message msgid "Revert" @@ -1176,7 +1287,6 @@ msgstr "Rotera vänster" msgid "Rotate Right" msgstr "Rotera höger" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Körs i bakgrunden" @@ -1185,22 +1295,26 @@ msgstr "Körs i bakgrunden" msgid "Running OCR..." msgstr "Kör OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "SANE-drivrutin" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Spara" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Spara alla" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Spara alla som bilder" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Spara alla som PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "Spara PDF" msgid "Save PDF Progress" msgstr "PDF-sparförlopp" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Spara markerade" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Spara markerade som bilder" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Spara markerade som PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Spara som en fil" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Spara som flera filer" @@ -1244,29 +1363,45 @@ msgstr "Sparar mängdskanningsresultat..." msgid "Saving {0}..." msgstr "Sparar {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Anpassa till fönsterstorlek" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Skala:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Skanna" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Skanningskonfiguration" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Skanna med standardprofil" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Skanna med ny profil" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Skanna med profilen {0}" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Skannad bild" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Skannerdelning" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Skannar sidan {0}" @@ -1280,11 +1415,14 @@ msgstr "Skannar sidan {0} (skanning {1})..." msgid "Scanning page {0}..." msgstr "Skannar sidan {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Söker efter enheter ..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Sekund (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Markera" @@ -1293,7 +1431,10 @@ msgstr "Markera" msgid "Select All" msgstr "Markera alla" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Välj enhet" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Välj källa" @@ -1302,7 +1443,6 @@ msgstr "Välj källa" msgid "Select a profile before clicking Scan." msgstr "Välj en profil före skanning." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Välj ett eller flera språk:" @@ -1311,8 +1451,7 @@ msgstr "Välj ett eller flera språk:" msgid "Selected ({0})" msgstr "Markerade ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Delade filer av Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Delade filer av Patch-T" msgid "Set Default" msgstr "Ange standard" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Inställningar" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Dela" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Dela också när NAPS2 är stängt" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Inställningar för delad skanner" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Det går att använda delade skannrar från andra datorer på det lokala nätverket genom att välja \"ESCL-drivrutin\" i NAPS2:s profilinställningar på den andra datorn." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Skarpare" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Kortkommando" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Visa" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Visa verktygsfältet \"Profiler\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Visa inbyggt TWAIN-förlopp" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Visa sidnummer" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Sidofält" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Enstaka sid filer" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Enkelskanning" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Fråga inte vid spara" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Dela" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Starta" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Stäng av skannerdelning" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" -msgstr "Anpassa till sid storlek" +msgstr "Anpassa till sidstorlek" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Ämne:" @@ -1358,12 +1544,11 @@ msgstr "Ämne:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF-fil (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN-drivrutin" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Tekniska detaljer" @@ -1372,6 +1557,10 @@ msgstr "Tekniska detaljer" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "OCR motorn är inte tillgänglig. Kontrollera och installera paketet som krävs:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "OCR-åtgärden överskred tidsgränsen." + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Filen kunde inte skrivas över eftersom den används." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Filen {0} finns redan. Vill du skriva över den?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Följande fil är krypterad och kräver ett lösenord för att öppnas: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Följande fil är krypterad och kräver ett lösenord för att öppnas:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,29 +1632,65 @@ msgstr "Den valda skannern är upptagen." msgid "The selected scanner is offline." msgstr "Den valda skannern är offline." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Arbetsprocessen kraschade." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Arbetsprocessen kraschade. Se Loggboken i Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Tema:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Tiff Val" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Tid mellan skanningar (sekunder):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Titel:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Verktyg" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" -msgstr "Twain-implementering:" +msgstr "TWAIN-implementering:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 in)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" -msgstr "" +msgstr "US Letter (8.5x11 in)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Ta bort tilldelning" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Ångra" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Ångra {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Okänd skanner" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" @@ -1477,7 +1702,7 @@ msgstr "Uppdatering körs" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Sökning efter uppdateringar är inaktiverad." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "Uppdatering pågår..." msgid "Uploading email..." msgstr "Laddar upp e-post..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Använd inbyggt användargränssnitt" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Använd fördefinierade inställningar" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Användarlösenord:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Användning av OCR kräver nedladdning av varje språk du vill skanna." @@ -1509,22 +1731,21 @@ msgstr "Användning av OCR kräver nedladdning av varje språk du vill skanna." #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message msgid "Version {0}" -msgstr "" +msgstr "Version {0}" #: UiStrings.resx$View$Message msgid "View" msgstr "Visa" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA-drivrutin" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Väntar på att TWAIN skall slutföras..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Väntar på auktorisering..." @@ -1532,19 +1753,19 @@ msgstr "Väntar på auktorisering..." msgid "Waiting for scan {0}..." msgstr "Väntar på skanning {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Vittröskelvärde:" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "WIA-version:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "År" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "År (00-99)" @@ -1561,28 +1782,25 @@ msgstr "Du saknar tillstånd att spara filer på den här platsen." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Det finns ändringar som inte sparats. Vill du verkligen avsluta och avbryta dessa ändringar?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" -msgstr "" +msgstr "Zooma" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Zooma faktisk storlek" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Zooma in" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Zooma ut" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" @@ -1590,7 +1808,7 @@ msgstr "tum" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,28 +1816,33 @@ msgstr "av {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} filer" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} enheter hittades." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} bild(er) skannad(e) på {1} i {2} kanske inte har sparats, och är återställningsbara. Vill du återställa dem?" diff --git a/NAPS2.Lib/Lang/po/templates.pot b/NAPS2.Lib/Lang/po/templates.pot index a9192521d0..819f40cc68 100644 --- a/NAPS2.Lib/Lang/po/templates.pot +++ b/NAPS2.Lib/Lang/po/templates.pot @@ -13,42 +13,26 @@ msgstr "" "X-Generator: Translate Toolkit 1.13.0\n" "X-Poedit-SourceCharset: iso-8859-1\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "" @@ -70,12 +54,15 @@ msgstr "" msgid "Acquiring data..." msgstr "" -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "" @@ -87,35 +74,35 @@ msgstr "" msgid "All Files" msgstr "" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "" @@ -127,16 +114,21 @@ msgstr "" msgid "Alternate Interleave" msgstr "" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." msgstr "" #: MiscResources.resx$AuthError$Message @@ -147,6 +139,10 @@ msgstr "" msgid "An error occurred when trying to auto save." msgstr "" +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "" + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "" @@ -169,7 +165,7 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "" #: MiscResources.resx$UpdateAvailable$Message @@ -184,7 +180,15 @@ msgstr "" msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "" @@ -216,19 +220,27 @@ msgstr "" msgid "Are you sure you want to delete {0} profiles?" msgstr "" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "" @@ -236,29 +248,27 @@ msgstr "" msgid "Auto" msgstr "" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "" @@ -271,8 +281,8 @@ msgstr "" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "" @@ -292,7 +302,6 @@ msgstr "" msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "" @@ -306,7 +315,7 @@ msgstr "" msgid "Black and White" msgstr "" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "" @@ -314,7 +323,6 @@ msgstr "" msgid "Brightness / Contrast" msgstr "" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "" @@ -323,24 +331,11 @@ msgstr "" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "" @@ -357,7 +352,7 @@ msgstr "" msgid "Center" msgstr "" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "" @@ -369,7 +364,7 @@ msgstr "" msgid "Checking..." msgstr "" -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "" @@ -377,7 +372,6 @@ msgstr "" msgid "Choose Profile" msgstr "" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "" @@ -391,7 +385,7 @@ msgstr "" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "" @@ -399,16 +393,30 @@ msgstr "" msgid "Close" msgstr "" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "" @@ -429,7 +437,7 @@ msgstr "" msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "" @@ -437,7 +445,7 @@ msgstr "" msgid "Crop" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "" @@ -445,10 +453,14 @@ msgstr "" msgid "Custom ({0}x{1} {2})" msgstr "" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "" @@ -458,22 +470,27 @@ msgid "Custom SMTP" msgstr "" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "" -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "" @@ -481,7 +498,6 @@ msgstr "" msgid "Deinterleave" msgstr "" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -495,7 +511,7 @@ msgstr "" msgid "Deskew Progress" msgstr "" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" @@ -503,16 +519,14 @@ msgstr "" msgid "Deskewing..." msgstr "" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "" @@ -526,12 +540,10 @@ msgstr "" msgid "Donate" msgstr "" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "" @@ -544,19 +556,47 @@ msgstr "" msgid "Download Needed" msgstr "" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -570,25 +610,32 @@ msgstr "" msgid "Email PDF Progress" msgstr "" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "" @@ -596,12 +643,15 @@ msgstr "" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -611,7 +661,7 @@ msgstr "" msgid "Exchangeable Image File (*.exif)" msgstr "" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "" @@ -623,24 +673,31 @@ msgstr "" msgid "Feeder" msgstr "" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" +msgstr "" + +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" msgstr "" #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" @@ -648,7 +705,6 @@ msgstr "" msgid "GIF File (*.gif)" msgstr "" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "" @@ -665,12 +721,11 @@ msgstr "" msgid "Grayscale" msgstr "" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "" @@ -678,6 +733,10 @@ msgstr "" msgid "Hue / Saturation" msgstr "" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "" @@ -690,12 +749,12 @@ msgstr "" msgid "Image Files" msgstr "" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "" @@ -739,6 +798,10 @@ msgstr "" msgid "Installation failed." msgstr "" +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "" @@ -751,11 +814,20 @@ msgstr "" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "" @@ -767,6 +839,10 @@ msgstr "" msgid "Language" msgstr "" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "" @@ -775,33 +851,48 @@ msgstr "" msgid "Legacy (native UI only)" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "" @@ -813,11 +904,19 @@ msgstr "" msgid "Move Up" msgstr "" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "" @@ -835,7 +934,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "" @@ -843,6 +942,10 @@ msgstr "" msgid "Name missing." msgstr "" +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "" @@ -855,7 +958,7 @@ msgstr "" msgid "Next" msgstr "" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "" @@ -864,12 +967,15 @@ msgstr "" msgid "No device selected." msgstr "" +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "" -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "" @@ -891,12 +997,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message #: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "" @@ -904,7 +1009,6 @@ msgstr "" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "" @@ -913,37 +1017,23 @@ msgstr "" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -951,13 +1041,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "" @@ -965,7 +1053,11 @@ msgstr "" msgid "One or more files could not be downloaded." msgstr "" -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "" @@ -973,11 +1065,15 @@ msgstr "" msgid "Operation in Progress" msgstr "" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "" @@ -985,7 +1081,7 @@ msgstr "" msgid "Overwrite File" msgstr "" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "" @@ -993,8 +1089,8 @@ msgstr "" msgid "PDF Document (*.pdf)" msgstr "" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "" @@ -1022,17 +1118,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "" @@ -1040,21 +1134,24 @@ msgstr "" msgid "Paste" msgstr "" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "" @@ -1062,7 +1159,7 @@ msgstr "" msgid "Preview" msgstr "" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "" @@ -1075,12 +1172,11 @@ msgstr "" msgid "Print" msgstr "" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "" @@ -1089,24 +1185,26 @@ msgstr "" msgid "Profiles" msgstr "" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "" -#: FRecover.resx$btnRecover.Text$Message #: UiStrings.resx$Recover$Message msgid "Recover" msgstr "" -#: FRecover.resx$$this.Text$Message #: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "" @@ -1119,9 +1217,15 @@ msgstr "" msgid "Recovery Progress" msgstr "" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "" @@ -1137,15 +1241,11 @@ msgstr "" msgid "Reset Image" msgstr "" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "" @@ -1153,6 +1253,14 @@ msgstr "" msgid "Reverse" msgstr "" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "" @@ -1173,7 +1281,6 @@ msgstr "" msgid "Rotate Right" msgstr "" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1182,7 +1289,6 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1191,6 +1297,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1217,6 +1328,11 @@ msgstr "" msgid "Save PDF Progress" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1225,11 +1341,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "" @@ -1241,29 +1357,45 @@ msgstr "" msgid "Saving {0}..." msgstr "" -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "" @@ -1277,11 +1409,14 @@ msgstr "" msgid "Scanning page {0}..." msgstr "" -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "" @@ -1290,7 +1425,10 @@ msgstr "" msgid "Select All" msgstr "" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "" @@ -1299,7 +1437,6 @@ msgstr "" msgid "Select a profile before clicking Scan." msgstr "" -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "" @@ -1308,8 +1445,7 @@ msgstr "" msgid "Selected ({0})" msgstr "" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1317,37 +1453,84 @@ msgstr "" msgid "Set Default" msgstr "" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "" @@ -1355,12 +1538,11 @@ msgstr "" msgid "TIFF File (*.tiff, *.tif)" msgstr "" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "" @@ -1369,6 +1551,10 @@ msgstr "" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1391,8 +1577,8 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" msgstr "" #: MiscResources.resx$DevicePaperJam$Message @@ -1440,19 +1626,39 @@ msgstr "" msgid "The selected scanner is offline." msgstr "" -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "" @@ -1464,6 +1670,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "" @@ -1484,21 +1706,18 @@ msgstr "" msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "" @@ -1512,16 +1731,15 @@ msgstr "" msgid "View" msgstr "" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "" -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1529,19 +1747,19 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "" -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "" @@ -1558,21 +1776,18 @@ msgstr "" msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "" @@ -1601,22 +1816,26 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + #: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "" diff --git a/NAPS2.Lib/Lang/po/th.po b/NAPS2.Lib/Lang/po/th.po index bbef54563b..aeec95ac96 100644 --- a/NAPS2.Lib/Lang/po/th.po +++ b/NAPS2.Lib/Lang/po/th.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:35\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Thai\n" "Language: th\n" @@ -19,130 +19,122 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "ค่าเริ่มต้นของปุ่ม \"บันทึก\"" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "ค่าเริ่มต้นของปุ่ม \"สแกน\"" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "พบ 1 อุปกรณ์" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" -msgstr "" - -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" +msgstr "สี 24 บิต" #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 มม.)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 มม.)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 มม.)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message msgid "About" -msgstr "" +msgstr "เกี่ยวกับ" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." +msgstr "กำลังรับข้อมูล..." + +#: UiStrings.resx$Action$Message +msgid "Action" msgstr "" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" -msgstr "" +msgstr "ขั้นสูง" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" -msgstr "" +msgstr "การตั้งค่าโปรไฟล์ขั้นสูง" #: MiscResources.resx$AllCount$Message msgid "All ({0})" -msgstr "" +msgstr "ทั้งหมด ({0})" #: MiscResources.resx$FileTypeAllFiles$Message msgid "All Files" -msgstr "" +msgstr "ไฟล์ทั้งหมด" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "" #: UiStrings.resx$AltDeinterleave$Message msgid "Alternate Deinterleave" -msgstr "" +msgstr "แยกหน้าแบบสลับทิศทาง" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" +msgstr "แทรกสลับหน้าแบบสลับทิศทาง" + +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" msgstr "" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" -msgstr "" +msgstr "จำเป็นต้องมีส่วนประกอบเพิ่มเติมเพื่อนำเข้าไฟล์ PDF นี้ คุณต้องการดาวน์โหลดทันทีหรือไม่" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." msgstr "" #: MiscResources.resx$AuthError$Message @@ -153,6 +145,10 @@ msgstr "" msgid "An error occurred when trying to auto save." msgstr "" +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "" + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "" @@ -175,22 +171,30 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "" #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "" +msgstr "มีการอัปเดตใหม่พร้อมใช้งาน" #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "" +msgstr "มีการอัปเดต OCR ใหม่ให้ใช้งานแล้ว" #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "" @@ -208,7 +212,7 @@ msgstr "" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "" +msgstr "คุณต้องการ ลบ \"{0}\" ใช่หรือไม่" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" @@ -222,63 +226,69 @@ msgstr "" msgid "Are you sure you want to delete {0} profiles?" msgstr "" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "อนุญาต" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "อัตโนมัติ" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" -msgstr "" +msgstr "การตั้งค่าบันทึกอัตโนมัติ" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "การเพิ่มจำนวนอัตโนมัติ (1 หลัก)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" -msgstr "" +msgstr "การเพิ่มจำนวนอัตโนมัติ (2 หลัก)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 มม.)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 มม.)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "" @@ -296,154 +306,152 @@ msgstr "" #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "ดีที่สุด" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" -msgstr "" +msgstr "ความลึกบิต:" #: MiscResources.resx$FileTypeBmp$Message msgid "Bitmap Files (*.bmp)" -msgstr "" +msgstr "ไฟล์บิตแมป (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" -msgstr "" +msgid "Black and White" +msgstr "ขาวดำ" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" -msgstr "" +msgstr "หน้าว่าง" #: UiStrings.resx$BrightnessContrast$Message msgid "Brightness / Contrast" -msgstr "" +msgstr "ความสว่าง / ความเปรียบต่าง" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" -msgstr "" +msgstr "ความสว่าง:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" -msgstr "" +msgstr "ยกเลิก" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" -msgstr "" +msgstr "ยกเลิกคำสั่งชุด" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." -msgstr "" +msgstr "กำลังยกเลิก...." #: SettingsResources.resx$HorizontalAlign_Center$Message msgid "Center" -msgstr "" +msgstr "กึ่งกลาง" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" -msgstr "" +msgstr "เปลี่ยนแปลง" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "" +msgstr "ตรวจสอบเวอร์ชันใหม่" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "" +msgstr "กำลังตรวจสอบ..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "" +msgstr "เลือกผู้ให้บริการอีเมล" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" -msgstr "" +msgstr "เลือกโปรไฟล์" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" -msgstr "" +msgstr "เลือกอุปกรณ์" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message msgid "Clear" -msgstr "" +msgstr "ล้าง" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "ล้างทั้งหมด" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" -msgstr "" +msgstr "ล้างภาพหลังจากบันทึก" #: MiscResources.resx$Close$Message msgid "Close" +msgstr "ปิด" + +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "รวมเอกสาร" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." msgstr "" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" -msgstr "" +msgstr "การบีบอัด:" + +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "เชื่อมต่อ" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "การเชื่อมต่อผิดพลาด" -#: FEditProfile.resx$label7.Text$Message #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "" #: UiStrings.resx$Copy$Message msgid "Copy" -msgstr "" +msgstr "คัดลอก" #: MiscResources.resx$CopyProgress$Message msgid "Copy Progress" -msgstr "" +msgstr "กำลังคัดลอก" #: MiscResources.resx$Copying$Message msgid "Copying..." -msgstr "" +msgstr "กำลังคัดลอก..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "" #: UiStrings.resx$Crop$Message msgid "Crop" -msgstr "" +msgstr "ครอบตัด" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "" @@ -451,10 +459,14 @@ msgstr "" msgid "Custom ({0}x{1} {2})" msgstr "" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "" @@ -464,150 +476,188 @@ msgid "Custom SMTP" msgstr "" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "" -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "มืด" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" -msgstr "" +msgstr "วัน (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" -msgstr "" +msgstr "ค่าเริ่มต้น" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" -msgstr "" +msgstr "ที่อยู่ไฟล์เริ่มต้น:" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" -msgstr "" +msgstr "แยกหน้าแบบสลับ" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" -msgstr "" +msgstr "ลบ" #: UiStrings.resx$Deskew$Message msgid "Deskew" -msgstr "" +msgstr "ปรับให้ตรง" #: MiscResources.resx$AutoDeskewProgress$Message msgid "Deskew Progress" msgstr "" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." -msgstr "" +msgstr "กำลังปรับให้ตรง..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" -msgstr "" +msgstr "อุปกรณ์:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "ปรับแก้เอกสาร" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "" +msgstr "บริจาค" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" -msgstr "" +msgstr "เสร็จ" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" -msgstr "" +msgstr "ดาวน์โหลด" #: MiscResources.resx$DownloadError$Message msgid "Download Error" -msgstr "" +msgstr "การดาวน์โหลดผิดพลาด" #: MiscResources.resx$DownloadNeeded$Message msgid "Download Needed" msgstr "" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" +msgstr "ความคืบหน้าการดาวน์โหลด" + +#: UiStrings.resx$Dpi$Message +msgid "Dpi" msgstr "" #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" +msgstr "แก้ไข" + +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" msgstr "" #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "ส่งอีเมลทั้งหมดเป็น PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message msgid "Email PDF" -msgstr "" +msgstr "ส่งอีเมล PDF" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" +msgstr "ความคืบหน้าการส่งอีเมล PDF" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" msgstr "" #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" -msgstr "" +msgstr "ตั้งค่าอีเมล" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" +msgstr "บันทึกอัตโนมัติ" + +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" msgstr "" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" -msgstr "" +msgstr "เข้ารหัส PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" -msgstr "" +msgstr "การเข้ารหัส" #: MiscResources.resx$FileTypeEmf$Message msgid "Enhanced Windows MetaFile (*.emf)" msgstr "" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" +msgstr "ผิดพลาด" + +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" msgstr "" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "" msgid "Exchangeable Image File (*.exif)" msgstr "" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "" @@ -627,61 +677,70 @@ msgstr "" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" -msgstr "" +msgstr "ตัวป้อนอัตโนมัติ" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" +msgstr "ชื่อไฟล์:" + +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" msgstr "" #: UiStrings.resx$Flip$Message msgid "Flip" +msgstr "กลับด้าน" + +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" msgstr "" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" -msgstr "" +msgstr "GIF File (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "" #: SettingsResources.resx$Source_Glass$Message msgid "Glass" -msgstr "" +msgstr "หน้ากระจก" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" -msgstr "" +msgstr "โทนสีเทา" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" -msgstr "" +msgstr "ชั่วโมง (0-23)" #: UiStrings.resx$HueSaturation$Message msgid "Hue / Saturation" +msgstr "เฉดสี / ความอิ่มตัว" + +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" msgstr "" #: UiStrings.resx$IconsFrom$Message @@ -690,18 +749,18 @@ msgstr "" #: UiStrings.resx$Image$Message msgid "Image" -msgstr "" +msgstr "รูปภาพ" #: MiscResources.resx$FileTypeImageFiles$Message msgid "Image Files" -msgstr "" +msgstr "ไฟล์รูปภาพ" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "" @@ -711,19 +770,19 @@ msgstr "" #: UiStrings.resx$Import$Message msgid "Import" -msgstr "" +msgstr "นำเข้า" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" -msgstr "" +msgstr "ความคืบหน้าการนำเข้า" #: MiscResources.resx$ImportingFormat$Message msgid "Importing {0}..." -msgstr "" +msgstr "กำลังนำเข้า {0}..." #: MiscResources.resx$Importing$Message msgid "Importing..." -msgstr "" +msgstr "กำลังนำเข้า...." #: MiscResources.resx$Install$Message msgid "Install {0}" @@ -745,23 +804,36 @@ msgstr "" msgid "Installation failed." msgstr "" +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" -msgstr "" +msgstr "แทรกสลับหน้า" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" -msgstr "" +msgstr "JPEG File (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000 File (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "" @@ -771,59 +843,86 @@ msgstr "" #: UiStrings.resx$Language$Message msgid "Language" +msgstr "ภาษา" + +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" msgstr "" #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" -msgstr "" +msgstr "ซ้าย" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "สว่าง" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" -msgstr "" +msgstr "นาที (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" -msgstr "" +msgstr "เดือน (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" -msgstr "" +msgstr "ข้อมูลเพิ่มเติม" #: UiStrings.resx$MoveDown$Message msgid "Move Down" -msgstr "" +msgstr "เลื่อนลง" #: UiStrings.resx$MoveUp$Message msgid "Move Up" +msgstr "เลื่อนขึ้น" + +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." msgstr "" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "" @@ -831,27 +930,31 @@ msgstr "" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" -msgstr "" +msgstr "ชื่อ (ถ้ามี)" #: MiscResources.resx$NameMissing$Message msgid "Name missing." msgstr "" +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" -msgstr "" +msgstr "ใหม่" #: UiStrings.resx$NewProfile$Message msgid "New Profile" @@ -859,9 +962,9 @@ msgstr "" #: UiStrings.resx$Next$Message msgid "Next" -msgstr "" +msgstr "ถัดไป" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "" @@ -870,12 +973,15 @@ msgstr "" msgid "No device selected." msgstr "" +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "" -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "" @@ -887,7 +993,7 @@ msgstr "" #: MiscResources.resx$NoUpdates$Message msgid "No updates available." -msgstr "" +msgstr "ยังไม่มีเวอร์ชันใหม่" #: SettingsResources.resx$TiffComp_None$Message msgid "None" @@ -897,11 +1003,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "" @@ -909,7 +1015,6 @@ msgstr "" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "" @@ -918,37 +1023,23 @@ msgstr "" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "ตกลง" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "" @@ -970,7 +1059,11 @@ msgstr "" msgid "One or more files could not be downloaded." msgstr "" -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "" @@ -978,11 +1071,15 @@ msgstr "" msgid "Operation in Progress" msgstr "" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "" @@ -990,84 +1087,85 @@ msgstr "" msgid "Overwrite File" msgstr "" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "" #: MiscResources.resx$FileTypePdf$Message msgid "PDF Document (*.pdf)" -msgstr "" +msgstr "เอกสาร PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" -msgstr "" +msgstr "การตั้งค่า PDF" #: MiscResources.resx$PdfSaved$Message msgid "PDF saved." -msgstr "" +msgstr "PDF บันทึกแล้ว" #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" -msgstr "" +msgstr "PNG File (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" -msgstr "" +msgstr "ขนาดหน้า:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" -msgstr "" +msgstr "แหล่งกระดาษ:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" -msgstr "" +msgstr "รหัสผ่าน" #: UiStrings.resx$Paste$Message msgid "Paste" -msgstr "" +msgstr "วาง" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "" #: UiStrings.resx$PreviewFormTitle$Message msgid "Preview" -msgstr "" +msgstr "ดูตัวอย่าง" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "" @@ -1078,39 +1176,42 @@ msgstr "" #: MiscResources.resx$Print$Message #: UiStrings.resx$Print$Message msgid "Print" -msgstr "" +msgstr "พิมพ์" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" -msgstr "" +msgstr "โปรไฟล์:" #: UiStrings.resx$Profiles$Message #: UiStrings.resx$ProfilesFormTitle$Message msgid "Profiles" +msgstr "โปรไฟล์" + +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" msgstr "" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "" -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "" @@ -1122,15 +1223,21 @@ msgstr "" msgid "Recovery Progress" msgstr "" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "ทำซ้ำ" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "" #: UiStrings.resx$Reorder$Message msgid "Reorder" -msgstr "" +msgstr "จัดเรียงหน้าใหม่" #: UiStrings.resx$Reset$Message msgid "Reset" @@ -1140,15 +1247,11 @@ msgstr "" msgid "Reset Image" msgstr "" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" -msgstr "" +msgstr "ความละเอียด:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "" @@ -1156,6 +1259,14 @@ msgstr "" msgid "Reverse" msgstr "" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "" @@ -1166,17 +1277,16 @@ msgstr "" #: UiStrings.resx$Rotate$Message msgid "Rotate" -msgstr "" +msgstr "หมุน" #: UiStrings.resx$RotateLeft$Message msgid "Rotate Left" -msgstr "" +msgstr "หมุนซ้าย" #: UiStrings.resx$RotateRight$Message msgid "Rotate Right" -msgstr "" +msgstr "หมุนขวา" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1185,27 +1295,31 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "บันทึก" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "บันทึกทั้งหมด" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "บันทึกทั้งหมดเป็นภาพ" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "บันทึกทั้งหมดเป็น PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message msgid "Save Images" -msgstr "" +msgstr "บันทึกภาพ" #: MiscResources.resx$SaveImagesProgress$Message msgid "Save Images Progress" @@ -1214,10 +1328,15 @@ msgstr "" #: MiscResources.resx$SavePdf$Message #: UiStrings.resx$SavePdf$Message msgid "Save PDF" -msgstr "" +msgstr "บันทึก PDF" #: MiscResources.resx$SavePdfProgress$Message msgid "Save PDF Progress" +msgstr "ความคืบหน้าการบันทึก PDF" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" msgstr "" #: UiStrings.resx$SaveSelectedAsImages$Message @@ -1226,13 +1345,13 @@ msgstr "" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "บันทึกรายการที่เลือกเป็น PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "" @@ -1244,29 +1363,45 @@ msgstr "" msgid "Saving {0}..." msgstr "" -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "" @@ -1280,20 +1415,26 @@ msgstr "" msgid "Scanning page {0}..." msgstr "" -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" -msgstr "" +msgstr "เลือก" #: UiStrings.resx$SelectAll$Message msgid "Select All" +msgstr "เลือกทั้งหมด" + +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" msgstr "" -#: FSelectDevice.resx$$this.Text$Message #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "" @@ -1302,7 +1443,6 @@ msgstr "" msgid "Select a profile before clicking Scan." msgstr "" -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "" @@ -1311,8 +1451,7 @@ msgstr "" msgid "Selected ({0})" msgstr "" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,50 +1459,96 @@ msgstr "" msgid "Set Default" msgstr "" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "ตั้งค่า" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" +msgstr "ความ​คม​ชัด" + +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Show$Message msgid "Show" msgstr "" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" +msgstr "เริ่ม" + +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "" #: MiscResources.resx$FileTypeTiff$Message msgid "TIFF File (*.tiff, *.tif)" -msgstr "" +msgstr "TIFF File (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "" @@ -1372,6 +1557,10 @@ msgstr "" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1380,11 +1569,11 @@ msgstr "" #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message msgid "The file '{0}' could not be imported." -msgstr "" +msgstr "ไม่สามารถนำเข้าไฟล์ '{0}' ได้" #: MiscResources.resx$ImportErrorNAPS2Pdf$Message msgid "The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported." -msgstr "" +msgstr "ไม่สามารถนำเข้าไฟล์ '{0}' ได้ สามารถนำเข้าได้เฉพาะไฟล์ PDF ที่สร้างโดย NAPS2 เท่านั้น" #: MiscResources.resx$FileInUse$Message msgid "The file could not be overwritten because it is currently in use." @@ -1394,8 +1583,8 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" msgstr "" #: MiscResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "" msgid "The selected scanner is offline." msgstr "" -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "เครื่องมือ" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "" @@ -1467,17 +1676,33 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "" #: MiscResources.resx$UpdateProgress$Message msgid "Update Progress" -msgstr "" +msgstr "ความคืบหน้าการอัปเดต" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "การตรวจสอบการอัปเดตถูกปิดใช้งาน" #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "" msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" -msgstr "" +msgstr "รหัสผ่านผู้ใช้:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "" @@ -1509,22 +1731,21 @@ msgstr "" #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message msgid "Version {0}" -msgstr "" +msgstr "เวอร์ชั่น {0}" #: UiStrings.resx$View$Message msgid "View" -msgstr "" +msgstr "เปิดดูภาพ" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "" -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1532,21 +1753,21 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "" -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" -msgstr "" +msgstr "ปี" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" -msgstr "" +msgstr "ปี (00-99)" #: MiscResources.resx$PdfNoPermissionToExtractContent$Message #: SdkResources.resx$PdfNoPermissionToExtractContent$Message @@ -1561,36 +1782,33 @@ msgstr "" msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "ซม." #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" -msgstr "" +msgstr "นิ้ว" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "มม." #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,28 +1816,33 @@ msgstr "" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "" diff --git a/NAPS2.Lib/Lang/po/tr.po b/NAPS2.Lib/Lang/po/tr.po index 282b3a7d03..021c709994 100644 --- a/NAPS2.Lib/Lang/po/tr.po +++ b/NAPS2.Lib/Lang/po/tr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Turkish\n" "Language: tr\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "\"Kaydet\" düğmesi öntanımlı eylemi:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "\"Tara\" düğmesi öntanımlı eylemi:" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "\"Tara\" menüsü öntanımlı profili değiştirir" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "1 aygıt bulundu." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" -msgstr "24 Bit Renk" - -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" +msgstr "24-bit Renk" #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3 (297x420 mm)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210x297 mm)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5 (148x210 mm)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -74,14 +58,17 @@ msgstr "Hakkında" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." -msgstr "Veri elde ediliyor..." +msgstr "Veri ediniliyor..." + +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Eylem" -#: FEditProfile.resx$btnAdvanced.Text$Message #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Gelişmiş" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Gelişmiş Profil Ayarları" @@ -93,35 +80,35 @@ msgstr "Tümü ({0})" msgid "All Files" msgstr "Tüm Dosyalar" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Açıklamalara İzin Ver" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "İçerik Kopyalamaya İzin Ver" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Erişilebilirlik İçin İçerik Kopyalamaya İzin Ver" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Belge Toparlamaya İzin Ver" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Belge Düzenlemeye İzin Ver" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Form Doldurmaya İzin Ver" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Tam Kalite Baskıya İzin Ver" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Baskıya İzin Ver" @@ -133,17 +120,22 @@ msgstr "Alternatif Tersine Yığım" msgid "Alternate Interleave" msgstr "Alternatif Yığım" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Alternatif Aktarım" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Her Zaman Sor" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Her Zaman Sor" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Bu PDF dosyasını içe aktarmak için ek bileşen gerekiyor. Şimdi indirmek ister misiniz?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Güncelleme kurulmaya çalışılırken hata oluştu." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "OKT çalıştırılırken hata oluştu." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Yetkilendirmeye çalışılırken hata oluştu." msgid "An error occurred when trying to auto save." msgstr "Kendiliğinden kaydetme denenirken hata oluştu." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Güncelleme kurulmaya çalışılırken hata oluştu." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Dosya kaydedilmeye çalışılırken hata oluştu." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Süren işlem var. Çıkmak ve süreci iptal etmek istediğinize emin misiniz?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Yığın tarama sırasında bilinmeyen bir hata oluştu." +msgid "An unknown error occurred during the batch scan." +msgstr "Toplu tarama sırasında bilinmeyen bir hata oluştu." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -184,13 +180,21 @@ msgstr "Güncelleme var." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "OCR için bir güncelleme var." +msgstr "OKT için güncelleme var." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple Sürücüsü" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Uygulama" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Taramadan sonra parlaklık/karşıtlık uygula" @@ -222,19 +226,27 @@ msgstr "{0} adet ögeyi silmek istediğinize emin misiniz?" msgid "Are you sure you want to delete {0} profiles?" msgstr "{0} adet profili silmek istediğinize emin misiniz?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Paylaşımı durdurmak istediğinize emin misiniz {0}?" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "{0} adet resimde yapılan değişiklikleri geri almak istediğinize emin misiniz?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Ata" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Ek Adı:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Yazar:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Yetkilendir" @@ -242,43 +254,41 @@ msgstr "Yetkilendir" msgid "Auto" msgstr "Kendiliğinden" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Kendiliğinden Kaydetme Ayarları" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "Kendiliğinden artan sayı (1 hane)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Kendiliğinden artan sayı (2 hane)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Kendiliğinden artan sayı (3 hane)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Kendiliğinden artan sayı (4 hane)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "Taradıktan sonra OCR'yi kendiliğinden çalıştır" +msgstr "Taradıktan sonra OKT'yi kendiliğinden çalıştır" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 mm)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 mm)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Yığın Tarama" @@ -296,9 +306,8 @@ msgstr "Yığın tarama hata nedeniyle durdu." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "En iyi" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Bit derinliği:" @@ -309,10 +318,10 @@ msgstr "Bitmap Dosyaları (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Siyah & Beyaz" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Boş Sayfalar" @@ -320,33 +329,19 @@ msgstr "Boş Sayfalar" msgid "Brightness / Contrast" msgstr "Parlaklık / Karşıtlık" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Parlaklık:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Tarayıcınızı bulamıyor musunuz? NAPS2 Flatpak'ın kısıtlamalarını okuyun." -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Vazgeç" @@ -363,7 +358,7 @@ msgstr "İptal ediliyor...." msgid "Center" msgstr "Merkez" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Değiştir" @@ -375,7 +370,7 @@ msgstr "Güncellemeleri denetle" msgid "Checking..." msgstr "Denetleniyor..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "E-Posta Sağlayıcı Seç" @@ -383,10 +378,9 @@ msgstr "E-Posta Sağlayıcı Seç" msgid "Choose Profile" msgstr "Profil Seç" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" -msgstr "Aygıtı seç" +msgstr "Aygıt seç" #: MiscResources.resx$Clear$Message #: UiStrings.resx$Clear$Message @@ -395,9 +389,9 @@ msgstr "Temizle" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Tümünü Temizle" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Kaydettikten sonra resimleri temizle" @@ -405,16 +399,30 @@ msgstr "Kaydettikten sonra resimleri temizle" msgid "Close" msgstr "Kapat" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Birleştir" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Tarayıcıyla iletişim kesildi." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Uyumluluk" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Sıkıştırma:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "Bağlan" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Bağlantı hatası." + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Karşıtlık:" @@ -433,9 +441,9 @@ msgstr "Kopyalanıyor..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Telif Hakkı {0} NAPS2 Katkıcıları" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Kapsam Eşiği" @@ -443,7 +451,7 @@ msgstr "Kapsam Eşiği" msgid "Crop" msgstr "Kırp" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Sayfa boyutuna kırp" @@ -451,10 +459,14 @@ msgstr "Sayfa boyutuna kırp" msgid "Custom ({0}x{1} {2})" msgstr "Özel ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Özel Sayfa Boyutu" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Özel Çözünürlük" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Özel Döndürme" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "Özel SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Özel..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Koyu" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Gün (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Öntanımlı" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Öntanımlı Dosya Yolu:" @@ -487,7 +504,6 @@ msgstr "Öntanımlı Dosya Yolu:" msgid "Deinterleave" msgstr "Boşluk Çıkar" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "Eğriliği Düzelt" msgid "Deskew Progress" msgstr "Eğriliği Düzeltme Süreci" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "Taranan sayfaların eğriliğini düzelt" @@ -509,35 +525,31 @@ msgstr "Taranan sayfaların eğriliğini düzelt" msgid "Deskewing..." msgstr "Eğrilik Düzeltiliyor..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Aygıt:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Boyutlar" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Görünüm adı:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Belge Düzeltimi" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "Bağış Yap" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Tamam" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "İndir" @@ -550,22 +562,50 @@ msgstr "İndirme Hatası" msgid "Download Needed" msgstr "İndirme Gerekli" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "İndirme Süreci" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "Dpi" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Çift Taraflı" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL Sürücüsü" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL Ağ Sürücüsü" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB Sürücüsü" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Düzenle" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "{0} ile düzenle" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Şununla düzenle..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Tümünü E-postala" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Tümünü PDF Olarak E-Postala" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,25 +616,32 @@ msgstr "PDF'yi E-postala" msgid "Email PDF Progress" msgstr "PDF'yi E-Postalama Süreci" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Seçilenleri E-postala" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Seçileni PDF Olarak E-Postala" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "E-Posta Ayarları" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Kendiliğinden Kaydetmeyi Etkinleştir" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Hata ayıklama günlüğünü etkinleştir" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "PDF'yi Şifrele" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Şifreleme" @@ -602,51 +649,61 @@ msgstr "Şifreleme" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Gelişmiş Windows Meta Dosyası (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Hata" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Uygulama başlatılırken hata {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" -msgstr "Tahmini indirme boyutu: {0} MB" +msgstr "Öngörülen indirme boyutu: {0} MB" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" msgstr "Takas Edilebilir Resim Dosyası (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Boş sayfaları dışarıda bırak" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Hızlı" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Besleyici" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Dosya Adı" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" -msgstr "Dosya yolu:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" +msgstr "Dosya Yolu:" + +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Beyaz dengesini düzelt ve gürültüyü kaldır" #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Ters Yüz Et" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Çift taraflı sayfaların arka yüzlerini çevirin" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Çift taraflı sayfaları ters yüz et" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Yüksek JPEG kalitesi için (80+), ayrıca en iyi sonuçlar için profilinizdeki Resim Kalitesi'ni yükseltin." @@ -654,7 +711,6 @@ msgstr "Yüksek JPEG kalitesi için (80+), ayrıca en iyi sonuçlar için profil msgid "GIF File (*.gif)" msgstr "GIF Dosyası (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Daha çok dil yükle" @@ -665,18 +721,17 @@ msgstr "Üst Kapak" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" -msgstr "Gri tonlama" +msgstr "Gri Tonlama" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Yatay hizalama:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Saat (0-23)" @@ -684,6 +739,10 @@ msgstr "Saat (0-23)" msgid "Hue / Saturation" msgstr "Renk Tonu / Doygunluk" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Ana Makine" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "Simgeler:" @@ -696,12 +755,12 @@ msgstr "Resim" msgid "Image Files" msgstr "Resim Dosyaları" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Resim Kalitesi" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Resim Ayarları" @@ -745,6 +804,10 @@ msgstr "Kurulum tamamlandı. Şimdi NAPS2'yi yeniden başlatmak etmek ister misi msgid "Installation failed." msgstr "Kurulum başarısız." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Arayüz" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Boşluk Ekle" @@ -755,24 +818,37 @@ msgstr "JPEG Dosyası (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG2000 Dosyası (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Jpeg Kalitesi" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Görüntüleri oturumlar arasında sakla" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Klavye Kısayolları" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Anahtar Sözcükler:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "Dil" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "Yorum Yap" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Sol" @@ -781,33 +857,48 @@ msgstr "Sol" msgid "Legacy (native UI only)" msgstr "Eski (yalnızca yerli kullanıcı arayüzü)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Açık" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "NAPS2'yi Beğendiniz Mi?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Resimleri NAPS2'ye yükle" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" -msgstr "OCR kullanarak aranabilir PDF oluştur" +msgstr "OKT kullanarak aranabilir PDF oluştur" + +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Elle IP" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "En yüksek kalite (büyük dosyalar)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Bellek Aktarımı" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Üst Veri" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Dakika (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Ay (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Daha çok bilgi" @@ -819,11 +910,19 @@ msgstr "Aşağı Taşı" msgid "Move Up" msgstr "Yukarı Taşı" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Çoklu Dil" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Çoklu Dil..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Çoklu tarama (taramalar arası sabit gecikme)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Çoklu tarama (taramalar arası komut istemi)" @@ -831,17 +930,17 @@ msgstr "Çoklu tarama (taramalar arası komut istemi)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 tümüyle ücretsizdir. Bağış yapmayı düşünün." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Ad (isteğe bağlı)" @@ -849,6 +948,10 @@ msgstr "Ad (isteğe bağlı)" msgid "Name missing." msgstr "Ad eksik." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Yerli Aktarım" + #: UiStrings.resx$New$Message msgid "New" msgstr "Yeni" @@ -861,7 +964,7 @@ msgstr "Yeni Profil" msgid "Next" msgstr "İleri" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Sonraki Tarama" @@ -870,12 +973,15 @@ msgstr "Sonraki Tarama" msgid "No device selected." msgstr "Hiçbir aygıt seçilmedi." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Hiçbir aygıt bulunamadı." + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Besleyicide sayfa yok." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Sağlayıcı seçilmedi." @@ -895,60 +1001,45 @@ msgstr "Hiçbiri" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Şimdi Değil" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Tarama sayısı:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "" +msgstr "OKT" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" -msgstr "OCR Dili Yükle" +msgstr "OKT İndir" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "OCR Süreci" +msgstr "OKT Süreci" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" -msgstr "OCR Kurulumu" +msgstr "OKT Kurulumu" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" -msgstr "OCR dili:" +msgstr "OKT dili:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "OCR kipi:" - -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message +msgstr "OKT kipi:" + #: UiStrings.resx$OK$Message msgid "OK" msgstr "Tamam" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "Hizalamayı taban alan ofset genişliği (WIA)" @@ -956,13 +1047,11 @@ msgstr "Hizalamayı taban alan ofset genişliği (WIA)" msgid "Old DSM" msgstr "Eski DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Sayfa başı tek dosya" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Tarama başı tek dosya" @@ -970,7 +1059,11 @@ msgstr "Tarama başı tek dosya" msgid "One or more files could not be downloaded." msgstr "Bir veya daha çok dosya indirilemedi." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Yalnızca tek NAPS2 örneğine izin ver" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Klasörü Aç" @@ -978,11 +1071,15 @@ msgstr "Klasörü Aç" msgid "Operation in Progress" msgstr "İşlem Sürüyor" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (yeni)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "Outlook Web Erişimi" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Çıktı" @@ -990,7 +1087,7 @@ msgstr "Çıktı" msgid "Overwrite File" msgstr "Dosyanın Üzerine Yaz" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "Sahip Parolası:" @@ -998,8 +1095,8 @@ msgstr "Sahip Parolası:" msgid "PDF Document (*.pdf)" msgstr "PDF Belgesi (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF Ayarları" @@ -1009,35 +1106,33 @@ msgstr "PDF kaydedildi." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "PNG Dosya (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Sayfa boyutu:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Kağıt kaynağı:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Parola" @@ -1045,31 +1140,34 @@ msgstr "Parola" msgid "Paste" msgstr "Yapıştır" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" -msgstr "Yer tutucular" +msgstr "Yer Tutucular" + +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Port" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "İleri İşleme" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Taramadan sonra OKT'yi kendiliğinden çalıştır" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Hazır olduğunda Başlat'a basın." #: UiStrings.resx$PreviewFormTitle$Message msgid "Preview" -msgstr "Önizleme" +msgstr "Ön İzle" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" -msgstr "Önizleme:" +msgstr "Ön İzleme:" #: UiStrings.resx$Previous$Message msgid "Previous" @@ -1080,12 +1178,11 @@ msgstr "Geri" msgid "Print" msgstr "Yazdır" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Profil Ayarları" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Profil:" @@ -1094,23 +1191,27 @@ msgstr "Profil:" msgid "Profiles" msgstr "Profiller" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Seçildiyse Sor" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Dosya yolu için komut iste" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Sağlayıcı" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "{0}. tarama için hazır." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Kurtar" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Taranmış Resimleri Kurtar" @@ -1122,9 +1223,15 @@ msgstr "Kurtarılıyor..." msgid "Recovery Progress" msgstr "Kurtarma Süreci" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Yinele" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Geri al: {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Bu ayarları anımsa" @@ -1140,15 +1247,11 @@ msgstr "Sıfırla" msgid "Reset Image" msgstr "Resmi Sıfırla" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Çözünürlük:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Öntanımlıları Geri Yükle" @@ -1156,6 +1259,14 @@ msgstr "Öntanımlıları Geri Yükle" msgid "Reverse" msgstr "Ters Çevir" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Tümünü Geri Al" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Seçimi Ters Çevir" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Geri Al" @@ -1176,31 +1287,34 @@ msgstr "Sola Çevir" msgid "Rotate Right" msgstr "Sağa Çevir" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Arka Planda Çalıştır" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "OCR Çalıştırılıyor..." +msgstr "OKT Çalıştırılıyor..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "SANE Sürücüsü" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Kaydet" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Tümünü Kaydet" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Tümünü Resim Olarak Kaydet" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Tümünü PDF Olarak Kaydet" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "PDF Kaydet" msgid "Save PDF Progress" msgstr "PDF Kaydetme Süreci" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Seçilenleri Kaydet" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Seçileni Resim Olarak Kaydet" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Seçileni PDF Olarak Kaydet" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Tek dosyaya kaydet" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Çoklu dosyalara kaydet" @@ -1244,29 +1363,45 @@ msgstr "Yığın sonuçları kaydediliyor..." msgid "Saving {0}..." msgstr "{0} kaydediliyor..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Pencereye Göre Ölçeklendir" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Ölçek:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Tara" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Tarama Yapılandırması" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Öntanımlı Profille Tara" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Yeni Profille Tara" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "{0} Profiliyle Tara" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Taranmış Resim" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Tarayıcı Paylaşımı" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "{0}. sayfa taranıyor" @@ -1280,11 +1415,14 @@ msgstr "{0}. sayfa taranıyor ({1}. tarama)..." msgid "Scanning page {0}..." msgstr "{0}. sayfa taranıyor..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Aygıtlar aranıyor..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Saniye (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Seç" @@ -1293,26 +1431,27 @@ msgstr "Seç" msgid "Select All" msgstr "Tümünü Seç" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Aygıt Seç" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Kaynağı Seç" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." -msgstr "Taraya tıklamadan önce profil seçiniz." +msgstr "Taraya tıklamadan önce profil seç." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" -msgstr "Bir veya daha çok dil seçin:" +msgstr "Bir ya da daha çok dil seç:" #: MiscResources.resx$SelectedCount$Message msgid "Selected ({0})" msgstr "Seçilen ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Dosyaları Patch-T'ye göre ayır" @@ -1320,37 +1459,84 @@ msgstr "Dosyaları Patch-T'ye göre ayır" msgid "Set Default" msgstr "Öntanımlı Olarak Ata" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Ayarlar" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Paylaş" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "NAPS2 kapalıyken de paylaş" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Paylaşılan Tarayıcı Ayarları" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Paylaşılan tarayıcılar, yerel ağdaki diğer bilgisayarların NAPS2 profil ayarlarında \"ESCL Sürücüsü\" seçilerek kullanılabilir." + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Keskinleştir" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Kısayol" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Göster" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "\"Profiller\" araç çubuğunu göster" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Özgün TWAIN ilerlemesini göster" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Sayfa numaralarını göster" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Kenar çubuğu" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "Tekli sayfa dosyaları" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "Tekli tarama" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Kaydetme istemini atla" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Böl" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Başlat" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Tarayıcı Paylaşımını Durdur" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Sayfa boyutuna ger" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Konu:" @@ -1358,19 +1544,22 @@ msgstr "Konu:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF Dosyası (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN Sürücüsü" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Teknik Ayrıntılar" #: MiscResources.resx$TesseractNotAvailable$Message #: SdkResources.resx$TesseractNotAvailable$Message msgid "The OCR engine is not available. Make sure to install the required package:" -msgstr "OCR motoru kullanılamıyor. Gerekli paketin kurulduğundan emin olun:" +msgstr "OKT motoru kullanılamıyor. Gerekli paketin kurulduğundan emin olun:" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "OKT işlemi zaman aşımına uğradı." #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message @@ -1394,9 +1583,9 @@ msgstr "Dosyanın üzerine yazılamaz çünkü şu anda kullanımda." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "{0} dosyası zaten var. Üzerine yazmak ister misiniz?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Şu dosya şifrelenmiş ve açmak için parola gerekiyor: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Bu dosya şifrelenmiş ve açmak için bir şifre gerekiyor:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,29 +1632,65 @@ msgstr "Seçilen tarayıcı meşgul." msgid "The selected scanner is offline." msgstr "Seçilen tarayıcı çevrim dışı." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Çalışma işlemi çöktü." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Çalışma işlemi çöktü. Windows olay görüntüleyicisini denetleyin." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Tema:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Tiff Seçenekleri" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Taramalar arası zaman (saniye):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Başlık:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Araçlar" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Twain Uyarlaması:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 inç)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" -msgstr "" +msgstr "US Letter (8.5x11 inç)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Atamayı Kaldır" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Geri al" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Geri al: {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Bilinmeyen Tarayıcı" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" @@ -1477,7 +1702,7 @@ msgstr "Güncelleme Süreci" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Güncelleme denetimi devre dışıdır." #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,24 +1712,21 @@ msgstr "Güncelleniyor..." msgid "Uploading email..." msgstr "E-posta yükleniyor..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Aygıt ara yüzünü kullan" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Öntanımlı ayarları kullan" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Kullanıcı Parolası:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." -msgstr "OCR kullanmak için taramak istediğiniz dili indirmeniz gerekir." +msgstr "OKT kullanmak için taramak istediğiniz dili indirmeniz gerekir." #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message @@ -1515,16 +1737,15 @@ msgstr "Sürüm {0}" msgid "View" msgstr "Görüntüle" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA Sürücüsü" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Tamamlamak için TWAIN bekleniyor..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Yetkilendirme bekleniyor..." @@ -1532,19 +1753,19 @@ msgstr "Yetkilendirme bekleniyor..." msgid "Waiting for scan {0}..." msgstr "{0}. tarama için bekleniyor..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Beyaz Eşiği" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Wia Sürümü:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Yıl" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Yıl (00-99)" @@ -1555,34 +1776,31 @@ msgstr "'{0}' dosyasından içeriği kopyalamaya yetkiniz yok." #: MiscResources.resx$DontHavePermission$Message msgid "You don't have permission to save files at this location." -msgstr "Bu konuma kaydetmek için yetkiniz bulunmuyor." +msgstr "Bu konuma kaydetmek için yetkiniz yok." #: MiscResources.resx$ExitWithUnsavedChanges$Message msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Kaydedilmemiş değişiklikleriniz var. Değişiklikleri iptal edip çıkmak istediğinize emin misiniz?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Yakınlık" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" -msgstr "Normal Yakınlaşma" +msgstr "Olağan Yakınlaşma" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Yakınlaştır" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Uzaklaştır" #: SettingsResources.resx$PageSizeUnit_Centimetre$Message msgid "cm" -msgstr "" +msgstr "cm" #: SettingsResources.resx$PageSizeUnit_Inch$Message msgid "in" @@ -1590,7 +1808,7 @@ msgstr "inç" #: SettingsResources.resx$PageSizeUnit_Millimetre$Message msgid "mm" -msgstr "" +msgstr "mm" #: MiscResources.resx$OfN$Message msgid "of {0}" @@ -1598,30 +1816,35 @@ msgstr " / {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} dosya" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "{0} aygıt bulundu." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" -msgstr "{1} {2} tarihinde taranan {0} adet resim kaydedilmedi ve kurtarılabilir. Kurtarmak ister msiniz?" +msgstr "{1} {2} tarihinde taranan {0} resim kaydedilmedi ve kurtarılabilir. Kurtarmak ister msiniz?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." diff --git a/NAPS2.Lib/Lang/po/uk.po b/NAPS2.Lib/Lang/po/uk.po index b45d46af51..73f4b86ac3 100644 --- a/NAPS2.Lib/Lang/po/uk.po +++ b/NAPS2.Lib/Lang/po/uk.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Ukrainian\n" "Language: uk\n" @@ -19,41 +19,25 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "100 т/д" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "Типова поведінка, коли натиснуто кнопку \"Зберегти\":" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "1200 т/д" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "Типова поведінка, коли натиснуто кнопку \"Сканувати\":" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "150 т/д" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "Вибір в меню \"Сканувати\" змінює типовий профіль" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "200 т/д" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "Знайдено 1 пристрій." #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" -msgstr "Кольорове 24-біти" - -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "300 т/д" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "400 т/д" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "600 т/д" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "800 т/д" +msgstr "Кольорове 24 біти" #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" @@ -76,12 +60,15 @@ msgstr "Про програму" msgid "Acquiring data..." msgstr "Отримання даних..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "Дія" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Додатково" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Розширені параметри профілю" @@ -93,65 +80,74 @@ msgstr "Всі ({0})" msgid "All Files" msgstr "Всі файли" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Дозволити аннотації" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Дозволити копіювати вміст" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Дозволити копіювати вміст для доступу" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Дозволити складання документу" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Дозволити модифікацію документу" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Дозволити заповнення форм" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Дозволити високоякісний друк" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Дозволити друк" #: UiStrings.resx$AltDeinterleave$Message msgid "Alternate Deinterleave" -msgstr "" +msgstr "Альтенативна віддаленість" #: UiStrings.resx$AltInterleave$Message msgid "Alternate Interleave" -msgstr "" +msgstr "Альтернативна міжчерговість" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "Завжди питати" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "Завжди запитувати" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" -msgstr "" +msgstr "Для імпорту цього файлу PDF потрбно встановити додатковий компонент. Чи завантажити його зараз?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "" +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "Під час цифрового розпізнавання трапилася помилка." #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." -msgstr "" +msgstr "Під час спроби авторизації трапилася помилка." #: MiscResources.resx$AutoSaveError$Message msgid "An error occurred when trying to auto save." -msgstr "" +msgstr "Під час спроби автоматичного збереження трапилася помилка." + +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Під час встановлення оновлення трапилася помилка." #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." @@ -159,11 +155,11 @@ msgstr "Під час збереження файлу трапилась пом #: MiscResources.resx$ErrorEmailing$Message msgid "An error occurred when trying to send the email." -msgstr "" +msgstr "Під час надсилання ел. пошти сталася помилка." #: MiscResources.resx$EmailError$Message msgid "An error occurred while trying to send an email." -msgstr "При відправці пошти сталася помилка." +msgstr "Під час надсилання ел. пошти сталася помилка." #: MiscResources.resx$UnknownDriverError$Message #: SdkResources.resx$UnknownDriverError$Message @@ -172,102 +168,116 @@ msgstr "Помилка драйвера сканування." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "" +msgstr "Операція в процесі виконання. Дійсно скасувати цю операцію та вийти?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "" +msgid "An unknown error occurred during the batch scan." +msgstr "Під час пакетного сканування трапилася невідома помилка." #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "" +msgstr "Доступне оновлення." #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "" +msgstr "Доступне оновлення пакету розпізнавання." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Драйвер Apple" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Пошта Apple" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "Застосунок" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" -msgstr "" +msgstr "Визначити яскравість/контрастність після сканування" #: UiStrings.resx$ApplyToSelected$Message msgid "Apply to all {0} selected images" -msgstr "" +msgstr "Застосувати до всіх {0} вибраних зображень" #: MiscResources.resx$ConfirmCancelBatch$Message msgid "Are you sure you want to cancel the batch scan?" -msgstr "" +msgstr "Дійсно скасувати пакетне сканування" #: MiscResources.resx$ConfirmClearItems$Message msgid "Are you sure you want to clear {0} item(s)?" -msgstr "Ви справді бажаєте очистити профіль(лі) {0}?" +msgstr "Дійсно очистити профіль(лі) {0}?" #: MiscResources.resx$ConfirmDelete$Message msgid "Are you sure you want to delete \"{0}\"?" -msgstr "" +msgstr "Дійсно вилучити \"{0}\"?" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" -msgstr "Ви дійсно хочете видалити профіль {0}?" +msgstr "Дійсно вилучити профіль {0}?" #: MiscResources.resx$ConfirmDeleteItems$Message msgid "Are you sure you want to delete {0} item(s)?" -msgstr "Ви справді бажаєте видалити {0} профілі(в)?" +msgstr "Дійсно вилучити {0} профілі(в)?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "Ви справді бажаєте видалити {0} профілів?" +msgstr "Дійсно вилучити {0} профілів?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "Дійсно прибрати спільний доступ {0}?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" -msgstr "Ви бажаєте відмінити зміни в {0} зображеннях ?" +msgstr "Дійсно скасувати зміни в {0} зображеннях ?" + +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "Призначити" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Ім'я вкладення:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Автор:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "Авторизація" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "Автоматично" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" -msgstr "" +msgstr "Автоматично зберігати налаштування" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Автоматичне збільшення номера ( 1 цифра )" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "Автоматичне збільшення номера (1 цифра)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" -msgstr "Автоматичне збільшення номера ( 2 цифри )" +msgstr "Автоматичне збільшення номера (2 цифри)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" -msgstr "Автоматичне збільшення номера ( 3 цифри )" +msgstr "Автоматичне збільшення номера (3 цифри)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" -msgstr "Автоматичне збільшення номера ( 4 цифри )" +msgstr "Автоматичне збільшення номера (4 цифри)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" -msgstr "" +msgstr "Автоматично розпізнавати після сканування" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" @@ -277,28 +287,27 @@ msgstr "B4 (250x353 мм)" msgid "B5 (176x250 mm)" msgstr "B5 (176x250 мм)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" -msgstr "" +msgstr "Пакетне сканування" #: MiscResources.resx$BatchStatusCancelled$Message msgid "Batch cancelled." -msgstr "" +msgstr "Пакетне сканування скасовано." #: MiscResources.resx$BatchStatusComplete$Message msgid "Batch completed successfully." -msgstr "" +msgstr "Пакетне сканування успішно виконано." #: MiscResources.resx$BatchStatusError$Message msgid "Batch scan stopped due to error." -msgstr "" +msgstr "Пакетне сканування зупинено через помилку." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "Найкраще" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Глибина в бітах:" @@ -309,18 +318,17 @@ msgstr "Файли зображень *.bmp" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Чорно-білий" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" -msgstr "" +msgstr "Порожні сторінки" #: UiStrings.resx$BrightnessContrast$Message msgid "Brightness / Contrast" -msgstr "" +msgstr "Яскравість / контрастність" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Яскравість:" @@ -329,61 +337,47 @@ msgstr "Яскравість:" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "Не знаходите ваш сканер? Перегляньте обмеження збірки Flatpak NAPS2." + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Скасувати" #: MiscResources.resx$CancelBatch$Message msgid "Cancel Batch" -msgstr "" +msgstr "Скасувати пакетне сканування" #: MiscResources.resx$BatchStatusCancelling$Message msgid "Cancelling...." -msgstr "" +msgstr "Скасування..." #: SettingsResources.resx$HorizontalAlign_Center$Message msgid "Center" msgstr "По центру" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" -msgstr "" +msgstr "Змінити" #: UiStrings.resx$CheckForUpdates$Message msgid "Check for updates" -msgstr "" +msgstr "Перевірка оновлень" #: MiscResources.resx$CheckingForUpdates$Message msgid "Checking..." -msgstr "" +msgstr "Виконується перевірка..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" -msgstr "" +msgstr "Виберіть постачальника послуг ел. пошти" #: MiscResources.resx$ChooseProfile$Message msgid "Choose Profile" msgstr "Виберіть профіль" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Виберіть пристрій" @@ -395,26 +389,40 @@ msgstr "Очистити" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "Очистити все" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" -msgstr "" +msgstr "Прибирати зображення після збереження" #: MiscResources.resx$Close$Message msgid "Close" -msgstr "" +msgstr "Закрити" + +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "Об'єднати" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "Обмін інформацією зі сканером було перервано." + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" -msgstr "" +msgstr "Сумісність" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" -msgstr "" +msgstr "Стиснення:" + +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "З'єднатися" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "Помилка під час з'єднання." -#: FEditProfile.resx$label7.Text$Message #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Контрастність:" @@ -425,119 +433,123 @@ msgstr "Копіювати" #: MiscResources.resx$CopyProgress$Message msgid "Copy Progress" -msgstr "" +msgstr "Копіювання" #: MiscResources.resx$Copying$Message msgid "Copying..." -msgstr "" +msgstr "Виконується копіювання..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "Всі права застережено {0} Розробники NAPS2" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" -msgstr "" +msgstr "Поріг покриття" #: UiStrings.resx$Crop$Message msgid "Crop" msgstr "Обрізати" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" -msgstr "" +msgstr "Обрізати за розмірами стоірнки" #: MiscResources.resx$CustomPageSizeFormat$Message msgid "Custom ({0}x{1} {2})" msgstr "Користувача ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Розмір сторінки користувача" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "Власна роздільна здатність" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Ротація користувачем" #: SettingsResources.resx$EmailProviderType_CustomSmtp$Message msgid "Custom SMTP" -msgstr "" +msgstr "Користувацький SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Інший..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "Темна" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "День (1-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" -msgstr "" +msgstr "Типово" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" -msgstr "" +msgstr "Типовий шлях до файлу:" #: UiStrings.resx$Deinterleave$Message msgid "Deinterleave" msgstr "Позбавити чергування" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" -msgstr "Видалити" +msgstr "Вилучити" #: UiStrings.resx$Deskew$Message msgid "Deskew" -msgstr "" +msgstr "Вирівняти перекіс" #: MiscResources.resx$AutoDeskewProgress$Message msgid "Deskew Progress" -msgstr "" +msgstr "Вирівнювання перекосу" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" -msgstr "" +msgstr "Вирівняти перекіс відсканованих сторінок" #: MiscResources.resx$AutoDeskewing$Message msgid "Deskewing..." -msgstr "" +msgstr "Вирівнювання виконується..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Пристрій:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" -msgstr "" +msgstr "Розміри" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Назва:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "Зміни до документу" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" -msgstr "" +msgstr "Ваш внесок" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Завершити" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Звантаження" @@ -548,24 +560,52 @@ msgstr "Помилка звантаження" #: MiscResources.resx$DownloadNeeded$Message msgid "Download Needed" -msgstr "" +msgstr "Потрібно звантажити" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Поступ звантаження" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "точок на дюйм" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Двосторонній" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "Драйвер ESCL" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "Мережевий драйвер ESCL" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "Драйвер USB для ESCL" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Редагувати" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "Редагувати у {0}" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "Редагувати у..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "Надіслати все" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "Надіслати все як PDF" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -574,27 +614,34 @@ msgstr "PDF поштою" #: MiscResources.resx$EmailPdfProgress$Message msgid "Email PDF Progress" -msgstr "" +msgstr "Надсилання PDF ел. поштою" + +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "Вибрано ел. пошту" #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "Вибрано надіслати PDF ел. поштою" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Параметри електронної пошти" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" -msgstr "" +msgstr "Увімкнути автоматичне збереження" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "Увімкнути журнал зневадження" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Шифрувати PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "Шифрування" @@ -602,59 +649,68 @@ msgstr "Шифрування" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Розширений метафайл (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Помилка" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "Помилка під час запуску застосунку {0}" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" -msgstr "Приблизний об’єм звантаження: {0} МБ" +msgstr "Приблизний розмір звантаження: {0} МБ" #: MiscResources.resx$FileTypeExif$Message msgid "Exchangeable Image File (*.exif)" msgstr "Файл зображень для обміну (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" -msgstr "" +msgstr "Прибрати порожні сторінки" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "Швидко" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "Автоподача" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Назва файлу" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" -msgstr "" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" +msgstr "Шлях файлу:" + +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "Виправляти баланс білого кольору та прибирати шум" #: UiStrings.resx$Flip$Message msgid "Flip" -msgstr "Відзеркалити" +msgstr "Віддзеркалити" + +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "Віддзеркалити задній бік двосторонніх сторінок" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" -msgstr "" +msgstr "Віддзеркалити двосторонні сторінки" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." -msgstr "" +msgstr "Для отримання кращої якості зображень JPEG (80+) рекомендується збільшити якість зображення у вашому профілі сканування." #: MiscResources.resx$FileTypeGif$Message msgid "GIF File (*.gif)" msgstr "GIF файл (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Встановити інші мови" @@ -671,22 +727,25 @@ msgstr "" msgid "Grayscale" msgstr "Відтінки сірого" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Горизонтальне вирівнювання:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Година (0-23)" #: UiStrings.resx$HueSaturation$Message msgid "Hue / Saturation" -msgstr "" +msgstr "Відтінок / насиченість" + +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP-адреса/назва хосту" #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" -msgstr "Піктограми взяті з:" +msgstr "Використання значків:" #: UiStrings.resx$Image$Message msgid "Image" @@ -694,20 +753,20 @@ msgstr "Зображення" #: MiscResources.resx$FileTypeImageFiles$Message msgid "Image Files" -msgstr "" +msgstr "Файли зображень" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" -msgstr "" +msgstr "Якість зображення" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Налаштування зображення" #: MiscResources.resx$ImageSaved$Message msgid "Image saved." -msgstr "" +msgstr "Зображення збережено." #: UiStrings.resx$Import$Message msgid "Import" @@ -715,19 +774,19 @@ msgstr "Імпортувати" #: MiscResources.resx$ImportProgress$Message msgid "Import Progress" -msgstr "" +msgstr "Імпорт" #: MiscResources.resx$ImportingFormat$Message msgid "Importing {0}..." -msgstr "" +msgstr "Виконується імпорт {0}..." #: MiscResources.resx$Importing$Message msgid "Importing..." -msgstr "" +msgstr "Виконується імпорт..." #: MiscResources.resx$Install$Message msgid "Install {0}" -msgstr "" +msgstr "Встановити {0}" #: MiscResources.resx$InstallComplete$Message msgid "Installation Complete" @@ -745,23 +804,36 @@ msgstr "Встановлення завершено. Перезапустити msgid "Installation failed." msgstr "Встановлення завершилося невдало." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "Інтерфейс" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "Чергування" #: MiscResources.resx$FileTypeJpeg$Message msgid "JPEG File (*.jpg, *.jpeg)" -msgstr "JPEG файл (*.jpg, *.jpeg)" +msgstr "Файл JPEG (*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "Файл JPEG2000 (*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "Якість JPEG" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "Зберігати зображення між сесіями" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "Клавіатурні скорочення" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Ключові слова:" @@ -773,59 +845,86 @@ msgstr "" msgid "Language" msgstr "Мова" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Вліво" #: SettingsResources.resx$TwainImpl_Legacy$Message msgid "Legacy (native UI only)" -msgstr "" +msgstr "Режим сумісности (лише рідний інтерфейс користувача)" + +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "Світла" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "Подобається NAPS2?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" -msgstr "" +msgstr "Завантажити зображення до NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Створити PDF файл з можливістю пошуку" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "Вручну IP" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Максимальна якість (великий розмір)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "Передача засобами пам'яти" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "Метадані" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Хвилини (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Місяць (1-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" -msgstr "" +msgstr "Докладно" #: UiStrings.resx$MoveDown$Message msgid "Move Down" -msgstr "Пересунути нижче" +msgstr "Перемістити нижче" #: UiStrings.resx$MoveUp$Message msgid "Move Up" -msgstr "Пересунути вище" +msgstr "Перемістити вище" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "Декілька мов" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "Декілька мов..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" -msgstr "" +msgstr "Сканування багатьох сторінок (фіксована затримка між скануваннями)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" -msgstr "" +msgstr "Сканування багатьох сторінок (запит на продовження між скануваннями)" #: MiscResources.resx$NAPS2$Message #: SdkResources.resx$NAPS2$Message @@ -839,16 +938,20 @@ msgstr "" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." -msgstr "" +msgstr "NAPS2 є вільним програмним забезпеченням. Запрошуємо зробити ваш внесок." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" -msgstr "" +msgstr "Ім'я (необов'язково)" #: MiscResources.resx$NameMissing$Message msgid "Name missing." msgstr "Відсутнє ім'я." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "Передача засобами програми" + #: UiStrings.resx$New$Message msgid "New" msgstr "Створити" @@ -861,24 +964,27 @@ msgstr "Створити профіль" msgid "Next" msgstr "Далі" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" -msgstr "" +msgstr "Наступна сторінка" #: MiscResources.resx$NoDeviceSelected$Message #: SdkResources.resx$NoDeviceSelected$Message msgid "No device selected." -msgstr "Не вибраний пристрій." +msgstr "Не вибрано пристрій." + +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "Не знайдено пристрої." #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." -msgstr "В пристрої відсутній папір." +msgstr "У пристрої відсутній папір." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." -msgstr "" +msgstr "Відсутній постачальник послуг" #: MiscResources.resx$NoDevicesFound$Message #: SdkResources.resx$NoDevicesFound$Message @@ -887,125 +993,116 @@ msgstr "Не знайдено жодного сканера." #: MiscResources.resx$NoUpdates$Message msgid "No updates available." -msgstr "" +msgstr "Відсутні оновлення" #: SettingsResources.resx$TiffComp_None$Message msgid "None" -msgstr "" +msgstr "Нічого" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "Не зараз" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" -msgstr "" +msgstr "Кількість сторінок:" #: UiStrings.resx$Ocr$Message msgid "OCR" -msgstr "Розпізнавання" +msgstr "Розпізнати" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" -msgstr "Ззавантаження файлів для розпізнавання" +msgstr "Звантаження файлів для розпізнавання" #: MiscResources.resx$OcrProgress$Message msgid "OCR Progress" -msgstr "" +msgstr "Виконується розпізнавання" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Налаштування параметрів розпізнавання" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Мова розпізнавання:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" -msgstr "" +msgstr "Режим розпізнавання:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" -msgstr "" +msgstr "Гаразд" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" -msgstr "" +msgstr "Ширина зсуву на основі вирівнювання (WIA)" #: SettingsResources.resx$TwainImpl_OldDsm$Message msgid "Old DSM" -msgstr "" +msgstr "Застарілий DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" -msgstr "" +msgstr "Кожна сторінка в окремий файл" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" -msgstr "" +msgstr "Все відскановане в одному файлі" #: MiscResources.resx$FilesCouldNotBeDownloaded$Message msgid "One or more files could not be downloaded." -msgstr "Один або декілька файлів завантажити не вдалось." +msgstr "Не вдалося звантажити один або більше файлів." + +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "Дозволити лише один примірник NAPS2" -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" -msgstr "" +msgstr "Відкрити каталог" #: MiscResources.resx$ActiveOperations$Message msgid "Operation in Progress" -msgstr "" +msgstr "Виконується операція" + +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (новий)" #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" -msgstr "" +msgstr "Результат" #: MiscResources.resx$OverwriteFile$Message msgid "Overwrite File" msgstr "Перезаписати файл" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" -msgstr "Гасло власника:" +msgstr "Пароль власника:" #: MiscResources.resx$FileTypePdf$Message msgid "PDF Document (*.pdf)" -msgstr "" +msgstr "Документ PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Налаштування PDF " #: MiscResources.resx$PdfSaved$Message msgid "PDF saved." -msgstr "" +msgstr "Файл PDF збережено." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" @@ -1025,49 +1122,50 @@ msgstr "" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" -msgstr "PNG файл (*.png)" +msgstr "Файл PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Розмір сторінки:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Подача паперу:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Пароль" #: UiStrings.resx$Paste$Message msgid "Paste" -msgstr "" +msgstr "Вставити" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "Заповнювачі" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "Порт" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" -msgstr "" +msgstr "Післяобробка" + +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "Автоматично розпізнавати після сканування" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." -msgstr "" +msgstr "Натисніть Старт, коли будете готові." #: UiStrings.resx$PreviewFormTitle$Message msgid "Preview" msgstr "Перегляд" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Перегляд:" @@ -1080,51 +1178,60 @@ msgstr "Попередня" msgid "Print" msgstr "Друк" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Налаштовування профілю" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" -msgstr "" +msgstr "Профіль:" #: UiStrings.resx$Profiles$Message #: UiStrings.resx$ProfilesFormTitle$Message msgid "Profiles" msgstr "Профілі" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "Запит на продовження, якщо вибрано" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" -msgstr "" +msgstr "Запит на шлях до файлу" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" -msgstr "" +msgstr "Постачальник" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." -msgstr "" +msgstr "Готовий сканувати {0)." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Відновити" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Відновити відскановані зображення" #: MiscResources.resx$Recovering$Message msgid "Recovering..." -msgstr "" +msgstr "Відновлення..." #: MiscResources.resx$RecoveryProgress$Message msgid "Recovery Progress" -msgstr "" +msgstr "Виконується відновлення" + +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "Повторити" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "Повторити {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Зберегти налаштуваання" @@ -1140,21 +1247,25 @@ msgstr "Перезавантажити зображення" msgid "Reset Image" msgstr "Перевантажити зображення" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Роздільна здатність:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" -msgstr "Відновити типові" +msgstr "Скинути до типових" #: UiStrings.resx$Reverse$Message msgid "Reverse" -msgstr "Зворотній" +msgstr "Зворотній порядок" + +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "Всі у зворотньому порядку" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "Вибрані у зворотньому порядку" #: UiStrings.resx$Revert$Message msgid "Revert" @@ -1176,31 +1287,34 @@ msgstr "Обернути ліворуч" msgid "Rotate Right" msgstr "Обернути праворуч" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" -msgstr "" +msgstr "Виконувати на задньому фоні" #: MiscResources.resx$RunningOcr$Message msgid "Running OCR..." -msgstr "" +msgstr "Виконується розпізнавання..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" -msgstr "" +msgstr "Драйвер SANE" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "Зберегти" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "Зберегти все" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "Зберегти все як зображення" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "Зберегти все у PDF" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1209,100 +1323,126 @@ msgstr "Зберегти як зображення" #: MiscResources.resx$SaveImagesProgress$Message msgid "Save Images Progress" -msgstr "" +msgstr "Виконується збереження зображень" #: MiscResources.resx$SavePdf$Message #: UiStrings.resx$SavePdf$Message msgid "Save PDF" -msgstr "Зберегти PDF" +msgstr "Зберегти у PDF" #: MiscResources.resx$SavePdfProgress$Message msgid "Save PDF Progress" -msgstr "" +msgstr "Виконується збереження PDF" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "Зберегти вибране" #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "Зберегти вибране як зображення" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "Зберегти вибране у PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" -msgstr "" +msgstr "Зберегти все в один файл" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" -msgstr "" +msgstr "Зберегти все в різні файли" #: MiscResources.resx$BatchStatusSaving$Message msgid "Saving batch results..." -msgstr "" +msgstr "Збереження результатиів пакетного сканування..." #: MiscResources.resx$SavingFormat$Message msgid "Saving {0}..." -msgstr "" +msgstr "Збереження {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Масштабувати у вікні" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Масштабувати:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Сканувати" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" -msgstr "" +msgstr "Налаштування сканування" + +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "Сканувати з типовим профілем" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "Сканувати з новим профілем" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "Сканувати з профілем {0}" #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Зіскановане зображення" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "Спільний доступ до сканера" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Сканування сторінки {0}" #: MiscResources.resx$BatchStatusScanPage$Message msgid "Scanning page {0} (scan {1})..." -msgstr "" +msgstr "Сканування сторінки {0} (скан {1})..." #: MiscResources.resx$BatchStatusPage$Message #: MiscResources.resx$ScanProgressPage$Message msgid "Scanning page {0}..." msgstr "Сканування сторінки {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "Пошук пристроїв..." + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Секунда (1-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" -msgstr "" +msgstr "Вибрати" #: UiStrings.resx$SelectAll$Message msgid "Select All" msgstr "Вибрати все" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "Виберіть пристрій" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" -msgstr "" +msgstr "Вибрати джерело" #: MiscResources.resx$SelectProfileBeforeScan$Message msgid "Select a profile before clicking Scan." msgstr "Виберіть профіль перед скануванням." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Виберіть одну або кілька мов::" @@ -1311,112 +1451,161 @@ msgstr "Виберіть одну або кілька мов::" msgid "Selected ({0})" msgstr "Вибрані ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" -msgstr "" +msgstr "Розділити файли за допомогою Patch-T" #: UiStrings.resx$SetDefault$Message msgid "Set Default" msgstr "Зробити типовим" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "Налаштування" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "Спільний доступ" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "Надавати доступ навіть тоді, коли NAPS2 буде закрито" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "Налаштування сканера у спільному доступі" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "Сканери у спільному доступі можна використовувати на віддалених комп'ютерах локальної мережі. Для цього потрібно вибрати \"Драйвер ESCL\" в налаштуваннях профіля віддаленого комп'ютерау. " + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" -msgstr "" +msgstr "Різкість" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "Скорочення" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Показати" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "Показувати панель \"Профілі\"" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "Показувати нативне сканування TWAIN" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "Показувати нумерацію сторінок" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "Бічна панель" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" -msgstr "" +msgstr "Файли з одинарною сторінкою" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" -msgstr "" +msgstr "Одноразове сканування" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" -msgstr "" +msgstr "Пропускати запит на збереження" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "Розділити" + +#: UiStrings.resx$Start$Message msgid "Start" -msgstr "" +msgstr "Старт" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "Зупинити спільний доступ до сканера" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" -msgstr "" +msgstr "Розтягнути до розміру сторінки" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Тема:" #: MiscResources.resx$FileTypeTiff$Message msgid "TIFF File (*.tiff, *.tif)" -msgstr "TIFF файл (*.tiff, *.tif)" +msgstr "Файл TIFF (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" -msgstr "TWAIN драйвер" +msgstr "Драйвер TWAIN" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" -msgstr "" +msgstr "Технічні деталі" #: MiscResources.resx$TesseractNotAvailable$Message #: SdkResources.resx$TesseractNotAvailable$Message msgid "The OCR engine is not available. Make sure to install the required package:" -msgstr "" +msgstr "Рушій розпізнавання (OCR) не доступний. Перевірте, чи встановлено такі пакети::" + +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "Час операції з розпізнавання вичерпано." #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" -msgstr "" +msgstr "Драйвер SANE не доступний. Перевірте, чи встановлено такі пакети:" #: MiscResources.resx$ImportErrorCouldNot$Message #: SdkResources.resx$ImportErrorCouldNot$Message msgid "The file '{0}' could not be imported." -msgstr "Файл '{0}' не може бути імпортований." +msgstr "Неможливо імпортувати файл '{0}'." #: MiscResources.resx$ImportErrorNAPS2Pdf$Message msgid "The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported." -msgstr "Не можна імпортувати файл '{0}'. Можуть бути імпортовані лише PDF файли створені NAPS2.." +msgstr "Неможливо імпортувати файл '{0}'. Можливо імпортувати лише файли PDF, які було створено NAPS2.." #: MiscResources.resx$FileInUse$Message msgid "The file could not be overwritten because it is currently in use." -msgstr "" +msgstr "Неможливо перезаписати файл, оскільки він зараз використовується." #: MiscResources.resx$ConfirmOverwriteFile$Message msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Файл {0} вже існує. Перезаписати його?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Введіть гасло для відкриття зашифрованого файлу: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "Цей файл зашифровано. Щоби його відкрити, потрібно зазначити пароль:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message msgid "The scanner has a paper jam." -msgstr "" +msgstr "Зминання паперу у сканері." #: MiscResources.resx$DeviceWarmingUp$Message #: SdkResources.resx$DeviceWarmingUp$Message msgid "The scanner is warming up." -msgstr "" +msgstr "Прогрівання сканеру." #: MiscResources.resx$DeviceCoverOpen$Message #: SdkResources.resx$DeviceCoverOpen$Message msgid "The scanner's cover is open." -msgstr "" +msgstr "Відкрито кришку сканеру." #: MiscResources.resx$DriverNotSupported$Message #: SdkResources.resx$DriverNotSupported$Message msgid "The selected driver is not supported on this system." -msgstr "" +msgstr "Вибраний драйвер не підтримується системою." #: MiscResources.resx$DeviceNotFound$Message #: SdkResources.resx$DeviceNotFound$Message @@ -1426,38 +1615,58 @@ msgstr "Не знайдено вибраний сканер." #: MiscResources.resx$NoFeederSupport$Message #: SdkResources.resx$NoFeederSupport$Message msgid "The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver." -msgstr "Цей сканер не підтримує автоматичну подачу паперу. Якщо Ваш сканер має автоматичну подачу паперу, спробуйте використати інший драйвер.." +msgstr "Вибраний сканер не підтримує автоматичну подачу паперу. Якщо ваш сканер все ж має автоматичну подачу паперу, спробуйте вибрати інший драйвер.." #: MiscResources.resx$NoDuplexSupport$Message #: SdkResources.resx$NoDuplexSupport$Message msgid "The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver." -msgstr "" +msgstr "Вибраний сканер не підтримує двостороннє сканування. Якщо ваш сканер все ж підтримує двостороннє сканування, спробуйте вибрати інший драйвер." #: MiscResources.resx$DeviceBusy$Message #: SdkResources.resx$DeviceBusy$Message msgid "The selected scanner is busy." -msgstr "" +msgstr "Вибраний сканер зайнятий." #: MiscResources.resx$DeviceOffline$Message #: SdkResources.resx$DeviceOffline$Message msgid "The selected scanner is offline." -msgstr "Вибраний сканер відключений." +msgstr "Вибраний сканер вимкнено." -#: FImageSettings.resx$groupTiff.Text$Message -msgid "Tiff Options" +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "Збій робочого процесу." + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "Збій робочого процесу. Перегляньте журнал подій Windows." + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "Тема:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TiffOptions$Message +msgid "Tiff Options" +msgstr "Параметри Tiff" + +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" -msgstr "" +msgstr "Затримка між скануванням (у секундах)" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Заголовок:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "Інструменти" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" -msgstr "" +msgstr "Реалізація Twain:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" @@ -1467,44 +1676,57 @@ msgstr "US Legal (8.5x11 дюймів)" msgid "US Letter (8.5x11 in)" msgstr "US Letter (8.5x11 дюймів)" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "Скинути" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "Повернути" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "Повернути {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "Невідомий сканер" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Зміни не збережено" #: MiscResources.resx$UpdateProgress$Message msgid "Update Progress" -msgstr "" +msgstr "Виконується оновлення" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "Перевірку оновлення вимкнено." #: MiscResources.resx$Updating$Message msgid "Updating..." -msgstr "" +msgstr "Оновлення..." #: MiscResources.resx$UploadingEmail$Message msgid "Uploading email..." -msgstr "" +msgstr "Завантаження ел.пошти..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Власний інтерфейс сканера WIA UI" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Попередньо визначені параметри" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" -msgstr "Гасло користувача:" +msgstr "Пароль користувача:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." -msgstr "Для розпізнавання потрібно завантажити відповідні мовні файли." +msgstr "Для розпізнавання потрібно звантажити відповідні мовні файли." #: MiscResources.resx$Version$Message #: SdkResources.resx$Version$Message @@ -1515,67 +1737,63 @@ msgstr "Версія {0}" msgid "View" msgstr "Вигляд" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" -msgstr "WIA драйвер" +msgstr "Драйвер WIA" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Очікування завершення від TWAIN..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." -msgstr "" +msgstr "Очікування авторизації..." #: MiscResources.resx$BatchStatusWaitingForScan$Message msgid "Waiting for scan {0}..." -msgstr "" +msgstr "Очікування на скан {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" -msgstr "" +msgstr "Поріг білого кольору" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Версія Wia:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Рік" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Рік (00-99)" #: MiscResources.resx$PdfNoPermissionToExtractContent$Message #: SdkResources.resx$PdfNoPermissionToExtractContent$Message msgid "You do not have permission to copy content from the file '{0}'." -msgstr "Ви не можете копіювати вміст файлу '{0}'." +msgstr "Відсутні права на копіювання вмісту файлу '{0}'." #: MiscResources.resx$DontHavePermission$Message msgid "You don't have permission to save files at this location." -msgstr "У вас немає прав для створення файлів у цьому каталозі.." +msgstr "Відсутні права для створення файлів у цьому каталозі.." #: MiscResources.resx$ExitWithUnsavedChanges$Message msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" -msgstr "Є незбережені зміни. Ви впевнені, що хочете вийти та відмовитись від них?" +msgstr "Є незбережені зміни. Дійсно вийти та відмовитись від них?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Масштаб" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Справжній розмір" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Збільшити" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Зменшити" @@ -1594,7 +1812,7 @@ msgstr "мм" #: MiscResources.resx$OfN$Message msgid "of {0}" -msgstr "з {0}" +msgstr "із {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" @@ -1604,31 +1822,36 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "{0} / {1} МБ" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} файлів" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "Знайдено {0} пристроїв." + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} т/д" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "Зображення ({0} шт.), відскановані на {1} в {2}, можливо не були збережені та можуть бути відновлені. Бажаєте їх відновити?" #: MiscResources.resx$ImagesSaved$Message msgid "{0} images saved." -msgstr "" +msgstr "{0} зображень збережено." #: MiscResources.resx$PdfStatus$Message #: UiStrings.resx$XOfY$Message msgid "{0} of {1}" -msgstr "{0} з {1}" +msgstr "{0} із {1}" diff --git a/NAPS2.Lib/Lang/po/ur.po b/NAPS2.Lib/Lang/po/ur.po index 54e2893e3b..04bd730d61 100644 --- a/NAPS2.Lib/Lang/po/ur.po +++ b/NAPS2.Lib/Lang/po/ur.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:35\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Urdu (Pakistan)\n" "Language: ur\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "" @@ -70,18 +54,22 @@ msgstr "" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message msgid "About" -msgstr "" +msgstr "کے بارے میں، \n" +"متعلق" #: MiscResources.resx$AcquiringData$Message msgid "Acquiring data..." msgstr "" -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "" @@ -93,35 +81,35 @@ msgstr "" msgid "All Files" msgstr "" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "" @@ -133,16 +121,21 @@ msgstr "" msgid "Alternate Interleave" msgstr "" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." msgstr "" #: MiscResources.resx$AuthError$Message @@ -153,6 +146,10 @@ msgstr "" msgid "An error occurred when trying to auto save." msgstr "" +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "" + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "" @@ -175,22 +172,30 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "" #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." -msgstr "" +msgstr "ایک اپ ڈیٹ دستیاب ہے۔" #: MiscResources.resx$OcrUpdateAvailable$Message msgid "An update to OCR is available." -msgstr "" +msgstr "OCR میں اپ ڈیٹ دستیاب ہے۔" #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "" @@ -222,49 +227,55 @@ msgstr "" msgid "Are you sure you want to delete {0} profiles?" msgstr "" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "" -#: FEmailSettings.resx$label1.Text$Message -msgid "Attachment Name:" +#: UiStrings.resx$Assign$Message +msgid "Assign" msgstr "" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AttachmentNameLabel$Message +msgid "Attachment Name:" +msgstr "منسلک کا نام:" + +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" -msgstr "" +msgstr "مصنف:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" -msgstr "" +msgstr "اجازت دیں" #: SettingsResources.resx$TiffComp_Auto$Message msgid "Auto" -msgstr "" +msgstr "خودکار" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" -msgstr "" +msgstr "خود کار طریقے سے محفوظ کرنے کی ترتیبات" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "" @@ -277,8 +288,8 @@ msgstr "" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "" @@ -298,7 +309,6 @@ msgstr "" msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "" @@ -309,10 +319,10 @@ msgstr "" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "" @@ -320,7 +330,6 @@ msgstr "" msgid "Brightness / Contrast" msgstr "" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "" @@ -329,24 +338,11 @@ msgstr "" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "" @@ -363,7 +359,7 @@ msgstr "" msgid "Center" msgstr "" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "" @@ -375,7 +371,7 @@ msgstr "" msgid "Checking..." msgstr "" -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "" @@ -383,7 +379,6 @@ msgstr "" msgid "Choose Profile" msgstr "" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "" @@ -397,7 +392,7 @@ msgstr "" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "" @@ -405,16 +400,30 @@ msgstr "" msgid "Close" msgstr "" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "" @@ -435,7 +444,7 @@ msgstr "" msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "" @@ -443,7 +452,7 @@ msgstr "" msgid "Crop" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "" @@ -451,10 +460,14 @@ msgstr "" msgid "Custom ({0}x{1} {2})" msgstr "" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "" @@ -464,22 +477,27 @@ msgid "Custom SMTP" msgstr "" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "" -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "" @@ -487,7 +505,6 @@ msgstr "" msgid "Deinterleave" msgstr "" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +518,7 @@ msgstr "" msgid "Deskew Progress" msgstr "" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" @@ -509,16 +526,14 @@ msgstr "" msgid "Deskewing..." msgstr "" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "" @@ -532,12 +547,10 @@ msgstr "" msgid "Donate" msgstr "" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "" @@ -550,19 +563,47 @@ msgstr "" msgid "Download Needed" msgstr "" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +617,32 @@ msgstr "" msgid "Email PDF Progress" msgstr "" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "" @@ -602,12 +650,15 @@ msgstr "" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +668,7 @@ msgstr "" msgid "Exchangeable Image File (*.exif)" msgstr "" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "" @@ -629,24 +680,31 @@ msgstr "" msgid "Feeder" msgstr "" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" +msgstr "" + +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" msgstr "" #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" @@ -654,7 +712,6 @@ msgstr "" msgid "GIF File (*.gif)" msgstr "" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "" @@ -671,12 +728,11 @@ msgstr "" msgid "Grayscale" msgstr "" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "" @@ -684,6 +740,10 @@ msgstr "" msgid "Hue / Saturation" msgstr "" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "" @@ -696,12 +756,12 @@ msgstr "" msgid "Image Files" msgstr "" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "" @@ -745,6 +805,10 @@ msgstr "" msgid "Installation failed." msgstr "" +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "" @@ -757,11 +821,20 @@ msgstr "" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "" @@ -773,6 +846,10 @@ msgstr "" msgid "Language" msgstr "" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "" @@ -781,33 +858,48 @@ msgstr "" msgid "Legacy (native UI only)" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "" @@ -819,11 +911,19 @@ msgstr "" msgid "Move Up" msgstr "" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "" @@ -841,7 +941,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "" @@ -849,6 +949,10 @@ msgstr "" msgid "Name missing." msgstr "" +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "" @@ -861,7 +965,7 @@ msgstr "" msgid "Next" msgstr "" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "" @@ -870,12 +974,15 @@ msgstr "" msgid "No device selected." msgstr "" +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "" -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "" @@ -897,11 +1004,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "" @@ -909,7 +1016,6 @@ msgstr "" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "" @@ -918,37 +1024,23 @@ msgstr "" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1048,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "" @@ -970,7 +1060,11 @@ msgstr "" msgid "One or more files could not be downloaded." msgstr "" -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "" @@ -978,11 +1072,15 @@ msgstr "" msgid "Operation in Progress" msgstr "" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "" @@ -990,7 +1088,7 @@ msgstr "" msgid "Overwrite File" msgstr "" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "" @@ -998,8 +1096,8 @@ msgstr "" msgid "PDF Document (*.pdf)" msgstr "" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "" @@ -1027,17 +1125,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "" @@ -1045,21 +1141,24 @@ msgstr "" msgid "Paste" msgstr "" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "" @@ -1067,7 +1166,7 @@ msgstr "" msgid "Preview" msgstr "" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "" @@ -1080,12 +1179,11 @@ msgstr "" msgid "Print" msgstr "" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "" @@ -1094,23 +1192,27 @@ msgstr "" msgid "Profiles" msgstr "" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "" -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "" @@ -1122,9 +1224,15 @@ msgstr "" msgid "Recovery Progress" msgstr "" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "" @@ -1140,15 +1248,11 @@ msgstr "" msgid "Reset Image" msgstr "" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "" @@ -1156,6 +1260,14 @@ msgstr "" msgid "Reverse" msgstr "" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "" @@ -1176,7 +1288,6 @@ msgstr "" msgid "Rotate Right" msgstr "" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1185,7 +1296,6 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1304,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1335,11 @@ msgstr "" msgid "Save PDF Progress" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1348,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "" @@ -1244,29 +1364,45 @@ msgstr "" msgid "Saving {0}..." msgstr "" -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "" @@ -1280,11 +1416,14 @@ msgstr "" msgid "Scanning page {0}..." msgstr "" -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "" @@ -1293,7 +1432,10 @@ msgstr "" msgid "Select All" msgstr "" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "" @@ -1302,7 +1444,6 @@ msgstr "" msgid "Select a profile before clicking Scan." msgstr "" -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "" @@ -1311,8 +1452,7 @@ msgstr "" msgid "Selected ({0})" msgstr "" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,37 +1460,84 @@ msgstr "" msgid "Set Default" msgstr "" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "" @@ -1358,12 +1545,11 @@ msgstr "" msgid "TIFF File (*.tiff, *.tif)" msgstr "" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "" @@ -1372,6 +1558,10 @@ msgstr "" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,8 +1584,8 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" msgstr "" #: MiscResources.resx$DevicePaperJam$Message @@ -1443,19 +1633,39 @@ msgstr "" msgid "The selected scanner is offline." msgstr "" -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "" @@ -1467,6 +1677,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "" @@ -1487,21 +1713,18 @@ msgstr "" msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "" @@ -1515,16 +1738,15 @@ msgstr "" msgid "View" msgstr "" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "" -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1532,19 +1754,19 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "" -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "" @@ -1561,21 +1783,18 @@ msgstr "" msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "" @@ -1604,22 +1823,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "" diff --git a/NAPS2.Lib/Lang/po/vi.po b/NAPS2.Lib/Lang/po/vi.po index 0ca2ed4332..6c83c53e14 100644 --- a/NAPS2.Lib/Lang/po/vi.po +++ b/NAPS2.Lib/Lang/po/vi.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Vietnamese\n" "Language: vi\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "" @@ -76,12 +60,15 @@ msgstr "Thông Tin Phần Mềm" msgid "Acquiring data..." msgstr "Thu thập dữ liệu..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "Nâng cao" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "Cài đặt chi tiết" @@ -93,35 +80,35 @@ msgstr "Tất cả ({0})" msgid "All Files" msgstr "Tất cả các tập tin" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "Cho phép ghi chú" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "Cho phép sao chép nội dung" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "Cho phép sao chép chỉnh sửa nội dung" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "Cho phép tài liệu Assembly" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "Cho phép thay đổi tài liệu" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "Cho phép Điền Form" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "Cho phép cải thiện bản In" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "Cho phép in" @@ -133,17 +120,22 @@ msgstr "" msgid "Alternate Interleave" msgstr "" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "Cách chuyển khác" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "Cần phải cài thêm phần mềm để nhập tập tin PDF này vào. Bạn có muốn tải ngay?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "Có lỗi khi đang cập nhật.." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "" #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "Có lỗi khi đang xác thực.." msgid "An error occurred when trying to auto save." msgstr "Có lỗi xảy ra khi lưu tự động." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "Có lỗi khi đang cập nhật.." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "Có lỗi khi lưu tập tin." @@ -175,8 +171,8 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "Đang thực thi lệnh nào đó. Bạn có muốn hủy lệnh và thoát?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." -msgstr "Có lỗi khi scan hàng loạt." +msgid "An unknown error occurred during the batch scan." +msgstr "" #: MiscResources.resx$UpdateAvailable$Message msgid "An update is available." @@ -190,7 +186,15 @@ msgstr "OCR có bản cập nhật mới.." msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "Áp dụng độ sáng / tương phản sau khi quét" @@ -222,19 +226,27 @@ msgstr "Bạn có chắc chắn muốn xóa {0} mục?" msgid "Are you sure you want to delete {0} profiles?" msgstr "Bạn có chắc chắn muốn xóa {0} hồ sơ?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "Bạn có chắc chắn muốn hoàn tác các thay đổi đến {0} hình ảnh?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "Tên tập tin đính kèm:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "Tác giả:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "Cho phép" @@ -242,29 +254,27 @@ msgstr "Cho phép" msgid "Auto" msgstr "Tự động" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "Cài đặt Tự động Lưu" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "Tự động tăng số (1 chữ số)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "Tự động tăng số (2 chữ số)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "Tự động tăng số (3 chữ số)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "Tự động tăng số (4 chữ số)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "Tự động thực thi OCR (nhận diện chữ trên bản scan) sau khi scan" @@ -277,8 +287,8 @@ msgstr "" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "Scan hàng loạt" @@ -298,7 +308,6 @@ msgstr "Scan hàng loạt dừng lại do lỗi." msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "Chiều sâu:" @@ -309,10 +318,10 @@ msgstr "" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "Đen / Trắng" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "Trang Trắng" @@ -320,7 +329,6 @@ msgstr "Trang Trắng" msgid "Brightness / Contrast" msgstr "Độ Sáng / Tương Phản" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "Độ sáng:" @@ -329,24 +337,11 @@ msgstr "Độ sáng:" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "Hủy" @@ -363,7 +358,7 @@ msgstr "Đang dừng...." msgid "Center" msgstr "Canh giữa" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "Thay đổi" @@ -375,7 +370,7 @@ msgstr "Kiểm tra phiên bản mới" msgid "Checking..." msgstr "Đang kiểm tra..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "Chọn nhà cung cấp Email" @@ -383,7 +378,6 @@ msgstr "Chọn nhà cung cấp Email" msgid "Choose Profile" msgstr "Chọn hồ sơ" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "Chọn Ổ Đĩa" @@ -397,7 +391,7 @@ msgstr "Xóa" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "Xóa ảnh sau khi lưu" @@ -405,16 +399,30 @@ msgstr "Xóa ảnh sau khi lưu" msgid "Close" msgstr "Đóng" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "Khả năng tương thích" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "Nén:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "Tương phản:" @@ -435,7 +443,7 @@ msgstr "Đang sao chép..." msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "Bảo hiểm Threshold" @@ -443,7 +451,7 @@ msgstr "Bảo hiểm Threshold" msgid "Crop" msgstr "Chỉnh sửa" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "Cắt về kích thước trang" @@ -451,10 +459,14 @@ msgstr "Cắt về kích thước trang" msgid "Custom ({0}x{1} {2})" msgstr "Tùy chỉnh ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "Tùy chỉnh kiểu giấy" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "Tùy chỉnh xoay" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "SMTP tự điền" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "Tùy chỉnh..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "Ngày(01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "Mặc định" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "Đường dẫn mặc định:" @@ -487,7 +504,6 @@ msgstr "Đường dẫn mặc định:" msgid "Deinterleave" msgstr "" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "" msgid "Deskew Progress" msgstr "" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" @@ -509,16 +525,14 @@ msgstr "" msgid "Deskewing..." msgstr "" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "Thiết bị:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "Kích thước" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "Hiển thị tên:" @@ -532,12 +546,10 @@ msgstr "" msgid "Donate" msgstr "" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "Xong" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "Tải Về" @@ -550,19 +562,47 @@ msgstr "Tải Về lỗi" msgid "Download Needed" msgstr "Cần Tải Về" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "Tiến độ tải về" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "Hai mặt" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "Sửa" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +616,32 @@ msgstr "" msgid "Email PDF Progress" msgstr "" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "Cài đặt Email" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "Kích hoạt Lưu tự động" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "Mã hóa PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "" @@ -602,12 +649,15 @@ msgstr "" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "Lỗi" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "Ước tính kích thước download: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "Loại trừ các trang trống" @@ -629,24 +679,31 @@ msgstr "" msgid "Feeder" msgstr "" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "Tên tệp" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "Đường dẫn:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "Lật" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "Lật 2 mặt trang" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "Để JPEG được chất lượng cao, nhớ tăng chất lượng hình trong thiết lập mẫu có sẵn của bạn.." @@ -654,7 +711,6 @@ msgstr "Để JPEG được chất lượng cao, nhớ tăng chất lượng hì msgid "GIF File (*.gif)" msgstr "" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "Thêm Ngôn ngữ" @@ -671,12 +727,11 @@ msgstr "" msgid "Grayscale" msgstr "" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "Chỉnh chiều ngang:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "Giờ (0-23)" @@ -684,6 +739,10 @@ msgstr "Giờ (0-23)" msgid "Hue / Saturation" msgstr "" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "" @@ -696,12 +755,12 @@ msgstr "Ảnh" msgid "Image Files" msgstr "TỆP ẢNH" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "Chất lượng ảnh" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "Cài đặt ảnh" @@ -745,6 +804,10 @@ msgstr "Cài đặt hoàn tất. Bạn có muốn khởi động lại NAPS2 bâ msgid "Installation failed." msgstr "Cài đặt thất bại." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "" @@ -757,11 +820,20 @@ msgstr "" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" +msgstr "Chất lượng hình ảnh" + +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" msgstr "" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "Từ khóa:" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "Ngôn ngữ" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "Trái" @@ -781,33 +857,48 @@ msgstr "Trái" msgid "Legacy (native UI only)" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "Tải hình ảnh vào NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "Hãy tìm kiếm các file PDF sử dụng OCR" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "Chất lượng tối đa (tập tin lớn)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "Phút (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "Tháng (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "Thêm thông tin" @@ -819,11 +910,19 @@ msgstr "Chuyển xuống" msgid "Move Up" msgstr "Chuyển lên" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "Quét nhiều (trễ cố định giữa quét)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "Quét nhiều (nhắc giữa quét)" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 miễn phí hoàn toàn. Bạn có thể ủng hộ tùy ý.." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "Tên (bắt buộc)" @@ -849,6 +948,10 @@ msgstr "Tên (bắt buộc)" msgid "Name missing." msgstr "Đặt tên lỗi." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "Mới" @@ -861,7 +964,7 @@ msgstr "Hồ sơ mới" msgid "Next" msgstr "Tới" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "Quét Trang tiếp" @@ -870,12 +973,15 @@ msgstr "Quét Trang tiếp" msgid "No device selected." msgstr "Không có thiết bị được lựa chọn." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "Không trang sau nằm trong máng." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "Chưa chọn nhà cung cấp.." @@ -897,11 +1003,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "bây giờ không thấy" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "Số trang quét:" @@ -909,7 +1015,6 @@ msgstr "Số trang quét:" msgid "OCR" msgstr "" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "Tải OCR" @@ -918,37 +1023,23 @@ msgstr "Tải OCR" msgid "OCR Progress" msgstr "Quá trình OCR" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "Cài đặt OCR" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "Ngôn ngữ OCR:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "Chế độ OCR:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "Đồng Ý" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "DSN Cũ" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "Một tập tin cho mỗi trang" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "Một tập tin cho mỗi quét" @@ -970,7 +1059,11 @@ msgstr "Một tập tin cho mỗi quét" msgid "One or more files could not be downloaded." msgstr "Một hoặc nhiều tập tin không thể được tải về." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "Mở Thư mục" @@ -978,11 +1071,15 @@ msgstr "Mở Thư mục" msgid "Operation in Progress" msgstr "Đang thực thi lệnh" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "Đầu ra" @@ -990,7 +1087,7 @@ msgstr "Đầu ra" msgid "Overwrite File" msgstr "Ghi đè Tiệp" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "" @@ -998,8 +1095,8 @@ msgstr "" msgid "PDF Document (*.pdf)" msgstr "Tài liệu PDF (*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "Cài đặt PDF" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "Tệp PNG (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "Kích thước trang:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "Trang gốc:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "Mật Khẩu" @@ -1045,21 +1140,24 @@ msgstr "Mật Khẩu" msgid "Paste" msgstr "Dán" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "Nhấn Start khi đã sẵn sàng." @@ -1067,7 +1165,7 @@ msgstr "Nhấn Start khi đã sẵn sàng." msgid "Preview" msgstr "Xem trước" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "Xem trước:" @@ -1080,12 +1178,11 @@ msgstr "Lùi lại" msgid "Print" msgstr "In" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "Cài đặt Hồ sơ" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "Hồ sơ:" @@ -1094,23 +1191,27 @@ msgstr "Hồ sơ:" msgid "Profiles" msgstr "" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "Hỏi nơi lưu." -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "Nhà cung cấp" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "Sẵn sàng cho quét {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "Phục hồi" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "Khôi phục hình ảnh scan" @@ -1122,9 +1223,15 @@ msgstr "Đang Khôi Phục..." msgid "Recovery Progress" msgstr "Khôi Phục thành công" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "Ghi các thiết lập" @@ -1140,15 +1247,11 @@ msgstr "Thiết lập lại" msgid "Reset Image" msgstr "Thiết lập lại hình ảnh" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "Nghị quyết:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "Khôi phục mặc định" @@ -1156,6 +1259,14 @@ msgstr "Khôi phục mặc định" msgid "Reverse" msgstr "Đảo ngược" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "Trở lại" @@ -1176,7 +1287,6 @@ msgstr "Xoay Trái" msgid "Rotate Right" msgstr "Xoay Phải" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "Chạy ngầm" @@ -1185,7 +1295,6 @@ msgstr "Chạy ngầm" msgid "Running OCR..." msgstr "Đang thực hiện OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1303,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "Lưu PDF" msgid "Save PDF Progress" msgstr "Lưu Tiến độ PDF" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "Lưu vào một tập tin duy nhất" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "Lưu vào nhiều file" @@ -1244,29 +1363,45 @@ msgstr "Lưu vào nhiều file..." msgid "Saving {0}..." msgstr "Đang Lưu..{0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "Tỉ lệ với cửa sổ" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "Tỉ lệ:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "Quét" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "Cấu hình quét" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "Quét Ảnh" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "Trang quét {0}" @@ -1280,11 +1415,14 @@ msgstr "Đang quét trang {0} (Quét {1})..." msgid "Scanning page {0}..." msgstr "Trang quét {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "Giây (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "Chọn" @@ -1293,7 +1431,10 @@ msgstr "Chọn" msgid "Select All" msgstr "Chọn hết" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "Chọn nguồn" @@ -1302,7 +1443,6 @@ msgstr "Chọn nguồn" msgid "Select a profile before clicking Scan." msgstr "Chọn một cấu hình trước khi bấm Scan." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "Chọn một hoặc nhiều ngôn ngữ:" @@ -1311,8 +1451,7 @@ msgstr "Chọn một hoặc nhiều ngôn ngữ:" msgid "Selected ({0})" msgstr "Chọn ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "Tập tin riêng biệt của Patch-T" @@ -1320,37 +1459,84 @@ msgstr "Tập tin riêng biệt của Patch-T" msgid "Set Default" msgstr "Về mặc định" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "Độ nét" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "Xem" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "quét đơn" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "Bỏ qua Lưu nhanh " -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "Bắt Đầu" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "Kéo dãn ra vừa kích thước trang" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "Tiêu đề:" @@ -1358,12 +1544,11 @@ msgstr "Tiêu đề:" msgid "TIFF File (*.tiff, *.tif)" msgstr "" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "Chi tiết kỹ thuật" @@ -1372,6 +1557,10 @@ msgstr "Chi tiết kỹ thuật" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "OCR chưa được cài. Vui lòng cài đặt gói liên quan để thao tác.:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "Các tập tin có thể không được ghi đè bởi vì nó hiện msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "Các tập tin {0} đã tồn tại. Bạn có muốn ghi đè không?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "Các tập tin sau đây được mã hóa và yêu cầu mật khẩu để mở: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "Máy quét chọn đang bận." msgid "The selected scanner is offline." msgstr "Máy quét chọn đang ẩn." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "Thông số Tiff" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "Thời gian giữa quét (giây):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "Tiêu đề:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Twain thực hiện:" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "Thay đổi chưa được lưu" @@ -1487,21 +1712,18 @@ msgstr "Đang cập nhật..." msgid "Uploading email..." msgstr "Đang tải lên email..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "Sử dụng giao diện người dùng tự nhiên" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "Sử dụng các thiết lập được xác định trước" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "Dùng Mật khẩu:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "Sử dụng OCR đòi hỏi bạn phải tải về từng ngôn ngữ mà bạn muốn quét." @@ -1515,16 +1737,15 @@ msgstr "Phiên bản {0}" msgid "View" msgstr "Xem" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "Đang chờ TWAIN để hoàn thành..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "Đang đợi cho phép..." @@ -1532,19 +1753,19 @@ msgstr "Đang đợi cho phép..." msgid "Waiting for scan {0}..." msgstr "Đang chờ quét {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "Ngưỡng trắng" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "Năm" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "Năm (00-99)" @@ -1561,21 +1782,18 @@ msgstr "Bạn không có quyền để lưu các tập tin tại vị trí này. msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "Bạn có thay đổi chưa được lưu. Bạn có chắc chắn muốn thoát ra và loại bỏ những thay đổi?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "Phóng" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "Phóng thực tế" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "Phóng To" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "Thu Nhỏ" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "{0} hình ảnh) quét trên {1} tại {2} có thể không được lưu lại, và có thể phục hồi được. Bạn có muốn khôi phục chúng?" diff --git a/NAPS2.Lib/Lang/po/zh-CN.po b/NAPS2.Lib/Lang/po/zh-CN.po index 43b0a5be60..b3ef9a8937 100644 --- a/NAPS2.Lib/Lang/po/zh-CN.po +++ b/NAPS2.Lib/Lang/po/zh-CN.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Chinese Simplified\n" "Language: zh\n" @@ -19,53 +19,37 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" -msgstr "" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" +msgstr "\"保存\"按钮默认操作:" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" -msgstr "" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" +msgstr "\"扫描\"按钮默认操作:" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" -msgstr "" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" +msgstr "\"扫描\"菜单可以更改默认配置文件" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" -msgstr "" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." +msgstr "找到 1 台设备" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24-位色" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" -msgstr "" +msgstr "A3(297 x 420 毫米)" #: SettingsResources.resx$PageSize_A4$Message msgid "A4 (210x297 mm)" -msgstr "" +msgstr "A4 (210 x 297 毫米)" #: SettingsResources.resx$PageSize_A5$Message msgid "A5 (148x210 mm)" -msgstr "" +msgstr "A5(149 x 210 毫米)" #: UiStrings.resx$About$Message #: UiStrings.resx$AboutFormTitle$Message @@ -76,52 +60,55 @@ msgstr "关于" msgid "Acquiring data..." msgstr "正在获取数据..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "操作" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "高级" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "高级配置文件设置" #: MiscResources.resx$AllCount$Message msgid "All ({0})" -msgstr "全部 ({0})" +msgstr "所有 ({0})" #: MiscResources.resx$FileTypeAllFiles$Message msgid "All Files" msgstr "所有文件" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "允许注释" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "允许复制内容" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" -msgstr "允许其他工具复制内容" +msgstr "允许复制内容以实现辅助功能" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "允许组合文件" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "允许修改文件" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "允许填写表单" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" -msgstr "允许全品质打印" +msgstr "允许完全质量打印" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "允许打印" @@ -133,17 +120,22 @@ msgstr "奇偶页分别排列(偶数页逆序)" msgid "Alternate Interleave" msgstr "取消奇偶页分别排列(偶数页逆序)" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" -msgstr "替代传输" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "总是询问" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" +msgstr "总是提示" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "需要额外的组件来导入这个 PDF 文件,您想要现在开始下载吗?" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." -msgstr "尝试安装更新时发生错误." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." +msgstr "运行OCR时出错。" #: MiscResources.resx$AuthError$Message msgid "An error occurred when trying to authorize." @@ -153,6 +145,10 @@ msgstr "尝试授权时发生错误." msgid "An error occurred when trying to auto save." msgstr "尝试自动保存时发生错误." +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "尝试安装更新时发生错误." + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "尝试存储文件时发生错误." @@ -172,10 +168,10 @@ msgstr "尝试扫描文件发生错误." #: MiscResources.resx$ExitWithActiveOperations$Message msgid "An operation is in progress. Are you sure you want to exit and cancel the operation?" -msgstr "操作进行中,是否取消操作并退出?" +msgstr "一个操作正在进行中。您确定要退出并取消操作吗?" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "批量扫描时发生未知错误." #: MiscResources.resx$UpdateAvailable$Message @@ -188,9 +184,17 @@ msgstr "OCR 有可用的更新." #: UiStrings.resx$AppleDriver$Message msgid "Apple Driver" -msgstr "" +msgstr "Apple 驱动程序" + +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "Apple Mail" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "应用" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "扫描后调整亮度/对比度" @@ -212,29 +216,37 @@ msgstr "您确定要删除“{0}”吗?" #: MiscResources.resx$ConfirmDeleteSingleProfile$Message msgid "Are you sure you want to delete the profile {0}?" -msgstr "您确定要删除这个{0}配置吗?" +msgstr "您确定要删除这个{0} 配置吗?" #: MiscResources.resx$ConfirmDeleteItems$Message msgid "Are you sure you want to delete {0} item(s)?" -msgstr "您确定要删除{0}个项目吗?" +msgstr "您确定要删除{0} 个项目吗?" #: MiscResources.resx$ConfirmDeleteMultipleProfiles$Message msgid "Are you sure you want to delete {0} profiles?" -msgstr "您确定要删除{0}个配置吗?" +msgstr "您确定要删除{0} 个配置吗?" + +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "您确定想要停止分享 {0} 吗?" #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" -msgstr "您确定要撤销您对{0}个图像的修改吗?" +msgstr "您确定要撤销您对{0} 个图像的修改吗?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "分配" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "附件名称:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "作者:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "授权" @@ -242,43 +254,41 @@ msgstr "授权" msgid "Auto" msgstr "自动" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "自动保存设置" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" msgstr "自动递增(1位数字)" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "自动递增(2位数字)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "自动递增(3位数字)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "自动递增(4位数字)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "扫描后自动进行 OCR 识别" #: SettingsResources.resx$PageSize_B4$Message msgid "B4 (250x353 mm)" -msgstr "" +msgstr "B4 (250x353 毫米)" #: SettingsResources.resx$PageSize_B5$Message msgid "B5 (176x250 mm)" -msgstr "" +msgstr "B5 (176x250 毫米)" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "批量扫描" @@ -296,9 +306,8 @@ msgstr "批量扫描由于错误而停止." #: SettingsResources.resx$OcrMode_Best$Message msgid "Best" -msgstr "" +msgstr "最佳" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "位深:" @@ -309,10 +318,10 @@ msgstr "位图文件 (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "黑白" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "空白页" @@ -320,33 +329,19 @@ msgstr "空白页" msgid "Brightness / Contrast" msgstr "亮度/对比度" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "亮度:" #: SettingsResources.resx$TiffComp_Ccitt4$Message msgid "CCITT4" -msgstr "" +msgstr "CCITT4" + +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "找不到您的扫描仪吗?了解关于 NAPS Flatpak 的限制。" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "取消" @@ -363,7 +358,7 @@ msgstr "正在取消...." msgid "Center" msgstr "居中" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "改变" @@ -375,7 +370,7 @@ msgstr "检查是否有新版本" msgid "Checking..." msgstr "正在检查..." -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "选择电子邮件服务商" @@ -383,7 +378,6 @@ msgstr "选择电子邮件服务商" msgid "Choose Profile" msgstr "选择配置" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "选择设备" @@ -395,9 +389,9 @@ msgstr "清除" #: UiStrings.resx$ClearAll$Message msgid "Clear All" -msgstr "" +msgstr "清除全部" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "保存后清除图片" @@ -405,16 +399,30 @@ msgstr "保存后清除图片" msgid "Close" msgstr "关闭" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "合并" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "与扫描设备的通信中断" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "兼容性" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "压缩:" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "连接" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "连接出错" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "对比度:" @@ -433,9 +441,9 @@ msgstr "正在复制..." #: UiStrings.resx$CopyrightFormat$Message msgid "Copyright {0} NAPS2 Contributors" -msgstr "" +msgstr "版权所有 {0} NAPS2 及其参与贡献者" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "阈值范围" @@ -443,7 +451,7 @@ msgstr "阈值范围" msgid "Crop" msgstr "裁剪" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "裁剪到页面大小" @@ -451,10 +459,14 @@ msgstr "裁剪到页面大小" msgid "Custom ({0}x{1} {2})" msgstr "自定义 ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "自定义纸张大小" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "自定义分辨率" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "自定义旋转" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "自定义 SMTP" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "自定义..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "深色" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "日 (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "默认" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "默认文件路径:" @@ -487,7 +504,6 @@ msgstr "默认文件路径:" msgid "Deinterleave" msgstr "奇偶页分别排列" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "倾斜校正" msgid "Deskew Progress" msgstr "倾斜校正进度" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "对已扫描页面进行倾斜校正" @@ -509,35 +525,31 @@ msgstr "对已扫描页面进行倾斜校正" msgid "Deskewing..." msgstr "倾斜校正中..." -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "设备:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "尺寸" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "显示名称:" #: UiStrings.resx$DocumentCorrection$Message msgid "Document Correction" -msgstr "" +msgstr "文档校正" #: MiscResources.resx$Donate$Message #: UiStrings.resx$Donate$Message msgid "Donate" msgstr "捐赠" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "完成" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "下载" @@ -550,22 +562,50 @@ msgstr "下载错误" msgid "Download Needed" msgstr "需要下载" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "下载进度" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "DPI" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "输稿器(双面)" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "ESCL 驱动程序" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "ESCL 网络驱动程序" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "ESCL USB 驱动程序" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "编辑" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "使用 {0} 编辑" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "编辑方式..." + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "发送所有页面" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" -msgstr "" +msgstr "发送所有页面(PDF)" #: MiscResources.resx$EmailPdf$Message #: UiStrings.resx$EmailPdf$Message @@ -576,25 +616,32 @@ msgstr "通过邮件发送 PDF" msgid "Email PDF Progress" msgstr "PDF 邮件发送进度" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "发送所选页" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" -msgstr "" +msgstr "发送所选页(PDF)" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "邮件设置" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "启用自动保存" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "启用调试日志" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "加密 PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "加密" @@ -602,12 +649,15 @@ msgstr "加密" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "增强的 Windows 图元文件 (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "错误" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "启动应用程序 {0} 出错" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,36 +667,43 @@ msgstr "估计下载大小: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "可交换图像文件 (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "排除空白页" #: SettingsResources.resx$OcrMode_Fast$Message msgid "Fast" -msgstr "" +msgstr "快速" #: SettingsResources.resx$Source_Feeder$Message msgid "Feeder" msgstr "输稿器" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "文件名" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" msgstr "文件路径:" +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" +msgstr "修复白平衡并移除噪点" + #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "翻转" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "翻转双面打印的背面" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "翻转偶数页面" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "对于 JPEG 高质量 (80+),还可以在配置文件中增加图像质量,以获得最佳效果." @@ -654,7 +711,6 @@ msgstr "对于 JPEG 高质量 (80+),还可以在配置文件中增加图像质 msgid "GIF File (*.gif)" msgstr "GIF 文件 (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "获取更多的语言" @@ -665,18 +721,17 @@ msgstr "玻璃稿台" #: SettingsResources.resx$EmailProviderType_Gmail$Message msgid "Gmail" -msgstr "" +msgstr "Gmail" #: SettingsResources.resx$BitDepth_8Grayscale$Message msgid "Grayscale" msgstr "灰阶" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "水平对齐:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "小时 (0-23)" @@ -684,6 +739,10 @@ msgstr "小时 (0-23)" msgid "Hue / Saturation" msgstr "色相/饱和度" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "IP/Host" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "图标来源:" @@ -696,12 +755,12 @@ msgstr "图像" msgid "Image Files" msgstr "图像文件" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "图像质量" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "图像设定" @@ -745,6 +804,10 @@ msgstr "安装完成。您需要立即重新启动 NAPS2 吗?" msgid "Installation failed." msgstr "安装失败." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "界面" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "取消奇偶页分别排列" @@ -755,24 +818,37 @@ msgstr "JPEG 文件(*.jpg, *.jpeg)" #: MiscResources.resx$FileTypeJp2$Message msgid "JPEG2000 File (*.jp2, *.jpx)" -msgstr "" +msgstr "JPEG 文件(*.jp2, *.jpx)" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "JPEG 品质" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "在整个会话期间保留图像" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "键盘快捷键" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "关键词:" #: SettingsResources.resx$TiffComp_Lzw$Message msgid "LZW" -msgstr "" +msgstr "LZW" #: UiStrings.resx$Language$Message msgid "Language" msgstr "语言" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "撰写评论" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "左对齐" @@ -781,33 +857,48 @@ msgstr "左对齐" msgid "Legacy (native UI only)" msgstr "传统(仅限本地界面)" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "浅色" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "喜欢NAPS2吗?" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "载入图片至 NAPS2" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "通过 OCR 生成可搜索的 PDF 文件" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "手动IP地址" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "最佳质量(大文件)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "内存传输" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "元数据" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "分钟 (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "月份 (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "更多信息" @@ -819,11 +910,19 @@ msgstr "下移" msgid "Move Up" msgstr "上移" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "多语言" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "多语言..." + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "多次扫描(每次扫描间固定延时)" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "多次扫描(每次扫描间提示)" @@ -831,17 +930,17 @@ msgstr "多次扫描(每次扫描间提示)" #: SdkResources.resx$NAPS2$Message #: UiStrings.resx$Naps2$Message msgid "NAPS2" -msgstr "" +msgstr "NAPS2" #: UiStrings.resx$Naps2TitleFormat$Message msgid "NAPS2 - {0}" -msgstr "" +msgstr "NAPS2 - {0}" #: MiscResources.resx$DonatePrompt$Message msgid "NAPS2 is completely free. Consider making a donation." msgstr "NAPS2 是完全免费的。请考虑捐款资助." -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "名称(可选)" @@ -849,6 +948,10 @@ msgstr "名称(可选)" msgid "Name missing." msgstr "请输入配置名称." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "本地传输" + #: UiStrings.resx$New$Message msgid "New" msgstr "新建" @@ -861,7 +964,7 @@ msgstr "新建配置文件" msgid "Next" msgstr "下一页" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "下一个扫描" @@ -870,12 +973,15 @@ msgstr "下一个扫描" msgid "No device selected." msgstr "未选择设备." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "未找到设备" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "输稿器中未发现纸张." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "未选择邮件服务商." @@ -895,13 +1001,13 @@ msgstr "无" #: UiStrings.resx$Naps2FullName$Message msgid "Not Another PDF Scanner" -msgstr "" +msgstr "Not Another PDF Scanner" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "现在不需要" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "扫描次数:" @@ -909,7 +1015,6 @@ msgstr "扫描次数:" msgid "OCR" msgstr "光学字符识别 (OCR)" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR 下载" @@ -918,37 +1023,23 @@ msgstr "OCR 下载" msgid "OCR Progress" msgstr "OCR 识别进度" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCR 设置" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR 语言:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "OCR 模式:" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "确定" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "基于对齐的偏移宽度 (WIA)" @@ -956,13 +1047,11 @@ msgstr "基于对齐的偏移宽度 (WIA)" msgid "Old DSM" msgstr "旧版 DSM" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "每页一个文件" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "每次扫描一个文件" @@ -970,7 +1059,11 @@ msgstr "每次扫描一个文件" msgid "One or more files could not be downloaded." msgstr "无法下载一个或多个文件 ." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "仅允许单个 NAPS2 实例" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "打开文件夹" @@ -978,11 +1071,15 @@ msgstr "打开文件夹" msgid "Operation in Progress" msgstr "正在进行操作" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "Outlook (新)" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "Outlook 网页访问" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "输出" @@ -990,7 +1087,7 @@ msgstr "输出" msgid "Overwrite File" msgstr "覆盖文件" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "文件所有者密码:" @@ -998,8 +1095,8 @@ msgstr "文件所有者密码:" msgid "PDF Document (*.pdf)" msgstr "PDF 文档(*.pdf)" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF 设置" @@ -1009,35 +1106,33 @@ msgstr "PDF 保存." #: SettingsResources.resx$PdfCompat_PdfA1B$Message msgid "PDF/A-1b" -msgstr "" +msgstr "PDF/A-1b" #: SettingsResources.resx$PdfCompat_PdfA2B$Message msgid "PDF/A-2b" -msgstr "" +msgstr "PDF/A-2b" #: SettingsResources.resx$PdfCompat_PdfA3B$Message msgid "PDF/A-3b" -msgstr "" +msgstr "PDF/A-3b" #: SettingsResources.resx$PdfCompat_PdfA3U$Message msgid "PDF/A-3u" -msgstr "" +msgstr "PDF/A-3u" #: MiscResources.resx$FileTypePng$Message msgid "PNG File (*.png)" msgstr "PNG 文件 (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "纸张大小:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "纸张来源:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "密码" @@ -1045,21 +1140,24 @@ msgstr "密码" msgid "Paste" msgstr "粘贴" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "格式符" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "端口" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "后处理" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "扫描后自动运行 OCR" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "准备好后点击开始." @@ -1067,7 +1165,7 @@ msgstr "准备好后点击开始." msgid "Preview" msgstr "预览" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "预览:" @@ -1080,12 +1178,11 @@ msgstr "上一页" msgid "Print" msgstr "打印" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "配置文件设置" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "配置文件:" @@ -1094,23 +1191,27 @@ msgstr "配置文件:" msgid "Profiles" msgstr "配置文件" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "当选中时提示" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "文件路径提示" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "服务供应商" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "已准备扫描 {0}." -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "修复" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "修复已扫描的图像" @@ -1122,9 +1223,15 @@ msgstr "正在修复..." msgid "Recovery Progress" msgstr "修复进度" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "重做" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "重做 {0}" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "记住这些设定" @@ -1140,15 +1247,11 @@ msgstr "重置" msgid "Reset Image" msgstr "重置图像" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "分辨率:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "还原默认值" @@ -1156,6 +1259,14 @@ msgstr "还原默认值" msgid "Reverse" msgstr "反转" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "反转全部" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "反转所选项" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "还原" @@ -1176,7 +1287,6 @@ msgstr "逆时针旋转" msgid "Rotate Right" msgstr "顺时针旋转" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "在后台运行" @@ -1185,22 +1295,26 @@ msgstr "在后台运行" msgid "Running OCR..." msgstr "执行 OCR..." -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "SANE 驱动" #: UiStrings.resx$Save$Message msgid "Save" -msgstr "" +msgstr "保存" + +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "保存所有" #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" -msgstr "" +msgstr "保存保存所有到图像" #: UiStrings.resx$SaveAllAsPdf$Message msgid "Save All as PDF" -msgstr "" +msgstr "保存所有到 PDF 文件" #: MiscResources.resx$SaveImages$Message #: UiStrings.resx$SaveImages$Message @@ -1220,19 +1334,24 @@ msgstr "保存 PDF" msgid "Save PDF Progress" msgstr "PDF 保存进度" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "保存所选" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" -msgstr "" +msgstr "保存所选为图片" #: UiStrings.resx$SaveSelectedAsPdf$Message msgid "Save Selected as PDF" -msgstr "" +msgstr "保存所选为PDF" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "保存为单个文件" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "保存为多个文件" @@ -1244,29 +1363,45 @@ msgstr "保存批量扫描结果..." msgid "Saving {0}..." msgstr "正在保存 {0}..." -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "使用窗口比例" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "缩放比例:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "扫描" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "扫描设置" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "使用默认配置文件扫描" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "使用新配置文件扫描" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "使用配置文件{0}扫描" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "已扫描的图像" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "扫描仪共享" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "正在扫描页面 {0}" @@ -1280,11 +1415,14 @@ msgstr "正在扫描页面 {0} (扫描 {1})..." msgid "Scanning page {0}..." msgstr "正在扫描页面 {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "正在搜索设备…" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "秒 (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "选择" @@ -1293,7 +1431,10 @@ msgstr "选择" msgid "Select All" msgstr "全选" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "选择设备" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "选择来源" @@ -1302,7 +1443,6 @@ msgstr "选择来源" msgid "Select a profile before clicking Scan." msgstr "请先选择配置文件,然后点击 [扫描] ." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "请选择一个或多个语言:" @@ -1311,8 +1451,7 @@ msgstr "请选择一个或多个语言:" msgid "Selected ({0})" msgstr "已选择 ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "通过 Patch-T 拆分文件" @@ -1320,37 +1459,84 @@ msgstr "通过 Patch-T 拆分文件" msgid "Set Default" msgstr "设为默认设置" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "设置" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "共享" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "即使在NAPS2关闭时也共享" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "共享扫描仪设置" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "共享的扫描仪可以通过在其他计算机的 NAPS2 配置中选择 “ESCL 驱动程序” 从本地网络上的其他计算机中使用。" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "锐化" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "快捷键" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "显示" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "显示\"配置文件\"工具栏" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "显示本地 TWAIN 进程" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "显示页码" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "侧边栏" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "单页文件" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "单次扫描" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "跳过保存提示" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "拆分" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "启动" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "停止扫描仪共享" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "扩展至纸张大小" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "主题:" @@ -1358,12 +1544,11 @@ msgstr "主题:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF 文件 (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN 驱动" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "技术细节" @@ -1372,6 +1557,10 @@ msgstr "技术细节" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "OCR 组件不可用。请确认已安装所需的软件包:" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "OCR操作超时" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "文件正在使用,无法覆盖." msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "文件 {0} 已存在,是否覆盖?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "以下文件已加密,请输入密码打开: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "以下文件已加密,请输入密码打开:" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,29 +1632,65 @@ msgstr "选定的扫描仪正在工作." msgid "The selected scanner is offline." msgstr "选定的扫描仪处于脱机状态." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "工作进程崩溃" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "工作进程崩溃。检查 Windows 事件查看器" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "主题:" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "Thunderbird" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "TIFF 格式设置" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "扫描之间的间隔(秒):" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "标题:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "工具" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "Twain 实现:" #: SettingsResources.resx$PageSize_Legal$Message msgid "US Legal (8.5x14 in)" -msgstr "" +msgstr "US Legal (8.5x14 英寸)" #: SettingsResources.resx$PageSize_Letter$Message msgid "US Letter (8.5x11 in)" -msgstr "" +msgstr "US Legal (8.5x14 英寸)" + +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "取消分配" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "撤销" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "撤消 {0}" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "未知扫描仪" #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" @@ -1477,7 +1702,7 @@ msgstr "更新进度" #: MiscResources.resx$UpdateCheckDisabled$Message msgid "Update checking is disabled." -msgstr "" +msgstr "更新检查已禁用" #: MiscResources.resx$Updating$Message msgid "Updating..." @@ -1487,21 +1712,18 @@ msgstr "正在更新..." msgid "Uploading email..." msgstr "上传邮件..." -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "使用本地界面 " -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "使用预定义的设置" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "用户密码:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "使用 OCR 需要下载您所需的语言." @@ -1515,16 +1737,15 @@ msgstr "版本 {0}" msgid "View" msgstr "查看" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA 驱动程序" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "正在等待 TWAIN 完成..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "等待授权..." @@ -1532,19 +1753,19 @@ msgstr "等待授权..." msgid "Waiting for scan {0}..." msgstr "正在等待扫描 {0}..." -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "白色阀值" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" -msgstr "" +msgstr "Wia 版本:" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "年份" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "年份 (00-99)" @@ -1561,21 +1782,18 @@ msgstr "您没有在该位置保存文件的权限." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "您有未保存的修改,确定要放弃这些修改并退出吗?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "缩放" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "缩放至实际大小" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "放大" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "缩小" @@ -1598,28 +1816,33 @@ msgstr "/ {0}" #: SettingsResources.resx$TwainImpl_X64$Message msgid "x64" -msgstr "" +msgstr "x64" #: MiscResources.resx$NamedPageSizeFormat$Message msgid "{0} ({1}x{2} {3})" -msgstr "" +msgstr "{0} ({1}x{2} {3})" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" -msgstr "" +msgstr "{0} / {1}" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" -msgstr "" +msgstr "{0} / {1} MB" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} 个文件" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "找到 {0} 台设备" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "{0} dpi" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "使用 {2} 在 {1} 上扫描的 {0} 张图像可能还未保存,现在可以恢复。您需要恢复这些图像吗?" diff --git a/NAPS2.Lib/Lang/po/zh-TW.po b/NAPS2.Lib/Lang/po/zh-TW.po index c78666412f..e03824f1e3 100644 --- a/NAPS2.Lib/Lang/po/zh-TW.po +++ b/NAPS2.Lib/Lang/po/zh-TW.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: naps2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-04-19 21:51+0000\n" -"PO-Revision-Date: 2022-11-28 04:41\n" +"PO-Revision-Date: 2025-08-30 22:28\n" "Last-Translator: \n" "Language-Team: Chinese Traditional\n" "Language: zh\n" @@ -19,42 +19,26 @@ msgstr "" "X-Crowdin-File: templates.pot\n" "X-Crowdin-File-ID: 75\n" -#: SettingsResources.resx$Dpi_100$Message -msgid "100 dpi" +#: UiStrings.resx$SaveButtonDefaultAction$Message +msgid "\"Save\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_1200$Message -msgid "1200 dpi" +#: UiStrings.resx$ScanButtonDefaultAction$Message +msgid "\"Scan\" button default action:" msgstr "" -#: SettingsResources.resx$Dpi_150$Message -msgid "150 dpi" +#: UiStrings.resx$ScanChangesDefaultProfile$Message +msgid "\"Scan\" menu changes default profile" msgstr "" -#: SettingsResources.resx$Dpi_200$Message -msgid "200 dpi" +#: UiStrings.resx$DeviceFoundSingular$Message +msgid "1 device found." msgstr "" #: SettingsResources.resx$BitDepth_24Color$Message msgid "24-bit Color" msgstr "24 位元色彩" -#: SettingsResources.resx$Dpi_300$Message -msgid "300 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_400$Message -msgid "400 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_600$Message -msgid "600 dpi" -msgstr "" - -#: SettingsResources.resx$Dpi_800$Message -msgid "800 dpi" -msgstr "" - #: SettingsResources.resx$PageSize_A3$Message msgid "A3 (297x420 mm)" msgstr "" @@ -76,12 +60,15 @@ msgstr "關於" msgid "Acquiring data..." msgstr "正在取得資料..." -#: FEditProfile.resx$btnAdvanced.Text$Message +#: UiStrings.resx$Action$Message +msgid "Action" +msgstr "" + #: UiStrings.resx$Advanced$Message msgid "Advanced" msgstr "高级" -#: FAdvancedScanSettings.resx$$this.Text$Message +#: UiStrings.resx$AdvancedProfileFormTitle$Message msgid "Advanced Profile Settings" msgstr "高级配置文件设置" @@ -93,35 +80,35 @@ msgstr "全部 ({0})" msgid "All Files" msgstr "所有檔案" -#: FPdfSettings.resx$clbPerms.Items6$Message +#: UiStrings.resx$AllowAnnotations$Message msgid "Allow Annotations" msgstr "允許註釋" -#: FPdfSettings.resx$clbPerms.Items4$Message +#: UiStrings.resx$AllowContentCopying$Message msgid "Allow Content Copying" msgstr "允許內容複製" -#: FPdfSettings.resx$clbPerms.Items5$Message +#: UiStrings.resx$AllowContentCopyingForAccessibility$Message msgid "Allow Content Copying for Accessibility" msgstr "允許對協助工具內容複製" -#: FPdfSettings.resx$clbPerms.Items3$Message +#: UiStrings.resx$AllowDocumentAssembly$Message msgid "Allow Document Assembly" msgstr "允許文件組件" -#: FPdfSettings.resx$clbPerms.Items2$Message +#: UiStrings.resx$AllowDocumentModification$Message msgid "Allow Document Modification" msgstr "允許文件修改" -#: FPdfSettings.resx$clbPerms.Items7$Message +#: UiStrings.resx$AllowFormFilling$Message msgid "Allow Form Filling" msgstr "允許表單填寫" -#: FPdfSettings.resx$clbPerms.Items1$Message +#: UiStrings.resx$AllowFullQualityPrinting$Message msgid "Allow Full Quality Printing" msgstr "允許完整品質列印" -#: FPdfSettings.resx$clbPerms.Items$Message +#: UiStrings.resx$AllowPrinting$Message msgid "Allow Printing" msgstr "允許列印" @@ -133,16 +120,21 @@ msgstr "" msgid "Alternate Interleave" msgstr "" -#: SettingsResources.resx$TwainImpl_MemXfer$Message -msgid "Alternative Transfer" +#: UiStrings.resx$AlwaysAsk$Message +msgid "Always Ask" +msgstr "" + +#: SettingsResources.resx$SaveButtonDefaultAction_AlwaysPrompt$Message +#: SettingsResources.resx$ScanButtonDefaultAction_AlwaysPrompt$Message +msgid "Always Prompt" msgstr "" #: MiscResources.resx$PdfImportComponentNeeded$Message msgid "An additional component is needed to import this PDF file. Would you like to download it now?" msgstr "" -#: MiscResources.resx$UpdateError$Message -msgid "An error occured when trying to install the update." +#: SdkResources.resx$OcrError$Message +msgid "An error occurred running OCR." msgstr "" #: MiscResources.resx$AuthError$Message @@ -153,6 +145,10 @@ msgstr "" msgid "An error occurred when trying to auto save." msgstr "" +#: MiscResources.resx$UpdateError$Message +msgid "An error occurred when trying to install the update." +msgstr "" + #: MiscResources.resx$ErrorSaving$Message msgid "An error occurred when trying to save the file." msgstr "儲存此檔時發生錯誤." @@ -175,7 +171,7 @@ msgid "An operation is in progress. Are you sure you want to exit and cancel the msgstr "" #: MiscResources.resx$BatchError$Message -msgid "An unknown error ocurred during the batch scan." +msgid "An unknown error occurred during the batch scan." msgstr "" #: MiscResources.resx$UpdateAvailable$Message @@ -190,7 +186,15 @@ msgstr "" msgid "Apple Driver" msgstr "" -#: FAdvancedScanSettings.resx$cbBrightnessContrastAfterScan.Text$Message +#: SettingsResources.resx$EmailProviderType_AppleMail$Message +msgid "Apple Mail" +msgstr "" + +#: UiStrings.resx$Application$Message +msgid "Application" +msgstr "" + +#: UiStrings.resx$BrightnessContrastAfterScan$Message msgid "Apply brightness/contrast after scan" msgstr "" @@ -222,19 +226,27 @@ msgstr "您確定要刪除 {0} 個項目嗎?" msgid "Are you sure you want to delete {0} profiles?" msgstr "您確定要刪除 {0} 個設定檔嗎?" +#: UiStrings.resx$ConfirmDeleteSharedDevice$Message +msgid "Are you sure you want to stop sharing {0}?" +msgstr "" + #: MiscResources.resx$ConfirmResetImages$Message msgid "Are you sure you want undo your changes to {0} image(s)?" msgstr "您確定要復原您的變更到 {0} 個圖像嗎?" -#: FEmailSettings.resx$label1.Text$Message +#: UiStrings.resx$Assign$Message +msgid "Assign" +msgstr "" + +#: UiStrings.resx$AttachmentNameLabel$Message msgid "Attachment Name:" msgstr "附件名稱:" -#: FPdfSettings.resx$label3.Text$Message +#: UiStrings.resx$AuthorLabel$Message msgid "Author:" msgstr "作者:" -#: FAuthorize.resx$$this.Text$Message +#: UiStrings.resx$AuthorizeFormTitle$Message msgid "Authorize" msgstr "" @@ -242,29 +254,27 @@ msgstr "" msgid "Auto" msgstr "" -#: FAutoSaveSettings.resx$$this.Text$Message -#: FEditProfile.resx$linkAutoSaveSettings.Text$Message #: UiStrings.resx$AutoSaveSettings$Message +#: UiStrings.resx$AutoSaveSettingsFormTitle$Message msgid "Auto Save Settings" msgstr "" -#: FPlaceholders.resx$label13.Text$Message -msgid "Auto-incrementing number (1 digit)" -msgstr "自動遞增數(1 個數字)" +#: UiStrings.resx$AutoIncrementing1Digit$Message +msgid "Auto-incrementing number (1 digits)" +msgstr "" -#: FPlaceholders.resx$label12.Text$Message +#: UiStrings.resx$AutoIncrementing2Digit$Message msgid "Auto-incrementing number (2 digits)" msgstr "自動遞增數(2 個數字)" -#: FPlaceholders.resx$label11.Text$Message +#: UiStrings.resx$AutoIncrementing3Digit$Message msgid "Auto-incrementing number (3 digits)" msgstr "自動遞增數(3 個數字)" -#: FPlaceholders.resx$label10.Text$Message +#: UiStrings.resx$AutoIncrementing4Digit$Message msgid "Auto-incrementing number (4 digits)" msgstr "自動遞增數(4 個數字)" -#: FOcrSetup.resx$checkBoxRunInBG.Text$Message #: UiStrings.resx$RunOcrAfterScanning$Message msgid "Automatically run OCR after scanning" msgstr "" @@ -277,8 +287,8 @@ msgstr "" msgid "B5 (176x250 mm)" msgstr "" -#: FBatchScan.resx$$this.Text$Message #: UiStrings.resx$BatchScan$Message +#: UiStrings.resx$BatchScanFormTitle$Message msgid "Batch Scan" msgstr "" @@ -298,7 +308,6 @@ msgstr "" msgid "Best" msgstr "" -#: FEditProfile.resx$label3.Text$Message #: UiStrings.resx$BitDepthLabel$Message msgid "Bit depth:" msgstr "位元深度:" @@ -309,10 +318,10 @@ msgstr "點陣圖檔 (*.bmp)" #: SettingsResources.resx$BitDepth_1BlackAndWhite$Message #: UiStrings.resx$BlackAndWhite$Message -msgid "Black & White" +msgid "Black and White" msgstr "黑白" -#: FAdvancedScanSettings.resx$groupBox3.Text$Message +#: UiStrings.resx$BlankPages$Message msgid "Blank Pages" msgstr "" @@ -320,7 +329,6 @@ msgstr "" msgid "Brightness / Contrast" msgstr "" -#: FEditProfile.resx$label6.Text$Message #: UiStrings.resx$BrightnessLabel$Message msgid "Brightness:" msgstr "亮度:" @@ -329,24 +337,11 @@ msgstr "亮度:" msgid "CCITT4" msgstr "" -#: FAdvancedScanSettings.resx$btnCancel.Text$Message -#: FAuthorize.resx$btnCancel.Text$Message -#: FAutoSaveSettings.resx$btnCancel.Text$Message -#: FBatchScan.resx$btnCancel.Text$Message -#: FDownloadProgress.resx$btnCancel.Text$Message -#: FEditProfile.resx$btnCancel.Text$Message -#: FEmailSettings.resx$btnCancel.Text$Message -#: FImageSettings.resx$btnCancel.Text$Message -#: FOcrLanguageDownload.resx$btnCancel.Text$Message -#: FOcrSetup.resx$btnCancel.Text$Message -#: FPageSize.resx$btnCancel.Text$Message -#: FPdfPassword.resx$btnCancel.Text$Message -#: FPdfSettings.resx$btnCancel.Text$Message -#: FPlaceholders.resx$btnCancel.Text$Message -#: FProgress.resx$btnCancel.Text$Message -#: FSelectDevice.resx$btnCancel.Text$Message +#: UiStrings.resx$CantFindScannerFlatpak$Message +msgid "Can't find your scanner? Read about limitations of the NAPS2 Flatpak." +msgstr "" + #: MiscResources.resx$Cancel$Message -#: OperationProgressNotifyWidget.resx$cancelToolStripMenuItem.Text$Message #: UiStrings.resx$Cancel$Message msgid "Cancel" msgstr "取消" @@ -363,7 +358,7 @@ msgstr "" msgid "Center" msgstr "置中" -#: FEmailSettings.resx$btnChangeProvider.Text$Message +#: UiStrings.resx$Change$Message msgid "Change" msgstr "" @@ -375,7 +370,7 @@ msgstr "" msgid "Checking..." msgstr "" -#: FEmailProvider.resx$$this.Text$Message +#: UiStrings.resx$EmailProviderFormTitle$Message msgid "Choose Email Provider" msgstr "" @@ -383,7 +378,6 @@ msgstr "" msgid "Choose Profile" msgstr "選擇設定檔" -#: FEditProfile.resx$btnChooseDevice.Text$Message #: UiStrings.resx$ChooseDevice$Message msgid "Choose device" msgstr "選擇裝置" @@ -397,7 +391,7 @@ msgstr "清除" msgid "Clear All" msgstr "" -#: FAutoSaveSettings.resx$cbClearAfterSave.Text$Message +#: UiStrings.resx$ClearAfterSaving$Message msgid "Clear images after saving" msgstr "" @@ -405,16 +399,30 @@ msgstr "" msgid "Close" msgstr "" -#: FAdvancedScanSettings.resx$groupBox2.Text$Message -#: FPdfSettings.resx$groupCompat.Text$Message +#: UiStrings.resx$Combine$Message +msgid "Combine" +msgstr "" + +#: SdkResources.resx$DeviceCommunicationFailure$Message +msgid "Communication with the scanning device was interrupted." +msgstr "" + +#: UiStrings.resx$Compatibility$Message msgid "Compatibility" msgstr "" -#: FImageSettings.resx$label3.Text$Message +#: UiStrings.resx$CompressionLabel$Message msgid "Compression:" msgstr "" -#: FEditProfile.resx$label7.Text$Message +#: UiStrings.resx$Connect$Message +msgid "Connect" +msgstr "" + +#: UiStrings.resx$ConnectionError$Message +msgid "Connection error." +msgstr "" + #: UiStrings.resx$ContrastLabel$Message msgid "Contrast:" msgstr "對比:" @@ -435,7 +443,7 @@ msgstr "" msgid "Copyright {0} NAPS2 Contributors" msgstr "" -#: FAdvancedScanSettings.resx$label3.Text$Message +#: UiStrings.resx$CoverageThreshold$Message msgid "Coverage Threshold" msgstr "" @@ -443,7 +451,7 @@ msgstr "" msgid "Crop" msgstr "裁剪" -#: FAdvancedScanSettings.resx$cbForcePageSizeCrop.Text$Message +#: UiStrings.resx$CropToPageSize$Message msgid "Crop to page size" msgstr "" @@ -451,10 +459,14 @@ msgstr "" msgid "Custom ({0}x{1} {2})" msgstr "自訂 ({0}x{1} {2})" -#: FPageSize.resx$$this.Text$Message +#: UiStrings.resx$PageSizeFormTitle$Message msgid "Custom Page Size" msgstr "自訂頁面大小" +#: UiStrings.resx$ResolutionFormTitle$Message +msgid "Custom Resolution" +msgstr "" + #: UiStrings.resx$CustomRotation$Message msgid "Custom Rotation" msgstr "自訂旋轉" @@ -464,22 +476,27 @@ msgid "Custom SMTP" msgstr "" #: SettingsResources.resx$PageSize_Custom$Message +#: SettingsResources.resx$Resolution_Custom$Message msgid "Custom..." msgstr "自訂..." -#: FPlaceholders.resx$label6.Text$Message +#: SettingsResources.resx$Theme_Dark$Message +msgid "Dark" +msgstr "" + +#: UiStrings.resx$Day2Digit$Message msgid "Day (01-31)" msgstr "天 (01-31)" #: SettingsResources.resx$PdfCompat_Default$Message +#: SettingsResources.resx$Theme_Default$Message #: SettingsResources.resx$TwainImpl_Default$Message #: SettingsResources.resx$WiaVersion_Default$Message #: UiStrings.resx$Default$Message msgid "Default" msgstr "" -#: FImageSettings.resx$label1.Text$Message -#: FPdfSettings.resx$label1.Text$Message +#: UiStrings.resx$DefaultFilePathLabel$Message msgid "Default File Path:" msgstr "" @@ -487,7 +504,6 @@ msgstr "" msgid "Deinterleave" msgstr "去掉間隔" -#: FRecover.resx$btnDelete.Text$Message #: MiscResources.resx$Delete$Message #: UiStrings.resx$Delete$Message msgid "Delete" @@ -501,7 +517,7 @@ msgstr "" msgid "Deskew Progress" msgstr "" -#: FAdvancedScanSettings.resx$cbAutoDeskew.Text$Message +#: UiStrings.resx$DeskewScannedPages$Message msgid "Deskew scanned pages" msgstr "" @@ -509,16 +525,14 @@ msgstr "" msgid "Deskewing..." msgstr "" -#: FEditProfile.resx$label1.Text$Message #: UiStrings.resx$DeviceLabel$Message msgid "Device:" msgstr "裝置:" -#: FPageSize.resx$label2.Text$Message +#: UiStrings.resx$Dimensions$Message msgid "Dimensions" msgstr "" -#: FEditProfile.resx$label8.Text$Message #: UiStrings.resx$DisplayNameLabel$Message msgid "Display name:" msgstr "顯示名稱:" @@ -532,12 +546,10 @@ msgstr "" msgid "Donate" msgstr "" -#: FBatchPrompt.resx$btnDone.Text$Message #: UiStrings.resx$Done$Message msgid "Done" msgstr "完成" -#: FOcrLanguageDownload.resx$btnDownload.Text$Message #: UiStrings.resx$Download$Message msgid "Download" msgstr "下載" @@ -550,19 +562,47 @@ msgstr "下載發生錯誤" msgid "Download Needed" msgstr "" -#: FDownloadProgress.resx$$this.Text$Message #: UiStrings.resx$DownloadProgressFormTitle$Message msgid "Download Progress" msgstr "下載進度" +#: UiStrings.resx$Dpi$Message +msgid "Dpi" +msgstr "" + #: SettingsResources.resx$Source_Duplex$Message msgid "Duplex" msgstr "雙面" +#: UiStrings.resx$EsclDriver$Message +msgid "ESCL Driver" +msgstr "" + +#: UiStrings.resx$EsclNetworkDriver$Message +msgid "ESCL Network Driver" +msgstr "" + +#: UiStrings.resx$EsclUsbDriver$Message +msgid "ESCL USB Driver" +msgstr "" + #: UiStrings.resx$Edit$Message msgid "Edit" msgstr "編輯" +#: UiStrings.resx$EditWithAppName$Message +msgid "Edit with {0}" +msgstr "" + +#: UiStrings.resx$EditWith$Message +#: UiStrings.resx$EditWithFormTitle$Message +msgid "Edit with..." +msgstr "" + +#: UiStrings.resx$EmailAll$Message +msgid "Email All" +msgstr "" + #: UiStrings.resx$EmailAllAsPdf$Message msgid "Email All as PDF" msgstr "" @@ -576,25 +616,32 @@ msgstr "電子郵件發送 PDF" msgid "Email PDF Progress" msgstr "" +#: UiStrings.resx$EmailSelected$Message +msgid "Email Selected" +msgstr "" + #: UiStrings.resx$EmailSelectedAsPdf$Message msgid "Email Selected as PDF" msgstr "" -#: FEmailSettings.resx$$this.Text$Message #: UiStrings.resx$EmailSettings$Message +#: UiStrings.resx$EmailSettingsFormTitle$Message msgid "Email Settings" msgstr "電子郵件設定" -#: FEditProfile.resx$cbAutoSave.Text$Message #: UiStrings.resx$EnableAutoSave$Message msgid "Enable Auto Save" msgstr "" -#: FPdfSettings.resx$cbEncryptPdf.Text$Message +#: UiStrings.resx$EnableDebugLogging$Message +msgid "Enable debug logging" +msgstr "" + +#: UiStrings.resx$EncryptPdf$Message msgid "Encrypt PDF" msgstr "加密 PDF" -#: FPdfSettings.resx$groupProtection.Text$Message +#: UiStrings.resx$Encryption$Message msgid "Encryption" msgstr "加密" @@ -602,12 +649,15 @@ msgstr "加密" msgid "Enhanced Windows MetaFile (*.emf)" msgstr "Windows 增強型中繼檔 (*.emf)" -#: FError.resx$$this.Text$Message #: MiscResources.resx$Error$Message +#: UiStrings.resx$ErrorFormTitle$Message msgid "Error" msgstr "錯誤" -#: FOcrLanguageDownload.resx$labelSizeEstimate.Text$Message +#: UiStrings.resx$ErrorStartingApplication$Message +msgid "Error starting application {0}" +msgstr "" + #: MiscResources.resx$EstimatedDownloadSize$Message #: UiStrings.resx$EstimatedDownloadSize$Message msgid "Estimated download size: {0} MB" @@ -617,7 +667,7 @@ msgstr "估計下載大小: {0} MB" msgid "Exchangeable Image File (*.exif)" msgstr "可交換式影像檔案 (*.exif)" -#: FAdvancedScanSettings.resx$cbExcludeBlankPages.Text$Message +#: UiStrings.resx$ExcludeBlankPages$Message msgid "Exclude blank pages" msgstr "" @@ -629,24 +679,31 @@ msgstr "" msgid "Feeder" msgstr "送紙器" -#: FPlaceholders.resx$label1.Text$Message -msgid "File Name" +#: UiStrings.resx$FileNameLabel$Message +msgid "File Name:" msgstr "檔案名稱" -#: FAutoSaveSettings.resx$lblFilePath.Text$Message -#: FBatchScan.resx$lblFilePath.Text$Message -msgid "File path:" +#: UiStrings.resx$FilePathLabel$Message +msgid "File Path:" +msgstr "" + +#: UiStrings.resx$OcrPreProcessing$Message +msgid "Fix white balance and remove noise" msgstr "" #: UiStrings.resx$Flip$Message msgid "Flip" msgstr "翻轉" -#: FAdvancedScanSettings.resx$cbFlipDuplex.Text$Message +#: UiStrings.resx$FlipBackSidesOfDuplexPages$Message +msgid "Flip back sides of duplex pages" +msgstr "" + +#: UiStrings.resx$FlipDuplexedPages$Message msgid "Flip duplexed pages" msgstr "" -#: FImageSettings.resx$lblWarning.Text$Message +#: UiStrings.resx$JpegQualityHelp$Message msgid "For high JPEG qualities (80+), also increase Image Quality in your profile for best results." msgstr "" @@ -654,7 +711,6 @@ msgstr "" msgid "GIF File (*.gif)" msgstr "GIF 檔 (*.gif)" -#: FOcrSetup.resx$linkGetLanguages.Text$Message #: UiStrings.resx$GetMoreLanguages$Message msgid "Get more languages" msgstr "獲取更多的語言" @@ -671,12 +727,11 @@ msgstr "" msgid "Grayscale" msgstr "灰階" -#: FEditProfile.resx$label9.Text$Message #: UiStrings.resx$HorizontalAlignLabel$Message msgid "Horizontal align:" msgstr "水平對齊:" -#: FPlaceholders.resx$label7.Text$Message +#: UiStrings.resx$Hour2Digit$Message msgid "Hour (0-23)" msgstr "小時 (0-23)" @@ -684,6 +739,10 @@ msgstr "小時 (0-23)" msgid "Hue / Saturation" msgstr "" +#: UiStrings.resx$IpHost$Message +msgid "IP/Host" +msgstr "" + #: UiStrings.resx$IconsFrom$Message msgid "Icons from:" msgstr "圖示來源:" @@ -696,12 +755,12 @@ msgstr "圖像" msgid "Image Files" msgstr "" -#: FAdvancedScanSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$ImageQuality$Message msgid "Image Quality" msgstr "" -#: FImageSettings.resx$$this.Text$Message #: UiStrings.resx$ImageSettings$Message +#: UiStrings.resx$ImageSettingsFormTitle$Message msgid "Image Settings" msgstr "圖像設定" @@ -745,6 +804,10 @@ msgstr "安裝完成。您要馬上將 NAPS2 重新啟動嗎?" msgid "Installation failed." msgstr "安裝失敗." +#: UiStrings.resx$Interface$Message +msgid "Interface" +msgstr "" + #: UiStrings.resx$Interleave$Message msgid "Interleave" msgstr "間隔" @@ -757,11 +820,20 @@ msgstr "JPEG 檔 (*.jpg, *.jpeg)" msgid "JPEG2000 File (*.jp2, *.jpx)" msgstr "" -#: FImageSettings.resx$groupJpeg.Text$Message +#: UiStrings.resx$JpegQuality$Message msgid "Jpeg Quality" msgstr "JPEG 品質" -#: FPdfSettings.resx$label6.Text$Message +#: UiStrings.resx$KeepSession$Message +msgid "Keep images across sessions" +msgstr "" + +#: UiStrings.resx$KeyboardShortcuts$Message +#: UiStrings.resx$KeyboardShortcutsFormTitle$Message +msgid "Keyboard Shortcuts" +msgstr "" + +#: UiStrings.resx$KeywordsLabel$Message msgid "Keywords:" msgstr "關鍵字:" @@ -773,6 +845,10 @@ msgstr "" msgid "Language" msgstr "語言" +#: MiscResources.resx$LeaveAReview$Message +msgid "Leave a Review" +msgstr "" + #: SettingsResources.resx$HorizontalAlign_Left$Message msgid "Left" msgstr "靠左" @@ -781,33 +857,48 @@ msgstr "靠左" msgid "Legacy (native UI only)" msgstr "" -#: FBatchScan.resx$rdLoadIntoNaps2.Text$Message +#: SettingsResources.resx$Theme_Light$Message +msgid "Light" +msgstr "" + +#: MiscResources.resx$ReviewPrompt$Message +msgid "Like NAPS2?" +msgstr "" + +#: UiStrings.resx$LoadIn$Message msgid "Load images into NAPS2" msgstr "" -#: FOcrSetup.resx$checkBoxEnableOcr.Text$Message #: UiStrings.resx$MakePdfsSearchable$Message msgid "Make PDFs searchable using OCR" msgstr "使用 OCR 讓 PDF 可搜尋" -#: FAdvancedScanSettings.resx$cbHighQuality.Text$Message +#: UiStrings.resx$ManualIp$Message +#: UiStrings.resx$ManualIpFormTitle$Message +msgid "Manual IP" +msgstr "" + +#: UiStrings.resx$MaximumQuality$Message msgid "Maximum quality (large files)" msgstr "最高品質 (大檔)" -#: FPdfSettings.resx$groupMetadata.Text$Message +#: SettingsResources.resx$TwainImpl_MemXfer$Message +msgid "Memory Transfer" +msgstr "" + +#: UiStrings.resx$Metadata$Message msgid "Metadata" msgstr "中繼資料" -#: FPlaceholders.resx$label8.Text$Message +#: UiStrings.resx$Minute2Digit$Message msgid "Minute (00-59)" msgstr "分鐘 (00-59)" -#: FPlaceholders.resx$label5.Text$Message +#: UiStrings.resx$Month2Digit$Message msgid "Month (01-12)" msgstr "月份 (01-12)" -#: FAutoSaveSettings.resx$linkPatchCodeInfo.Text$Message -#: FBatchScan.resx$linkPatchCodeInfo.Text$Message +#: UiStrings.resx$MoreInfo$Message msgid "More info" msgstr "" @@ -819,11 +910,19 @@ msgstr "下移" msgid "Move Up" msgstr "上移" -#: FBatchScan.resx$rdMultipleScansDelay.Text$Message +#: UiStrings.resx$OcrMultiLangFormTitle$Message +msgid "Multiple Languages" +msgstr "" + +#: UiStrings.resx$MultipleLanguages$Message +msgid "Multiple Languages..." +msgstr "" + +#: UiStrings.resx$MultipleScansDelay$Message msgid "Multiple scans (fixed delay between scans)" msgstr "" -#: FBatchScan.resx$rdMultipleScansPrompt.Text$Message +#: UiStrings.resx$MultipleScansPrompt$Message msgid "Multiple scans (prompt between scans)" msgstr "" @@ -841,7 +940,7 @@ msgstr "" msgid "NAPS2 is completely free. Consider making a donation." msgstr "" -#: FPageSize.resx$label1.Text$Message +#: UiStrings.resx$NameOptional$Message msgid "Name (optional)" msgstr "" @@ -849,6 +948,10 @@ msgstr "" msgid "Name missing." msgstr "缺少名稱." +#: SettingsResources.resx$TwainImpl_NativeXfer$Message +msgid "Native Transfer" +msgstr "" + #: UiStrings.resx$New$Message msgid "New" msgstr "新增" @@ -861,7 +964,7 @@ msgstr "新增設定檔" msgid "Next" msgstr "下一頁" -#: FBatchPrompt.resx$$this.Text$Message +#: UiStrings.resx$BatchPromptFormTitle$Message msgid "Next Scan" msgstr "" @@ -870,12 +973,15 @@ msgstr "" msgid "No device selected." msgstr "尚未選擇裝置." +#: UiStrings.resx$NoDevicesFound$Message +msgid "No devices found." +msgstr "" + #: MiscResources.resx$NoPagesInFeeder$Message #: SdkResources.resx$NoPagesInFeeder$Message msgid "No pages are in the feeder." msgstr "沒有頁面在送紙器中." -#: FEmailSettings.resx$lblProvider.Text$Message #: SettingsResources.resx$EmailProvider_NotSelected$Message msgid "No provider selected." msgstr "" @@ -897,11 +1003,11 @@ msgstr "" msgid "Not Another PDF Scanner" msgstr "" -#: FRecover.resx$btnCancel.Text$Message +#: UiStrings.resx$NotNow$Message msgid "Not Now" msgstr "現在不要" -#: FBatchScan.resx$lblNumberOfScans.Text$Message +#: UiStrings.resx$NumberOfScansLabel$Message msgid "Number of scans:" msgstr "" @@ -909,7 +1015,6 @@ msgstr "" msgid "OCR" msgstr "光學辨識(OCR)" -#: FOcrLanguageDownload.resx$$this.Text$Message #: UiStrings.resx$OcrDownloadFormTitle$Message msgid "OCR Download" msgstr "OCR 下載" @@ -918,37 +1023,23 @@ msgstr "OCR 下載" msgid "OCR Progress" msgstr "" -#: FOcrSetup.resx$$this.Text$Message #: UiStrings.resx$OcrSetupFormTitle$Message msgid "OCR Setup" msgstr "OCR 安裝" -#: FOcrSetup.resx$label1.Text$Message #: UiStrings.resx$OcrLanguageLabel$Message msgid "OCR language:" msgstr "OCR 語言:" -#: FOcrSetup.resx$labelOcrMode.Text$Message #: UiStrings.resx$OcrModeLabel$Message msgid "OCR mode:" msgstr "" -#: FAdvancedScanSettings.resx$btnOK.Text$Message -#: FAutoSaveSettings.resx$btnOK.Text$Message -#: FEditProfile.resx$btnOK.Text$Message -#: FEmailSettings.resx$btnOK.Text$Message -#: FError.resx$btnOK.Text$Message -#: FImageSettings.resx$btnOK.Text$Message -#: FOcrSetup.resx$btnOK.Text$Message -#: FPageSize.resx$btnOK.Text$Message -#: FPdfPassword.resx$btnOK.Text$Message -#: FPdfSettings.resx$btnOK.Text$Message -#: FPlaceholders.resx$btnOK.Text$Message #: UiStrings.resx$OK$Message msgid "OK" msgstr "確定" -#: FAdvancedScanSettings.resx$cbWiaOffsetWidth.Text$Message +#: UiStrings.resx$OffsetWidth$Message msgid "Offset width based on alignment (WIA)" msgstr "" @@ -956,13 +1047,11 @@ msgstr "" msgid "Old DSM" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerPage.Text$Message -#: FBatchScan.resx$rdFilePerPage.Text$Message +#: UiStrings.resx$OneFilePerPage$Message msgid "One file per page" msgstr "" -#: FAutoSaveSettings.resx$rdFilePerScan.Text$Message -#: FBatchScan.resx$rdFilePerScan.Text$Message +#: UiStrings.resx$OneFilePerScan$Message msgid "One file per scan" msgstr "" @@ -970,7 +1059,11 @@ msgstr "" msgid "One or more files could not be downloaded." msgstr "無法下載一個或多個檔案." -#: NotifyWidget.resx$openFolderToolStripMenuItem.Text$Message +#: UiStrings.resx$SingleInstanceDesc$Message +msgid "Only allow a single NAPS2 instance" +msgstr "" + +#: UiStrings.resx$OpenFolder$Message msgid "Open Folder" msgstr "" @@ -978,11 +1071,15 @@ msgstr "" msgid "Operation in Progress" msgstr "" +#: SettingsResources.resx$EmailProviderType_OutlookNew$Message +msgid "Outlook (new)" +msgstr "" + #: SettingsResources.resx$EmailProviderType_OutlookWeb$Message msgid "Outlook Web Access" msgstr "" -#: FBatchScan.resx$groupboxOutput.Text$Message +#: UiStrings.resx$Output$Message msgid "Output" msgstr "" @@ -990,7 +1087,7 @@ msgstr "" msgid "Overwrite File" msgstr "覆寫檔案" -#: FPdfSettings.resx$lblOwnerPassword.Text$Message +#: UiStrings.resx$OwnerPasswordLabel$Message msgid "Owner Password:" msgstr "擁有者密碼:" @@ -998,8 +1095,8 @@ msgstr "擁有者密碼:" msgid "PDF Document (*.pdf)" msgstr "" -#: FPdfSettings.resx$$this.Text$Message #: UiStrings.resx$PdfSettings$Message +#: UiStrings.resx$PdfSettingsFormTitle$Message msgid "PDF Settings" msgstr "PDF 設定" @@ -1027,17 +1124,15 @@ msgstr "" msgid "PNG File (*.png)" msgstr "PNG 檔 (*.png)" -#: FEditProfile.resx$label4.Text$Message #: UiStrings.resx$PageSizeLabel$Message msgid "Page size:" msgstr "頁面大小:" -#: FEditProfile.resx$label2.Text$Message #: UiStrings.resx$PaperSourceLabel$Message msgid "Paper source:" msgstr "紙張來源:" -#: FPdfPassword.resx$$this.Text$Message +#: UiStrings.resx$PdfPasswordFormTitle$Message msgid "Password" msgstr "密碼" @@ -1045,21 +1140,24 @@ msgstr "密碼" msgid "Paste" msgstr "" -#: FAutoSaveSettings.resx$linkPlaceholders.Text$Message -#: FBatchScan.resx$linkPlaceholders.Text$Message -#: FEmailSettings.resx$linkPlaceholders.Text$Message -#: FImageSettings.resx$linkPlaceholders.Text$Message -#: FPdfSettings.resx$linkPlaceholders.Text$Message -#: FPlaceholders.resx$$this.Text$Message -#: FPlaceholders.resx$gboxPlaceholders.Text$Message +#: UiStrings.resx$Placeholders$Message +#: UiStrings.resx$PlaceholdersFormTitle$Message msgid "Placeholders" msgstr "版面配置區" -#: FAdvancedScanSettings.resx$groupBox4.Text$Message +#: UiStrings.resx$Port$Message +msgid "Port" +msgstr "" + +#: UiStrings.resx$PostProcessing$Message msgid "Post-processing" msgstr "" -#: FBatchScan.resx$lblStatus.Text$Message +#: UiStrings.resx$PreemptivelyOcrAfterScanning$Message +msgid "Pre-emptively run OCR after scanning" +msgstr "" + +#: UiStrings.resx$PressStartWhenReady$Message msgid "Press Start when ready." msgstr "" @@ -1067,7 +1165,7 @@ msgstr "" msgid "Preview" msgstr "預覽" -#: FPlaceholders.resx$label2.Text$Message +#: UiStrings.resx$PreviewLabel$Message msgid "Preview:" msgstr "預覽:" @@ -1080,12 +1178,11 @@ msgstr "上一頁" msgid "Print" msgstr "列印" -#: FEditProfile.resx$$this.Text$Message #: UiStrings.resx$EditProfileFormTitle$Message msgid "Profile Settings" msgstr "設定檔設定" -#: FBatchScan.resx$lblProfile.Text$Message +#: UiStrings.resx$ProfileLabel$Message msgid "Profile:" msgstr "" @@ -1094,23 +1191,27 @@ msgstr "" msgid "Profiles" msgstr "設定檔" -#: FAutoSaveSettings.resx$cbPromptForFilePath.Text$Message +#: SettingsResources.resx$SaveButtonDefaultAction_PromptIfSelected$Message +msgid "Prompt If Selected" +msgstr "" + +#: UiStrings.resx$PromptForFilePath$Message msgid "Prompt for file path" msgstr "" -#: FEmailSettings.resx$groupBox1.Text$Message +#: UiStrings.resx$Provider$Message msgid "Provider" msgstr "" -#: FBatchPrompt.resx$lblStatus.Text$Message +#: UiStrings.resx$ReadyForScan$Message msgid "Ready for scan {0}." msgstr "" -#: FRecover.resx$btnRecover.Text$Message +#: UiStrings.resx$Recover$Message msgid "Recover" msgstr "修復" -#: FRecover.resx$$this.Text$Message +#: UiStrings.resx$RecoverFormTitle$Message msgid "Recover Scanned Images" msgstr "復原掃瞄過的圖像" @@ -1122,9 +1223,15 @@ msgstr "" msgid "Recovery Progress" msgstr "" -#: FEmailSettings.resx$cbRememberSettings.Text$Message -#: FImageSettings.resx$cbRememberSettings.Text$Message -#: FPdfSettings.resx$cbRememberSettings.Text$Message +#: UiStrings.resx$Redo$Message +msgid "Redo" +msgstr "" + +#: UiStrings.resx$RedoFormat$Message +msgid "Redo {0}" +msgstr "" + +#: UiStrings.resx$RememberTheseSettings$Message msgid "Remember these settings" msgstr "記住這些設定" @@ -1140,15 +1247,11 @@ msgstr "重設" msgid "Reset Image" msgstr "重設圖像" -#: FEditProfile.resx$label5.Text$Message #: UiStrings.resx$ResolutionLabel$Message msgid "Resolution:" msgstr "解析度:" -#: FAdvancedScanSettings.resx$btnRestoreDefaults.Text$Message -#: FEmailSettings.resx$btnRestoreDefaults.Text$Message -#: FImageSettings.resx$btnRestoreDefaults.Text$Message -#: FPdfSettings.resx$btnRestoreDefaults.Text$Message +#: UiStrings.resx$RestoreDefaults$Message msgid "Restore Defaults" msgstr "還原預設值" @@ -1156,6 +1259,14 @@ msgstr "還原預設值" msgid "Reverse" msgstr "反轉" +#: UiStrings.resx$ReverseAll$Message +msgid "Reverse All" +msgstr "" + +#: UiStrings.resx$ReverseSelected$Message +msgid "Reverse Selected" +msgstr "" + #: UiStrings.resx$Revert$Message msgid "Revert" msgstr "還原" @@ -1176,7 +1287,6 @@ msgstr "向左旋轉" msgid "Rotate Right" msgstr "向右旋轉" -#: FProgress.resx$btnRunInBG.Text$Message #: UiStrings.resx$RunInBackground$Message msgid "Run in Background" msgstr "" @@ -1185,7 +1295,6 @@ msgstr "" msgid "Running OCR..." msgstr "" -#: FEditProfile.resx$rdSANE.Text$Message #: UiStrings.resx$SaneDriver$Message msgid "SANE Driver" msgstr "" @@ -1194,6 +1303,11 @@ msgstr "" msgid "Save" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveAll$Message +#: UiStrings.resx$SaveAll$Message +msgid "Save All" +msgstr "" + #: UiStrings.resx$SaveAllAsImages$Message msgid "Save All as Images" msgstr "" @@ -1220,6 +1334,11 @@ msgstr "儲存 PDF" msgid "Save PDF Progress" msgstr "" +#: SettingsResources.resx$SaveButtonDefaultAction_SaveSelected$Message +#: UiStrings.resx$SaveSelected$Message +msgid "Save Selected" +msgstr "" + #: UiStrings.resx$SaveSelectedAsImages$Message msgid "Save Selected as Images" msgstr "" @@ -1228,11 +1347,11 @@ msgstr "" msgid "Save Selected as PDF" msgstr "" -#: FBatchScan.resx$rdSaveToSingleFile.Text$Message +#: UiStrings.resx$SaveToSingleFile$Message msgid "Save to a single file" msgstr "" -#: FBatchScan.resx$rdSaveToMultipleFiles.Text$Message +#: UiStrings.resx$SaveToMultipleFiles$Message msgid "Save to multiple files" msgstr "" @@ -1244,29 +1363,45 @@ msgstr "" msgid "Saving {0}..." msgstr "" -#: TiffViewerCtl.resx$_tsStretch.ToolTipText$Message +#: UiStrings.resx$ScaleWithWindow$Message msgid "Scale With Window" msgstr "使用視窗比例" -#: FEditProfile.resx$label10.Text$Message #: UiStrings.resx$ScaleLabel$Message msgid "Scale:" msgstr "縮放比例:" -#: FBatchPrompt.resx$btnScan.Text$Message #: MiscResources.resx$Scan$Message #: UiStrings.resx$Scan$Message msgid "Scan" msgstr "掃瞄" -#: FBatchScan.resx$groupboxScanConfig.Text$Message +#: UiStrings.resx$ScanConfig$Message msgid "Scan Configuration" msgstr "" +#: SettingsResources.resx$ScanButtonDefaultAction_ScanWithDefaultProfile$Message +#: UiStrings.resx$ScanWithDefaultProfile$Message +msgid "Scan With Default Profile" +msgstr "" + +#: UiStrings.resx$ScanWithNewProfile$Message +msgid "Scan With New Profile" +msgstr "" + +#: UiStrings.resx$ScanWithProfile$Message +msgid "Scan With Profile {0}" +msgstr "" + #: MiscResources.resx$ScannedImage$Message msgid "Scanned Image" msgstr "已掃瞄的圖像" +#: UiStrings.resx$ScannerSharing$Message +#: UiStrings.resx$ScannerSharingFormTitle$Message +msgid "Scanner Sharing" +msgstr "" + #: MiscResources.resx$ScanPageProgress$Message msgid "Scanning page {0}" msgstr "掃瞄頁面 {0}" @@ -1280,11 +1415,14 @@ msgstr "" msgid "Scanning page {0}..." msgstr "掃瞄頁面 {0}..." -#: FPlaceholders.resx$label9.Text$Message +#: UiStrings.resx$SearchingForDevices$Message +msgid "Searching for devices..." +msgstr "" + +#: UiStrings.resx$Second2Digit$Message msgid "Second (00-59)" msgstr "秒鐘 (00-59)" -#: FSelectDevice.resx$btnSelect.Text$Message #: UiStrings.resx$Select$Message msgid "Select" msgstr "" @@ -1293,7 +1431,10 @@ msgstr "" msgid "Select All" msgstr "全選" -#: FSelectDevice.resx$$this.Text$Message +#: UiStrings.resx$SelectDevice$Message +msgid "Select Device" +msgstr "" + #: UiStrings.resx$SelectSource$Message msgid "Select Source" msgstr "" @@ -1302,7 +1443,6 @@ msgstr "" msgid "Select a profile before clicking Scan." msgstr "按下 [掃瞄] 前請先選擇設定檔." -#: FOcrLanguageDownload.resx$label3.Text$Message #: UiStrings.resx$OcrSelectLanguageLabel$Message msgid "Select one or more languages:" msgstr "請選擇一個或多個語言:" @@ -1311,8 +1451,7 @@ msgstr "請選擇一個或多個語言:" msgid "Selected ({0})" msgstr "已選 ({0})" -#: FAutoSaveSettings.resx$rdSeparateByPatchT.Text$Message -#: FBatchScan.resx$rdSeparateByPatchT.Text$Message +#: UiStrings.resx$SeparateByPatchT$Message msgid "Separate files by Patch-T" msgstr "" @@ -1320,37 +1459,84 @@ msgstr "" msgid "Set Default" msgstr "設為預設" +#: UiStrings.resx$Settings$Message +#: UiStrings.resx$SettingsFormTitle$Message +msgid "Settings" +msgstr "" + +#: UiStrings.resx$Share$Message +msgid "Share" +msgstr "" + +#: UiStrings.resx$ShareAsService$Message +msgid "Share even when NAPS2 is closed" +msgstr "" + +#: UiStrings.resx$SharedDeviceFormTitle$Message +msgid "Shared Scanner Settings" +msgstr "" + +#: UiStrings.resx$ScannerSharingIntro$Message +msgid "Shared scanners can be used from other computers on the local network by selecting \"ESCL Driver\" in the other computer's NAPS2 profile settings." +msgstr "" + #: UiStrings.resx$Sharpen$Message msgid "Sharpen" msgstr "" -#: FPdfSettings.resx$cbShowOwnerPassword.Text$Message -#: FPdfSettings.resx$cbShowUserPassword.Text$Message +#: UiStrings.resx$Shortcut$Message +msgid "Shortcut" +msgstr "" + +#: UiStrings.resx$Show$Message msgid "Show" msgstr "顯示" -#: FImageSettings.resx$cbSinglePageTiff.Text$Message +#: UiStrings.resx$ShowProfilesToolbar$Message +msgid "Show \"Profiles\" toolbar" +msgstr "" + +#: UiStrings.resx$ShowNativeTwainProgress$Message +msgid "Show native TWAIN progress" +msgstr "" + +#: UiStrings.resx$ShowPageNumbers$Message +msgid "Show page numbers" +msgstr "" + +#: UiStrings.resx$ToggleSidebar$Message +msgid "Sidebar" +msgstr "" + +#: UiStrings.resx$SinglePageFiles$Message msgid "Single page files" msgstr "" -#: FBatchScan.resx$rdSingleScan.Text$Message +#: UiStrings.resx$SingleScan$Message msgid "Single scan" msgstr "" -#: FImageSettings.resx$cbSkipSavePrompt.Text$Message -#: FPdfSettings.resx$cbSkipSavePrompt.Text$Message +#: UiStrings.resx$SkipSavePrompt$Message msgid "Skip save prompt" msgstr "" -#: FBatchScan.resx$btnStart.Text$Message +#: UiStrings.resx$Split$Message +msgid "Split" +msgstr "" + +#: UiStrings.resx$Start$Message msgid "Start" msgstr "" -#: FAdvancedScanSettings.resx$cbForcePageSize.Text$Message +#: UiStrings.resx$StopScannerSharing$Message +msgid "Stop Scanner Sharing" +msgstr "" + +#: UiStrings.resx$StretchToPageSize$Message msgid "Stretch to page size" msgstr "" -#: FPdfSettings.resx$label5.Text$Message +#: UiStrings.resx$SubjectLabel$Message msgid "Subject:" msgstr "主旨:" @@ -1358,12 +1544,11 @@ msgstr "主旨:" msgid "TIFF File (*.tiff, *.tif)" msgstr "TIFF 檔 (*.tiff, *.tif)" -#: FEditProfile.resx$rdTWAIN.Text$Message #: UiStrings.resx$TwainDriver$Message msgid "TWAIN Driver" msgstr "TWAIN 驅動程式" -#: FError.resx$linkDetails.Text$Message +#: UiStrings.resx$TechnicalDetails$Message msgid "Technical Details" msgstr "" @@ -1372,6 +1557,10 @@ msgstr "" msgid "The OCR engine is not available. Make sure to install the required package:" msgstr "" +#: SdkResources.resx$OcrTimeout$Message +msgid "The OCR operation timed out." +msgstr "" + #: MiscResources.resx$SaneNotAvailable$Message #: SdkResources.resx$SaneNotAvailable$Message msgid "The SANE driver is not available. Make sure to install the required packages:" @@ -1394,9 +1583,9 @@ msgstr "" msgid "The file {0} already exists. Do you want to overwrite it?" msgstr "檔案 {0} 已經存在,您要將它覆寫嗎?" -#: FPdfPassword.resx$lblPrompt.Text$Message -msgid "The following file is encrypted and requires a password to open: {0}" -msgstr "下列檔案有加密,需要密碼來開啟: {0}" +#: UiStrings.resx$EncryptedFilePrompt$Message +msgid "The following file is encrypted and requires a password to open:" +msgstr "" #: MiscResources.resx$DevicePaperJam$Message #: SdkResources.resx$DevicePaperJam$Message @@ -1443,19 +1632,39 @@ msgstr "" msgid "The selected scanner is offline." msgstr "所選的掃瞄器離線." -#: FImageSettings.resx$groupTiff.Text$Message +#: SdkResources.resx$WorkerCrash$Message +msgid "The worker process crashed." +msgstr "" + +#: SdkResources.resx$WorkerCrashWindows$Message +msgid "The worker process crashed. Check the Windows event viewer." +msgstr "" + +#: UiStrings.resx$ThemeLabel$Message +msgid "Theme:" +msgstr "" + +#: SettingsResources.resx$EmailProviderType_Thunderbird$Message +msgid "Thunderbird" +msgstr "" + +#: UiStrings.resx$TiffOptions$Message msgid "Tiff Options" msgstr "" -#: FBatchScan.resx$lblTimeBetweenScans.Text$Message +#: UiStrings.resx$TimeBetweenScansLabel$Message msgid "Time between scans (seconds):" msgstr "" -#: FPdfSettings.resx$label4.Text$Message +#: UiStrings.resx$TitleLabel$Message msgid "Title:" msgstr "標題:" -#: FAdvancedScanSettings.resx$label1.Text$Message +#: UiStrings.resx$Tools$Message +msgid "Tools" +msgstr "" + +#: UiStrings.resx$TwainImplLabel$Message msgid "Twain Implementation:" msgstr "" @@ -1467,6 +1676,22 @@ msgstr "" msgid "US Letter (8.5x11 in)" msgstr "" +#: UiStrings.resx$Unassign$Message +msgid "Unassign" +msgstr "" + +#: UiStrings.resx$Undo$Message +msgid "Undo" +msgstr "" + +#: UiStrings.resx$UndoFormat$Message +msgid "Undo {0}" +msgstr "" + +#: SdkResources.resx$UnknownScanner$Message +msgid "Unknown Scanner" +msgstr "" + #: MiscResources.resx$UnsavedChanges$Message msgid "Unsaved Changes" msgstr "未儲存的變更" @@ -1487,21 +1712,18 @@ msgstr "" msgid "Uploading email..." msgstr "" -#: FEditProfile.resx$rdbNative.Text$Message #: UiStrings.resx$UseNativeUi$Message msgid "Use native UI" msgstr "使用原生使用者介面" -#: FEditProfile.resx$rdbConfig.Text$Message #: UiStrings.resx$UsePredefinedSettings$Message msgid "Use predefined settings" msgstr "使用預定義設定" -#: FPdfSettings.resx$lblUserPassword.Text$Message +#: UiStrings.resx$UserPasswordLabel$Message msgid "User Password:" msgstr "使用者密碼:" -#: FOcrLanguageDownload.resx$label1.Text$Message #: UiStrings.resx$OcrDownloadSummaryText$Message msgid "Using OCR requires you to download each language you want to scan." msgstr "使用 OCR 需要您去下載每個您想要掃瞄的語言." @@ -1515,16 +1737,15 @@ msgstr "版本 {0}" msgid "View" msgstr "檢視" -#: FEditProfile.resx$rdWIA.Text$Message #: UiStrings.resx$WiaDriver$Message msgid "WIA Driver" msgstr "WIA 驅動程式" -#: FTwainGui.resx$label1.Text$Message +#: UiStrings.resx$WaitingForTwain$Message msgid "Waiting for TWAIN to complete..." msgstr "等待 TWAIN 完成..." -#: FAuthorize.resx$lblWaiting.Text$Message +#: UiStrings.resx$WaitingForAuthorization$Message msgid "Waiting for authorization..." msgstr "" @@ -1532,19 +1753,19 @@ msgstr "" msgid "Waiting for scan {0}..." msgstr "" -#: FAdvancedScanSettings.resx$label2.Text$Message +#: UiStrings.resx$WhiteThreshold$Message msgid "White Threshold" msgstr "" -#: FAdvancedScanSettings.resx$label4.Text$Message +#: UiStrings.resx$WiaVersionLabel$Message msgid "Wia Version:" msgstr "" -#: FPlaceholders.resx$label3.Text$Message +#: UiStrings.resx$Year4Digit$Message msgid "Year" msgstr "年份" -#: FPlaceholders.resx$label4.Text$Message +#: UiStrings.resx$Year2Digit$Message msgid "Year (00-99)" msgstr "年份 (00-99)" @@ -1561,21 +1782,18 @@ msgstr "您沒有在該位置儲存檔案的權限。." msgid "You have unsaved changes. Are you sure you want to exit and discard those changes?" msgstr "您有未儲存的變更。您確定要結束並放棄那些變更嗎?" -#: TiffViewerCtl.resx$_tsZoom.ToolTipText$Message #: UiStrings.resx$Zoom$Message msgid "Zoom" msgstr "縮放" -#: TiffViewerCtl.resx$_tsZoomActual.ToolTipText$Message +#: UiStrings.resx$ZoomActual$Message msgid "Zoom Actual" msgstr "縮放實際大小" -#: TiffViewerCtl.resx$_tsZoomPlus.ToolTipText$Message #: UiStrings.resx$ZoomIn$Message msgid "Zoom In" msgstr "" -#: TiffViewerCtl.resx$_tsZoomOut.ToolTipText$Message #: UiStrings.resx$ZoomOut$Message msgid "Zoom Out" msgstr "" @@ -1604,22 +1822,27 @@ msgstr "" msgid "{0} ({1}x{2} {3})" msgstr "" -#: FProgress.resx$labelNumber.Text$Message #: MiscResources.resx$ProgressFormat$Message msgid "{0} / {1}" msgstr "" -#: FDownloadProgress.resx$labelSub.Text$Message #: MiscResources.resx$SizeProgress$Message msgid "{0} / {1} MB" msgstr "" -#: FDownloadProgress.resx$labelTop.Text$Message #: MiscResources.resx$FilesProgressFormat$Message msgid "{0} / {1} files" msgstr "{0} / {1} 檔案" -#: FRecover.resx$lblPrompt.Text$Message +#: UiStrings.resx$DevicesFound$Message +msgid "{0} devices found." +msgstr "" + +#: SettingsResources.resx$DpiFormat$Message +msgid "{0} dpi" +msgstr "" + +#: UiStrings.resx$RecoverPrompt$Message msgid "{0} image(s) scanned on {1} at {2} may not have been saved, and are recoverable. Do you want to recover them?" msgstr "在 {1} 上已掃瞄 {0} 個圖像,在 {2} 可能沒有被儲存,且可復原。您要將它們復原嗎?" diff --git a/NAPS2.Sdk/Logging/ErrorOutput.cs b/NAPS2.Lib/Logging/ErrorOutput.cs similarity index 100% rename from NAPS2.Sdk/Logging/ErrorOutput.cs rename to NAPS2.Lib/Logging/ErrorOutput.cs diff --git a/NAPS2.Sdk/Logging/EventParams.cs b/NAPS2.Lib/Logging/EventParams.cs similarity index 100% rename from NAPS2.Sdk/Logging/EventParams.cs rename to NAPS2.Lib/Logging/EventParams.cs diff --git a/NAPS2.Sdk/Logging/EventType.cs b/NAPS2.Lib/Logging/EventType.cs similarity index 100% rename from NAPS2.Sdk/Logging/EventType.cs rename to NAPS2.Lib/Logging/EventType.cs diff --git a/NAPS2.Sdk/Logging/IEventLogger.cs b/NAPS2.Lib/Logging/IEventLogger.cs similarity index 100% rename from NAPS2.Sdk/Logging/IEventLogger.cs rename to NAPS2.Lib/Logging/IEventLogger.cs diff --git a/NAPS2.Lib/Logging/LazyLogger.cs b/NAPS2.Lib/Logging/LazyLogger.cs new file mode 100644 index 0000000000..020bbba770 --- /dev/null +++ b/NAPS2.Lib/Logging/LazyLogger.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Logging; + +namespace NAPS2.Logging; + +public class LazyLogger : ILogger +{ + private readonly Lazy _inner; + + public LazyLogger(Func inner) + { + _inner = new Lazy(inner); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + _inner.Value.Log(logLevel, eventId, state, exception, formatter); + } + + public bool IsEnabled(LogLevel logLevel) + { + return _inner.Value.IsEnabled(logLevel); + } + + public IDisposable? BeginScope(TState state) where TState : notnull + { + return _inner.Value.BeginScope(state); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Logging/Log.cs b/NAPS2.Lib/Logging/Log.cs similarity index 54% rename from NAPS2.Sdk/Logging/Log.cs rename to NAPS2.Lib/Logging/Log.cs index 636712ab7b..8917d2f40b 100644 --- a/NAPS2.Sdk/Logging/Log.cs +++ b/NAPS2.Lib/Logging/Log.cs @@ -1,11 +1,14 @@ -namespace NAPS2.Logging; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace NAPS2.Logging; /// /// Logging functionality. /// public static class Log { - private static ILogger _logger = new DebugLogger(); + private static ILogger _logger = NullLogger.Instance; private static IEventLogger _eventLogger = new NullEventLogger(); public static ILogger Logger @@ -20,19 +23,34 @@ public static IEventLogger EventLogger set => _eventLogger = value ?? throw new ArgumentNullException(nameof(value)); } - public static void Error(string message, params object[] args) + public static void Debug(string message) + { + _logger.LogDebug(message); + } + + public static void DebugException(string message, Exception exception) + { + _logger.LogDebug(exception, message); + } + + public static void Info(string message) + { + _logger.LogInformation(message); + } + + public static void Error(string message) { - _logger.Error(string.Format(message, args)); + _logger.LogError(message); } public static void ErrorException(string message, Exception exception) { - _logger.ErrorException(message, exception); + _logger.LogError(exception, message); } public static void FatalException(string message, Exception exception) { - _logger.FatalException(message, exception); + _logger.LogCritical(exception, message); } public static void Event(EventType eventType, EventParams eventParams) diff --git a/NAPS2.Sdk/Logging/NullEventLogger.cs b/NAPS2.Lib/Logging/NullEventLogger.cs similarity index 100% rename from NAPS2.Sdk/Logging/NullEventLogger.cs rename to NAPS2.Lib/Logging/NullEventLogger.cs diff --git a/NAPS2.Sdk/Logging/StubErrorOutput.cs b/NAPS2.Lib/Logging/StubErrorOutput.cs similarity index 100% rename from NAPS2.Sdk/Logging/StubErrorOutput.cs rename to NAPS2.Lib/Logging/StubErrorOutput.cs diff --git a/NAPS2.Lib/Modules/AutoFacHelper.cs b/NAPS2.Lib/Modules/AutoFacHelper.cs index 0be281c13f..5c79db6b14 100644 --- a/NAPS2.Lib/Modules/AutoFacHelper.cs +++ b/NAPS2.Lib/Modules/AutoFacHelper.cs @@ -6,13 +6,16 @@ namespace NAPS2.Modules; public class AutoFacHelper { - public static IContainer FromModules(params IModule[] modules) + public static IContainer FromModules(params IModule?[] modules) { var builder = new ContainerBuilder(); builder.RegisterSource(); foreach (var module in modules) { - builder.RegisterModule(module); + if (module != null) + { + builder.RegisterModule(module); + } } return builder.Build(); } diff --git a/NAPS2.Lib/Modules/CommonModule.cs b/NAPS2.Lib/Modules/CommonModule.cs index ede9927e65..91af8e51ac 100644 --- a/NAPS2.Lib/Modules/CommonModule.cs +++ b/NAPS2.Lib/Modules/CommonModule.cs @@ -1,39 +1,35 @@ using Autofac; +using Microsoft.Extensions.Logging; using NAPS2.EtoForms; using NAPS2.ImportExport; using NAPS2.ImportExport.Email; using NAPS2.ImportExport.Email.Mapi; -using NAPS2.ImportExport.Images; -using NAPS2.ImportExport.Pdf; using NAPS2.Ocr; +using NAPS2.Pdf; using NAPS2.Platform.Windows; using NAPS2.Recovery; +using NAPS2.Remoting; +using NAPS2.Remoting.Server; using NAPS2.Remoting.Worker; using NAPS2.Scan; using NAPS2.Scan.Internal; -using NAPS2.Unmanaged; -using ILogger = NAPS2.Logging.ILogger; namespace NAPS2.Modules; +/// +/// Core module used by all entry points. +/// public class CommonModule : Module { protected override void Load(ContainerBuilder builder) { - // Import - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().AsSelf(); - // Export - builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().AsSelf().SingleInstance(); // Scan - builder.RegisterType().As(); + builder.RegisterType().As().SingleInstance(); builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); @@ -41,29 +37,61 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As(); builder.RegisterType().AsSelf(); builder.RegisterType().AsSelf(); - builder.RegisterType().AsSelf(); // Config - builder.Register(_ => new Naps2Config(Path.Combine(Paths.Executable, "appsettings.xml"), - Path.Combine(Paths.AppData, "config.xml"))).SingleInstance(); + // TODO: Make this a usable path on Mac/Linux + var config = new Naps2Config(Path.Combine(Paths.Executable, "appsettings.xml"), + Path.Combine(Paths.AppData, "config.xml")); + builder.RegisterInstance(config); + builder.RegisterBuildCallback(ctx => + { + if (EtoPlatform.HasCurrent) + { + EtoPlatform.Current.ColorScheme.Config = ctx.Resolve(); + } + }); - // Host - builder.RegisterType().As().SingleInstance(); + // Remoting + builder.Register(_ => WorkerFactory.CreateDefault()).SingleInstance(); + builder.Register(ctx => + new SharedDeviceManager( + ctx.Resolve(), + ctx.Resolve(), + Path.Combine(Paths.AppData, "sharing.xml"))).SingleInstance(); + builder.RegisterInstance(ProcessCoordinator.CreateDefault()); + + // Logging + var lazyLogger = new LazyLogger(() => + NLogConfig.CreateLogger(() => config.Get(c => c.EnableDebugLogging))); + NLogConfig.EnvDebugLogging = config.Get(c => c.EnableDebugLogging); + builder.RegisterInstance(lazyLogger); // Misc builder.RegisterType().As(); builder.RegisterType().As(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterInstance(new UiImageList()); + builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance(); - builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf().SingleInstance(); // TODO: Use PdfiumWorkerCoordinator? builder.RegisterType().As(); - builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterType().As(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf().SingleInstance(); + + // ScanningContext has several properties that need to be populated. We do some here, and also some in + // GuiModule/ConsoleModule/WorkerModule as they each have their own needs. + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterBuildCallback(ctx => + { + var scanningContext = ctx.Resolve(); + scanningContext.WorkerFactory = ctx.Resolve(); + scanningContext.Logger = ctx.Resolve(); + scanningContext.TempFolderPath = Paths.Temp; + scanningContext.RecoveryPath = Paths.Recovery; + }); //container.Resolve().PdfRenderer = container.Resolve(); @@ -72,6 +100,7 @@ protected override void Load(ContainerBuilder builder) var config = ctx.Resolve(); return new ProfileManager( Path.Combine(Paths.AppData, "profiles.xml"), + // TODO: Make this a usable path on Mac/Linux Path.Combine(AssemblyHelper.EntryFolder, "profiles.xml"), config.Get(c => c.LockSystemProfiles), config.Get(c => c.LockUnspecifiedDevices), @@ -89,13 +118,11 @@ protected override void Load(ContainerBuilder builder) }).SingleInstance(); builder.Register(ctx => { - var tesseractPath = PlatformCompat.System.UseSystemTesseract - ? "tesseract" - : NativeLibrary.FindExePath(PlatformCompat.System.TesseractExecutableName); - return new TesseractOcrEngine( - tesseractPath, - ctx.Resolve().TessdataBasePath, - Paths.Temp); + var engine = TesseractOcrEngine.BundledWithModes(ctx.Resolve().TessdataBasePath); + var errorOutput = ctx.Resolve(); + engine.OcrError += (_, args) => errorOutput.DisplayError(SdkResources.OcrError, args.Exception); + engine.OcrTimeout += (_, _) => errorOutput.DisplayError(SdkResources.OcrTimeout); + return engine; }).SingleInstance(); } } \ No newline at end of file diff --git a/NAPS2.Lib/Modules/ConsoleModule.cs b/NAPS2.Lib/Modules/ConsoleModule.cs index a5e1a1ec9c..4f96224904 100644 --- a/NAPS2.Lib/Modules/ConsoleModule.cs +++ b/NAPS2.Lib/Modules/ConsoleModule.cs @@ -1,11 +1,16 @@ using Autofac; using NAPS2.Automation; using NAPS2.EtoForms; -using NAPS2.ImportExport.Pdf; +using NAPS2.EtoForms.Notifications; +using NAPS2.Ocr; +using NAPS2.Pdf; using NAPS2.Scan; namespace NAPS2.Modules; +/// +/// Console-specific module used by ConsoleEntryPoint. +/// public class ConsoleModule : Module { private readonly AutomatedScanningOptions _options; @@ -22,11 +27,26 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As(); - builder.RegisterType().As(); + if (_options.Progress) + { + builder.RegisterType().As(); + } + else + { + builder.RegisterType().As(); + } + builder.RegisterType().As(); // TODO: We might want an eto-based dialog helper, or at least handle dialogs in a more user-friendly way than just silently doing nothing builder.RegisterType().As(); builder.RegisterType().AsSelf().WithParameter("writer", Console.Out); builder.RegisterType().As(); builder.RegisterType().As(); + + builder.RegisterBuildCallback(ctx => + { + var scanningContext = ctx.Resolve(); + scanningContext.FileStorageManager = ctx.Resolve(); + scanningContext.OcrEngine = ctx.Resolve(); + }); } } \ No newline at end of file diff --git a/NAPS2.Lib/Modules/ContextModule.cs b/NAPS2.Lib/Modules/ContextModule.cs deleted file mode 100644 index 6a1634d004..0000000000 --- a/NAPS2.Lib/Modules/ContextModule.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Autofac; -using NAPS2.Scan; -using NLog; - -namespace NAPS2.Modules; - -public class ContextModule : Module -{ - protected override void Load(ContainerBuilder builder) - { - builder.RegisterBuildCallback(ctx => - { - ctx.Resolve().TempFolderPath = Paths.Temp; - ctx.Resolve().RecoveryPath = Paths.Recovery; - }); - - Log.Logger = new NLogLogger(); -#if DEBUG - Trace.Listeners.Add(new NLogTraceListener()); -#endif - } -} \ No newline at end of file diff --git a/NAPS2.Lib/Modules/GuiModule.cs b/NAPS2.Lib/Modules/GuiModule.cs new file mode 100644 index 0000000000..0a0f9549fe --- /dev/null +++ b/NAPS2.Lib/Modules/GuiModule.cs @@ -0,0 +1,48 @@ +using Autofac; +using NAPS2.EtoForms; +using NAPS2.EtoForms.Desktop; +using NAPS2.EtoForms.Notifications; +using NAPS2.ImportExport; +using NAPS2.Ocr; +using NAPS2.Pdf; +using NAPS2.Scan; +using NAPS2.Scan.Batch; +using NAPS2.Update; + +namespace NAPS2.Modules; + +/// +/// GUI-specific module used by GuiEntryPoint. +/// +public class GuiModule : Module +{ + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterInstance(EtoPlatform.Current.DarkModeProvider); + builder.RegisterInstance(EtoPlatform.Current.ColorScheme); + + builder.RegisterBuildCallback(ctx => + { + var scanningContext = ctx.Resolve(); + scanningContext.FileStorageManager = ctx.Resolve(); + scanningContext.OcrEngine = ctx.Resolve(); + }); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Modules/RecoveryModule.cs b/NAPS2.Lib/Modules/RecoveryModule.cs index 123a01a874..0ef0c2f4ec 100644 --- a/NAPS2.Lib/Modules/RecoveryModule.cs +++ b/NAPS2.Lib/Modules/RecoveryModule.cs @@ -3,6 +3,9 @@ namespace NAPS2.Modules; +/// +/// Recovery folder setup used by all entry points except the worker (which initializes based on the parent process). +/// public class RecoveryModule : Module { protected override void Load(ContainerBuilder builder) diff --git a/NAPS2.Lib/Modules/StaticInitModule.cs b/NAPS2.Lib/Modules/StaticInitModule.cs new file mode 100644 index 0000000000..f5c0e71014 --- /dev/null +++ b/NAPS2.Lib/Modules/StaticInitModule.cs @@ -0,0 +1,24 @@ +using Autofac; +using NLog; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace NAPS2.Modules; + +/// +/// Static class initialization module used by all entry points except tests. +/// +public class StaticInitModule : Module +{ + protected override void Load(ContainerBuilder builder) + { + builder.RegisterBuildCallback(ctx => { Log.Logger = ctx.Resolve(); }); + Trace.Listeners.Add(new NLogTraceListener()); + TaskScheduler.UnobservedTaskException += UnhandledTaskException; + } + + private static void UnhandledTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + { + Log.FatalException("An error occurred that caused the task to terminate.", e.Exception); + e.SetObserved(); + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Modules/WorkerModule.cs b/NAPS2.Lib/Modules/WorkerModule.cs index 3efd708499..094c44df6a 100644 --- a/NAPS2.Lib/Modules/WorkerModule.cs +++ b/NAPS2.Lib/Modules/WorkerModule.cs @@ -4,15 +4,27 @@ namespace NAPS2.Modules; +/// +/// Worker-specific module used by WorkerEntryPoint. +/// public class WorkerModule : Module { protected override void Load(ContainerBuilder builder) { - builder.Register(ctx => new ScanningContext(ctx.Resolve())); + // Bindings for ITwainController as used by WorkerServiceImpl #if MAC - builder.RegisterType().As(); + builder.RegisterType().As(); +#elif NET6_0_OR_GREATER + if (OperatingSystem.IsWindows()) + { + builder.RegisterType().As(); + } + else + { + builder.RegisterType().As(); + } #else - builder.RegisterType().As(); + builder.RegisterType().As(); #endif } } \ No newline at end of file diff --git a/NAPS2.Lib/NAPS2.Lib.csproj b/NAPS2.Lib/NAPS2.Lib.csproj index 3c93268327..88eb1d3435 100644 --- a/NAPS2.Lib/NAPS2.Lib.csproj +++ b/NAPS2.Lib/NAPS2.Lib.csproj @@ -1,34 +1,49 @@  - net6;net462 - net6;net6-macos10.15;net462 + net9 + + $(TargetFrameworks);net9-macos + enable true NAPS2 NAPS2 - Not Another PDF Scanner NAPS2 - Not Another PDF Scanner - Copyright 2009, 2012-2020 NAPS2 Contributors; Icons from http://www.fatcow.com/free-icons - Debug;Release;DebugLang + Debug;Release;DebugLang;Release-Msi;Release-Zip - + MAC + + $(DefineConstants);MSI + + + $(DefineConstants);ZIP + + + $(DefineConstants);DEBUG + - + + + - - - - - + + + + + + + + @@ -44,6 +59,8 @@ <_Parameter1>NAPS2.Lib.Gtk + + @@ -76,10 +93,6 @@ ResXFileCodeGenerator - - ResXFileCodeGenerator - - diff --git a/NAPS2.Lib/NLogConfig.cs b/NAPS2.Lib/NLogConfig.cs new file mode 100644 index 0000000000..220464b18e --- /dev/null +++ b/NAPS2.Lib/NLogConfig.cs @@ -0,0 +1,66 @@ +using System.Text; +using NLog; +using NLog.Config; +using NLog.Extensions.Logging; +using NLog.Filters; +using NLog.LayoutRenderers; +using NLog.Targets; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace NAPS2; + +public static class NLogConfig +{ + public static ILogger CreateLogger(Func enableDebugLogging) + { + LogManager.Setup().SetupExtensions(ext => + { + ext.RegisterLayoutRenderer("exception"); + }); + var config = new LoggingConfiguration(); + var target = new FileTarget + { + FileName = Path.Combine(Paths.AppData, "errorlog.txt"), + Layout = "${longdate} ${processid} ${message} ${exception:format=tostring}", + ArchiveAboveSize = 100000, + MaxArchiveFiles = 1, + ConcurrentWrites = true + }; + var debugTarget = new FileTarget + { + FileName = Path.Combine(Paths.AppData, "debuglog.txt"), + Layout = "${longdate} ${processid} ${message} ${exception:format=tostring}", + ArchiveAboveSize = 100000, + MaxArchiveFiles = 1, + ConcurrentWrites = true + }; + config.AddTarget("errorlogfile", target); + config.AddTarget("debuglogfile", debugTarget); + config.LoggingRules.Add(new LoggingRule("*", LogLevel.Info, target)); + var debugRule = new LoggingRule("*", LogLevel.Trace, debugTarget); + debugRule.Filters.Add(new WhenMethodFilter(_ => enableDebugLogging() ? FilterResult.Log : FilterResult.Ignore)); + config.LoggingRules.Add(debugRule); + LogManager.Configuration = config; + return new NLogLoggerFactory().CreateLogger("NAPS2"); + } + + /// + /// The debug logging flag as stored in an environment variable. This is used by worker processes to propagate from + /// the parent process without needing to access the config directly. + /// + public static bool EnvDebugLogging + { + get => Environment.GetEnvironmentVariable("NAPS2_DEBUG_LOGGING") == "1"; + set => Environment.SetEnvironmentVariable("NAPS2_DEBUG_LOGGING", value ? "1" : "0"); + } + + private class CustomExceptionLayoutRenderer : ExceptionLayoutRenderer + { + protected override void AppendToString(StringBuilder sb, Exception ex) + { + // Note we don't want to use the AppendDemystified() helper + // https://github.com/benaadams/Ben.Demystifier/issues/85 + sb.Append(ex.Demystify()); + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/NLogLogger.cs b/NAPS2.Lib/NLogLogger.cs deleted file mode 100644 index bf921ee88c..0000000000 --- a/NAPS2.Lib/NLogLogger.cs +++ /dev/null @@ -1,43 +0,0 @@ -using NLog; -using NLog.Config; -using NLog.Targets; -using ILogger = NAPS2.Logging.ILogger; - -namespace NAPS2; - -public class NLogLogger : ILogger -{ - private readonly Logger _logger; - - public NLogLogger() - { - var config = new LoggingConfiguration(); - var target = new FileTarget - { - FileName = Path.Combine(Paths.AppData, "errorlog.txt"), - Layout = "${longdate} ${processid} ${message} ${exception:format=tostring}", - ArchiveAboveSize = 100000, - MaxArchiveFiles = 5 - }; - config.AddTarget("errorlogfile", target); - var rule = new LoggingRule("*", LogLevel.Debug, target); - config.LoggingRules.Add(rule); - LogManager.Configuration = config; - _logger = LogManager.GetLogger("NAPS2"); - } - - public void Error(string message) - { - _logger.Error(message); - } - - public void ErrorException(string message, Exception exception) - { - _logger.Error(exception, message); - } - - public void FatalException(string message, Exception exception) - { - _logger.Fatal(exception, message); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/Language.cs b/NAPS2.Lib/Ocr/Language.cs similarity index 100% rename from NAPS2.Sdk/Ocr/Language.cs rename to NAPS2.Lib/Ocr/Language.cs diff --git a/NAPS2.Lib/Ocr/OcrOperationManager.cs b/NAPS2.Lib/Ocr/OcrOperationManager.cs index 7c536d9538..bb5a2ebab6 100644 --- a/NAPS2.Lib/Ocr/OcrOperationManager.cs +++ b/NAPS2.Lib/Ocr/OcrOperationManager.cs @@ -12,7 +12,7 @@ namespace NAPS2.Ocr; /// For example, we start with 0/1, then 0/2 when a new page is queued, then 2/2 once completed, then 0/1 if another /// page is then queued. /// -public class OcrOperationManager +internal class OcrOperationManager { // TODO: Consider implementing/using a MultiDict private readonly Dictionary> _ongoingTasks = new(); @@ -48,7 +48,7 @@ private void OcrStarted(object? sender, OcrEventArgs e) } op = _currentOp; op.Status.MaxProgress += 1; - _ongoingTasks.GetOrSet((OcrController) sender!, () => new HashSet()).Add(e.ResultTask); + _ongoingTasks.GetOrSet((OcrController) sender!, () => []).Add(e.ResultTask); } op.InvokeStatusChanged(); if (newOp) diff --git a/NAPS2.Lib/Ocr/TesseractLanguageData.cs b/NAPS2.Lib/Ocr/TesseractLanguageData.cs new file mode 100644 index 0000000000..c7b435a451 --- /dev/null +++ b/NAPS2.Lib/Ocr/TesseractLanguageData.cs @@ -0,0 +1,173 @@ +namespace NAPS2.Ocr; + +public class TesseractLanguageData +{ + public class TesseractLanguage + { + public TesseractLanguage(string filename, string code, string langName, double size, string sha256, bool rtl = false) + { + Filename = filename; + Code = code; + LangName = langName; + Size = size; + Sha256 = sha256; + RTL = rtl; + } + + public string Filename { get; } + + public string Code { get; } + + public string LangName { get; } + + public double Size { get; } + + public string Sha256 { get; } + + public bool RTL { get; } + } + + protected TesseractLanguageData(TesseractLanguage[] data) + { + Data = data; + LanguageMap = data.ToDictionary(x => $"ocr-{x.Code}", x => new Language(x.Code, x.LangName, x.RTL)); + } + + public Dictionary LanguageMap { get; set; } + + public TesseractLanguage[] Data { get; set; } + + #region Tesseract Language Data (auto-generated) + + public static readonly TesseractLanguageData Latest = new(new[] + { + new TesseractLanguage("afr.traineddata.zip", "afr", "Afrikaans", 5.44, "bb6a056b43944df862815fb173c9c582ec4410c2794cfd6b632cb31611d0476a"), + new TesseractLanguage("amh.traineddata.zip", "amh", "Amharic", 5.55, "4d79215805af87c39720d036e3d005e781d49ef7bc309d40824ab689242cc168"), + new TesseractLanguage("ara.traineddata.zip", "ara", "Arabic", 2.29, "24d2a74937150e6d41a69f2758aea3817d7f80ed53f780adaa492a83e249b62f", true), + new TesseractLanguage("asm.traineddata.zip", "asm", "Assamese", 2.82, "ae1f04d3a4f5324c223a9330f5dd0836bd926fde70cae5d2362e10c77af99750"), + new TesseractLanguage("aze.traineddata.zip", "aze", "Azerbaijani", 5.69, "c9b7e5d86c7aa95cdfc5ba0f475c21a43793811761b890c54610c76a9d8c6027", true), + new TesseractLanguage("aze_cyrl.traineddata.zip", "aze_cyrl", "Azerbaijani (Cyrillic)", 2.88, "b4fc131f77af3b04729e614021b02b993d88b6e8dc6b856c2e890e8da5de44b2"), + new TesseractLanguage("bel.traineddata.zip", "bel", "Belarusian", 5.86, "67c36d8b9886a2e6412f31950b2a5daeae8196f5bb73a48782eb273b354dc1fc"), + new TesseractLanguage("ben.traineddata.zip", "ben", "Bengali", 1.84, "814d18592e4bae3ce62ffb680cb242f5ddf94b57f825ce2e60acad20d4eb0d39"), + new TesseractLanguage("bod.traineddata.zip", "bod", "Tibetan", 2.44, "4b0d1db9cfc932d4cf9d8fb0d140362c3f70b21e4c12cca595c9674e90ce9495"), + new TesseractLanguage("bos.traineddata.zip", "bos", "Bosnian", 4.08, "b996240751fcc05415b56db5809c05383cdaf47d2eddd51e2a5e20445f8111d3"), + new TesseractLanguage("bre.traineddata.zip", "bre", "Breton", 6.46, "c9485e27c69a834f3e61f502f92453387e8975aceaa601538c7d94c7ccb02d2b"), + new TesseractLanguage("bul.traineddata.zip", "bul", "Bulgarian", 4.32, "75efd60898e3f6e254f3a9059d53548b92140ff14c603cbc3d46061d7a2d257d"), + new TesseractLanguage("cat.traineddata.zip", "cat", "Catalan", 3.20, "e8897072d0b5f0b37834083bddecb08a71374c39c19daba14ae5e905dcdad3b0"), + new TesseractLanguage("ceb.traineddata.zip", "ceb", "Cebuano", 1.52, "cb0f10ae09ea0b4df4c0d5ddc8fe7ce370a7fb4d6ff3e23a8d96a70b1a36ccba"), + new TesseractLanguage("ces.traineddata.zip", "ces", "Czech", 8.43, "fd9eb6ec0e646f48c2786bdc508b8a9e59d3d1ca034eff1dfbc930e1864fe9ba"), + new TesseractLanguage("chi_sim.traineddata.zip", "chi_sim", "Chinese (Simplified)", 20.73, "21ebe92da47078c237b776d5ef79b3668b6a5b76f1ec527588e60d57230f097d"), + new TesseractLanguage("chi_sim_vert.traineddata.zip", "chi_sim_vert", "Chinese (Simplified, Vertical)", 2.81, "6d1a3352fca9b0d5e54a40e29e97d7baaa1e459146954dcfa54a03cf7c3d4893"), + new TesseractLanguage("chi_tra.traineddata.zip", "chi_tra", "Chinese (Traditional)", 27.26, "a2127d36753638ab6a018d544b4f518b9b1cf68738f8da586e4004a22eb16500"), + new TesseractLanguage("chi_tra_vert.traineddata.zip", "chi_tra_vert", "Chinese (Traditional, Vertical)", 2.70, "83d0c40f93bc801e48b3a3ca7490f7477742dae17b96496d1256b2442c5c50b8"), + new TesseractLanguage("chr.traineddata.zip", "chr", "Cherokee", 0.92, "116a17bbb257975708595a6f5b6f59e3e593a58397cca603fd141cda51285758"), + new TesseractLanguage("cos.traineddata.zip", "cos", "Corsican", 2.64, "28abc11555b277c4d5f7ed928de06867ebd8b4a1f6e61ec85cc16fc783cbca4e"), + new TesseractLanguage("cym.traineddata.zip", "cym", "Welsh", 3.97, "c3d12e77a487a4e2b3200f890236e2b3842a3102856f5dd57a6cd4a325b26603"), + new TesseractLanguage("dan.traineddata.zip", "dan", "Danish", 5.72, "e778c64ef4c3d39d3dd4d38cf5a3e7adabd19adaa291248893e3e3ee7fcf75d7"), + new TesseractLanguage("deu.traineddata.zip", "deu", "German", 7.58, "134f41cb8a47e139cf988ea255f03a5b278d50eabad438204f708abf5a6442ce"), + new TesseractLanguage("deu_latf.traineddata.zip", "deu_latf", "German (Fraktur)", 12.45, "285f53f9cbe1ea79da37d087feabb4caac94e551e8d3e4f476b392c81c43fcf6"), + new TesseractLanguage("div.traineddata.zip", "div", "Maldivian", 1.70, "e5d67c91ef76570aa05e068a292d6f658c0a84ccf73d97ba2f94edd7cdbea0af", true), + new TesseractLanguage("dzo.traineddata.zip", "dzo", "Dzongkha", 0.75, "f890900540ddf88909954639f35689ffc9d40c78af27d70600fc510302162857"), + new TesseractLanguage("ell.traineddata.zip", "ell", "Greek", 3.93, "9dede46367708f74917386b88a08e7ca41829aa4278a0b187af594fc6a584ae8"), + new TesseractLanguage("eng.traineddata.zip", "eng", "English", 12.29, "9d167994617c5fd827bf6b31f298e69cd05e6d30b9965715e78191df5606389e"), + new TesseractLanguage("enm.traineddata.zip", "enm", "English (Middle)", 4.58, "248b730c6b77634f1679a6411a02e069f6c7b7da1374f77a173eb2e00b34081f"), + new TesseractLanguage("epo.traineddata.zip", "epo", "Esperanto", 6.50, "fc6630ede1d9cbfe84ddb2889fd2fe7470313cfb3087598d71cc7c4e18d0c837"), + new TesseractLanguage("equ.traineddata.zip", "equ", "Math / equation detection", 0.79, "b18f660b80fa9d352fe8d2261e14bd3463b4037e72c49b9fc88c01297ac1a95e"), + new TesseractLanguage("est.traineddata.zip", "est", "Estonian", 8.49, "e9f38932eb9497a686f70ed8b586d0fef920cb140ff7b9978f32349a0db46d19"), + new TesseractLanguage("eus.traineddata.zip", "eus", "Basque", 6.07, "0e3bce4aa08bcc4c53fb6d5e8884d1e973406c086af1bec947c96b6348742d16"), + new TesseractLanguage("fao.traineddata.zip", "fao", "Faroese", 3.69, "4999b8f539cc1a432cdc52ac69cbb21654da994f292f42df4d4a025ae27623b3"), + new TesseractLanguage("fas.traineddata.zip", "fas", "Persian", 0.71, "3f3d75e2f645a262241c80b79e799f0a9523fadcf4958d1170a583972c124ce0", true), + new TesseractLanguage("fil.traineddata.zip", "fil", "Filipino", 2.31, "acb7b499d00e65087275d53e997444e5a32d49ffe543534e254cfc45d38274b3"), + new TesseractLanguage("fin.traineddata.zip", "fin", "Finnish", 12.19, "e16d491a9ee90e66786624ee1e8e3db0c57ad334162b888dc06e33340f697d47"), + new TesseractLanguage("fra.traineddata.zip", "fra", "French", 6.55, "00b779d1373d837f927d34f1e66d9b99d0339ce5dba82d80b928c5eba431ad8a"), + new TesseractLanguage("frm.traineddata.zip", "frm", "French (Middle)", 8.30, "2339bf1c78224ff827eca19e2d77d01b70599733fe4fc972b75127a442870ef6"), + new TesseractLanguage("fry.traineddata.zip", "fry", "Frisian (Western)", 2.42, "0106765e7634dd3e2b2c0587eb66635cf647cb90ed21cfc1434e5ab191a4f702"), + new TesseractLanguage("gla.traineddata.zip", "gla", "Gaelic", 3.33, "4543b56903859324ac465e8b238346c2aca653479fba93e1fdb0bbe22583de66"), + new TesseractLanguage("gle.traineddata.zip", "gle", "Irish", 2.55, "e9981b837e37c3e9f25cebc1904eab35a2c1cb2dcb882fefe3f14b3e224b2538"), + new TesseractLanguage("glg.traineddata.zip", "glg", "Galician", 5.38, "cecfdfc25bd2f31a303ddeaab4158eee8b93d355205ade8a749d6698f8cbcf06"), + new TesseractLanguage("grc.traineddata.zip", "grc", "Greek (Ancient)", 3.98, "c66c23954dd2d950b27d70966ed56373b6ee75a41bcfddf6a9e032c951ea0312"), + new TesseractLanguage("guj.traineddata.zip", "guj", "Gujarati", 1.88, "9584daea9406462710e22d034b45367afec73ecbea966b42fd9fbcb79b9c3116"), + new TesseractLanguage("hat.traineddata.zip", "hat", "Haitian", 3.40, "3f985b227a7fd53a57be8e0cd8de0e09e8b9f995cf88671338dfca52119ce7c7"), + new TesseractLanguage("heb.traineddata.zip", "heb", "Hebrew", 2.53, "66af43320edf32521cb3b86d5362f1f98ccbcb843f98d575267c83a4a94de4f9", true), + new TesseractLanguage("hin.traineddata.zip", "hin", "Hindi", 2.22, "4d5d7d7b5851815ce27471e153633c343f227f2ce2cb08e8ac6ff7955c8847e1"), + new TesseractLanguage("hrv.traineddata.zip", "hrv", "Croatian", 7.23, "4f263d15d567dfea924338e02ebfaf37ab500f8ee1f121f7c0e5f73ee577fb89"), + new TesseractLanguage("hun.traineddata.zip", "hun", "Hungarian", 9.58, "63ad9219c6ceb4aab27fb937fc4bc7c40f803bd8f3a58e94a796b695be89c2b3"), + new TesseractLanguage("hye.traineddata.zip", "hye", "Armenian", 2.97, "96fc9d9883b9743095ebdc11aa3c3843909f0b0d77a91ce69a4adc82609b6c60"), + new TesseractLanguage("iku.traineddata.zip", "iku", "Inuktitut", 3.10, "72a7fa284e0b8412b9e7c5ad11f474d26b67c8c9e1cb576d2bc334273e1503bb"), + new TesseractLanguage("ind.traineddata.zip", "ind", "Indonesian", 4.24, "945916910f6c3a2a2a0bae1323e0509ca04a3e01f0daf2c43d90180cca642edd"), + new TesseractLanguage("isl.traineddata.zip", "isl", "Icelandic", 4.96, "1a984bfc814cb1cbc83d97a3e887ccee20d1e1c5eac0b624bb1f8121d781cd97"), + new TesseractLanguage("ita.traineddata.zip", "ita", "Italian", 7.80, "53136c0def889f2acad686e96e6f40b7a7d53e5537dfbbf956ba351892bbf8e6"), + new TesseractLanguage("ita_old.traineddata.zip", "ita_old", "Italian (Old)", 8.80, "d4629a523f749e1c79ba391fbb45809b886b0357c2af78634603c1126a1a6621"), + new TesseractLanguage("jav.traineddata.zip", "jav", "Javanese", 4.74, "a51fa159417e0d502d0f0e9ca845560ec58223d0c5507f2482558f22e04cfd39"), + new TesseractLanguage("jpn.traineddata.zip", "jpn", "Japanese", 16.77, "7ce9a993f7fb67099483d126820352768001ca356975ba4a691c23fe7c3e2245"), + new TesseractLanguage("jpn_vert.traineddata.zip", "jpn_vert", "Japanese (Vertical)", 3.88, "da584c3e0368ddcad57fa82a7a4063af778bf7fd941675ca7c747c3492bc64e5"), + new TesseractLanguage("kan.traineddata.zip", "kan", "Kannada", 3.66, "884f6ee097c45ebb2b28e52c8763c9220cdbfe4899b464b8f3eb8fd32471ffac"), + new TesseractLanguage("kat.traineddata.zip", "kat", "Georgian", 4.29, "45c0978cef610a6348f67e2fc89f0d6021a2ef611f6c45a515e413b5dc72c9e1"), + new TesseractLanguage("kat_old.traineddata.zip", "kat_old", "Georgian (Old)", 0.92, "ff05139f6eabfba3e99ea392036da63a3e432fca2ab3e815d952edf72c232353"), + new TesseractLanguage("kaz.traineddata.zip", "kaz", "Kazakh", 5.70, "39e0e34b750982b1e0bde53b88130dae8d6a51c12a95ae7ac8688b0d9ddc0398"), + new TesseractLanguage("khm.traineddata.zip", "khm", "Khmer (Central)", 2.05, "334e251c0674e7105eda8ab910c064735f53ba91ed549f14b89652ce2befed7b"), + new TesseractLanguage("kir.traineddata.zip", "kir", "Kirghiz", 10.46, "c7e26b19ee7e49c1912c4abafa8aac42ebea7516b26a779ed4fe7982c6885eb9"), + new TesseractLanguage("kor.traineddata.zip", "kor", "Korean", 7.66, "637fb7623cb4f9d255c9a7366070b2efccc20aa1895d76cc6abda63cc76ce0f9"), + new TesseractLanguage("kor_vert.traineddata.zip", "kor_vert", "Korean (Vertical)", 1.18, "19be9d55142b0fec23c065ddceae763f42b4b8e5802a9e7433a04b83cb3c66eb"), + new TesseractLanguage("kmr.traineddata.zip", "kmr", "Kurmanji", 9.69, "40385118857473589a3ddfb280114f89309dc45205657132d16805d678e07129"), + new TesseractLanguage("lao.traineddata.zip", "lao", "Lao", 6.52, "338e3cfa48970a97cd2be6f64dd160b3b1865ea47dc3add0b88abe0ccbc81e1b"), + new TesseractLanguage("lat.traineddata.zip", "lat", "Latin", 5.38, "733ae53125ec83615cb636de9768b48998c830c5d9826f3cad3f0dd3b4be3ec7"), + new TesseractLanguage("lav.traineddata.zip", "lav", "Latvian", 5.32, "c92311b0f4d04919c95203abb9f457c050dadea100ef769afc831aa5ead5c97f"), + new TesseractLanguage("lit.traineddata.zip", "lit", "Lithuanian", 6.42, "5b31dc6b4ace6d0a89aff824449057c38261ca0a46187fec658cd88c0e528f7e"), + new TesseractLanguage("ltz.traineddata.zip", "ltz", "Luxembourgish", 3.49, "8a71a214502fb9ee25bf3112182473aa512d9dc9bb2cddb2d5864655e038db62"), + new TesseractLanguage("mal.traineddata.zip", "mal", "Malayalam", 4.73, "7ab1f2013f48af8c2a3852394735d1726d3b9f0c643179f26ac424655633bc9f"), + new TesseractLanguage("mar.traineddata.zip", "mar", "Marathi", 2.84, "60a366dc46f88dab81abaef7409e39f7ec90aa61f594914df4d76fbfb0f48d57"), + new TesseractLanguage("mkd.traineddata.zip", "mkd", "Macedonian", 2.83, "a4898b829bc6c5a9b70f39b56b8c4daa17c3984011ed50286bd7a62d520704d8"), + new TesseractLanguage("mlt.traineddata.zip", "mlt", "Maltese", 4.17, "56c59a7a78e98d4a228f3d15f29adaef92433e577fb0cdb468f685243c315d3f"), + new TesseractLanguage("mon.traineddata.zip", "mon", "Mongolian", 2.53, "dac6ff62379b211f97b8e4f8bb507ccafc0ff1c1ee7e0d8bf783fac6d0de4f0d"), + new TesseractLanguage("mri.traineddata.zip", "mri", "Maori", 1.05, "adffa1d84438509ca06ef0fd6dbe9850e40017e97a119247e535c1b4d7bed6cd"), + new TesseractLanguage("msa.traineddata.zip", "msa", "Malay", 4.73, "d2e2debc66462e90be6b61554c4d2b0b4f94e0b491e65e520b7dac03078f1e9c"), + new TesseractLanguage("mya.traineddata.zip", "mya", "Burmese", 5.15, "6b829f14edafdd00f673b5aa9945b430adf8f42a1f15e97940097232b592c81a"), + new TesseractLanguage("nep.traineddata.zip", "nep", "Nepali", 2.04, "727823700b6f52abeaab649f82a43f0d270ce26630101a77587b0e238a5f476f"), + new TesseractLanguage("nld.traineddata.zip", "nld", "Dutch", 12.59, "572f6fc496f7998f10e1a229ca103c0f6bb685d837648c04315efc98b98524c3"), + new TesseractLanguage("nor.traineddata.zip", "nor", "Norwegian", 7.45, "78e3e8884930f529950d4e0ecc2bc52aea0e21092d7d6fc2aa7fedb58ce5530f"), + new TesseractLanguage("oci.traineddata.zip", "oci", "Occitan", 6.09, "cc643881830d43003fcf507d00e5a08f627e14eb0bbd32ea73f2809f2af2d244"), + new TesseractLanguage("ori.traineddata.zip", "ori", "Oriya", 2.04, "35cfe0a66de80cdf443b1e997a60a1804a973df94d978e6c03d2a1a84e9e6927"), +// new TesseractLanguage { Filename = "osd.traineddata.zip", Code = "osd", LangName = "", Size = 8.22, Sha1 = "8162903ddc718157e6feeabbfdafe0e375a38001" }, + new TesseractLanguage("pan.traineddata.zip", "pan", "Panjabi", 1.66, "42bd9b864b13a56ab57bb75cc0998f3aa6e9219270324682667351560695ce91"), + new TesseractLanguage("pol.traineddata.zip", "pol", "Polish", 9.89, "c7a892bcb49f237159662865fa18703ccc03f0dd8d1573fd8ffc9057d664e962"), + new TesseractLanguage("por.traineddata.zip", "por", "Portuguese", 7.38, "0f8913e7e691237e0ff946ba993ed32a1c1eafcf7957263dc4a54f1b2a99411d"), + new TesseractLanguage("pus.traineddata.zip", "pus", "Pushto", 2.73, "2e2eeca1e2791e415e141ba770766145299ba84f978587368708e7624b294a22"), + new TesseractLanguage("que.traineddata.zip", "que", "Quechua", 4.93, "c45b94df8262c791509a365c49fcd0a451f4b6329790355563e3df2e7ceef772"), + new TesseractLanguage("ron.traineddata.zip", "ron", "Romanian", 5.65, "79d51616c67ad813608b3a82dd10cfc1ee874463d5dce11cc61c12de3f48a58c"), + new TesseractLanguage("rus.traineddata.zip", "rus", "Russian", 9.74, "0dd9b6ab808045a706ad312283b1841b1ba0c5e196287e7a773f66e1e629b5e6"), + new TesseractLanguage("san.traineddata.zip", "san", "Sanskrit", 10.86, "eae9a65200412eae292878956c4e4ef4f9d0e738f7b54a651e5e808f49e5b523"), + new TesseractLanguage("sin.traineddata.zip", "sin", "Sinhala", 2.19, "0ebf36eda329d05eb4b9f7e829250a51ef6201fa005bce61a2822a026f88f4c0"), + new TesseractLanguage("slk.traineddata.zip", "slk", "Slovakian", 7.50, "ed7e2cc03e9a293536d356f8e62707448c2376f147b6b8cff4e9b53c9f545659"), + new TesseractLanguage("slv.traineddata.zip", "slv", "Slovenian", 4.93, "5dc96ba037c189462dd9baa804a6ee1ce00006484f4548fe922045ecb38fb4f0"), + new TesseractLanguage("snd.traineddata.zip", "snd", "Sindhi", 2.70, "08157a44f943463cbc50e365150881ce4aea88782ad5194e1137e2b7609bd1f1"), + new TesseractLanguage("spa.traineddata.zip", "spa", "Spanish", 9.03, "820a0ec5e083188b366ac3e5d79f02fa1bb51c274c3e01ce6943826bacc38981"), + new TesseractLanguage("spa_old.traineddata.zip", "spa_old", "Spanish (Old)", 9.79, "0ec4bed63b5950fb62281cc32b4b307327f8883b5e61ee4d7579d00b18053533"), + new TesseractLanguage("sqi.traineddata.zip", "sqi", "Albanian", 4.13, "063dc592eed9c5adc41b006b2b52fe84f0103f144499b1a6d4423bd301c375df"), + new TesseractLanguage("srp.traineddata.zip", "srp", "Serbian", 3.92, "13c63b4992906d5c016924ece6ab29d8b27cc1bf7111dd7243ccb680e799fac2"), + new TesseractLanguage("srp_latn.traineddata.zip", "srp_latn", "Serbian (Latin)", 5.65, "eb0106cc402d15f1191d3a20be0a8c4fbf18f6e0318be253721860cf87d2b3f6"), + new TesseractLanguage("sun.traineddata.zip", "sun", "Sundanese", 1.46, "59e4f195d8f861bebd6ced50617e107f6526ae99707f090dab60be5300ec1635"), + new TesseractLanguage("swa.traineddata.zip", "swa", "Swahili", 3.52, "b1efe3243d6e10b0af8bcdb2f7dce37c31d80c86eef811745a89fa75274c9900"), + new TesseractLanguage("swe.traineddata.zip", "swe", "Swedish", 8.42, "409610afe61aeb74b91918e1c5a1e0eaa484d942cab231513bcd2e3339e0944f"), + new TesseractLanguage("syr.traineddata.zip", "syr", "Syriac", 3.09, "401fa37551c581fb3a75681f7dc7dd124f72efbff785293b75b61c34a821525e"), + new TesseractLanguage("tam.traineddata.zip", "tam", "Tamil", 2.65, "6781e9f4f0a96813a6435429e5f975f8dd06f068daec6226fb5fff8042508ddc"), + new TesseractLanguage("tat.traineddata.zip", "tat", "Tatar", 1.74, "93e218b0e3362dde43b16cdda4c8617e190c5c008337b0232d6110e1e51bb57c"), + new TesseractLanguage("tel.traineddata.zip", "tel", "Telugu", 2.85, "19ad3382a1399afa50c7e9a39fb0f0c852fd21f2c4da747042462bd8c8a51d8b"), + new TesseractLanguage("tgk.traineddata.zip", "tgk", "Tajik", 2.62, "ef35f09f7b1c2dc4883548bd1a0f1d5e3bfe5d0938b7c4ef03e70ea37f3053c2"), + new TesseractLanguage("tgl.traineddata.zip", "tgl", "Tagalog", 3.13, "3600aeb0eb65c96b34a0625f94009417ff584947fd370628029c1526e5f0d96f"), + new TesseractLanguage("tha.traineddata.zip", "tha", "Thai", 1.73, "e590d1b4daa75e8d99704174c0a574d5fdf2750952d3d9bae50fa1adb1c5b9be"), + new TesseractLanguage("tir.traineddata.zip", "tir", "Tigrinya", 1.18, "be42a4f3647745b0af276f6d61ab5348846589cd3efd88c9908efd3d27201aff"), + new TesseractLanguage("ton.traineddata.zip", "ton", "Tonga (Tonga Islands)", 1.13, "67f5dba9dccbe6b79f9b036afc714dfe065d077237636fcf45e6851b85c549b5"), + new TesseractLanguage("tur.traineddata.zip", "tur", "Turkish", 9.58, "7141de10ca977abdab21080afe5dc2025d6cec1661fba5803099bacc25033091"), + new TesseractLanguage("uig.traineddata.zip", "uig", "Uighur", 3.55, "18f9899c48078c59b9acc5b64f198bfd6fca09a24ee9aca4e97efcbd24a8dc27"), + new TesseractLanguage("ukr.traineddata.zip", "ukr", "Ukrainian", 6.48, "c14a794e98c24976c84f41f05217fba1155953f3a6062b9f63d6122e0936f6f7"), + new TesseractLanguage("urd.traineddata.zip", "urd", "Urdu", 1.97, "7a83e5e191c40387422c6b6903b7d4e865f35782269b66760876b4f76d1f3cb3", true), + new TesseractLanguage("uzb.traineddata.zip", "uzb", "Uzbek", 7.48, "9e0502ef7fbd0e45cbd843ad5921784416d149ec47f9c43b01a1236da1e5e064"), + new TesseractLanguage("uzb_cyrl.traineddata.zip", "uzb_cyrl", "Uzbek (Cyrillic)", 2.78, "b719f58617c56517018cd2c35618622aa8c10c7de2466f64fb0aa452ccb6ef28"), + new TesseractLanguage("vie.traineddata.zip", "vie", "Vietnamese", 4.06, "3fd45915b39bc97cb23091ae5b87bd0f8ef0400237c31689f3587dafcc69b730"), + new TesseractLanguage("yid.traineddata.zip", "yid", "Yiddish", 2.38, "798b6bc403f98ba573be631663794ac314ec7083c1871e2a9b0baf214b1c73ff"), + new TesseractLanguage("yor.traineddata.zip", "yor", "Yoruba", 1.14, "e41bc668af832e13758bc94f8aa9fa9358f34a7e1b16d7193e5f5f1de9e83467"), + }); + + #endregion +} \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/TesseractLanguageManager.cs b/NAPS2.Lib/Ocr/TesseractLanguageManager.cs similarity index 72% rename from NAPS2.Sdk/Ocr/TesseractLanguageManager.cs rename to NAPS2.Lib/Ocr/TesseractLanguageManager.cs index 048bac821d..731b4e16f3 100644 --- a/NAPS2.Sdk/Ocr/TesseractLanguageManager.cs +++ b/NAPS2.Lib/Ocr/TesseractLanguageManager.cs @@ -4,11 +4,11 @@ namespace NAPS2.Ocr; public class TesseractLanguageManager { - private static readonly List Mirrors = new() - { + private static readonly List Mirrors = + [ new(@"https://github.com/cyanfish/naps2-components/releases/download/tesseract-4.0.0b4/{0}"), new(@"https://sourceforge.net/projects/naps2/files/components/tesseract-4.0.0b4/{0}/download") - }; + ]; private readonly TesseractLanguageData _languageData = TesseractLanguageData.Latest; @@ -16,25 +16,22 @@ public TesseractLanguageManager(string basePath) { TessdataBasePath = GetTessdataBasePath(basePath); LanguageComponents = _languageData.Data.Select(x => - new MultiFileExternalComponent($"ocr-{x.Code}", TessdataBasePath, new[] { $"best/{x.Code}.traineddata", $"fast/{x.Code}.traineddata" }, - new DownloadInfo(x.Filename, Mirrors, x.Size, x.Sha1, DownloadFormat.Zip))); + new MultiFileExternalComponent($"ocr-{x.Code}", TessdataBasePath, + new[] { $"best/{x.Code}.traineddata", $"fast/{x.Code}.traineddata" }, + new DownloadInfo(x.Filename, Mirrors, x.Size, x.Sha256, DownloadFormat.Zip))); } private string GetTessdataBasePath(string basePath) { - var legacyBasePath = Path.Combine(basePath, "tesseract-4.0.0b4"); var newBasePath = Path.Combine(basePath, "tesseract4"); - if (Directory.Exists(legacyBasePath) && !Directory.Exists(newBasePath)) + var legacyBasePath = Path.Combine(basePath, "tesseract-4.0.0b4"); + if (Directory.Exists(newBasePath)) + { + return newBasePath; + } + if (Directory.Exists(legacyBasePath)) { - try - { - Directory.Move(legacyBasePath, newBasePath); - } - catch (Exception) - { - // Ignore errors and keep the legacy path, e.g. if the components folder is read-only - return legacyBasePath; - } + return legacyBasePath; } return newBasePath; } diff --git a/NAPS2.Lib/Operation/OperationBase.cs b/NAPS2.Lib/Operation/OperationBase.cs index 85c4d45547..bed6fb6d43 100644 --- a/NAPS2.Lib/Operation/OperationBase.cs +++ b/NAPS2.Lib/Operation/OperationBase.cs @@ -89,6 +89,12 @@ private void StartTask(Func> action) }).AssertNoAwait(); } + protected void FailedToStart() + { + InvokeFinished(); + _tcs.TrySetResult(false); + } + protected void InvokeFinished() { IsFinished = true; diff --git a/NAPS2.Lib/Paths.cs b/NAPS2.Lib/Paths.cs index 7db0146900..b568afacfa 100644 --- a/NAPS2.Lib/Paths.cs +++ b/NAPS2.Lib/Paths.cs @@ -19,8 +19,16 @@ static Paths() if (string.IsNullOrEmpty(userAppData) && OperatingSystem.IsMacOS()) { // Not sure if this is necessary but older macOS (10.15) didn't seem to get the appdata path + // TODO: Technically this should be "%HOME%/.config" but keeping this for now, for backwards compatibility userAppData = Environment.ExpandEnvironmentVariables("/Users/%USER%/.config"); } + else if (OperatingSystem.IsMacOS()) + { + // For .NET 8, the macOS ApplicationData path has changed from the Linux-style "~/.config" to the more + // correct "~/Library/Application Support". For backwards compatibility we keep .config for now. + // TODO: Migrate macOS application data to "~/Library/Application Support" + userAppData = Environment.ExpandEnvironmentVariables("%HOME%/.config"); + } #else var subfolder = "NAPS2"; #endif @@ -78,9 +86,9 @@ public static void ClearTemp() Directory.CreateDirectory(TempPath); } } - catch (Exception e) + catch (Exception) { - Log.ErrorException("Error clearing temp files", e); + // Ignore errors clearing temp files } } diff --git a/NAPS2.Lib/ImportExport/Pdf/EtoPdfPasswordProvider.cs b/NAPS2.Lib/Pdf/EtoPdfPasswordProvider.cs similarity index 95% rename from NAPS2.Lib/ImportExport/Pdf/EtoPdfPasswordProvider.cs rename to NAPS2.Lib/Pdf/EtoPdfPasswordProvider.cs index 549f7b9e50..3a24ebef98 100644 --- a/NAPS2.Lib/ImportExport/Pdf/EtoPdfPasswordProvider.cs +++ b/NAPS2.Lib/Pdf/EtoPdfPasswordProvider.cs @@ -1,7 +1,7 @@ using NAPS2.EtoForms; using NAPS2.EtoForms.Ui; -namespace NAPS2.ImportExport.Pdf; +namespace NAPS2.Pdf; public class EtoPdfPasswordProvider : IPdfPasswordProvider { diff --git a/NAPS2.Lib/ImportExport/Pdf/PdfSettings.cs b/NAPS2.Lib/Pdf/PdfSettings.cs similarity index 95% rename from NAPS2.Lib/ImportExport/Pdf/PdfSettings.cs rename to NAPS2.Lib/Pdf/PdfSettings.cs index 240304048d..1b4c5a58ff 100644 --- a/NAPS2.Lib/ImportExport/Pdf/PdfSettings.cs +++ b/NAPS2.Lib/Pdf/PdfSettings.cs @@ -1,6 +1,6 @@ using NAPS2.Config.Model; -namespace NAPS2.ImportExport.Pdf; +namespace NAPS2.Pdf; public class PdfSettings { diff --git a/NAPS2.Lib/ImportExport/Pdf/SavePdfOperation.cs b/NAPS2.Lib/Pdf/SavePdfOperation.cs similarity index 73% rename from NAPS2.Lib/ImportExport/Pdf/SavePdfOperation.cs rename to NAPS2.Lib/Pdf/SavePdfOperation.cs index 59a5a39af1..246ebc5215 100644 --- a/NAPS2.Lib/ImportExport/Pdf/SavePdfOperation.cs +++ b/NAPS2.Lib/Pdf/SavePdfOperation.cs @@ -1,15 +1,16 @@ -using NAPS2.ImportExport.Email; +using NAPS2.ImportExport; +using NAPS2.ImportExport.Email; using NAPS2.Ocr; -namespace NAPS2.ImportExport.Pdf; +namespace NAPS2.Pdf; -public class SavePdfOperation : OperationBase +internal class SavePdfOperation : OperationBase { - private readonly IPdfExporter _pdfExporter; + private readonly PdfExporter _pdfExporter; private readonly IOverwritePrompt _overwritePrompt; private readonly IEmailProviderFactory? _emailProviderFactory; - public SavePdfOperation(IPdfExporter pdfExporter, IOverwritePrompt overwritePrompt, + public SavePdfOperation(PdfExporter pdfExporter, IOverwritePrompt overwritePrompt, IEmailProviderFactory? emailProviderFactory = null) { _pdfExporter = pdfExporter; @@ -24,16 +25,11 @@ public SavePdfOperation(IPdfExporter pdfExporter, IOverwritePrompt overwriteProm public string? FirstFileSaved { get; private set; } public bool Start(string fileName, Placeholders placeholders, ICollection images, - PdfSettings pdfSettings, OcrParams ocrParams) - { - return Start(fileName, placeholders, images, pdfSettings, ocrParams, false, null); - } - - public bool Start(string fileName, Placeholders placeholders, ICollection images, - PdfSettings pdfSettings, OcrParams ocrParams, bool email, EmailMessage? emailMessage) + PdfSettings pdfSettings, OcrParams ocrParams, EmailMessage? emailMessage = null, + string? overwriteFile = null) { // TODO: This needs tests. And ideally simplification. - ProgressTitle = email ? MiscResources.EmailPdfProgress : MiscResources.SavePdfProgress; + ProgressTitle = emailMessage != null ? MiscResources.EmailPdfProgress : MiscResources.SavePdfProgress; var subFileName = placeholders.Substitute(fileName); Status = new OperationStatus { @@ -46,37 +42,40 @@ public bool Start(string fileName, Placeholders placeholders, ICollection new[] { x }).ToArray() : new[] { images.ToArray() }; + var imagesByFile = pdfSettings.SinglePagePdfs + ? images.Select(x => new[] { x }).ToArray() + : new[] { images.ToArray() }; RunAsync(async () => { bool result = false; try { - int digits = (int)Math.Floor(Math.Log10(images.Count)) + 1; + int digits = (int) Math.Floor(Math.Log10(images.Count)) + 1; int i = 0; foreach (var imagesForFile in imagesByFile) { var currentFileName = placeholders.Substitute(fileName, true, i, singleFile ? 0 : digits); + // TODO: Overwrite prompt non-single file? Status.StatusText = string.Format(MiscResources.SavingFormat, Path.GetFileName(currentFileName)); InvokeStatusChanged(); if (singleFile && IsFileInUse(currentFileName, out var ex)) @@ -93,7 +92,11 @@ public bool Start(string fileName, Placeholders placeholders, ICollection +/// A class to help manage the lifecycle of the NAPS2 GUI. +/// +public abstract class ApplicationLifecycle +{ + private readonly ProcessCoordinator _processCoordinator; + private readonly IOsServiceManager _serviceManager; + private readonly Naps2Config _config; + + private bool _unregisterSharingService; + + protected ApplicationLifecycle(ProcessCoordinator processCoordinator, IOsServiceManager serviceManager, + Naps2Config config) + { + _processCoordinator = processCoordinator; + _serviceManager = serviceManager; + _config = config; + } + + public virtual void ParseArgs(string[] args) + { + _unregisterSharingService = args.Any(x => + x.Equals("/UnregisterSharingService", StringComparison.InvariantCultureIgnoreCase)); + if (_unregisterSharingService) + { + if (OperatingSystem.IsMacOS()) + { + // Avoid terminating our own process if we were spawned from the server process + setsid(); + } + _serviceManager.Unregister(); + } + } + + [DllImport("libc")] + private static extern int setsid(); + + public virtual void ExitIfRedundant() + { + if (_unregisterSharingService) + { + Environment.Exit(0); + } + HandleSingleInstance(); + } + + protected virtual void HandleSingleInstance() + { + // Only start one instance if configured for SingleInstance + if (_config.Get(c => c.SingleInstance)) + { + if (!_processCoordinator.TryTakeInstanceLock()) + { + Log.Debug("Failed to get SingleInstance lock"); + var process = _processCoordinator.GetProcessWithInstanceLock(); + if (process != null) + { + // Another instance of NAPS2 is running, so send it the "Activate" signal + Log.Debug($"Activating process {process.Id}"); + + // For new processes, wait until the process is at least 5 seconds old. + // This might be useful in cases where multiple NAPS2 processes are started at once, e.g. clicking + // to open a group of files associated with NAPS2. + int processAge = (DateTime.Now - process.StartTime).Milliseconds; + int timeout = (5000 - processAge).Clamp(100, 5000); + + SetMainWindowToForeground(process); + bool ok = true; + if (Environment.GetCommandLineArgs() is [_, var arg] && File.Exists(arg)) + { + Log.Debug($"Sending OpenFileRequest for {arg}"); + ok = _processCoordinator.OpenFile(process, timeout, arg); + } + if (ok && _processCoordinator.Activate(process, timeout)) + { + // Successful, so this instance should be closed + Environment.Exit(0); + } + } + } + } + } + + protected virtual void SetMainWindowToForeground(Process process) + { + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Platform/IOpenWith.cs b/NAPS2.Lib/Platform/IOpenWith.cs new file mode 100644 index 0000000000..269b69bc2a --- /dev/null +++ b/NAPS2.Lib/Platform/IOpenWith.cs @@ -0,0 +1,10 @@ +namespace NAPS2.Platform; + +public interface IOpenWith +{ + public IEnumerable GetEntries(string fileExt); + public void OpenWith(string entryId, IEnumerable filePaths); + public IMemoryImage? LoadIcon(OpenWithEntry entry); +} + +public record OpenWithEntry(string Id, string Name, string IconPath, int IconIndex); \ No newline at end of file diff --git a/NAPS2.Lib/Platform/IOsServiceManager.cs b/NAPS2.Lib/Platform/IOsServiceManager.cs new file mode 100644 index 0000000000..985788fc97 --- /dev/null +++ b/NAPS2.Lib/Platform/IOsServiceManager.cs @@ -0,0 +1,15 @@ +namespace NAPS2.Platform; + +/// +/// Abstraction for OS-specific "run on startup" registration logic. +/// +public interface IOsServiceManager +{ + bool CanRegister { get; } + + bool IsRegistered { get; } + + bool Register(); + + void Unregister(); +} \ No newline at end of file diff --git a/NAPS2.Lib/Platform/Windows/StillImage.cs b/NAPS2.Lib/Platform/Windows/StillImage.cs index 78b77e1036..d80c97dd27 100644 --- a/NAPS2.Lib/Platform/Windows/StillImage.cs +++ b/NAPS2.Lib/Platform/Windows/StillImage.cs @@ -43,9 +43,7 @@ public void ParseArgs(string[] args) // TODO: Does it make sense to add IStillImage::(Un)RegisterLaunchApplication to NAPS2.Wia.Native? // https://docs.microsoft.com/en-us/previous-versions/windows/hardware/drivers/ff543798(v=vs.85) // Instead of modifying the registry directly. -#if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif public void Register() { var exe = AssemblyHelper.EntryFile; @@ -71,9 +69,7 @@ public void Register() key3.SetValue("Name", "NAPS2"); } -#if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif public void Unregister() { Registry.LocalMachine.DeleteSubKey(REGKEY_AUTOPLAY_HANDLER_NAPS2, false); diff --git a/NAPS2.Lib/Recovery/RecoverableFolder.cs b/NAPS2.Lib/Recovery/RecoverableFolder.cs index a8e98578d9..f49dd6d1db 100644 --- a/NAPS2.Lib/Recovery/RecoverableFolder.cs +++ b/NAPS2.Lib/Recovery/RecoverableFolder.cs @@ -1,5 +1,5 @@ using System.Collections.Immutable; -using NAPS2.ImportExport.Images; +using NAPS2.ImportExport; using NAPS2.Scan; using NAPS2.Serialization; @@ -8,37 +8,76 @@ namespace NAPS2.Recovery; public class RecoverableFolder : IDisposable { private readonly ScanningContext _scanningContext; - private readonly ImportPostProcessor _importPostProcessor; private readonly DirectoryInfo _directory; private readonly FileStream _lockFile; private readonly RecoveryIndex _recoveryIndex; private bool _disposed; - public RecoverableFolder(ScanningContext scanningContext, ImportPostProcessor importPostProcessor, - DirectoryInfo directory) + public static RecoverableFolder? TryCreate(ScanningContext scanningContext, DirectoryInfo directory) { - _scanningContext = scanningContext; - _importPostProcessor = importPostProcessor; - _directory = directory; + string indexFilePath = Path.Combine(directory.FullName, RecoveryStorageManager.INDEX_FILE_NAME); string lockFilePath = Path.Combine(directory.FullName, RecoveryStorageManager.LOCK_FILE_NAME); - _lockFile = new FileStream(lockFilePath, FileMode.Open, FileAccess.Read, FileShare.None); + if (!File.Exists(lockFilePath)) + { + MaybeCleanUp(directory); + return null; + } + var lockFile = new FileStream(lockFilePath, FileMode.Open, FileAccess.Read, FileShare.None); try { var serializer = new XmlSerializer(); - _recoveryIndex = serializer.DeserializeFromFile(Path.Combine(directory.FullName, "index.xml")); - ImageCount = _recoveryIndex.Images.Count; - ScannedDateTime = directory.LastWriteTime; - // TODO: Consider auto-delete in this case - // TODO: Also in the case where you have a lock file but no index is written (especially if no images are present) - if (ImageCount == 0) throw new ArgumentException("No images to recover in this folder"); + var recoveryIndex = serializer.DeserializeFromFile(indexFilePath); + var imageCount = recoveryIndex.Images.Count; + var scannedDateTime = directory.LastWriteTime; + if (imageCount == 0) + { + lockFile.Dispose(); + MaybeCleanUp(directory); + return null; + } + return new RecoverableFolder(scanningContext, directory, lockFile, recoveryIndex, + imageCount, scannedDateTime); } catch (Exception) { - _lockFile.Dispose(); - throw; + lockFile.Dispose(); + MaybeCleanUp(directory); + return null; } } + private static void MaybeCleanUp(DirectoryInfo directory) + { + // Clean up empty folders immediately, folders with files after a week + var cutoff = DateTime.Now - TimeSpan.FromDays(7); + var files = directory.GetFiles(); + if (files.Any(x => + x.LastWriteTime > cutoff && + x.Name != RecoveryStorageManager.LOCK_FILE_NAME && + x.Name != RecoveryStorageManager.INDEX_FILE_NAME)) + { + return; + } + try + { + directory.Delete(true); + } + catch (IOException) + { + } + } + + public RecoverableFolder(ScanningContext scanningContext, DirectoryInfo directory, FileStream lockFile, + RecoveryIndex recoveryIndex, int imageCount, DateTime scannedDateTime) + { + _scanningContext = scanningContext; + _directory = directory; + _lockFile = lockFile; + _recoveryIndex = recoveryIndex; + ImageCount = imageCount; + ScannedDateTime = scannedDateTime; + } + public int ImageCount { get; } public DateTime ScannedDateTime { get; } @@ -62,6 +101,36 @@ public void TryDelete() } } + private Dictionary CreateStorageKeys() + { + return _recoveryIndex.Images + .Select(indexImage => indexImage.FileName) + .Distinct() + .Select(originalFileName => new StorageKey(_scanningContext, _directory, originalFileName!)) + .ToDictionary(x => x.OriginalFileName); + } + + public IEnumerable FastRecover() + { + if (_disposed) throw new ObjectDisposedException(nameof(RecoverableFolder)); + + var storageKeys = CreateStorageKeys(); + var recoveredImages = new List(); + foreach (RecoveryIndexImage indexImage in _recoveryIndex.Images) + { + var storageKey = storageKeys[indexImage.FileName!]; + storageKey.EnsureMigrated(move: true); + if (storageKey.NewPath != null) + { + recoveredImages.Add(CreateRecoveredImage(new RecoveryParams(), indexImage, storageKey)); + } + } + + // Delete the old folder (which should be mostly empty now) + TryDelete(); + return recoveredImages; + } + public bool TryRecover(Action imageCallback, RecoveryParams recoveryParams, ProgressHandler progress) { @@ -71,55 +140,92 @@ public bool TryRecover(Action imageCallback, RecoveryParams reco int totalProgress = ImageCount; progress.Report(currentProgress, totalProgress); - foreach (RecoveryIndexImage indexImage in _recoveryIndex.Images) + var storageKeys = CreateStorageKeys(); + foreach (var indexImage in _recoveryIndex.Images) { if (progress.IsCancellationRequested) { return false; } - - string imagePath = Path.Combine(_directory.FullName, indexImage.FileName!); - var ext = Path.GetExtension(imagePath); - string newPath = _scanningContext.FileStorageManager!.NextFilePath() + ext; - - try + var storageKey = storageKeys[indexImage.FileName!]; + storageKey.EnsureMigrated(move: false); + if (storageKey.NewPath != null) { - File.Copy(imagePath, newPath); + var recoveredImage = CreateRecoveredImage(recoveryParams, indexImage, storageKey); + imageCallback(recoveredImage); } - catch (Exception e) - { - // TODO: Should we treat FileNotFound differently than other exceptions? i.e. continue on FNF, abort otherwise - Log.ErrorException("Could not recover image", e); - - currentProgress++; - progress.Report(currentProgress, totalProgress); - continue; - } - - var storage = new ImageFileStorage(newPath); - var recoveredImage = CreateRecoveredImage(recoveryParams, storage, indexImage); - imageCallback(recoveredImage); - currentProgress++; progress.Report(currentProgress, totalProgress); } + // Now that we've recovered successfully, we can safely delete the old folder TryDelete(); return true; } - private ProcessedImage CreateRecoveredImage(RecoveryParams recoveryParams, IImageStorage storage, - RecoveryIndexImage indexImage) + private ProcessedImage CreateRecoveredImage(RecoveryParams recoveryParams, RecoveryIndexImage indexImage, + StorageKey storageKey) { - var processedImage = _scanningContext.CreateProcessedImage(storage, indexImage.BitDepth.ToBitDepth(), - indexImage.HighQuality, -1, indexImage.TransformList!.ToImmutableList()); + var processedImage = _scanningContext.CreateProcessedImage( + storageKey.CreateStorage(), + indexImage.HighQuality, + -1, + PageSize.Parse(indexImage.PageSize), + indexImage.TransformList!.ToImmutableList(), + storageKey.RefCount); + storageKey.RefCount ??= processedImage.StorageRefCount; // TODO: Make this take a lazy rendered image or something - processedImage = _importPostProcessor.AddPostProcessingData(processedImage, + processedImage = ImportPostProcessor.AddPostProcessingData(processedImage, null, recoveryParams.ThumbnailSize, new BarcodeDetectionOptions(), true); return processedImage; } + + private record StorageKey( + ScanningContext ScanningContext, + DirectoryInfo OriginalDirectory, + string OriginalFileName) + { + public void EnsureMigrated(bool move) + { + if (Migrated) return; + Migrated = true; + try + { + var newPath = GetNewPath(); + if (move) + { + File.Move(OriginalPath, newPath); + } + else + { + File.Copy(OriginalPath, newPath); + } + NewPath = newPath; + } + catch (Exception e) + { + Log.ErrorException("Could not recover image", e); + } + } + + public string OriginalPath => Path.Combine(OriginalDirectory.FullName, OriginalFileName); + + public string? NewPath { get; private set; } + + public bool Migrated { get; private set; } + + public IImageStorage? Storage { get; private set; } + + public RefCount? RefCount { get; set; } + + public IImageStorage CreateStorage() => + Storage ??= new ImageFileStorage(NewPath ?? throw new InvalidOperationException()); + + private string GetNewPath() => + ScanningContext.FileStorageManager!.NextFilePath() + Path.GetExtension(OriginalFileName); + } } \ No newline at end of file diff --git a/NAPS2.Lib/Recovery/RecoveryIndex.cs b/NAPS2.Lib/Recovery/RecoveryIndex.cs index a097c037d2..a3e65db1ca 100644 --- a/NAPS2.Lib/Recovery/RecoveryIndex.cs +++ b/NAPS2.Lib/Recovery/RecoveryIndex.cs @@ -8,7 +8,7 @@ public class RecoveryIndex public RecoveryIndex() { - Images = new List(); + Images = []; } public int Version { get; set; } diff --git a/NAPS2.Lib/Recovery/RecoveryIndexImage.cs b/NAPS2.Lib/Recovery/RecoveryIndexImage.cs index a5a55b0c8b..4781ce8d0e 100644 --- a/NAPS2.Lib/Recovery/RecoveryIndexImage.cs +++ b/NAPS2.Lib/Recovery/RecoveryIndexImage.cs @@ -8,7 +8,7 @@ public class RecoveryIndexImage public List? TransformList { get; set; } - public ScanBitDepth BitDepth { get; set; } - public bool HighQuality { get; set; } + + public string? PageSize { get; set; } } \ No newline at end of file diff --git a/NAPS2.Lib/Recovery/RecoveryManager.cs b/NAPS2.Lib/Recovery/RecoveryManager.cs index fa861b014d..a3b4f68f57 100644 --- a/NAPS2.Lib/Recovery/RecoveryManager.cs +++ b/NAPS2.Lib/Recovery/RecoveryManager.cs @@ -6,17 +6,10 @@ namespace NAPS2.Recovery; public class RecoveryManager { private readonly ScanningContext _scanningContext; - private readonly ImportPostProcessor _importPostProcessor; public RecoveryManager(ScanningContext scanningContext) - : this(scanningContext, new ImportPostProcessor()) - { - } - - public RecoveryManager(ScanningContext scanningContext, ImportPostProcessor importPostProcessor) { _scanningContext = scanningContext; - _importPostProcessor = importPostProcessor; } public RecoverableFolder? GetLatestRecoverableFolder() @@ -43,11 +36,11 @@ public RecoveryManager(ScanningContext scanningContext, ImportPostProcessor impo { try { - return new RecoverableFolder(_scanningContext, _importPostProcessor, directory); + return RecoverableFolder.TryCreate(_scanningContext, directory); } catch (Exception) { - // Some problem, e.g. the folder is already locked or has no images + // Some problem, e.g. the folder is already locked return null; } } diff --git a/NAPS2.Lib/Recovery/RecoveryOperation.cs b/NAPS2.Lib/Recovery/RecoveryOperation.cs index bd47ca9eee..e348abd5e2 100644 --- a/NAPS2.Lib/Recovery/RecoveryOperation.cs +++ b/NAPS2.Lib/Recovery/RecoveryOperation.cs @@ -13,7 +13,7 @@ public RecoveryOperation(IFormFactory formFactory, RecoveryManager recoveryManag _formFactory = formFactory; _recoveryManager = recoveryManager; - ProgressTitle = MiscResources.ImportProgress; + ProgressTitle = MiscResources.RecoveryProgress; AllowCancel = true; AllowBackground = true; } @@ -39,7 +39,8 @@ public bool Start(Action imageCallback, RecoveryParams recoveryP { try { - return recoverableFolder.TryRecover(imageCallback, recoveryParams, ProgressHandler); + return recoverableFolder.TryRecover(imageCallback, recoveryParams, + ProgressHandler); } finally { diff --git a/NAPS2.Lib/Recovery/RecoveryStorageManager.cs b/NAPS2.Lib/Recovery/RecoveryStorageManager.cs index af3bf25bf6..d6314fa374 100644 --- a/NAPS2.Lib/Recovery/RecoveryStorageManager.cs +++ b/NAPS2.Lib/Recovery/RecoveryStorageManager.cs @@ -20,6 +20,7 @@ namespace NAPS2.Recovery; /// public class RecoveryStorageManager : IDisposable { + public const string INDEX_FILE_NAME = "index.xml"; public const string LOCK_FILE_NAME = ".lock"; private static readonly TimeSpan WriteThrottleInterval = TimeSpan.FromMilliseconds(100); @@ -90,12 +91,12 @@ private void WriteIndex(IEnumerable images) return new RecoveryIndexImage { FileName = Path.GetFileName(storage.FullPath), - BitDepth = processedImage.Metadata.BitDepth.ToScanBitDepth(), HighQuality = processedImage.Metadata.Lossless, + PageSize = processedImage.Metadata.PageSize?.ToString(), TransformList = processedImage.TransformState.Transforms.ToList() }; }).ToList(); - _serializer.SerializeToFile(Path.Combine(RecoveryFolderPath, "index.xml"), recoveryIndex); + _serializer.SerializeToFile(Path.Combine(RecoveryFolderPath, INDEX_FILE_NAME), recoveryIndex); } } diff --git a/NAPS2.Lib/Remoting/ProcessCoordinator.cs b/NAPS2.Lib/Remoting/ProcessCoordinator.cs new file mode 100644 index 0000000000..206e57932f --- /dev/null +++ b/NAPS2.Lib/Remoting/ProcessCoordinator.cs @@ -0,0 +1,123 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; +using GrpcDotNetNamedPipes; +using static NAPS2.Remoting.ProcessCoordinatorService; + +namespace NAPS2.Remoting; + +/// +/// Manages communication and coordination between multiple NAPS2 GUI processes. Specifically: +/// - Allows sending messages to other NAPS2 processes via named pipes +/// - Allows taking the SingleInstance lock (or checking which process currently owns it) +/// This is different than the worker service - workers are owned by the parent process and are considered part of the +/// same unit. Instead, this class handles the case where the user (or a system feature like StillImage) opens NAPS2 +/// twice. +/// +public class ProcessCoordinator(string basePath, string pipeNameFormat) +{ + private const string LOCK_FILE_NAME = "instance.lock"; + private const string PROC_FILE_NAME = "instance.proc"; + + public static ProcessCoordinator CreateDefault() => + new(Paths.AppData, "NAPS2_PIPE_v2_{0}"); + + private NamedPipeServer? _server; + private FileStream? _instanceLock; + + private string LockFilePath => Path.Combine(basePath, LOCK_FILE_NAME); + private string ProcFilePath => Path.Combine(basePath, PROC_FILE_NAME); + + private string GetPipeName(Process process) + { + return string.Format(pipeNameFormat, process.Id); + } + + public void StartServer(ProcessCoordinatorServiceBase service) + { + _server = new NamedPipeServer(GetPipeName(Process.GetCurrentProcess())); + ProcessCoordinatorService.BindService(_server.ServiceBinder, service); + _server.Start(); + } + + public void KillServer() + { + _server?.Kill(); + } + + private ProcessCoordinatorServiceClient GetClient(Process recipient, int timeout) => + new(new NamedPipeChannel(".", GetPipeName(recipient), + new NamedPipeChannelOptions { ConnectionTimeout = timeout })); + + private bool TrySendMessage(Process recipient, int timeout, + Func send, [NotNullWhen(true)] out TResponse? response) + { + var client = GetClient(recipient, timeout); + try + { + response = send(client)!; + return true; + } + catch (Exception) + { + response = default; + return false; + } + } + + public bool Activate(Process recipient, int timeout) => + TrySendMessage(recipient, timeout, client => client.Activate(new ActivateRequest()), out _); + + public bool CloseWindow(Process recipient, int timeout) => + TrySendMessage(recipient, timeout, client => client.CloseWindow(new CloseWindowRequest()), out _); + + public bool ScanWithDevice(Process recipient, int timeout, string device) => + TrySendMessage(recipient, timeout, + client => client.ScanWithDevice(new ScanWithDeviceRequest { Device = device }), out _); + + public bool OpenFile(Process recipient, int timeout, params string[] paths) + { + var req = new OpenFileRequest(); + req.Path.AddRange(paths); + return TrySendMessage(recipient, timeout, client => client.OpenFile(req), out _); + } + + public bool StopSharingServer(Process recipient, int timeout) => + TrySendMessage(recipient, timeout, client => client.StopSharingServer(new StopSharingServerRequest()), + out var response) && response.Stopped; + + public bool TryTakeInstanceLock() + { + if (_instanceLock != null) + { + return true; + } + try + { + _instanceLock = new FileStream(LockFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); + using var procFile = new FileStream(ProcFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read); + procFile.SetLength(0); + using var writer = new StreamWriter(procFile, Encoding.UTF8, 1024); + writer.WriteLine(Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture)); + } + catch (Exception) + { + return false; + } + return true; + } + + public Process? GetProcessWithInstanceLock() + { + try + { + using var reader = new FileStream(ProcFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + var id = int.Parse(new StreamReader(reader).ReadLine()?.Trim() ?? "", CultureInfo.InvariantCulture); + return Process.GetProcessById(id); + } + catch (Exception) + { + return null; + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Remoting/ProcessCoordinatorService.proto b/NAPS2.Lib/Remoting/ProcessCoordinatorService.proto new file mode 100644 index 0000000000..0c5ccdcb1f --- /dev/null +++ b/NAPS2.Lib/Remoting/ProcessCoordinatorService.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package NAPS2.Remoting; + +import "google/protobuf/empty.proto"; + +service ProcessCoordinatorService { + rpc Activate (ActivateRequest) returns (google.protobuf.Empty) {} + rpc CloseWindow (CloseWindowRequest) returns (google.protobuf.Empty) {} + rpc StopSharingServer (StopSharingServerRequest) returns (StopSharingServerResponse) {} + rpc ScanWithDevice (ScanWithDeviceRequest) returns (google.protobuf.Empty) {} + rpc OpenFile (OpenFileRequest) returns (google.protobuf.Empty) {} +} + +message ActivateRequest { +} + +message CloseWindowRequest { +} + +message StopSharingServerRequest { +} + +message StopSharingServerResponse { + bool stopped = 1; +} + +message ScanWithDeviceRequest { + string device = 1; +} + +message OpenFileRequest { + repeated string path = 1; +} \ No newline at end of file diff --git a/NAPS2.Lib/Remoting/Server/ISharedDeviceManager.cs b/NAPS2.Lib/Remoting/Server/ISharedDeviceManager.cs new file mode 100644 index 0000000000..02a91c0149 --- /dev/null +++ b/NAPS2.Lib/Remoting/Server/ISharedDeviceManager.cs @@ -0,0 +1,16 @@ +using System.Collections.Immutable; + +namespace NAPS2.Remoting.Server; + +public interface ISharedDeviceManager +{ + // TODO: Maybe have Start/Stop return a task so we ensure that deregistration occurs before the app closes? + void StartSharing(); + void StopSharing(); + void AddSharedDevice(SharedDevice device); + void RemoveSharedDevice(SharedDevice device); + void ReplaceSharedDevice(SharedDevice original, SharedDevice replacement); + ImmutableList SharedDevices { get; } + event EventHandler SharingServerStopped; + void InvokeSharingServerStopped(); +} \ No newline at end of file diff --git a/NAPS2.Lib/Remoting/Server/SharedDevice.cs b/NAPS2.Lib/Remoting/Server/SharedDevice.cs new file mode 100644 index 0000000000..3457955880 --- /dev/null +++ b/NAPS2.Lib/Remoting/Server/SharedDevice.cs @@ -0,0 +1,17 @@ +using NAPS2.Scan; + +namespace NAPS2.Remoting.Server; + +public record SharedDevice +{ + public required string Name { get; init; } + public required ScanDevice Device { get; init; } + public int Port { get; init; } + public int TlsPort { get; init; } + + public virtual bool Equals(SharedDevice? other) => + other is not null && Name == other.Name && Device == other.Device; + + public override int GetHashCode() => + Name.GetHashCode() * 23 + Device.GetHashCode(); +} \ No newline at end of file diff --git a/NAPS2.Lib/Remoting/Server/SharedDeviceManager.cs b/NAPS2.Lib/Remoting/Server/SharedDeviceManager.cs new file mode 100644 index 0000000000..78e6aa6a02 --- /dev/null +++ b/NAPS2.Lib/Remoting/Server/SharedDeviceManager.cs @@ -0,0 +1,200 @@ +using System.Collections.Immutable; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using Microsoft.Extensions.Logging; +using NAPS2.Config.Model; +using NAPS2.Escl.Server; +using NAPS2.Scan; + +namespace NAPS2.Remoting.Server; + +public class SharedDeviceManager : ISharedDeviceManager +{ + private const int STARTUP_RETRY_INTERVAL = 10_000; + + private readonly ILogger _logger; + private readonly Naps2Config _config; + private readonly FileConfigScope _scope; + private readonly ScanServer _server; + private FileStream? _lockFile; + private Timer? _startTimer; + private bool _userStarted; + + public SharedDeviceManager(ScanningContext scanningContext, Naps2Config config, string sharedDevicesConfigPath) + { + _logger = scanningContext.Logger; + _config = config; + _scope = ConfigScope.File(sharedDevicesConfigPath, new ConfigStorageSerializer(), + ConfigScopeMode.ReadWrite); + _server = new ScanServer(scanningContext, new EsclServer()); + _server.SetDefaultIcon(Icons.scanner_128); + _server.SecurityPolicy = config.Get(c => c.EsclSecurityPolicy); + if (config.Get(c => c.EsclServerCertificatePath) is { } certPath && !string.IsNullOrWhiteSpace(certPath)) + { + try + { + _server.Certificate = Path.GetExtension(certPath) == ".pfx" + ? X509CertificateLoader.LoadPkcs12FromFile(certPath, null) + : X509CertificateLoader.LoadCertificateFromFile(certPath); + if (!_server.Certificate.HasPrivateKey) + { + _logger.LogDebug( + $"Certificate has no private key. Make sure it's installed in the local computer's certificate store. \"{certPath}\""); + } + } + catch (Exception ex) + { + _logger.LogError(ex, + $"Could not read X509 certificate from EsclServerCertificatePath \"{certPath}\""); + } + } + _server.InstanceId = _scope.GetOrDefault(c => c.InstanceId) ?? Guid.NewGuid(); + RegisterDevicesFromConfig(); + } + + public void StartSharing() + { + if (_config.Get(c => c.DisableScannerSharing)) + { + return; + } + lock (this) + { + if (_userStarted) return; + _userStarted = true; + if (!TryStart()) + { + // Retry after some interval in case the shared devices changed on disk or the sharing lock frees up + _startTimer ??= new Timer(_ => TryStart(), null, STARTUP_RETRY_INTERVAL, STARTUP_RETRY_INTERVAL); + } + } + } + + private bool TryStart() + { + lock (this) + { + // Only start if (1) we haven't stopped, (2) we have devices to share, and (3) we can take the exclusive + // sharing lock (so multiple NAPS2 instances don't try to share duplicates of the same devices) + if (_userStarted && SharedDevices.Any() && TakeLock()) + { + ResetStartTimer(); + _server.Start().ContinueWith(t => + _logger.LogError(t.Exception, "Error starting ScanServer"), TaskContinuationOptions.OnlyOnFaulted); + return true; + } + return false; + } + } + + public void StopSharing() + { + lock (this) + { + if (!_userStarted) return; + _userStarted = false; + ResetStartTimer(); + _server.Stop().ContinueWith(t => + _logger.LogError(t.Exception, "Error stopping ScanServer"), TaskContinuationOptions.OnlyOnFaulted); + ReleaseLock(); + } + } + + private bool TakeLock() + { + try + { + var path = Path.Combine(Paths.AppData, "sharing.lock"); + _lockFile = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Read, FileShare.None); + return true; + } + catch (IOException) + { + return false; + } + } + + private void ResetStartTimer() + { + _startTimer?.Dispose(); + _startTimer = null; + } + + private void ReleaseLock() + { + _lockFile?.Dispose(); + _lockFile = null; + } + + public void AddSharedDevice(SharedDevice device) + { + var devices = SharedDevices; + if (devices.Contains(device)) + { + // Ignore adding duplicates + return; + } + devices = devices.Add(device); + SharedDevices = devices; + RegisterOnServer(device); + if (_startTimer != null) + { + // If startup was deferred, don't wait for the timer before retrying since we adding a device might allow + // us to start + TryStart(); + } + } + + public void RemoveSharedDevice(SharedDevice device) + { + var devices = SharedDevices; + devices = devices.Remove(device); + SharedDevices = devices; + UnregisterOnServer(device); + } + + public void ReplaceSharedDevice(SharedDevice original, SharedDevice replacement) + { + var devices = SharedDevices; + if (original != replacement && devices.Contains(replacement)) + { + // Delete if the new config is a duplicate + RemoveSharedDevice(original); + return; + } + devices = devices.Replace(original, replacement); + SharedDevices = devices; + UnregisterOnServer(original); + RegisterOnServer(replacement); + } + + public ImmutableList SharedDevices + { + get => _scope.GetOr(c => c.SharedDevices, ImmutableList.Empty); + private set + { + if (!_scope.Has(c => c.InstanceId)) + { + _scope.Set(c => c.InstanceId, _server.InstanceId); + } + _scope.Set(c => c.SharedDevices, value); + } + } + + public event EventHandler? SharingServerStopped; + public void InvokeSharingServerStopped() => SharingServerStopped?.Invoke(this, EventArgs.Empty); + + private void RegisterDevicesFromConfig() + { + foreach (var device in SharedDevices) + { + RegisterOnServer(device); + } + } + + private void RegisterOnServer(SharedDevice device) => + _server.RegisterDevice(device.Device, device.Name, device.Port, device.TlsPort); + + private void UnregisterOnServer(SharedDevice device) => + _server.UnregisterDevice(device.Device, device.Name); +} \ No newline at end of file diff --git a/NAPS2.Lib/Remoting/Server/SharingConfig.cs b/NAPS2.Lib/Remoting/Server/SharingConfig.cs new file mode 100644 index 0000000000..625007b51a --- /dev/null +++ b/NAPS2.Lib/Remoting/Server/SharingConfig.cs @@ -0,0 +1,16 @@ +using System.Collections.Immutable; +using NAPS2.Config.Model; + +namespace NAPS2.Remoting.Server; + +[Config] +public class SharingConfig +{ + /// + /// A unique ID for the NAPS2 instance, so that if you have the same model of scanner connected to different + /// computers, they still will have unique derived UUIDs. + /// + public Guid? InstanceId { get; set; } + + public ImmutableList SharedDevices { get; set; } = []; +} \ No newline at end of file diff --git a/NAPS2.Lib/Scan/Batch/BatchScanPerformer.cs b/NAPS2.Lib/Scan/Batch/BatchScanPerformer.cs index 4f8ac4f31b..d54a05efd4 100644 --- a/NAPS2.Lib/Scan/Batch/BatchScanPerformer.cs +++ b/NAPS2.Lib/Scan/Batch/BatchScanPerformer.cs @@ -4,22 +4,22 @@ using NAPS2.EtoForms.Ui; using NAPS2.ImportExport; using NAPS2.ImportExport.Images; -using NAPS2.ImportExport.Pdf; using NAPS2.Ocr; +using NAPS2.Pdf; namespace NAPS2.Scan.Batch; public class BatchScanPerformer : IBatchScanPerformer { private readonly IScanPerformer _scanPerformer; - private readonly IPdfExporter _pdfExporter; + private readonly PdfExporter _pdfExporter; private readonly IOperationFactory _operationFactory; private readonly IFormFactory _formFactory; private readonly Naps2Config _config; private readonly IProfileManager _profileManager; private readonly ThumbnailController _thumbnailController; - public BatchScanPerformer(IScanPerformer scanPerformer, IPdfExporter pdfExporter, + public BatchScanPerformer(IScanPerformer scanPerformer, PdfExporter pdfExporter, IOperationFactory operationFactory, IFormFactory formFactory, Naps2Config config, IProfileManager profileManager, ThumbnailController thumbnailController) @@ -44,7 +44,7 @@ public async Task PerformBatchScan(BatchSettings settings, IFormBase batchForm, private class BatchState { private readonly IScanPerformer _scanPerformer; - private readonly IPdfExporter _pdfExporter; + private readonly PdfExporter _pdfExporter; private readonly IOperationFactory _operationFactory; private readonly IFormFactory _formFactory; private readonly Naps2Config _config; @@ -60,7 +60,7 @@ private class BatchState private ScanParams _scanParams; private List> _scans; - public BatchState(IScanPerformer scanPerformer, IPdfExporter pdfExporter, IOperationFactory operationFactory, + public BatchState(IScanPerformer scanPerformer, PdfExporter pdfExporter, IOperationFactory operationFactory, IFormFactory formFactory, Naps2Config config, IProfileManager profileManager, ThumbnailController thumbnailController, BatchSettings settings, Action progressCallback, CancellationToken cancelToken, IFormBase batchForm, @@ -93,7 +93,7 @@ public BatchState(IScanPerformer scanPerformer, IPdfExporter pdfExporter, IOpera OcrCancelToken = _cancelToken, ThumbnailSize = thumbnailController.RenderSize }; - _scans = new List>(); + _scans = []; } public async Task Do() @@ -273,10 +273,8 @@ private async Task Save(Placeholders placeholders, int i, List i { subPath = placeholders.Substitute(subPath, true, 0, 1); } - // TODO: Make copies of images and dispose try { - // TODO: This is broken due to not accessing the child fields directly var exportParams = new PdfExportParams( _config.Get(c => c.PdfSettings.Metadata), _config.Get(c => c.PdfSettings.Encryption), @@ -294,7 +292,7 @@ private async Task Save(Placeholders placeholders, int i, List i else { var op = _operationFactory.Create(); - op.Start(subPath, placeholders, images, _config.Get(c => c.ImageSettings), true); + op.Start(subPath, placeholders, images, _config.Get(c => c.ImageSettings), batch: true); await op.Success; } } diff --git a/NAPS2.Lib/Scan/DeviceCapsCache.cs b/NAPS2.Lib/Scan/DeviceCapsCache.cs new file mode 100644 index 0000000000..44881f4d47 --- /dev/null +++ b/NAPS2.Lib/Scan/DeviceCapsCache.cs @@ -0,0 +1,130 @@ +using Eto.Drawing; +using NAPS2.Escl.Client; +using NAPS2.EtoForms; + +namespace NAPS2.Scan; + +public class DeviceCapsCache +{ + private readonly Dictionary _capsCache = new(); + private readonly Dictionary _iconCache = new(); + + private readonly IScanPerformer _scanPerformer; + private readonly ImageContext _imageContext; + private readonly Naps2Config _config; + + public DeviceCapsCache(IScanPerformer scanPerformer, ImageContext imageContext, Naps2Config config) + { + _scanPerformer = scanPerformer; + _imageContext = imageContext; + _config = config; + } + + public ScanCaps? GetCachedCaps(ScanProfile profile) + { + if (profile.DriverName == null || profile.Device == null) return null; + var key = GetDeviceKey(profile); + lock (_capsCache) + { + return _capsCache.Get(key); + } + } + + public async Task QueryCaps(ScanProfile profile) + { + if (profile.DriverName == null || profile.Device == null) return null; + var key = GetDeviceKey(profile); + bool contains; + lock (_capsCache) + { + contains = _capsCache.ContainsKey(key); + } + if (!contains) + { + try + { + var caps = await _scanPerformer.GetCaps(profile); + lock (_capsCache) + { + _capsCache[key] = caps; + } + } + catch (Exception ex) + { + Log.DebugException("Error getting device caps", ex); + return null; + } + } + lock (_capsCache) + { + return _capsCache[key]; + } + } + + public Image? GetCachedIcon(ScanDevice? device) => GetCachedIcon(device?.IconUri); + + public Image? GetCachedIcon(string? iconUri) + { + if (iconUri == null) return null; + lock (_iconCache) + { + return _iconCache.Get(iconUri); + } + } + + public async Task LoadIcon(ScanDevice? device) => await LoadIcon(device?.IconUri); + + public async Task LoadIcon(string? iconUri) + { + if (iconUri == null) return null; + bool contains; + lock (_iconCache) + { + contains = _iconCache.ContainsKey(iconUri); + } + if (!contains) + { + try + { + var icon = await DoLoadIcon(iconUri); + lock (_iconCache) + { + _iconCache[iconUri] = icon; + } + } + catch (Exception ex) + { + Log.DebugException($"Error loading device icon from {iconUri}", ex); + return null; + } + } + lock (_iconCache) + { + return _iconCache[iconUri]; + } + } + + private async Task DoLoadIcon(string iconUri) + { + IMemoryImage? image; + if (iconUri.StartsWith("file://")) + { + string path = new Uri(iconUri).LocalPath; + image = _imageContext.Load(path); + } + else + { + var client = EsclClient.GetHttpClient(_config.Get(c => c.EsclSecurityPolicy)); + var imageBytes = await client.GetByteArrayAsync(iconUri); + image = _imageContext.Load(imageBytes); + } + return image.ToEtoImage(); + } + + private DeviceKey GetDeviceKey(ScanProfile profile) + { + return new DeviceKey(profile.DriverName!, profile.Device!.ID, profile.WiaVersion, profile.TwainImpl); + } + + private record DeviceKey(string DriverName, string DeviceId, WiaApiVersion WiaVersion, TwainImpl TwainImpl); +} \ No newline at end of file diff --git a/NAPS2.Lib/Scan/DeviceChoice.cs b/NAPS2.Lib/Scan/DeviceChoice.cs new file mode 100644 index 0000000000..c711845f14 --- /dev/null +++ b/NAPS2.Lib/Scan/DeviceChoice.cs @@ -0,0 +1,18 @@ +namespace NAPS2.Scan; + +public record DeviceChoice +{ + public static DeviceChoice ForDevice(ScanDevice device) => new() { Device = device, Driver = device.Driver }; + + public static DeviceChoice ForAlwaysAsk(Driver driver) => new() { AlwaysAsk = true, Driver = driver }; + + public static readonly DeviceChoice None = new(); + + private DeviceChoice() + { + } + + public ScanDevice? Device { get; private init; } + public Driver Driver { get; private init; } + public bool AlwaysAsk { get; private init; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Exceptions/NoDevicesFoundException.cs b/NAPS2.Lib/Scan/Exceptions/NoDevicesFoundException.cs similarity index 100% rename from NAPS2.Sdk/Scan/Exceptions/NoDevicesFoundException.cs rename to NAPS2.Lib/Scan/Exceptions/NoDevicesFoundException.cs diff --git a/NAPS2.Lib/Scan/IDevicePrompt.cs b/NAPS2.Lib/Scan/IDevicePrompt.cs new file mode 100644 index 0000000000..3d681b97c1 --- /dev/null +++ b/NAPS2.Lib/Scan/IDevicePrompt.cs @@ -0,0 +1,6 @@ +namespace NAPS2.Scan; + +public interface IDevicePrompt +{ + public Task PromptForDevice(ScanOptions options, bool allowAlwaysAsk); +} \ No newline at end of file diff --git a/NAPS2.Lib/Scan/IScanPerformer.cs b/NAPS2.Lib/Scan/IScanPerformer.cs index 653899f086..eb6e187aed 100644 --- a/NAPS2.Lib/Scan/IScanPerformer.cs +++ b/NAPS2.Lib/Scan/IScanPerformer.cs @@ -8,7 +8,11 @@ namespace NAPS2.Scan; /// public interface IScanPerformer { - Task PromptForDevice(ScanProfile scanProfile, IntPtr dialogParent = default); + Task PromptForDevice(ScanProfile scanProfile, bool allowAlwaysAsk = true, IntPtr dialogParent = default); + + IAsyncEnumerable GetDevices(ScanProfile scanProfile, CancellationToken cancelToken = default); + + Task GetCaps(ScanProfile scanProfile, CancellationToken cancelToken = default); IAsyncEnumerable PerformScan(ScanProfile scanProfile, ScanParams scanParams, IntPtr dialogParent = default, CancellationToken cancelToken = default); } \ No newline at end of file diff --git a/NAPS2.Lib/Scan/PaperSourceProfileCaps.cs b/NAPS2.Lib/Scan/PaperSourceProfileCaps.cs new file mode 100644 index 0000000000..b3ec57fd54 --- /dev/null +++ b/NAPS2.Lib/Scan/PaperSourceProfileCaps.cs @@ -0,0 +1,35 @@ +using NAPS2.Serialization; + +namespace NAPS2.Scan; + +public class PaperSourceProfileCaps +{ + static PaperSourceProfileCaps() + { + XmlSerializer.RegisterCustomSerializer(new Serializer()); + } + + public List? Values { get; set; } + + private class Serializer : CustomXmlSerializer + { + protected override void Serialize(PaperSourceProfileCaps obj, XElement element) + { + if (obj.Values != null) + { + element.Value = string.Join(",", obj.Values.Select(x => x.ToString())); + } + } + + protected override PaperSourceProfileCaps Deserialize(XElement element) + { + var caps = new PaperSourceProfileCaps(); + if (!string.IsNullOrWhiteSpace(element.Value)) + { + caps.Values = element.Value.Split(',') + .Select(x => (ScanSource) Enum.Parse(typeof(ScanSource), x)).ToList(); + } + return caps; + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Scan/PerSourceProfileCaps.cs b/NAPS2.Lib/Scan/PerSourceProfileCaps.cs new file mode 100644 index 0000000000..a83f9dccb8 --- /dev/null +++ b/NAPS2.Lib/Scan/PerSourceProfileCaps.cs @@ -0,0 +1,46 @@ +using System.Globalization; +using NAPS2.Serialization; + +namespace NAPS2.Scan; + +public class PerSourceProfileCaps +{ + static PerSourceProfileCaps() + { + XmlSerializer.RegisterCustomSerializer(new Serializer()); + } + + public PageSize? ScanArea { get; set; } + public List? Resolutions { get; set; } + + private class Serializer : CustomXmlSerializer + { + protected override void Serialize(PerSourceProfileCaps obj, XElement element) + { + if (obj.ScanArea != null) + { + element.Add(new XElement("ScanArea", obj.ScanArea)); + } + if (obj.Resolutions is { Count: > 0 }) + { + element.Add(new XElement("Resolutions", + string.Join(",", obj.Resolutions.Select(x => x.ToString(CultureInfo.InvariantCulture))))); + } + } + + protected override PerSourceProfileCaps Deserialize(XElement element) + { + var caps = new PerSourceProfileCaps(); + if (element.Element("ScanArea") is { Value.Length: > 0 } scanArea) + { + caps.ScanArea = PageSize.Parse(scanArea.Value); + } + if (element.Element("Resolutions") is { Value.Length: > 0 } resolutions) + { + caps.Resolutions = resolutions.Value.Split(',') + .Select(x => int.Parse(x, NumberStyles.Integer, CultureInfo.InvariantCulture)).ToList(); + } + return caps; + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Scan/ScanOperation.cs b/NAPS2.Lib/Scan/ScanOperation.cs index 8d7319bb3a..4a9ca859c0 100644 --- a/NAPS2.Lib/Scan/ScanOperation.cs +++ b/NAPS2.Lib/Scan/ScanOperation.cs @@ -27,6 +27,10 @@ public ScanOperation(ScanOptions options) public void Progress(int current, int total) { + if (current > 0 && total > 0) + { + Status.IndeterminateProgress = false; + } Status.CurrentProgress = current; Status.MaxProgress = total; InvokeStatusChanged(); diff --git a/NAPS2.Sdk/Scan/ScanParams.cs b/NAPS2.Lib/Scan/ScanParams.cs similarity index 100% rename from NAPS2.Sdk/Scan/ScanParams.cs rename to NAPS2.Lib/Scan/ScanParams.cs diff --git a/NAPS2.Lib/Scan/ScanPerformer.cs b/NAPS2.Lib/Scan/ScanPerformer.cs index 864b24e6cd..5af8032c8b 100644 --- a/NAPS2.Lib/Scan/ScanPerformer.cs +++ b/NAPS2.Lib/Scan/ScanPerformer.cs @@ -5,6 +5,7 @@ using NAPS2.Scan.Exceptions; using NAPS2.Scan.Internal; #if !MAC +using NAPS2.Scan.Internal.Wia; using NAPS2.Wia; #endif @@ -12,6 +13,22 @@ namespace NAPS2.Scan; internal class ScanPerformer : IScanPerformer { + public static Driver ParseDriver(string? value) + { + return value switch + { + DriverNames.WIA => Driver.Wia, + DriverNames.SANE => Driver.Sane, + DriverNames.TWAIN => Driver.Twain, + DriverNames.ESCL => Driver.Escl, + DriverNames.APPLE => Driver.Apple, + _ => Driver.Default + }; + } + + public static string SystemDefaultDriverName => + ScanOptionsValidator.SystemDefaultDriver.ToString().ToLowerInvariant(); + private readonly ScanningContext _scanningContext; private readonly IDevicePrompt _devicePrompt; private readonly Naps2Config _config; @@ -40,16 +57,40 @@ public ScanPerformer(IDevicePrompt devicePrompt, Naps2Config config, OperationPr _ocrOperationManager = ocrOperationManager; } - public async Task PromptForDevice(ScanProfile scanProfile, IntPtr dialogParent = default) + public async Task PromptForDevice(ScanProfile scanProfile, bool allowAlwaysAsk, + IntPtr dialogParent = default) + { + try + { + var options = BuildOptions(scanProfile, new ScanParams(), dialogParent, true); + return await _devicePrompt.PromptForDevice(options, allowAlwaysAsk); + } + catch (Exception error) + { + HandleError(error); + return DeviceChoice.None; + } + } + + public IAsyncEnumerable GetDevices(ScanProfile scanProfile, CancellationToken cancelToken = default) + { + var options = BuildOptions(scanProfile, new ScanParams(), IntPtr.Zero, true); + var controller = CreateScanController(new ScanParams()); + return controller.GetDevices(options, cancelToken); + } + + public async Task GetCaps(ScanProfile scanProfile, CancellationToken cancelToken = default) { - var options = BuildOptions(scanProfile, new ScanParams(), dialogParent); - return await PromptForDevice(options); + var options = BuildOptions(scanProfile, new ScanParams(), IntPtr.Zero, false); + options.Device = scanProfile.Device?.ToScanDevice(options.Driver); + var controller = CreateScanController(new ScanParams()); + return await controller.GetCaps(options, cancelToken); } public async IAsyncEnumerable PerformScan(ScanProfile scanProfile, ScanParams scanParams, IntPtr dialogParent = default, [EnumeratorCancellation] CancellationToken cancelToken = default) { - var options = BuildOptions(scanProfile, scanParams, dialogParent); + var options = BuildOptions(scanProfile, scanParams, dialogParent, false); // Make sure we get a real driver value (not just "Default") options = _scanOptionsValidator.ValidateAll(options, _scanningContext, false); @@ -59,14 +100,32 @@ public async IAsyncEnumerable PerformScan(ScanProfile scanProfil yield break; } - var localPostProcessor = new LocalPostProcessor(_scanningContext, ConfigureOcrController(scanParams)); - var controller = new ScanController(_scanningContext, localPostProcessor, _scanOptionsValidator, - _scanBridgeFactory); + var controller = CreateScanController(scanParams); var op = new ScanOperation(options); controller.PageStart += (sender, args) => op.NextPage(args.PageNumber); - controller.ScanEnd += (sender, args) => op.Completed(); - controller.ScanError += (sender, args) => HandleError(args.Exception); + controller.ScanEnd += (sender, args) => + { + // Close the progress window before showing the error dialog + op.Completed(); + if (args.Error != null) + { + HandleError(args.Error); + } + }; + controller.DeviceUriChanged += (sender, args) => + { + if (scanProfile.Device != null) + { + scanProfile.Device = scanProfile.Device with + { + IconUri = args.IconUri, + ConnectionUri = args.ConnectionUri + }; + _profileManager.Save(); + } + }; + controller.PropagateErrors = false; TranslateProgress(controller, op); ShowOperation(op, options, scanParams); @@ -74,7 +133,7 @@ public async IAsyncEnumerable PerformScan(ScanProfile scanProfil var images = controller.Scan(options, op.CancelToken); - if (scanProfile.EnableAutoSave && scanProfile.AutoSaveSettings != null) + if (scanProfile.EnableAutoSave && scanProfile.AutoSaveSettings != null && !scanParams.NoAutoSave) { images = _autoSaver.Save(scanProfile.AutoSaveSettings, images); } @@ -105,6 +164,14 @@ public async IAsyncEnumerable PerformScan(ScanProfile scanProfil } } + private ScanController CreateScanController(ScanParams scanParams) + { + var localPostProcessor = new LocalPostProcessor(_scanningContext, ConfigureOcrController(scanParams)); + var controller = new ScanController(_scanningContext, localPostProcessor, _scanOptionsValidator, + _scanBridgeFactory); + return controller; + } + private OcrController ConfigureOcrController(ScanParams scanParams) { OcrController ocrController = new OcrController(_scanningContext); @@ -137,24 +204,22 @@ private void HandleError(Exception error) private void ShowOperation(ScanOperation op, ScanOptions scanOptions, ScanParams scanParams) { bool isWia10 = scanOptions.Driver == Driver.Wia && scanOptions.WiaOptions.WiaApiVersion == WiaApiVersion.Wia10; - if (scanParams.NoUI || scanOptions.UseNativeUI && !isWia10) + bool showingTwainProgress = scanOptions.Driver == Driver.Twain && scanOptions.TwainOptions.ShowProgress; + if (scanParams.NoUI || scanOptions.UseNativeUI && !isWia10 || showingTwainProgress) { return; } - Task.Run(() => + Invoker.Current.InvokeDispatch(() => { - Invoker.Current.SafeInvoke(() => + if (scanParams.Modal) { - if (scanParams.Modal) - { - _operationProgress.ShowModalProgress(op); - } - else - { - _operationProgress.ShowBackgroundProgress(op); - } - }); + _operationProgress.ShowModalProgress(op); + } + else + { + _operationProgress.ShowBackgroundProgress(op); + } }); } @@ -168,14 +233,12 @@ private void TranslateProgress(ScanController controller, ScanOperation op) (_, args) => op.Progress((int) Math.Round(args.Value * 1000), 1000); } - private ScanOptions BuildOptions(ScanProfile scanProfile, ScanParams scanParams, IntPtr dialogParent) + private ScanOptions BuildOptions(ScanProfile scanProfile, ScanParams scanParams, IntPtr dialogParent, + bool isDeviceQuery) { var options = new ScanOptions { - Driver = scanProfile.DriverName == DriverNames.WIA ? Driver.Wia - : scanProfile.DriverName == DriverNames.SANE ? Driver.Sane - : scanProfile.DriverName == DriverNames.TWAIN ? Driver.Twain - : Driver.Default, + Driver = ParseDriver(scanProfile.DriverName), WiaOptions = { WiaApiVersion = scanProfile.WiaVersion, @@ -183,29 +246,35 @@ private ScanOptions BuildOptions(ScanProfile scanProfile, ScanParams scanParams, }, TwainOptions = { - Adapter = scanProfile.TwainImpl == TwainImpl.Legacy ? TwainAdapter.Legacy : TwainAdapter.NTwain, - Dsm = scanProfile.TwainImpl == TwainImpl.X64 - ? TwainDsm.NewX64 - : scanProfile.TwainImpl == TwainImpl.OldDsm || scanProfile.TwainImpl == TwainImpl.Legacy - ? TwainDsm.Old - : TwainDsm.New, - TransferMode = scanProfile.TwainImpl == TwainImpl.MemXfer - ? TwainTransferMode.Memory - : TwainTransferMode.Native, + Dsm = scanProfile.TwainImpl switch + { + TwainImpl.X64 => TwainDsm.NewX64, + TwainImpl.OldDsm or TwainImpl.Legacy => TwainDsm.Old, + _ => TwainDsm.New + }, + TransferMode = scanProfile.TwainImpl switch + { + TwainImpl.Default or TwainImpl.X64 => TwainTransferMode.Default, + TwainImpl.MemXfer => TwainTransferMode.Memory, + _ => TwainTransferMode.Native + }, + ShowProgress = scanProfile.TwainProgress, IncludeWiaDevices = false - // TODO: Consider adding a user option for TwainOptions.ShowProgress instead of our progress window }, SaneOptions = { - KeyValueOptions = scanProfile.KeyValueOptions != null - ? new KeyValueScanOptions(scanProfile.KeyValueOptions) - : new KeyValueScanOptions() + // We use a worker process for SANE so we should clean up after each operation + KeepInitialized = false }, - NetworkOptions = + EsclOptions = { - Ip = scanProfile.ProxyConfig?.Ip, - Port = scanProfile.ProxyConfig?.Port + SecurityPolicy = _config.Get(c => c.EsclSecurityPolicy), + SearchTimeout = isDeviceQuery ? 60_000 : 5000 }, + KeyValueOptions = scanProfile.KeyValueOptions != null + ? new KeyValueScanOptions(scanProfile.KeyValueOptions) + : new KeyValueScanOptions(), + ExcludeLocalIPs = true, BarcodeDetectionOptions = { DetectBarcodes = scanParams.DetectPatchT || @@ -215,9 +284,10 @@ private ScanOptions BuildOptions(ScanProfile scanProfile, ScanParams scanParams, OcrParams = scanParams.OcrParams ?? OcrParams.Empty, Brightness = scanProfile.Brightness, Contrast = scanProfile.Contrast, - Dpi = scanProfile.Resolution.ToIntDpi(), + Dpi = scanProfile.Resolution.Dpi, Quality = scanProfile.Quality, AutoDeskew = scanProfile.AutoDeskew, + RotateDegrees = scanProfile.RotateDegrees, BitDepth = scanProfile.BitDepth.ToBitDepth(), DialogParent = dialogParent, MaxQuality = scanProfile.MaxQuality, @@ -234,7 +304,7 @@ private ScanOptions BuildOptions(ScanProfile scanProfile, ScanParams scanParams, StretchToPageSize = scanProfile.ForcePageSize, UseNativeUI = scanProfile.UseNativeUI, Device = null, // Set after - PageSize = null, // Set after + PageSize = null // Set after }; var pageDimensions = scanProfile.PageSize.PageDimensions() ?? scanProfile.CustomPageSize; @@ -254,7 +324,7 @@ private async Task PopulateDevice(ScanProfile scanProfile, ScanOptions opt // If a device wasn't specified, prompt the user to pick one if (string.IsNullOrEmpty(scanProfile.Device?.ID)) { - options.Device = await PromptForDevice(options); + options.Device = (await _devicePrompt.PromptForDevice(options, false)).Device; if (options.Device == null) { return false; @@ -263,52 +333,15 @@ private async Task PopulateDevice(ScanProfile scanProfile, ScanOptions opt // Persist the device in the profile if configured to do so if (_config.Get(c => c.AlwaysRememberDevice)) { - scanProfile.Device = options.Device; + scanProfile.Device = ScanProfileDevice.FromScanDevice(options.Device); _profileManager.Save(); } } else { - options.Device = scanProfile.Device; + options.Device = scanProfile.Device?.ToScanDevice(options.Driver); } return true; } - - private async Task PromptForDevice(ScanOptions options) - { -#if !MAC - // TODO: Not sure how best to handle this for console - if (options.Driver == Driver.Wia) - { - // WIA has a nice built-in device selection dialog, so use it - using var deviceManager = new WiaDeviceManager((WiaVersion) options.WiaOptions.WiaApiVersion); - try - { - var wiaDevice = Invoker.Current.InvokeGet(() => deviceManager.PromptForDevice(options.DialogParent)); - if (wiaDevice == null) - { - return null; - } - return new ScanDevice(wiaDevice.Id(), wiaDevice.Name()); - } - catch (WiaException ex) when (ex.ErrorCode == WiaErrorCodes.NO_DEVICE_AVAILABLE) - { - throw new NoDevicesFoundException(); - } - catch (WiaException ex) - { - throw new ScanDriverUnknownException(ex); - } - } -#endif - - // Other drivers do not, so use a generic dialog - var deviceList = await new ScanController(_scanningContext).GetDeviceList(options); - if (deviceList.Count == 0) - { - throw new NoDevicesFoundException(); - } - return Invoker.Current.InvokeGet(() => _devicePrompt.PromptForDevice(deviceList, options.DialogParent)); - } } \ No newline at end of file diff --git a/NAPS2.Lib/Scan/ScanProfile.cs b/NAPS2.Lib/Scan/ScanProfile.cs index 5b45779beb..d6ae182b63 100644 --- a/NAPS2.Lib/Scan/ScanProfile.cs +++ b/NAPS2.Lib/Scan/ScanProfile.cs @@ -3,6 +3,7 @@ using System.Reflection; using System.Xml.Serialization; using NAPS2.ImportExport; +using NAPS2.Serialization; namespace NAPS2.Scan; @@ -19,18 +20,23 @@ public ScanProfile() BitDepth = ScanBitDepth.C24Bit; PageAlign = ScanHorizontalAlign.Right; PageSize = ScanPageSize.Letter; - Resolution = ScanDpi.Dpi200; + Resolution.Dpi = 200; PaperSource = ScanSource.Glass; Quality = 75; BlankPageWhiteThreshold = 70; - BlankPageCoverageThreshold = 25; + BlankPageCoverageThreshold = 15; WiaDelayBetweenScansSeconds = 2.0; } public ScanProfile Clone() { - var profile = (ScanProfile) MemberwiseClone(); - return profile; + // Easy deep copy. Ideally we'd do this in a more efficient way. + var copy = this.ToXml().FromXml(); + // Copy XmlIgnore properties + copy.UpgradedFrom = UpgradedFrom; + copy.IsLocked = IsLocked; + copy.IsDeviceLocked = IsDeviceLocked; + return copy; } public override string ToString() => DisplayName; @@ -47,13 +53,11 @@ public ScanProfile Clone() [XmlIgnore] public bool IsDeviceLocked { get; set; } - public ScanDevice? Device { get; set; } + public ScanProfileDevice? Device { get; set; } - public string? DriverName { get; set; } - - public ScanProxyConfig? ProxyConfig { get; set; } + public ScanProfileCaps? Caps { get; set; } - public string? ProxyDriverName { get; set; } + public string? DriverName { get; set; } public string DisplayName { get; set; } = ""; @@ -81,7 +85,7 @@ public ScanProfile Clone() public PageDimensions? CustomPageSize { get; set; } - public ScanDpi Resolution { get; set; } + public ScanResolution Resolution { get; set; } = new(); public ScanSource PaperSource { get; set; } @@ -93,6 +97,8 @@ public ScanProfile Clone() public bool AutoDeskew { get; set; } + public double RotateDegrees { get; set; } + public bool BrightnessContrastAfterScan { get; set; } public bool ForcePageSize { get; set; } @@ -101,6 +107,8 @@ public ScanProfile Clone() public TwainImpl TwainImpl { get; set; } + public bool TwainProgress { get; set; } + public bool ExcludeBlankPages { get; set; } public int BlankPageWhiteThreshold { get; set; } @@ -122,13 +130,6 @@ public ScanProfile Clone() public KeyValueScanOptions? KeyValueOptions { get; set; } } -public record ScanProxyConfig -{ - public string Name { get; init; } = ""; - public string Ip { get; init; } = ""; - public int? Port { get; init; } -} - /// /// User configuration for the Auto Save feature, which saves to a file immediately after scanning. /// @@ -145,8 +146,11 @@ public record AutoSaveSettings /// public enum TwainImpl { + // The default is currently equivalent ot MemXfer [LocalizedDescription(typeof(SettingsResources), "TwainImpl_Default")] Default, + [LocalizedDescription(typeof(SettingsResources), "TwainImpl_NativeXfer")] + NativeXfer, [LocalizedDescription(typeof(SettingsResources), "TwainImpl_MemXfer")] MemXfer, [LocalizedDescription(typeof(SettingsResources), "TwainImpl_OldDsm")] @@ -188,22 +192,16 @@ public enum ScanBitDepth /// public enum ScanDpi { - [LocalizedDescription(typeof(SettingsResources), "Dpi_100")] Dpi100, - [LocalizedDescription(typeof(SettingsResources), "Dpi_150")] Dpi150, - [LocalizedDescription(typeof(SettingsResources), "Dpi_200")] Dpi200, - [LocalizedDescription(typeof(SettingsResources), "Dpi_300")] Dpi300, - [LocalizedDescription(typeof(SettingsResources), "Dpi_400")] Dpi400, - [LocalizedDescription(typeof(SettingsResources), "Dpi_600")] Dpi600, - [LocalizedDescription(typeof(SettingsResources), "Dpi_800")] Dpi800, - [LocalizedDescription(typeof(SettingsResources), "Dpi_1200")] - Dpi1200 + Dpi1200, + Dpi2400, + Dpi4800 } /// @@ -325,6 +323,11 @@ public static class ScanEnumExtensions return attrs.Select(x => x.PageDimensions).SingleOrDefault(); } + public static PageSize ToPageSize(this PageDimensions pageDimensions) + { + return new PageSize(pageDimensions.Width, pageDimensions.Height, (PageSizeUnit) pageDimensions.Unit); + } + public static int ToIntDpi(this ScanDpi enumValue) { switch (enumValue) @@ -345,6 +348,10 @@ public static int ToIntDpi(this ScanDpi enumValue) return 800; case ScanDpi.Dpi1200: return 1200; + case ScanDpi.Dpi2400: + return 2400; + case ScanDpi.Dpi4800: + return 4800; default: throw new ArgumentException(); } @@ -372,7 +379,7 @@ public static string Description(this Enum enumValue) object[] attrs = enumValue.GetType().GetField(enumValue.ToString())!.GetCustomAttributes(typeof(DescriptionAttribute), false); - return attrs.Cast().Select(x => x.Description).Single(); + return attrs.Cast().Select(x => x.Description).SingleOrDefault() ?? ""; } public static BitDepth ToBitDepth(this ScanBitDepth bitDepth) diff --git a/NAPS2.Lib/Scan/ScanProfileCaps.cs b/NAPS2.Lib/Scan/ScanProfileCaps.cs new file mode 100644 index 0000000000..611526670e --- /dev/null +++ b/NAPS2.Lib/Scan/ScanProfileCaps.cs @@ -0,0 +1,10 @@ +namespace NAPS2.Scan; + +public class ScanProfileCaps +{ + public PaperSourceProfileCaps? PaperSources { get; set; } + public bool? FeederCheck { get; set; } + public PerSourceProfileCaps? Glass { get; set; } + public PerSourceProfileCaps? Feeder { get; set; } + public PerSourceProfileCaps? Duplex { get; set; } +} \ No newline at end of file diff --git a/NAPS2.Lib/Scan/ScanProfileDevice.cs b/NAPS2.Lib/Scan/ScanProfileDevice.cs new file mode 100644 index 0000000000..78f6a99ab7 --- /dev/null +++ b/NAPS2.Lib/Scan/ScanProfileDevice.cs @@ -0,0 +1,27 @@ +using System.Diagnostics.CodeAnalysis; + +namespace NAPS2.Scan; + +// ScanDevice used to only have ID and Name, but now it has Driver too. We need ScanProfileDevice for backwards compat +// when serializing. +public record ScanProfileDevice(string ID, string Name, string? IconUri = null, string? ConnectionUri = null) +{ + [return: NotNullIfNotNull("device")] + public static ScanProfileDevice? FromScanDevice(ScanDevice? device) + { + if (device == null) + { + return null; + } + return new ScanProfileDevice(device.ID, device.Name, device.IconUri, device.ConnectionUri); + } + + public ScanDevice ToScanDevice(Driver driver) + { + return new ScanDevice(driver, ID, Name, IconUri, ConnectionUri); + } + + private ScanProfileDevice() : this("", "") + { + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Scan/ScanResolution.cs b/NAPS2.Lib/Scan/ScanResolution.cs new file mode 100644 index 0000000000..41e3503ecf --- /dev/null +++ b/NAPS2.Lib/Scan/ScanResolution.cs @@ -0,0 +1,34 @@ +using System.Globalization; +using NAPS2.Serialization; + +namespace NAPS2.Scan; + +public class ScanResolution +{ + static ScanResolution() + { + XmlSerializer.RegisterCustomSerializer(new Serializer()); + } + + public int Dpi { get; set; } + + // For backwards-compatibility reasons, we serialize this as "Dpi100" instead of just serializing an integer. + private class Serializer : CustomXmlSerializer + { + protected override void Serialize(ScanResolution obj, XElement element) + { + element.Value = $"Dpi{obj.Dpi.ToString(CultureInfo.InvariantCulture)}"; + } + + protected override ScanResolution Deserialize(XElement element) + { + var value = element.Value; + if (value.StartsWith("Dpi") && int.TryParse(value.Substring(3), NumberStyles.Integer, + CultureInfo.InvariantCulture, out int dpi)) + { + return new ScanResolution { Dpi = dpi }; + } + return new ScanResolution(); + } + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Update/UpdateChecker.cs b/NAPS2.Lib/Update/UpdateChecker.cs index 83fa65abde..a7f7508880 100644 --- a/NAPS2.Lib/Update/UpdateChecker.cs +++ b/NAPS2.Lib/Update/UpdateChecker.cs @@ -44,12 +44,12 @@ public UpdateChecker(IOperationFactory operationFactory, OperationProgress opera var updateFile = release["files"]!.Value(UPDATE_FILE_EXT); if (updateFile == null) continue; - var sha1 = updateFile.Value("sha1"); - var sig = updateFile.Value("sig"); - if (sha1 == null || sig == null) continue; + var sha256 = updateFile.Value("sha256"); + var sig256 = updateFile.Value("sig256"); + if (sha256 == null || sig256 == null) continue; - return new UpdateInfo(versionName, updateFile.Value("url")!, Convert.FromBase64String(sha1), - Convert.FromBase64String(sig)); + return new UpdateInfo(versionName, updateFile.Value("url")!, Convert.FromBase64String(sha256), + Convert.FromBase64String(sig256)); } return null; } diff --git a/NAPS2.Lib/Update/UpdateInfo.cs b/NAPS2.Lib/Update/UpdateInfo.cs index 0bd14f86f5..1c77f2820a 100644 --- a/NAPS2.Lib/Update/UpdateInfo.cs +++ b/NAPS2.Lib/Update/UpdateInfo.cs @@ -2,19 +2,19 @@ public class UpdateInfo { - public UpdateInfo(string name, string downloadUrl, byte[] sha1, byte[] signature) + public UpdateInfo(string name, string downloadUrl, byte[] sha256, byte[] signature256) { Name = name; DownloadUrl = downloadUrl; - Sha1 = sha1; - Signature = signature; + Sha256 = sha256; + Signature256 = signature256; } public string Name { get; } public string DownloadUrl { get; } - public byte[] Sha1 { get; } + public byte[] Sha256 { get; } - public byte[] Signature { get; } + public byte[] Signature256 { get; } } \ No newline at end of file diff --git a/NAPS2.Lib/Update/UpdateOperation.cs b/NAPS2.Lib/Update/UpdateOperation.cs index f639d18c09..8e80192cf0 100644 --- a/NAPS2.Lib/Update/UpdateOperation.cs +++ b/NAPS2.Lib/Update/UpdateOperation.cs @@ -20,19 +20,6 @@ public class UpdateOperation : OperationBase private string? _tempFolder; private string? _tempPath; - static UpdateOperation() - { - try - { - const int tls13 = 12288; - ServicePointManager.SecurityProtocol = (SecurityProtocolType) tls13; - } - catch (NotSupportedException) - { - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; - } - } - public UpdateOperation(ErrorOutput errorOutput, DesktopController desktopController, DesktopFormProvider desktopFormProvider) { @@ -170,18 +157,19 @@ private void AtomicReplaceFile(string source, string dest) private bool VerifyHash() { - using var sha = SHA1.Create(); + using var sha = SHA256.Create(); using FileStream stream = File.OpenRead(_tempPath!); byte[] checksum = sha.ComputeHash(stream); - return checksum.SequenceEqual(_update!.Sha1); + return checksum.SequenceEqual(_update!.Sha256); } private bool VerifySignature() { - var cert = new X509Certificate2(ClientCreds_.naps2_public); + var cert = X509CertificateLoader.LoadCertificate(ClientCreds_.naps2_public); var csp = cert.GetRSAPublicKey(); if (csp == null) return false; - return csp.VerifyHash(_update!.Sha1, _update.Signature, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); + return csp.VerifyHash(_update!.Sha256, _update.Signature256, HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); } private void DownloadProgress(object sender, DownloadProgressChangedEventArgs e) diff --git a/NAPS2.Lib/Util/CompilerAttributes.cs b/NAPS2.Lib/Util/CompilerAttributes.cs deleted file mode 100644 index 8230a289b1..0000000000 --- a/NAPS2.Lib/Util/CompilerAttributes.cs +++ /dev/null @@ -1,61 +0,0 @@ -// https://sergiopedri.medium.com/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb -// ReSharper disable once CheckNamespace - -namespace System.Runtime.CompilerServices -{ - internal static class IsExternalInit - { - } - - /// Specifies that a type has required members or that a member is required. - [AttributeUsage( - AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, - AllowMultiple = false, Inherited = false)] - internal sealed class RequiredMemberAttribute : Attribute - { - } - - /// - /// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. - /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] - internal sealed class CompilerFeatureRequiredAttribute : Attribute - { - public CompilerFeatureRequiredAttribute(string featureName) - { - FeatureName = featureName; - } - - /// - /// The name of the compiler feature. - /// - public string FeatureName { get; } - - /// - /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . - /// - public bool IsOptional { get; init; } - - /// - /// The used for the ref structs C# feature. - /// - public const string RefStructs = nameof(RefStructs); - - /// - /// The used for the required members C# feature. - /// - public const string RequiredMembers = nameof(RequiredMembers); - } -} - -namespace System.Diagnostics.CodeAnalysis -{ - /// - /// Specifies that this constructor sets all required members for the current type, and callers - /// do not need to set any required members themselves. - /// - [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] - internal sealed class SetsRequiredMembersAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/NAPS2.Lib/Util/CultureHelper.cs b/NAPS2.Lib/Util/CultureHelper.cs index 2c38913a8e..e038f2461c 100644 --- a/NAPS2.Lib/Util/CultureHelper.cs +++ b/NAPS2.Lib/Util/CultureHelper.cs @@ -54,6 +54,12 @@ public void SetCulturesFromConfig() public IEnumerable<(string langCode, string langName)> GetAvailableCultures() { +#if NET6_0_OR_GREATER + // For self-contained builds we don't have separate DLL files we can check for existence + // TODO: Don't want to hard code this... it defeats the whole purpose of autodetection + var exclude = new HashSet { "bn", "ur" }; + return GetAllCultures().Where(x => !exclude.Contains(x.langCode)); +#else foreach (var (langCode, langName) in GetAllCultures()) { // Only include those languages for which localized resources exist @@ -65,5 +71,6 @@ public void SetCulturesFromConfig() yield return (langCode, langName); } } +#endif } } \ No newline at end of file diff --git a/NAPS2.Sdk/Util/IOverwritePrompt.cs b/NAPS2.Lib/Util/IOverwritePrompt.cs similarity index 59% rename from NAPS2.Sdk/Util/IOverwritePrompt.cs rename to NAPS2.Lib/Util/IOverwritePrompt.cs index d86389017a..91d696a538 100644 --- a/NAPS2.Sdk/Util/IOverwritePrompt.cs +++ b/NAPS2.Lib/Util/IOverwritePrompt.cs @@ -1,17 +1,14 @@ namespace NAPS2.Util; -// TODO: Refactor to Eto and move to NAPS2.EtoForms (or something non-eto and non-winforms if I want operations in the Sdk...) /// /// A base class for objects that can prompt the user to overwrite an existing file. -/// -/// Implementors: WinFormsOverwritePrompt, ConsoleOverwritePrompt /// public interface IOverwritePrompt { /// /// Asks the user if they would like to overwrite the specified file. /// - /// If DialogResult.Cancel is specified, the current operation should be cancelled even if there are other files to write. + /// If OverwriteResponse.Cancel is specified, the current operation should be cancelled even if there are other files to write. /// /// The path of the file to overwrite. /// Yes, No, or Cancel. diff --git a/NAPS2.Sdk/Util/ListMutation.cs b/NAPS2.Lib/Util/ListMutation.cs similarity index 68% rename from NAPS2.Sdk/Util/ListMutation.cs rename to NAPS2.Lib/Util/ListMutation.cs index 395ec88ce9..0025945719 100644 --- a/NAPS2.Sdk/Util/ListMutation.cs +++ b/NAPS2.Lib/Util/ListMutation.cs @@ -14,6 +14,9 @@ public void Apply(List list, ISelectable selectable) public abstract void Apply(List list, ref ListSelection selection); + /// + /// Moves the selection one element later in the list. + /// public class MoveDown : ListMutation { public override void Apply(List list, ref ListSelection selection) @@ -33,6 +36,9 @@ public override void Apply(List list, ref ListSelection selection) } } + /// + /// Moves the selection one earlier in the list. + /// public class MoveUp : ListMutation { public override void Apply(List list, ref ListSelection selection) @@ -51,6 +57,9 @@ public override void Apply(List list, ref ListSelection selection) } } + /// + /// Moves the selection to the specified index in the list. + /// public class MoveTo : ListMutation { private readonly int _destinationIndex; @@ -86,6 +95,9 @@ public override void Apply(List list, ref ListSelection selection) } } + /// + /// Converts lists in the order 1,3,5,2,4,6 to 1,2,3,4,5,6. + /// public class Interleave : ListMutation { public override void Apply(List list, ref ListSelection selection) @@ -107,6 +119,9 @@ public override void Apply(List list, ref ListSelection selection) } } + /// + /// Converts lists in the order 1,2,3,4,5,6 to 1,3,5,2,4,6. + /// public class Deinterleave : ListMutation { public override void Apply(List list, ref ListSelection selection) @@ -132,6 +147,9 @@ public override void Apply(List list, ref ListSelection selection) } } + /// + /// Converts lists in the order 1,3,5,6,4,2 to 1,2,3,4,5,6. + /// public class AltInterleave : ListMutation { public override void Apply(List list, ref ListSelection selection) @@ -153,6 +171,9 @@ public override void Apply(List list, ref ListSelection selection) } } + /// + /// Converts lists in the order 1,2,3,4,5,6 to 1,3,5,6,4,2. + /// public class AltDeinterleave : ListMutation { public override void Apply(List list, ref ListSelection selection) @@ -178,6 +199,9 @@ public override void Apply(List list, ref ListSelection selection) } } + /// + /// Reverses the order of the entire list. + /// public class ReverseAll : ListMutation { public override void Apply(List list, ref ListSelection selection) @@ -186,6 +210,9 @@ public override void Apply(List list, ref ListSelection selection) } } + /// + /// Reverses the order of the selection. + /// public class ReverseSelection : ListMutation { public override void Apply(List list, ref ListSelection selection) @@ -202,33 +229,34 @@ public override void Apply(List list, ref ListSelection selection) } } } - + + /// + /// Deletes all elements from the list. + /// public class DeleteAll : ListMutation { public override void Apply(List list, ref ListSelection selection) { - foreach (var item in list) - { - (item as IDisposable)?.Dispose(); - } list.Clear(); selection = ListSelection.Empty(); } } + /// + /// Deletes the selection from the list. + /// public class DeleteSelected : ListMutation { public override void Apply(List list, ref ListSelection selection) { - foreach (var item in selection) - { - (item as IDisposable)?.Dispose(); - } list.RemoveAll(selection); selection = ListSelection.Empty(); } } + /// + /// Inserts the given item at the given index. + /// public class InsertAt : ListMutation { private readonly int _index; @@ -246,6 +274,9 @@ public override void Apply(List list, ref ListSelection selection) } } + /// + /// Inserts the given item after the given predecessor (or at the end of the list if none). + /// public class InsertAfter : ListMutation { private readonly T _itemToInsert; @@ -261,7 +292,6 @@ public override void Apply(List list, ref ListSelection selection) { // Default to the end of the list int index = list.Count; - // Use the index after the last item from the same source (if it exists) if (_predecessor != null) { int lastIndex = list.IndexOf(_predecessor); @@ -274,6 +304,39 @@ public override void Apply(List list, ref ListSelection selection) } } + /// + /// Inserts the given item before the given successor (or at the start of the list if none). + /// + public class InsertBefore : ListMutation + { + private readonly T _itemToInsert; + private readonly T? _successor; + + public InsertBefore(T itemToInsert, T? successor) + { + _itemToInsert = itemToInsert; + _successor = successor; + } + + public override void Apply(List list, ref ListSelection selection) + { + // Default to the start of the list + int index = 0; + if (_successor != null) + { + int lastIndex = list.IndexOf(_successor); + if (lastIndex != -1) + { + index = lastIndex; + } + } + list.Insert(index, _itemToInsert); + } + } + + /// + /// Replaces the selection with the given item. + /// public class ReplaceWith : ListMutation { private readonly T _newItem; @@ -308,6 +371,53 @@ public override void Apply(List list, ref ListSelection selection) } } + /// + /// Replaces the given sequence of items with a different sequence of items. Both sequences must be non-empty, + /// and if the original sequence isn't present nothing happens. + /// + public class ReplaceRange : ListMutation + { + private readonly List _oldRange; + private readonly List _newRange; + + public ReplaceRange(List oldRange, List newRange) + { + if (oldRange.Count == 0 || newRange.Count == 0) throw new ArgumentException(); + _oldRange = oldRange; + _newRange = newRange; + } + + public override void Apply(List list, ref ListSelection selection) + { + int index = -1; + for (int i = 0; i < list.Count; i++) + { + if (EqualityComparer.Default.Equals(list[i], _oldRange[0])) + { + bool match = true; + for (int j = 0; j < _oldRange.Count; j++) + { + if (!EqualityComparer.Default.Equals(list[i + j], _oldRange[j])) + { + match = false; + break; + } + } + if (match) + { + index = i; + break; + } + } + } + list.RemoveRange(index, _oldRange.Count); + list.InsertRange(index, _newRange); + } + } + + /// + /// Appends the given item(s) to the end of the list. + /// public class Append : ListMutation { private readonly List _items; @@ -322,6 +432,29 @@ public Append(params T[] items) _items = items.ToList(); } + public override void Apply(List list, ref ListSelection selection) + { + list.AddRange(_items); + } + } + + /// + /// Appends the given item(s) to the end of the list and selects them. + /// + public class AppendAndSelect : ListMutation + { + private readonly List _items; + + public AppendAndSelect(IEnumerable items) + { + _items = items.ToList(); + } + + public AppendAndSelect(params T[] items) + { + _items = items.ToList(); + } + public override void Apply(List list, ref ListSelection selection) { list.AddRange(_items); diff --git a/NAPS2.Sdk/Util/ListSelection.cs b/NAPS2.Lib/Util/ListSelection.cs similarity index 87% rename from NAPS2.Sdk/Util/ListSelection.cs rename to NAPS2.Lib/Util/ListSelection.cs index f6babe733b..a798ef9ae7 100644 --- a/NAPS2.Sdk/Util/ListSelection.cs +++ b/NAPS2.Lib/Util/ListSelection.cs @@ -2,7 +2,6 @@ namespace NAPS2.Util; -// TODO: Move this to another namespace public static class ListSelection { public static ListSelection From(IEnumerable list) where T : notnull => @@ -21,18 +20,20 @@ public static ListSelection Empty() where T : notnull => public class ListSelection : ICollection, IEquatable> where T : notnull { - private readonly HashSet _internalSelection; + private readonly List _internalSelection; + private readonly HashSet _internalSelectionSet; public ListSelection(IEnumerable selectedItems) { - _internalSelection = new HashSet(selectedItems); + _internalSelection = selectedItems.ToList(); + _internalSelectionSet = _internalSelection.ToHashSet(); } public IEnumerable ToSelectedIndices(List list) => list.IndiciesOf(_internalSelection); public int Count => _internalSelection.Count; - public bool Contains(T item) => _internalSelection.Contains(item); + public bool Contains(T item) => _internalSelectionSet.Contains(item); public bool IsReadOnly => true; public void Add(T item) => throw new NotSupportedException(); @@ -49,7 +50,7 @@ public bool Equals(ListSelection? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return _internalSelection.SetEquals(other._internalSelection); + return _internalSelectionSet.SetEquals(other._internalSelectionSet); } public override bool Equals(object? obj) diff --git a/NAPS2.Sdk/Util/OverwriteResponse.cs b/NAPS2.Lib/Util/OverwriteResponse.cs similarity index 100% rename from NAPS2.Sdk/Util/OverwriteResponse.cs rename to NAPS2.Lib/Util/OverwriteResponse.cs diff --git a/NAPS2.Lib/Util/ProcessHelper.cs b/NAPS2.Lib/Util/ProcessHelper.cs new file mode 100644 index 0000000000..2c98067bdf --- /dev/null +++ b/NAPS2.Lib/Util/ProcessHelper.cs @@ -0,0 +1,45 @@ +namespace NAPS2.Util; + +public static class ProcessHelper +{ + public static void OpenUrl(string url) => Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + + public static void OpenFile(string file) + { + Process.Start(new ProcessStartInfo + { + UseShellExecute = true, + FileName = file, + Verb = "open" + }); + } + + public static void OpenFolder(string folder) => OpenFile(folder); + + public static bool TryRun(string command, string args, int timeoutMs) + { + try + { + var process = Process.Start(new ProcessStartInfo(command, args) + { + RedirectStandardOutput = true, + RedirectStandardError = true + }); + if (process != null) + { + process.WaitForExit(timeoutMs); + bool result = process.HasExited && process.ExitCode == 0; + if (!process.HasExited) + { + process.Kill(); + } + return result; + } + return false; + } + catch (Exception) + { + return false; + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Util/StubOverwritePrompt.cs b/NAPS2.Lib/Util/StubOverwritePrompt.cs similarity index 67% rename from NAPS2.Sdk/Util/StubOverwritePrompt.cs rename to NAPS2.Lib/Util/StubOverwritePrompt.cs index 97901879a2..5ee15b83a4 100644 --- a/NAPS2.Sdk/Util/StubOverwritePrompt.cs +++ b/NAPS2.Lib/Util/StubOverwritePrompt.cs @@ -1,6 +1,6 @@ namespace NAPS2.Util; -public class StubOverwritePrompt : IOverwritePrompt +internal class StubOverwritePrompt : IOverwritePrompt { public OverwriteResponse ConfirmOverwrite(string path) => OverwriteResponse.No; } \ No newline at end of file diff --git a/NAPS2.Lib/Util/UriHelper.cs b/NAPS2.Lib/Util/UriHelper.cs new file mode 100644 index 0000000000..5adfe194f5 --- /dev/null +++ b/NAPS2.Lib/Util/UriHelper.cs @@ -0,0 +1,36 @@ +using System.Text; + +namespace NAPS2.Util; + +public class UriHelper +{ + // From https://stackoverflow.com/a/35734486/2112909 + public static string FilePathToFileUrl(string filePath) + { + StringBuilder uri = new StringBuilder(); + foreach (char v in filePath) + { + if ((v >= 'a' && v <= 'z') || (v >= 'A' && v <= 'Z') || (v >= '0' && v <= '9') || + v == '+' || v == '/' || v == ':' || v == '.' || v == '-' || v == '_' || v == '~' || + v > '\xFF') + { + uri.Append(v); + } + else if (v == Path.DirectorySeparatorChar || v == Path.AltDirectorySeparatorChar) + { + uri.Append('/'); + } + else + { + uri.Append(String.Format("%{0:X2}", (int)v)); + } + } + if (uri.Length >= 2 && uri[0] == '/' && uri[1] == '/') // UNC path + uri.Insert(0, "file:"); + else + uri.Insert(0, uri.Length > 0 && uri[0] == '/' ? "file://" : "file:///"); + return uri.ToString(); + } + + public static Uri FilePathToFileUri(string filePath) => new(FilePathToFileUrl(filePath)); +} \ No newline at end of file diff --git a/NAPS2.Sdk.Samples/HelloWorldSample.cs b/NAPS2.Sdk.Samples/HelloWorldSample.cs index 3b678bbc99..8d8e73e36d 100644 --- a/NAPS2.Sdk.Samples/HelloWorldSample.cs +++ b/NAPS2.Sdk.Samples/HelloWorldSample.cs @@ -14,12 +14,9 @@ public static async Task Run() var controller = new ScanController(scanningContext); ScanDevice device = (await controller.GetDeviceList()).First(); var options = new ScanOptions { Device = device }; - await foreach(var image in controller.Scan(options)) + await foreach (var image in controller.Scan(options)) { - using (image) - { - Console.WriteLine("Scanned a page!"); - } + Console.WriteLine("Scanned a page!"); } } } \ No newline at end of file diff --git a/NAPS2.Sdk.Samples/LICENSE b/NAPS2.Sdk.Samples/LICENSE index 81dc3ffbf4..1784122ed1 100644 --- a/NAPS2.Sdk.Samples/LICENSE +++ b/NAPS2.Sdk.Samples/LICENSE @@ -1,7 +1,7 @@ NAPS2.Sdk.Samples https://www.github.com/cyanfish/naps2/ -Copyright 2022 NAPS2 Contributors +Copyright 2009-2025 NAPS2 Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/NAPS2.Sdk.Samples/NAPS2.Sdk.Samples.csproj b/NAPS2.Sdk.Samples/NAPS2.Sdk.Samples.csproj index 5847a04e63..e5f9bff28e 100644 --- a/NAPS2.Sdk.Samples/NAPS2.Sdk.Samples.csproj +++ b/NAPS2.Sdk.Samples/NAPS2.Sdk.Samples.csproj @@ -1,7 +1,7 @@  - net6;net462 + net8;net462 CA1416 @@ -10,8 +10,13 @@ + + + + + \ No newline at end of file diff --git a/NAPS2.Sdk.Samples/NetworkSharingSample.cs b/NAPS2.Sdk.Samples/NetworkSharingSample.cs new file mode 100644 index 0000000000..9e12072924 --- /dev/null +++ b/NAPS2.Sdk.Samples/NetworkSharingSample.cs @@ -0,0 +1,51 @@ +using NAPS2.Escl.Server; +using NAPS2.Images.Gdi; +using NAPS2.Remoting.Server; +using NAPS2.Scan; + +namespace NAPS2.Sdk.Samples; + +public class NetworkSharingSample +{ + public static async Task Server() + { + // NAPS2 can share scanners across the local network using the ESCL protocol with the NAPS2.Escl.Server package. + // On the server, you need to set up ScanServer with the device(s) to share. + // On the client, you just scan as usual using Driver.Escl. + + using var scanningContext = new ScanningContext(new GdiImageContext()); + + // Get the device to share + var controller = new ScanController(scanningContext); + ScanDevice device = (await controller.GetDeviceList()).First(); + + // Set up the server (you'll need to reference NAPS2.Escl.Server to be able to create an EsclServer object). + using var scanServer = new ScanServer(scanningContext, new EsclServer()); + + // Register a device to be shared + scanServer.RegisterDevice(device); + + // Run the server until the user presses Enter + await scanServer.Start(); + Console.ReadLine(); + await scanServer.Stop(); + } + + public static async Task Client() + { + using var scanningContext = new ScanningContext(new GdiImageContext()); + var controller = new ScanController(scanningContext); + + // Find the shared device using Driver.Escl + ScanDevice device = (await controller.GetDeviceList(Driver.Escl)).First(); + + // Set up options + var options = new ScanOptions { Device = device }; + + // Do the scan + await foreach (var image in controller.Scan(options)) + { + Console.WriteLine("Scanned a page!"); + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Samples/OcrSample.cs b/NAPS2.Sdk.Samples/OcrSample.cs new file mode 100644 index 0000000000..08a3dc7ab5 --- /dev/null +++ b/NAPS2.Sdk.Samples/OcrSample.cs @@ -0,0 +1,43 @@ +using NAPS2.Images.Gdi; +using NAPS2.Ocr; +using NAPS2.Pdf; +using NAPS2.Scan; + +namespace NAPS2.Sdk.Samples; + +public class OcrSample +{ + public static async Task OcrAndExportPdf() + { + // Exporting PDFs with OCR requires the optional NAPS2.Tesseract.Binaries Nuget package to be installed. + // Or, alternatively, you can use the system-installed Tesseract or provide a custom path to a Tesseract EXE. + + using var scanningContext = new ScanningContext(new GdiImageContext()); + + // The NAPS2.Tesseract.Binaries package doesn't include all the actual language data (1GB+ for 100+ languages). + // You can download .traineddata files from one of these repos: + // - https://github.com/tesseract-ocr/tessdata_fast + // - https://github.com/tesseract-ocr/tessdata_best + // Then specify the folder where those .traineddata files are stored. + scanningContext.OcrEngine = TesseractOcrEngine.Bundled(@"C:\path\to\my\traineddata\files\"); + + // Or if you know Tesseract is installed on the system PATH you can just do this without needing any extra + // packages or downloads. + scanningContext.OcrEngine = TesseractOcrEngine.System(); + + // Or if you have a custom path to the tesseract EXE you can do this. + scanningContext.OcrEngine = TesseractOcrEngine.Custom(@"C:\path\to\tesseract.exe"); + + // Scan some images + var controller = new ScanController(scanningContext); + var devices = await controller.GetDeviceList(); + var options = new ScanOptions { Device = devices.First() }; + var images = await controller.Scan(options).ToListAsync(); + + // Export to PDF with OCR + var pdfExporter = new PdfExporter(scanningContext); + // We specify the language code for OCR. This is based on the name of the .traineddata file, and is found here: + // https://tesseract-ocr.github.io/tessdoc/Data-Files#data-files-for-version-400-november-29-2016 + await pdfExporter.Export("doc.pdf", images, ocrParams: new OcrParams("eng")); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Samples/PdfImportSample.cs b/NAPS2.Sdk.Samples/PdfImportSample.cs new file mode 100644 index 0000000000..e7228648d9 --- /dev/null +++ b/NAPS2.Sdk.Samples/PdfImportSample.cs @@ -0,0 +1,40 @@ +using NAPS2.Images.Gdi; +using NAPS2.Pdf; +using NAPS2.Scan; + +namespace NAPS2.Sdk.Samples; + +public class PdfImportSample +{ + public static async Task PdfImportAppendAndExport() + { + // Importing PDFs requires the optional NAPS2.Pdfium.Binaries Nuget package to be installed. + + // Set up + using var scanningContext = new ScanningContext(new GdiImageContext()); + var pdfImporter = new PdfImporter(scanningContext); + var images = new List(); + + // Import original PDF + await foreach (var image in pdfImporter.Import("original.pdf")) + { + images.Add(image); + } + + // Set up scanning + var controller = new ScanController(scanningContext); + var devices = await controller.GetDeviceList(); + var options = new ScanOptions { Device = devices.First() }; + + // Append newly scanned images + await foreach (var image in controller.Scan(options)) + { + images.Add(image); + } + + // Save all images to a new PDF + // This will retain the structure of the original PDF pages (i.e. they aren't rasterized to flat images) + var pdfExporter = new PdfExporter(scanningContext); + await pdfExporter.Export("doc.pdf", images); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Samples/ScanAndSaveSample.cs b/NAPS2.Sdk.Samples/ScanAndSaveSample.cs new file mode 100644 index 0000000000..305877fed5 --- /dev/null +++ b/NAPS2.Sdk.Samples/ScanAndSaveSample.cs @@ -0,0 +1,39 @@ +using NAPS2.Images.Gdi; +using NAPS2.Pdf; +using NAPS2.Scan; + +namespace NAPS2.Sdk.Samples; + +public class ScanAndSaveSample +{ + public static async Task ScanAndSave() + { + // Set up + using var scanningContext = new ScanningContext(new GdiImageContext()); + var controller = new ScanController(scanningContext); + + // Query for available scanning devices + var devices = await controller.GetDeviceList(); + + // Set scanning options + var options = new ScanOptions + { + Device = devices.First(), + PaperSource = PaperSource.Feeder, + PageSize = PageSize.A4, + Dpi = 300 + }; + + // Scan and save images + int i = 1; + await foreach (var image in controller.Scan(options)) + { + image.Save($"page{i++}.jpg"); + } + + // Scan and save PDF + var images = await controller.Scan(options).ToListAsync(); + var pdfExporter = new PdfExporter(scanningContext); + await pdfExporter.Export("doc.pdf", images); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Samples/ScanToBitmapSample.cs b/NAPS2.Sdk.Samples/ScanToBitmapSample.cs index cda08a8a3e..a9b441dee5 100644 --- a/NAPS2.Sdk.Samples/ScanToBitmapSample.cs +++ b/NAPS2.Sdk.Samples/ScanToBitmapSample.cs @@ -30,7 +30,6 @@ public static async Task Run() var options = new ScanOptions { Dpi = 300, - Driver = driver, Device = device }; diff --git a/NAPS2.Sdk.Samples/TwainSample.cs b/NAPS2.Sdk.Samples/TwainSample.cs new file mode 100644 index 0000000000..8bda208372 --- /dev/null +++ b/NAPS2.Sdk.Samples/TwainSample.cs @@ -0,0 +1,36 @@ +using NAPS2.Images.Gdi; +using NAPS2.Scan; + +namespace NAPS2.Sdk.Samples; + +public class TwainSample +{ + public static async Task Run() + { + // Scanning with TWAIN on Windows must happen from a 32-bit process. You can do this by building your exe as + // 32-bit, but the better solution is to install the NAPS2.Sdk.Worker.Win32 Nuget package, which includes a + // pre-compiled 32-bit NAPS2.Worker.exe. Then you only need to call ScanningContext.SetUpWin32Worker and you + // should be able to scan with TWAIN. + // + // If you want to use a worker process but don't want to use a pre-compiled exe (or want to set up your own + // logging etc), you can also build your own worker exe with the same name (and call WorkerServer.Run in its + // Main method). + + using var scanningContext = new ScanningContext(new GdiImageContext()); + + // Set up the worker; this includes starting a worker process in the background so it will be ready to respond + // when we need it + scanningContext.SetUpWin32Worker(); + + var controller = new ScanController(scanningContext); + + // As we're not using the default (WIA) driver, we need to specify it when listing devices or scanning + ScanDevice device = (await controller.GetDeviceList(Driver.Twain)).First(); + var options = new ScanOptions { Device = device }; + + await foreach (var image in controller.Scan(options)) + { + Console.WriteLine("Scanned a page!"); + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.ScannerTests/NAPS2.Sdk.ScannerTests.csproj b/NAPS2.Sdk.ScannerTests/NAPS2.Sdk.ScannerTests.csproj index ffacab11c7..1f10275fe0 100644 --- a/NAPS2.Sdk.ScannerTests/NAPS2.Sdk.ScannerTests.csproj +++ b/NAPS2.Sdk.ScannerTests/NAPS2.Sdk.ScannerTests.csproj @@ -1,8 +1,8 @@ - net6;net462 - net6 + net9-windows + net9 NAPS2.Sdk.ScannerTests @@ -13,12 +13,11 @@ - - - - - - + + + + + diff --git a/NAPS2.Sdk.Tests/Asserts/ImageAsserts.cs b/NAPS2.Sdk.Tests/Asserts/ImageAsserts.cs index 3cf2ee04b3..c5637612fa 100644 --- a/NAPS2.Sdk.Tests/Asserts/ImageAsserts.cs +++ b/NAPS2.Sdk.Tests/Asserts/ImageAsserts.cs @@ -7,11 +7,14 @@ namespace NAPS2.Sdk.Tests.Asserts; public static class ImageAsserts { + // For bigger cross-platform differences + public const double XL_RMSE_THRESHOLD = 9.5; + // For slight cross-platform differences - public const double XPLAT_RMSE_THRESHOLD = 6.5; + public const double XPLAT_RMSE_THRESHOLD = 6.9; // JPEG artifacts seem to consistently create a RMSE of about 2.5. - public const double GENERAL_RMSE_THRESHOLD = 3.5; + public const double GENERAL_RMSE_THRESHOLD = 3.6; public const double NULL_RMSE_THRESHOLD = 0.6; @@ -22,19 +25,22 @@ public static class ImageAsserts public static void Similar(byte[] first, ProcessedImage second, double rmseThreshold = GENERAL_RMSE_THRESHOLD, bool ignoreResolution = false) { - using var rendered = second.Render(); - Similar(TestImageContextFactory.Get().Load(first), rendered, rmseThreshold, ignoreResolution); + using var firstLoaded = TestImageContextFactory.Get().Load(first); + using var secondRendered = second.Render(); + Similar(firstLoaded, secondRendered, rmseThreshold, ignoreResolution); } public static void Similar(byte[] first, string secondPath, double rmseThreshold = GENERAL_RMSE_THRESHOLD, bool ignoreResolution = false) { - using var second = TestImageContextFactory.Get().Load(secondPath); - Similar(TestImageContextFactory.Get().Load(first), second, rmseThreshold, ignoreResolution); + using var firstLoaded = TestImageContextFactory.Get().Load(first); + using var secondRendered = TestImageContextFactory.Get().Load(secondPath); + Similar(firstLoaded, secondRendered, rmseThreshold, ignoreResolution); } public static void Similar(byte[] first, IMemoryImage second, double rmseThreshold = GENERAL_RMSE_THRESHOLD, bool ignoreResolution = false) { - Similar(TestImageContextFactory.Get().Load(first), second, rmseThreshold, ignoreResolution); + using var firstLoaded = TestImageContextFactory.Get().Load(first); + Similar(firstLoaded, second, rmseThreshold, ignoreResolution); } public static void Similar(IMemoryImage first, IMemoryImage second, @@ -45,13 +51,15 @@ public static void Similar(IMemoryImage first, IMemoryImage second, public static void NotSimilar(byte[] first, ProcessedImage second, double rmseThreshold = GENERAL_RMSE_THRESHOLD, bool ignoreResolution = false) { - using var rendered = second.Render(); - NotSimilar(TestImageContextFactory.Get().Load(first), rendered, rmseThreshold, ignoreResolution); + using var firstLoaded = TestImageContextFactory.Get().Load(first); + using var secondRendered = second.Render(); + NotSimilar(firstLoaded, secondRendered, rmseThreshold, ignoreResolution); } public static void NotSimilar(byte[] first, IMemoryImage second, double rmseThreshold = GENERAL_RMSE_THRESHOLD, bool ignoreResolution = false) { - NotSimilar(TestImageContextFactory.Get().Load(first), second, rmseThreshold, ignoreResolution); + using var firstLoaded = TestImageContextFactory.Get().Load(first); + NotSimilar(firstLoaded, second, rmseThreshold, ignoreResolution); } public static void NotSimilar(IMemoryImage first, IMemoryImage second, @@ -60,10 +68,10 @@ public static void NotSimilar(IMemoryImage first, IMemoryImage second, Similar(first, second, rmseThreshold, ignoreResolution, false); } - private static unsafe void Similar(IMemoryImage first, IMemoryImage second, + private static void Similar(IMemoryImage first, IMemoryImage second, double rmseThreshold, bool ignoreResolution, bool isSimilar) { - if (first.PixelFormat == ImagePixelFormat.Unsupported || second.PixelFormat == ImagePixelFormat.Unsupported) + if (first.PixelFormat == ImagePixelFormat.Unknown || second.PixelFormat == ImagePixelFormat.Unknown) { throw new InvalidOperationException($"Unsupported pixel formats {first.PixelFormat} {second.PixelFormat}"); } @@ -80,8 +88,14 @@ private static unsafe void Similar(IMemoryImage first, IMemoryImage second, first.VerticalResolution + RESOLUTION_THRESHOLD); } - first = first.PerformTransform(new ColorBitDepthTransform()); - second = second.PerformTransform(new ColorBitDepthTransform()); + if (first.PixelFormat is ImagePixelFormat.BW1 or ImagePixelFormat.Gray8) + { + first = first.CopyWithPixelFormat(ImagePixelFormat.RGB24); + } + if (second.PixelFormat is ImagePixelFormat.BW1 or ImagePixelFormat.Gray8) + { + second = second.CopyWithPixelFormat(ImagePixelFormat.RGB24); + } var op = new RmseBitwiseImageOp(); op.Perform(first, second); @@ -96,6 +110,24 @@ private static unsafe void Similar(IMemoryImage first, IMemoryImage second, } } + public static bool IsSimilar(IMemoryImage first, IMemoryImage second, double rmseThreshold = GENERAL_RMSE_THRESHOLD) + { + if (first.Width != second.Width || first.Height != second.Height) return false; + + if (first.PixelFormat is ImagePixelFormat.BW1 or ImagePixelFormat.Gray8) + { + first = first.CopyWithPixelFormat(ImagePixelFormat.RGB24); + } + if (second.PixelFormat is ImagePixelFormat.BW1 or ImagePixelFormat.Gray8) + { + second = second.CopyWithPixelFormat(ImagePixelFormat.RGB24); + } + + var op = new RmseBitwiseImageOp(); + op.Perform(first, second); + return op.Rmse <= rmseThreshold; + } + public static unsafe void PixelColors(IMemoryImage image, PixelColorData colorData) { using var pixelReader = new RgbPixelReader(image); @@ -108,14 +140,14 @@ public static unsafe void PixelColors(IMemoryImage image, PixelColorData colorDa var expected = (r, g, b); if (color != expected) { - throw new AssertActualExpectedException(expected, color, $"Mismatched color at ({x}, {y})"); + throw new XunitException($"Mismatched color at ({x}, {y}): expected {expected}, got {color}"); } } } public class PixelColorData : IEnumerable<((int x, int y), (int r, int g, int b))> { - private readonly List<((int x, int y), (int r, int g, int b))> _colors = new(); + private readonly List<((int x, int y), (int r, int g, int b))> _colors = []; public void Add((int x, int y) pos, (int r, int g, int b) color) { diff --git a/NAPS2.Sdk.Tests/Asserts/PdfAsserts.cs b/NAPS2.Sdk.Tests/Asserts/PdfAsserts.cs index 25d72a5723..17c5d67ce1 100644 --- a/NAPS2.Sdk.Tests/Asserts/PdfAsserts.cs +++ b/NAPS2.Sdk.Tests/Asserts/PdfAsserts.cs @@ -1,9 +1,11 @@ -using Codeuctivity; -using NAPS2.ImportExport.Pdf; -using NAPS2.ImportExport.Pdf.Pdfium; +using System.Text.RegularExpressions; +using Codeuctivity; +using NAPS2.Pdf; +using NAPS2.Pdf.Pdfium; using PdfSharpCore.Pdf.IO; using PdfSharpCore.Pdf.Security; using Xunit; +using Xunit.Sdk; namespace NAPS2.Sdk.Tests.Asserts; @@ -18,8 +20,20 @@ public static void AssertPageCount(int count, string filePath) Assert.Equal(count, doc.PageCount); } + public static void AssertPageSize(PageSize pageSize, int precision, string filePath) + { + Assert.True(File.Exists(filePath)); + var doc = PdfReader.Open(filePath, PdfDocumentOpenMode.InformationOnly); + Assert.Equal((double) pageSize.WidthInInches, doc.Pages[0].Width.Inch, precision); + Assert.Equal((double) pageSize.HeightInInches, doc.Pages[0].Height.Inch, precision); + } + public static async Task AssertCompliant(string profile, string filePath) { + if (string.IsNullOrEmpty(profile)) + { + return; + } Assert.True(File.Exists(filePath)); var report = await LazyPdfAValidator.Value.ValidateWithDetailedReportAsync(filePath); Assert.True(report.Jobs.Job.ValidationReport.IsCompliant); @@ -28,12 +42,20 @@ public static async Task AssertCompliant(string profile, string filePath) public static void AssertContainsTextOnce(string text, string filePath) { - Assert.Equal(1, CountText(text, filePath)); + var value = CountText(text, filePath); + if (value != 1) + { + throw new XunitException($"Unexpected count for \"{text}\": expected {1}, got {value}"); + } } public static void AssertDoesNotContainText(string text, string filePath) { - Assert.Equal(0, CountText(text, filePath)); + var value = CountText(text, filePath); + if (value != 0) + { + throw new XunitException($"Unexpected count for \"{text}\": expected {0}, got {value}"); + } } private static int CountText(string text, string filePath) @@ -44,7 +66,8 @@ private static int CountText(string text, string filePath) { int startIndex = 0; int index; - while ((index = pageText.IndexOf(text, startIndex, StringComparison.InvariantCulture)) != -1) + var normalized = Regex.Replace(pageText, "\\s+", " "); + while ((index = normalized.IndexOf(text, startIndex, StringComparison.InvariantCulture)) != -1) { count++; startIndex = index + 1; @@ -81,7 +104,7 @@ public static void AssertImages(string filePath, params byte[][] expectedImages) public static void AssertImages(string filePath, string password, params byte[][] expectedImages) { Assert.True(File.Exists(filePath)); - var renderer = new PdfiumPdfRenderer(); + var renderer = new PdfiumPdfRenderer { NoExtraction = true }; using var expectedImagesRendered = expectedImages.Select(data => TestImageContextFactory.Get().Load(data)).ToDisposableList(); var renderSizes = PdfRenderSize.FromIndividualPageSizes( @@ -103,10 +126,19 @@ public static void AssertImageFilter(string filePath, int pageIndex, params stri using var doc = PdfDocument.Load(filePath); Assert.InRange(pageIndex, 0, doc.PageCount - 1); using var page = doc.GetPage(pageIndex); - using var obj = PdfiumImageExtractor.GetSingleImageObject(page); + using var obj = PdfiumImageExtractor.GetSingleImageObject(page, true); Assert.NotNull(obj); Assert.True(obj.HasImageFilters(filters), $"Expected filters: {string.Join(",", filters)}, actual: {string.Join(",", obj.GetImageFilters())}"); } } + + public static void AssertVersion(int version, string filePath) + { + lock (PdfiumNativeLibrary.Instance) + { + using var doc = PdfDocument.Load(filePath); + Assert.Equal(version, doc.Version); + } + } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/BinaryResources.Designer.cs b/NAPS2.Sdk.Tests/BinaryResources.Designer.cs index b061f60eb2..1886c23abb 100644 --- a/NAPS2.Sdk.Tests/BinaryResources.Designer.cs +++ b/NAPS2.Sdk.Tests/BinaryResources.Designer.cs @@ -138,5 +138,15 @@ internal static byte[] stock_dog_jpeg { return ((byte[])(obj)); } } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] testcert { + get { + object obj = ResourceManager.GetObject("testcert", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/NAPS2.Sdk.Tests/BinaryResources.resx b/NAPS2.Sdk.Tests/BinaryResources.resx index 8002ce885f..586fefe379 100644 --- a/NAPS2.Sdk.Tests/BinaryResources.resx +++ b/NAPS2.Sdk.Tests/BinaryResources.resx @@ -142,4 +142,7 @@ Resources\ocr_test_hebrew.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Resources\testcert.pfx;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/ContextualTests.cs b/NAPS2.Sdk.Tests/ContextualTests.cs index c1fa4826f3..783cbc6ddb 100644 --- a/NAPS2.Sdk.Tests/ContextualTests.cs +++ b/NAPS2.Sdk.Tests/ContextualTests.cs @@ -1,8 +1,13 @@ -using System.Threading; -using NAPS2.ImportExport.Pdf; +using System.Collections.Immutable; +using System.Threading; +using Microsoft.Extensions.Logging; using NAPS2.Ocr; +using NAPS2.Pdf; using NAPS2.Scan; +using NAPS2.Sdk.Tests.Asserts; using NAPS2.Unmanaged; +using NSubstitute; +using Xunit.Abstractions; namespace NAPS2.Sdk.Tests; @@ -13,18 +18,26 @@ public ContextualTests() FolderPath = Path.GetFullPath(Path.Combine("naps2_test_temp", Path.GetRandomFileName())); Folder = Directory.CreateDirectory(FolderPath); - ImageContext = TestImageContextFactory.Get(new PdfiumPdfRenderer()); + ImageContext = TestImageContextFactory.Get(); ScanningContext = new ScanningContext(ImageContext); ScanningContext.TempFolderPath = Path.Combine(FolderPath, "temp"); Directory.CreateDirectory(ScanningContext.TempFolderPath); } + public ContextualTests(ITestOutputHelper testOutputHelper) + : this() + { + ScanningContext.Logger = new TestLogger(testOutputHelper); + } + public ImageContext ImageContext { get; } // TODO: We can probably do some processed image lifecycle checking by ensuring the scanning context has no // registered images after running a test. public ScanningContext ScanningContext { get; } + public ILogger Logger => ScanningContext.Logger; + public string FolderPath { get; } public DirectoryInfo Folder { get; } @@ -39,12 +52,15 @@ public ProcessedImage CreateScannedImage() return ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); } - public IEnumerable CreateScannedImages(params byte[][] images) + public List CreateScannedImages(params byte[][] images) { - foreach (var image in images) - { - yield return ScanningContext.CreateProcessedImage(LoadImage(image)); - } + return images.Select(image => ScanningContext.CreateProcessedImage(LoadImage(image))).ToList(); + } + + public void SetUpFileStorage() + { + ScanningContext.RecoveryPath = Path.Combine(FolderPath, "recovery"); + ScanningContext.FileStorageManager = FileStorageManager.CreateFolder(ScanningContext.RecoveryPath); } public void SetUpOcr() @@ -54,13 +70,58 @@ public void SetUpOcr() var fast = Path.Combine(FolderPath, "fast"); Directory.CreateDirectory(fast); - var testRoot = Environment.GetEnvironmentVariable("NAPS2_TEST_ROOT"); - var tesseractPath = NativeLibrary.FindExePath(PlatformCompat.System.TesseractExecutableName, testRoot); + var depsRoot = Environment.GetEnvironmentVariable("NAPS2_TEST_DEPS"); + var tesseractPath = NativeLibrary.FindExePath(PlatformCompat.System.TesseractExecutableName, depsRoot); CopyResourceToFile(BinaryResources.eng_traineddata, fast, "eng.traineddata"); CopyResourceToFile(BinaryResources.heb_traineddata, fast, "heb.traineddata"); - ScanningContext.OcrEngine = new TesseractOcrEngine(tesseractPath, FolderPath, FolderPath); + ScanningContext.OcrEngine = TesseractOcrEngine.CustomWithModes(tesseractPath, FolderPath); + } + + public void SetUpFakeOcr(Dictionary ocrTextByImage = null, string ifNoMatch = null, int delay = 200) + { + var ocrMock = Substitute.For(); + ocrMock.ProcessImage(ScanningContext, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns( + async x => + { + var path = (string) x[1]; + var ocrParams = (OcrParams) x[2]; + var ocrImage = ImageContext.Load(path); + await Task.Delay(delay); + + OcrResult CreateOcrResult(string text) + { + var word = new OcrResultElement(text, ocrParams.LanguageCode!, false, + (10, 10, 10, 10), 20, 10, ImmutableList.Empty); + var line = new OcrResultElement(text, ocrParams.LanguageCode!, false, + (10, 10, 10, 10), 20, 10, ImmutableList.Create(word)); + var list = ImmutableList.Create(line); + return new((0, 0, 100, 100), list, list); + } + + if (ocrTextByImage != null) + { + // Lock so we don't try to access images simultaneously + lock (ocrTextByImage) + { + foreach (var image in ocrTextByImage.Keys) + { + if (ImageAsserts.IsSimilar(image, ocrImage)) + { + return CreateOcrResult(ocrTextByImage[image]); + } + } + } + } + if (ifNoMatch != null) + { + return CreateOcrResult(ifNoMatch); + } + return null; + }); + ScanningContext.OcrEngine = ocrMock; } - + public string CopyResourceToFile(byte[] resource, string folder, string fileName) { string path = Path.Combine(folder, fileName); @@ -118,4 +179,36 @@ public bool IsDisposed(IMemoryImage image) return true; } } + + private class TestLogger : ILogger + { + private readonly ITestOutputHelper _testOutputHelper; + + public TestLogger(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + // We don't log tracing to the test output as it fills up the limited output space too quickly + if (logLevel == LogLevel.Trace) return; + + _testOutputHelper.WriteLine(state.ToString()); + if (exception != null) + { + _testOutputHelper.WriteLine(exception.ToString()); + } + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/CurrentPlatformFlags.cs b/NAPS2.Sdk.Tests/CurrentPlatformFlags.cs new file mode 100644 index 0000000000..021d9fcaa0 --- /dev/null +++ b/NAPS2.Sdk.Tests/CurrentPlatformFlags.cs @@ -0,0 +1,64 @@ +using System.Runtime.InteropServices; + +namespace NAPS2.Sdk.Tests; + +public static class CurrentPlatformFlags +{ + public static PlatformFlags Get() + { + var p = PlatformFlags.None; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + p |= PlatformFlags.Windows; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + p |= PlatformFlags.Mac; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + p |= PlatformFlags.Linux; + } + if (RuntimeInformation.ProcessArchitecture == Architecture.X64) + { + p |= PlatformFlags.X64; + } + if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + { + p |= PlatformFlags.Arm64; + } + if (TestImageContextFactory.Get().ImageType.Name == "ImageSharpImage") + { + p |= PlatformFlags.ImageSharpImage; + } + if (TestImageContextFactory.Get().ImageType.Name == "WpfImage") + { + p |= PlatformFlags.WpfImage; + } + if (TestImageContextFactory.Get().ImageType.Name == "GdiImage") + { + p |= PlatformFlags.GdiImage; + } + if (TestImageContextFactory.Get().ImageType.Name == "MacImage") + { + p |= PlatformFlags.MacImage; + } + if (TestImageContextFactory.Get().ImageType.Name == "GtkImage") + { + p |= PlatformFlags.GtkImage; + } + return p; + } + + public static bool Has(PlatformFlags match) + { + var flags = Get(); + return (match & flags) == match; + } + + public static bool HasAny(PlatformFlags match) + { + var flags = Get(); + return (match & flags) != 0; + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Dependencies/DownloadFormatTestsData.Designer.cs b/NAPS2.Sdk.Tests/Dependencies/DownloadFormatTestsData.Designer.cs deleted file mode 100644 index e11be5757c..0000000000 --- a/NAPS2.Sdk.Tests/Dependencies/DownloadFormatTestsData.Designer.cs +++ /dev/null @@ -1,83 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace NAPS2.Sdk.Tests.Dependencies { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class DownloadFormatTestsData { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal DownloadFormatTestsData() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NAPS2.Sdk.Tests.Dependencies.DownloadFormatTestsData", typeof(DownloadFormatTestsData).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] stock_cat { - get { - object obj = ResourceManager.GetObject("stock_cat", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] stock_dog { - get { - object obj = ResourceManager.GetObject("stock_dog", resourceCulture); - return ((byte[])(obj)); - } - } - } -} diff --git a/NAPS2.Sdk.Tests/Dependencies/DownloadFormatTestsData.resx b/NAPS2.Sdk.Tests/Dependencies/DownloadFormatTestsData.resx deleted file mode 100644 index 6d2addf932..0000000000 --- a/NAPS2.Sdk.Tests/Dependencies/DownloadFormatTestsData.resx +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - - ..\Resources\stock-cat.jpeg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - ..\Resources\stock-dog.jpeg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/ImageResources.Designer.cs b/NAPS2.Sdk.Tests/ImageResources.Designer.cs index 492411e109..306aa8becc 100644 --- a/NAPS2.Sdk.Tests/ImageResources.Designer.cs +++ b/NAPS2.Sdk.Tests/ImageResources.Designer.cs @@ -11,32 +11,46 @@ namespace NAPS2.Sdk.Tests { using System; - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [System.Diagnostics.DebuggerNonUserCodeAttribute()] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class ImageResources { - private static System.Resources.ResourceManager resourceMan; + private static global::System.Resources.ResourceManager resourceMan; - private static System.Globalization.CultureInfo resourceCulture; + private static global::System.Globalization.CultureInfo resourceCulture; - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal ImageResources() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Resources.ResourceManager ResourceManager { + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { get { - if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("NAPS2.Sdk.Tests.ImageResources", typeof(ImageResources).Assembly); + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NAPS2.Sdk.Tests.ImageResources", typeof(ImageResources).Assembly); resourceMan = temp; } return resourceMan; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Globalization.CultureInfo Culture { + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -45,62 +59,119 @@ internal static System.Globalization.CultureInfo Culture { } } - internal static byte[] dog { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] animals_tiff { get { - object obj = ResourceManager.GetObject("dog", resourceCulture); + object obj = ResourceManager.GetObject("animals_tiff", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_bw { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] blank1 { get { - object obj = ResourceManager.GetObject("dog_bw", resourceCulture); + object obj = ResourceManager.GetObject("blank1", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_bw_24bit { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] blank2 { get { - object obj = ResourceManager.GetObject("dog_bw_24bit", resourceCulture); + object obj = ResourceManager.GetObject("blank2", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_bw_jpg { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] bw_alternating { get { - object obj = ResourceManager.GetObject("dog_bw_jpg", resourceCulture); + object obj = ResourceManager.GetObject("bw_alternating", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_bw_bmp { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] cat { get { - object obj = ResourceManager.GetObject("dog_bw_bmp", resourceCulture); + object obj = ResourceManager.GetObject("cat", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_bw_tiff { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] deskewed { get { - object obj = ResourceManager.GetObject("dog_bw_tiff", resourceCulture); + object obj = ResourceManager.GetObject("deskewed", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_bw_p300 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog { get { - object obj = ResourceManager.GetObject("dog_bw_p300", resourceCulture); + object obj = ResourceManager.GetObject("dog", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_bw_invertpal { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_99w { get { - object obj = ResourceManager.GetObject("dog_bw_invertpal", resourceCulture); + object obj = ResourceManager.GetObject("dog_99w", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_alpha { + get { + object obj = ResourceManager.GetObject("dog_alpha", resourceCulture); return ((byte[])(obj)); } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_alpha_sc_50pct { + get { + object obj = ResourceManager.GetObject("dog_alpha_sc_50pct", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_alpha_tiff { + get { + object obj = ResourceManager.GetObject("dog_alpha_tiff", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// internal static byte[] dog_b_n300 { get { object obj = ResourceManager.GetObject("dog_b_n300", resourceCulture); @@ -108,6 +179,9 @@ internal static byte[] dog_b_n300 { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// internal static byte[] dog_b_p300 { get { object obj = ResourceManager.GetObject("dog_b_p300", resourceCulture); @@ -115,6 +189,99 @@ internal static byte[] dog_b_p300 { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_b_p300_thumb_256 { + get { + object obj = ResourceManager.GetObject("dog_b_p300_thumb_256", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_bmp { + get { + object obj = ResourceManager.GetObject("dog_bmp", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_bw { + get { + object obj = ResourceManager.GetObject("dog_bw", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_bw_24bit { + get { + object obj = ResourceManager.GetObject("dog_bw_24bit", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_bw_bmp { + get { + object obj = ResourceManager.GetObject("dog_bw_bmp", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_bw_invertpal { + get { + object obj = ResourceManager.GetObject("dog_bw_invertpal", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_bw_jpg { + get { + object obj = ResourceManager.GetObject("dog_bw_jpg", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_bw_p300 { + get { + object obj = ResourceManager.GetObject("dog_bw_p300", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_bw_tiff { + get { + object obj = ResourceManager.GetObject("dog_bw_tiff", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// internal static byte[] dog_c_5_10_15_20 { get { object obj = ResourceManager.GetObject("dog_c_5_10_15_20", resourceCulture); @@ -122,6 +289,9 @@ internal static byte[] dog_c_5_10_15_20 { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// internal static byte[] dog_c_n300 { get { object obj = ResourceManager.GetObject("dog_c_n300", resourceCulture); @@ -129,6 +299,9 @@ internal static byte[] dog_c_n300 { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// internal static byte[] dog_c_p300 { get { object obj = ResourceManager.GetObject("dog_c_p300", resourceCulture); @@ -136,195 +309,299 @@ internal static byte[] dog_c_p300 { } } - internal static byte[] dog_h_n300 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_cat_combined { get { - object obj = ResourceManager.GetObject("dog_h_n300", resourceCulture); + object obj = ResourceManager.GetObject("dog_cat_combined", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_h_p300 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_cat_combined_bw { get { - object obj = ResourceManager.GetObject("dog_h_p300", resourceCulture); + object obj = ResourceManager.GetObject("dog_cat_combined_bw", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_r_180 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_clustered_gray { get { - object obj = ResourceManager.GetObject("dog_r_180", resourceCulture); + object obj = ResourceManager.GetObject("dog_clustered_gray", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_r_n45 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_exif { get { - object obj = ResourceManager.GetObject("dog_r_n45", resourceCulture); + object obj = ResourceManager.GetObject("dog_exif", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_r_p46 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_gray { get { - object obj = ResourceManager.GetObject("dog_r_p46", resourceCulture); + object obj = ResourceManager.GetObject("dog_gray", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_r_p90 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_gray_24bit_png { get { - object obj = ResourceManager.GetObject("dog_r_p90", resourceCulture); + object obj = ResourceManager.GetObject("dog_gray_24bit_png", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_sc_50pct { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_gray_24bit_tiff { get { - object obj = ResourceManager.GetObject("dog_sc_50pct", resourceCulture); + object obj = ResourceManager.GetObject("dog_gray_24bit_tiff", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_sh_n1000 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_gray_8bit { get { - object obj = ResourceManager.GetObject("dog_sh_n1000", resourceCulture); + object obj = ResourceManager.GetObject("dog_gray_8bit", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_sh_p1000 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_gray_bmp { get { - object obj = ResourceManager.GetObject("dog_sh_p1000", resourceCulture); + object obj = ResourceManager.GetObject("dog_gray_bmp", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_s_n300 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_gray_png { get { - object obj = ResourceManager.GetObject("dog_s_n300", resourceCulture); + object obj = ResourceManager.GetObject("dog_gray_png", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_s_p300 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_gray_tiff { get { - object obj = ResourceManager.GetObject("dog_s_p300", resourceCulture); + object obj = ResourceManager.GetObject("dog_gray_tiff", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_thumb_256 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_h_n300 { get { - object obj = ResourceManager.GetObject("dog_thumb_256", resourceCulture); + object obj = ResourceManager.GetObject("dog_h_n300", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] deskewed { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_h_p300 { get { - object obj = ResourceManager.GetObject("deskewed", resourceCulture); + object obj = ResourceManager.GetObject("dog_h_p300", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] skewed { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_huge { get { - object obj = ResourceManager.GetObject("skewed", resourceCulture); + object obj = ResourceManager.GetObject("dog_huge", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] skewed_bw { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_huge_png { get { - object obj = ResourceManager.GetObject("skewed_bw", resourceCulture); + object obj = ResourceManager.GetObject("dog_huge_png", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] stock_cat { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_jp2 { get { - object obj = ResourceManager.GetObject("stock_cat", resourceCulture); + object obj = ResourceManager.GetObject("dog_jp2", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_b_p300_thumb_256 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_low_quality { get { - object obj = ResourceManager.GetObject("dog_b_p300_thumb_256", resourceCulture); + object obj = ResourceManager.GetObject("dog_low_quality", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] patcht { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_mask { get { - object obj = ResourceManager.GetObject("patcht", resourceCulture); + object obj = ResourceManager.GetObject("dog_mask", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] ocr_test { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_png { get { - object obj = ResourceManager.GetObject("ocr_test", resourceCulture); + object obj = ResourceManager.GetObject("dog_png", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] image_upc_barcode { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_r_180 { get { - object obj = ResourceManager.GetObject("image_upc_barcode", resourceCulture); + object obj = ResourceManager.GetObject("dog_r_180", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_low_quality { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_r_n45 { get { - object obj = ResourceManager.GetObject("dog_low_quality", resourceCulture); + object obj = ResourceManager.GetObject("dog_r_n45", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_alpha { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_r_p46 { get { - object obj = ResourceManager.GetObject("dog_alpha", resourceCulture); + object obj = ResourceManager.GetObject("dog_r_p46", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_alpha_tiff { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_r_p90 { get { - object obj = ResourceManager.GetObject("dog_alpha_tiff", resourceCulture); + object obj = ResourceManager.GetObject("dog_r_p90", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_mask { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_s_n300 { get { - object obj = ResourceManager.GetObject("dog_mask", resourceCulture); + object obj = ResourceManager.GetObject("dog_s_n300", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_jp2 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_s_p300 { get { - object obj = ResourceManager.GetObject("dog_jp2", resourceCulture); + object obj = ResourceManager.GetObject("dog_s_p300", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_png { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_sc_50pct { get { - object obj = ResourceManager.GetObject("dog_png", resourceCulture); + object obj = ResourceManager.GetObject("dog_sc_50pct", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_bmp { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_sh_n1000 { get { - object obj = ResourceManager.GetObject("dog_bmp", resourceCulture); + object obj = ResourceManager.GetObject("dog_sh_n1000", resourceCulture); return ((byte[])(obj)); } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_sh_p1000 { + get { + object obj = ResourceManager.GetObject("dog_sh_p1000", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dog_thumb_256 { + get { + object obj = ResourceManager.GetObject("dog_thumb_256", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// internal static byte[] dog_tiff { get { object obj = ResourceManager.GetObject("dog_tiff", resourceCulture); @@ -332,72 +609,102 @@ internal static byte[] dog_tiff { } } - internal static byte[] dog_huge { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] filled_form_annotated { get { - object obj = ResourceManager.GetObject("dog_huge", resourceCulture); + object obj = ResourceManager.GetObject("filled_form_annotated", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_huge_png { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] image_upc_barcode { get { - object obj = ResourceManager.GetObject("dog_huge_png", resourceCulture); + object obj = ResourceManager.GetObject("image_upc_barcode", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] animals_tiff { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] notblank { get { - object obj = ResourceManager.GetObject("animals_tiff", resourceCulture); + object obj = ResourceManager.GetObject("notblank", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_clustered_gray { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] ocr_test { get { - object obj = ResourceManager.GetObject("dog_clustered_gray", resourceCulture); + object obj = ResourceManager.GetObject("ocr_test", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_gray { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] patcht { get { - object obj = ResourceManager.GetObject("dog_gray", resourceCulture); + object obj = ResourceManager.GetObject("patcht", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_gray_png { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] patcht_cropped_bl { get { - object obj = ResourceManager.GetObject("dog_gray_png", resourceCulture); + object obj = ResourceManager.GetObject("patcht_cropped_bl", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_gray_24bit_png { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] patcht_cropped_br { get { - object obj = ResourceManager.GetObject("dog_gray_24bit_png", resourceCulture); + object obj = ResourceManager.GetObject("patcht_cropped_br", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_gray_bmp { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] skewed { get { - object obj = ResourceManager.GetObject("dog_gray_bmp", resourceCulture); + object obj = ResourceManager.GetObject("skewed", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_gray_tiff { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] skewed_bw { get { - object obj = ResourceManager.GetObject("dog_gray_tiff", resourceCulture); + object obj = ResourceManager.GetObject("skewed_bw", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] dog_gray_24bit_tiff { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] stock_cat { get { - object obj = ResourceManager.GetObject("dog_gray_24bit_tiff", resourceCulture); + object obj = ResourceManager.GetObject("stock_cat", resourceCulture); return ((byte[])(obj)); } } diff --git a/NAPS2.Sdk.Tests/ImageResources.resx b/NAPS2.Sdk.Tests/ImageResources.resx index 1cc2b3c466..430f3d067e 100644 --- a/NAPS2.Sdk.Tests/ImageResources.resx +++ b/NAPS2.Sdk.Tests/ImageResources.resx @@ -202,6 +202,9 @@ Resources\skewed_bw.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Resources\cat.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Resources\stock-cat.jpeg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -211,6 +214,12 @@ Resources\patcht.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Resources\patcht_cropped_bl.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\patcht_cropped_br.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Resources\ocr_test.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -223,6 +232,9 @@ Resources\dog_alpha.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Resources\dog_alpha_sc_50pct.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Resources\dog_alpha.tiff;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -256,6 +268,9 @@ Resources\dog_gray.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Resources\dog_gray_8bit.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Resources\dog_gray.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -271,4 +286,31 @@ Resources\dog_gray_24bit.tiff;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Resources\dog_99w.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\blank1.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\blank2.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\notblank.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\bw_alternating.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\filled_form_annotated.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\dog_exif.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\dog_cat_combined.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\dog_cat_combined_bw.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/BarcodeTests.cs b/NAPS2.Sdk.Tests/Images/BarcodeTests.cs index a3be4a8d41..abcccde987 100644 --- a/NAPS2.Sdk.Tests/Images/BarcodeTests.cs +++ b/NAPS2.Sdk.Tests/Images/BarcodeTests.cs @@ -12,43 +12,43 @@ public class BarcodeTests : ContextualTests public void DetectPatchT() { var image = LoadImage(ImageResources.patcht); - var detection = BarcodeDetector.Detect(image, new BarcodeDetectionOptions + var barcode = BarcodeDetector.Detect(image, new BarcodeDetectionOptions { DetectBarcodes = true, PatchTOnly = true }); - Assert.True(detection.IsAttempted); - Assert.True(detection.IsBarcodePresent); - Assert.True(detection.IsPatchT); + Assert.True(barcode.IsDetectionAttempted); + Assert.True(barcode.IsDetected); + Assert.True(barcode.IsPatchT); } [Fact] public void DetectUpc() { var image = LoadImage(ImageResources.image_upc_barcode); - var detection = BarcodeDetector.Detect(image, new BarcodeDetectionOptions + var barcode = BarcodeDetector.Detect(image, new BarcodeDetectionOptions { DetectBarcodes = true, PatchTOnly = false }); - Assert.True(detection.IsAttempted); - Assert.True(detection.IsBarcodePresent); - Assert.False(detection.IsPatchT); - Assert.Equal("725272730706", detection.DetectedText); + Assert.True(barcode.IsDetectionAttempted); + Assert.True(barcode.IsDetected); + Assert.False(barcode.IsPatchT); + Assert.Equal("725272730706", barcode.DetectedText); } [Fact] public void DetectNothing() { var image = LoadImage(ImageResources.dog); - var detection = BarcodeDetector.Detect(image, new BarcodeDetectionOptions + var barcode = BarcodeDetector.Detect(image, new BarcodeDetectionOptions { DetectBarcodes = true, PatchTOnly = false }); - Assert.True(detection.IsAttempted); - Assert.False(detection.IsBarcodePresent); - Assert.False(detection.IsPatchT); - Assert.Null(detection.DetectedText); + Assert.True(barcode.IsDetectionAttempted); + Assert.False(barcode.IsDetected); + Assert.False(barcode.IsPatchT); + Assert.Null(barcode.DetectedText); } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/BitwisePerfTests.cs b/NAPS2.Sdk.Tests/Images/BitwisePerfTests.cs index 22b0cadcbf..f1504d66d0 100644 --- a/NAPS2.Sdk.Tests/Images/BitwisePerfTests.cs +++ b/NAPS2.Sdk.Tests/Images/BitwisePerfTests.cs @@ -18,8 +18,8 @@ public BitwisePerfTests(ITestOutputHelper output) [Fact] public void CopyFast() { - var image1 = CreateAndFill(ImagePixelFormat.ARGB32); - var image2 = CreateAndFill(ImagePixelFormat.ARGB32); + using var image1 = CreateAndFill(ImagePixelFormat.ARGB32); + using var image2 = CreateAndFill(ImagePixelFormat.ARGB32); using var _ = Timer(); new CopyBitwiseImageOp().Perform(image1, image2); @@ -28,8 +28,8 @@ public void CopyFast() [Fact] public void CopyColor() { - var image1 = CreateAndFill(ImagePixelFormat.RGB24); - var image2 = CreateAndFill(ImagePixelFormat.ARGB32); + using var image1 = CreateAndFill(ImagePixelFormat.RGB24); + using var image2 = CreateAndFill(ImagePixelFormat.ARGB32); using var _ = Timer(); new CopyBitwiseImageOp().Perform(image1, image2); @@ -38,8 +38,8 @@ public void CopyColor() [Fact] public void CopyToGray() { - var image1 = CreateAndFill(ImagePixelFormat.ARGB32); - var image2 = CreateAndFill(ImagePixelFormat.Gray8); + using var image1 = CreateAndFill(ImagePixelFormat.ARGB32); + using var image2 = CreateAndFill(ImagePixelFormat.Gray8); using var _ = Timer(); new CopyBitwiseImageOp().Perform(image1, image2); @@ -48,8 +48,8 @@ public void CopyToGray() [Fact] public void CopyFromGray() { - var image1 = CreateAndFill(ImagePixelFormat.Gray8); - var image2 = CreateAndFill(ImagePixelFormat.ARGB32); + using var image1 = CreateAndFill(ImagePixelFormat.Gray8); + using var image2 = CreateAndFill(ImagePixelFormat.ARGB32); using var _ = Timer(); new CopyBitwiseImageOp().Perform(image1, image2); @@ -58,8 +58,8 @@ public void CopyFromGray() [Fact] public void CopyToBit() { - var image1 = CreateAndFill(ImagePixelFormat.ARGB32); - var image2 = CreateAndFill(ImagePixelFormat.BW1); + using var image1 = CreateAndFill(ImagePixelFormat.ARGB32); + using var image2 = CreateAndFill(ImagePixelFormat.BW1); using var _ = Timer(); new CopyBitwiseImageOp().Perform(image1, image2); @@ -68,8 +68,8 @@ public void CopyToBit() [Fact] public void CopyFromBit() { - var image1 = CreateAndFill(ImagePixelFormat.BW1); - var image2 = CreateAndFill(ImagePixelFormat.ARGB32); + using var image1 = CreateAndFill(ImagePixelFormat.BW1); + using var image2 = CreateAndFill(ImagePixelFormat.ARGB32); using var _ = Timer(); new CopyBitwiseImageOp().Perform(image1, image2); @@ -78,8 +78,8 @@ public void CopyFromBit() [Fact] public void CopyAlignedBit() { - var image1 = CreateAndFill(ImagePixelFormat.BW1); - var image2 = CreateAndFill(ImagePixelFormat.BW1); + using var image1 = CreateAndFill(ImagePixelFormat.BW1); + using var image2 = CreateAndFill(ImagePixelFormat.BW1); using var _ = Timer(); new CopyBitwiseImageOp @@ -93,8 +93,8 @@ public void CopyAlignedBit() [Fact] public void CopyUnalignedBit() { - var image1 = CreateAndFill(ImagePixelFormat.BW1); - var image2 = CreateAndFill(ImagePixelFormat.BW1); + using var image1 = CreateAndFill(ImagePixelFormat.BW1); + using var image2 = CreateAndFill(ImagePixelFormat.BW1); using var _ = Timer(); new CopyBitwiseImageOp @@ -108,7 +108,7 @@ public void CopyUnalignedBit() [Fact] public void Brightness() { - var image = CreateAndFill(ImagePixelFormat.ARGB32); + using var image = CreateAndFill(ImagePixelFormat.ARGB32); using var _ = Timer(); new BrightnessBitwiseImageOp(0.5f).Perform(image); @@ -117,7 +117,7 @@ public void Brightness() [Fact] public void Contrast() { - var image = CreateAndFill(ImagePixelFormat.ARGB32); + using var image = CreateAndFill(ImagePixelFormat.ARGB32); using var _ = Timer(); new ContrastBitwiseImageOp(0.5f).Perform(image); @@ -126,7 +126,7 @@ public void Contrast() [Fact] public void HueShift() { - var image = CreateAndFill(ImagePixelFormat.ARGB32); + using var image = CreateAndFill(ImagePixelFormat.ARGB32); using var _ = Timer(); new HueShiftBitwiseImageOp(0.5f).Perform(image); @@ -135,7 +135,7 @@ public void HueShift() [Fact] public void Saturation() { - var image = CreateAndFill(ImagePixelFormat.ARGB32); + using var image = CreateAndFill(ImagePixelFormat.ARGB32); using var _ = Timer(); new SaturationBitwiseImageOp(0.5f).Perform(image); @@ -145,8 +145,8 @@ public void Saturation() public void Sharpness() { // Using a smaller size as sharpening is super slow - var image = CreateAndFill(ImagePixelFormat.ARGB32, SIZE / 4); - var image2 = CreateAndFill(ImagePixelFormat.ARGB32, SIZE / 4); + using var image = CreateAndFill(ImagePixelFormat.ARGB32, SIZE / 4); + using var image2 = CreateAndFill(ImagePixelFormat.ARGB32, SIZE / 4); using var _ = Timer(); new SharpenBitwiseImageOp(0.5f).Perform(image, image2); @@ -156,8 +156,19 @@ public void Sharpness() public void BilateralFilter() { // Using a smaller size as sharpening is super slow - var image = CreateAndFill(ImagePixelFormat.ARGB32, SIZE / 4); - var image2 = CreateAndFill(ImagePixelFormat.ARGB32, SIZE / 4); + using var image = CreateAndFill(ImagePixelFormat.ARGB32, SIZE / 4); + using var image2 = CreateAndFill(ImagePixelFormat.ARGB32, SIZE / 4); + + using var _ = Timer(); + new BilateralFilterOp().Perform(image, image2); + } + + [Fact] + public void BilateralFilterGray() + { + // Using a smaller size as sharpening is super slow + using var image = CreateAndFill(ImagePixelFormat.Gray8, SIZE / 4); + using var image2 = CreateAndFill(ImagePixelFormat.Gray8, SIZE / 4); using var _ = Timer(); new BilateralFilterOp().Perform(image, image2); @@ -166,7 +177,7 @@ public void BilateralFilter() [Fact] public void LogicalPixelFormat() { - var image = CreateAndFill(ImagePixelFormat.ARGB32); + using var image = CreateAndFill(ImagePixelFormat.ARGB32); using var _ = Timer(); new LogicalPixelFormatOp().Perform(image); @@ -175,7 +186,7 @@ public void LogicalPixelFormat() [Fact] public void Fill() { - var image = CreateAndFill(ImagePixelFormat.ARGB32); + using var image = CreateAndFill(ImagePixelFormat.ARGB32); using var imageLock = image.Lock(LockMode.ReadWrite, out var data); using var _ = Timer(); @@ -185,7 +196,7 @@ public void Fill() [Fact] public void Invert() { - var image = CreateAndFill(ImagePixelFormat.ARGB32); + using var image = CreateAndFill(ImagePixelFormat.ARGB32); using var imageLock = image.Lock(LockMode.ReadWrite, out var data); using var _ = Timer(); diff --git a/NAPS2.Sdk.Tests/Images/BlankDetectorTests.cs b/NAPS2.Sdk.Tests/Images/BlankDetectorTests.cs new file mode 100644 index 0000000000..a9459659cf --- /dev/null +++ b/NAPS2.Sdk.Tests/Images/BlankDetectorTests.cs @@ -0,0 +1,58 @@ +using NAPS2.Images.Bitwise; +using Xunit; + +namespace NAPS2.Sdk.Tests.Images; + +public class BlankDetectorTests : ContextualTests +{ + private const int WHITE_THRESHOLD = 70; + private const int COVERAGE_THRESHOLD = 15; + + [Theory] + [MemberData(nameof(TestCases))] + public void Blank1(ImagePixelFormat pixelFormat) + { + var image = GetTestImage(ImageResources.blank1, pixelFormat); + var op = new BlankDetectionImageOp(WHITE_THRESHOLD, COVERAGE_THRESHOLD); + op.Perform(image); + Assert.True(op.IsBlank); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void Blank2(ImagePixelFormat pixelFormat) + { + var image = GetTestImage(ImageResources.blank2, pixelFormat); + var op = new BlankDetectionImageOp(WHITE_THRESHOLD, COVERAGE_THRESHOLD); + op.Perform(image); + Assert.True(op.IsBlank); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void NotBlank(ImagePixelFormat pixelFormat) + { + var image = GetTestImage(ImageResources.notblank, pixelFormat); + var op = new BlankDetectionImageOp(WHITE_THRESHOLD, COVERAGE_THRESHOLD); + op.Perform(image); + Assert.False(op.IsBlank); + } + + private IMemoryImage GetTestImage(byte[] resource, ImagePixelFormat pixelFormat) + { + var image = LoadImage(resource); + if (pixelFormat == ImagePixelFormat.BW1) + { + return image.PerformTransform(new BlackWhiteTransform(WHITE_THRESHOLD * 20 - 1000)); + } + return image.CopyWithPixelFormat(pixelFormat); + } + + public static IEnumerable TestCases = + [ + new object[] { ImagePixelFormat.ARGB32 }, + new object[] { ImagePixelFormat.RGB24 }, + new object[] { ImagePixelFormat.Gray8 }, + new object[] { ImagePixelFormat.BW1 } + ]; +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/CopyBitwiseImageOpTests.cs b/NAPS2.Sdk.Tests/Images/CopyBitwiseImageOpTests.cs index 599b02ac18..d9c28afb05 100644 --- a/NAPS2.Sdk.Tests/Images/CopyBitwiseImageOpTests.cs +++ b/NAPS2.Sdk.Tests/Images/CopyBitwiseImageOpTests.cs @@ -71,4 +71,87 @@ public void BlackWhiteToRgba() var dest = original.CopyWithPixelFormat(ImagePixelFormat.ARGB32); ImageAsserts.Similar(original, dest, 0); } + + [Fact] + public void BlackWhiteInvertSource() + { + var srcBuffer = new byte[] { 0xFA, 0x03 }; + var srcInfo = new PixelInfo(8, 2, SubPixelType.InvertedBit); + + var dstBuffer = new byte[] { 0, 0 }; + var dstInfo = new PixelInfo(8, 2, SubPixelType.Bit); + + new CopyBitwiseImageOp().Perform(srcBuffer, srcInfo, dstBuffer, dstInfo); + + var expectedBuffer = new byte[] { 0x05, 0xFC }; + Assert.Equal(expectedBuffer, dstBuffer); + } + + [Fact] + public void BlackWhiteInvertDest() + { + var srcBuffer = new byte[] { 0xFA, 0x03 }; + var srcInfo = new PixelInfo(8, 2, SubPixelType.Bit); + + var dstBuffer = new byte[2]; + var dstInfo = new PixelInfo(8, 2, SubPixelType.InvertedBit); + + new CopyBitwiseImageOp().Perform(srcBuffer, srcInfo, dstBuffer, dstInfo); + + var expectedBuffer = new byte[] { 0x05, 0xFC }; + Assert.Equal(expectedBuffer, dstBuffer); + } + + [Fact] + public void BlackWhiteInvertSourceAndDest() + { + var srcBuffer = new byte[] { 0xFA, 0x03 }; + var srcInfo = new PixelInfo(8, 2, SubPixelType.InvertedBit); + + var dstBuffer = new byte[] { 0, 0 }; + var dstInfo = new PixelInfo(8, 2, SubPixelType.InvertedBit); + + new CopyBitwiseImageOp().Perform(srcBuffer, srcInfo, dstBuffer, dstInfo); + + var expectedBuffer = new byte[] { 0xFA, 0x03 }; + Assert.Equal(expectedBuffer, dstBuffer); + } + + [Fact] + public void BlackWhiteInvertSourceToGray() + { + var srcBuffer = new byte[] { 0xFA, 0x03 }; + var srcInfo = new PixelInfo(8, 2, SubPixelType.InvertedBit); + + var dstBuffer = new byte[16]; + var dstInfo = new PixelInfo(8, 2, SubPixelType.Gray); + + new CopyBitwiseImageOp().Perform(srcBuffer, srcInfo, dstBuffer, dstInfo); + + var expectedBuffer = new byte[] + { + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, // 0x05 expanded + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00 // 0xFC expanded + }; + Assert.Equal(expectedBuffer, dstBuffer); + } + + [Fact] + public void BlackWhiteInvertDestFromGray() + { + var srcBuffer = new byte[] + { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0x00, // 0xFA expanded + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF // 0x03 expanded + }; + var srcInfo = new PixelInfo(8, 2, SubPixelType.Gray); + + var dstBuffer = new byte[2]; + var dstInfo = new PixelInfo(8, 2, SubPixelType.InvertedBit); + + new CopyBitwiseImageOp().Perform(srcBuffer, srcInfo, dstBuffer, dstInfo); + + var expectedBuffer = new byte[] { 0x05, 0xFC }; + Assert.Equal(expectedBuffer, dstBuffer); + } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/DeskewTests.cs b/NAPS2.Sdk.Tests/Images/DeskewTests.cs index 3a4b25b70d..86e76c8e33 100644 --- a/NAPS2.Sdk.Tests/Images/DeskewTests.cs +++ b/NAPS2.Sdk.Tests/Images/DeskewTests.cs @@ -27,7 +27,7 @@ public void DeskewTransform() var image = LoadImage(ImageResources.skewed); var transform = Deskewer.GetDeskewTransform(image); var deskewedImage = image.PerformTransform(transform); - ImageAsserts.Similar(ImageResources.deskewed, deskewedImage, ImageAsserts.XPLAT_RMSE_THRESHOLD); + ImageAsserts.Similar(ImageResources.deskewed, deskewedImage, ImageAsserts.XL_RMSE_THRESHOLD); } [Fact] diff --git a/NAPS2.Sdk.Tests/Images/GdiImageTests.cs b/NAPS2.Sdk.Tests/Images/GdiImageTests.cs index 352e05776c..e09b99b4e7 100644 --- a/NAPS2.Sdk.Tests/Images/GdiImageTests.cs +++ b/NAPS2.Sdk.Tests/Images/GdiImageTests.cs @@ -7,9 +7,7 @@ namespace NAPS2.Sdk.Tests.Images; -#if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif public class GdiImageTests { [Fact] @@ -32,7 +30,7 @@ public void LoadInvertedPaletteBlackAndWhiteImage() { var bitmap = new Bitmap(new MemoryStream(ImageResources.dog_bw_invertpal)); - var image = new GdiImage(new GdiImageContext(), bitmap); + var image = new GdiImage(bitmap); Assert.True(image.FixedPixelFormat); Assert.Equal(ImagePixelFormat.BW1, image.PixelFormat); Assert.Equal(Color.Black.ToArgb(), image.Bitmap.Palette.Entries[0].ToArgb()); @@ -49,7 +47,7 @@ public void LoadNonGrayscale8BitImage() p.Entries[128] = Color.Blue; bitmap.Palette = p; - var image = new GdiImage(new GdiImageContext(), bitmap); + var image = new GdiImage(bitmap); Assert.True(image.FixedPixelFormat); Assert.Equal(ImagePixelFormat.RGB24, image.PixelFormat); } @@ -59,7 +57,7 @@ public void Load48BitImage() { var bitmap = new Bitmap(1, 1, PixelFormat.Format48bppRgb); - var image = new GdiImage(new GdiImageContext(), bitmap); + var image = new GdiImage(bitmap); Assert.True(image.FixedPixelFormat); Assert.Equal(ImagePixelFormat.RGB24, image.PixelFormat); } @@ -69,7 +67,7 @@ public void Load64BitImage() { var bitmap = new Bitmap(1, 1, PixelFormat.Format64bppArgb); - var image = new GdiImage(new GdiImageContext(), bitmap); + var image = new GdiImage(bitmap); Assert.True(image.FixedPixelFormat); Assert.Equal(ImagePixelFormat.ARGB32, image.PixelFormat); } diff --git a/NAPS2.Sdk.Tests/Images/ImageBenchmarkTests.cs b/NAPS2.Sdk.Tests/Images/ImageBenchmarkTests.cs new file mode 100644 index 0000000000..e2226923e7 --- /dev/null +++ b/NAPS2.Sdk.Tests/Images/ImageBenchmarkTests.cs @@ -0,0 +1,35 @@ +using Xunit; + +namespace NAPS2.Sdk.Tests.Images; + +public class ImageBenchmarkTests : ContextualTests +{ + [BenchmarkFact] + public void Save300Jpeg() + { + var filePath = Path.Combine(FolderPath, "test"); + var image = LoadImage(ImageResources.dog); + + for (int i = 0; i < 300; i++) + { + image.Save(filePath + i + ".jpg"); + } + } + + [BenchmarkFact] + public void SaveHugeJpeg() + { + var filePath = Path.Combine(FolderPath, "test"); + var image = LoadImage(ImageResources.dog_huge); + + image.Save(filePath + ".jpg"); + } + + public class BenchmarkFact : FactAttribute + { + public BenchmarkFact() + { + Skip = "comment out this line to run benchmarks"; + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/ImageExportHelperTests.cs b/NAPS2.Sdk.Tests/Images/ImageExportHelperTests.cs index 9f163a7219..13c41e6a6a 100644 --- a/NAPS2.Sdk.Tests/Images/ImageExportHelperTests.cs +++ b/NAPS2.Sdk.Tests/Images/ImageExportHelperTests.cs @@ -5,20 +5,13 @@ namespace NAPS2.Sdk.Tests.Images; public class ImageExportHelperTests : ContextualTests { - private ImageExportHelper _helper; - - public ImageExportHelperTests() - { - _helper = new ImageExportHelper(); - } - [Fact] public void SaveSmallestFormat_BlackAndWhite() { var bw = LoadImage(ImageResources.dog_bw).PerformTransform(new BlackWhiteTransform()); var path = Path.Combine(FolderPath, "test"); - var fullPath = _helper.SaveSmallestFormat(path, bw, BitDepth.BlackAndWhite, false, -1, out var format); + var fullPath = ImageExportHelper.SaveSmallestFormat(path, bw, false, -1, out var format); AssertPng(format, fullPath, ImageResources.dog_bw); } @@ -29,42 +22,42 @@ public void SaveSmallestFormat_BlackAndWhiteWithColorBitDepth() var bw = LoadImage(ImageResources.dog_bw).PerformTransform(new BlackWhiteTransform()); var path = Path.Combine(FolderPath, "test"); - var fullPath = _helper.SaveSmallestFormat(path, bw, BitDepth.Color, false, -1, out var format); + var fullPath = ImageExportHelper.SaveSmallestFormat(path, bw, false, -1, out var format); AssertPng(format, fullPath, ImageResources.dog_bw); } [Fact] - public void SaveSmallestFormat_ColorWithBlackWhiteBitDepth() + public void SaveSmallestFormat_ColorHighQuality() { var color = LoadImage(ImageResources.dog); var path = Path.Combine(FolderPath, "test"); - var fullPath = _helper.SaveSmallestFormat(path, color, BitDepth.BlackAndWhite, false, -1, out var format); + var fullPath = ImageExportHelper.SaveSmallestFormat(path, color, true, -1, out var format); - AssertPng(format, fullPath, ImageResources.dog_bw, ImageAsserts.XPLAT_RMSE_THRESHOLD); + AssertPng(format, fullPath, ImageResources.dog); } [Fact] - public void SaveSmallestFormat_ColorHighQuality() + public void SaveSmallestFormat_LogicalBlackWhite() { - var color = LoadImage(ImageResources.dog); + var bw = LoadImage(ImageResources.dog_bw_24bit); var path = Path.Combine(FolderPath, "test"); - var fullPath = _helper.SaveSmallestFormat(path, color, BitDepth.Color, true, -1, out var format); + var fullPath = ImageExportHelper.SaveSmallestFormat(path, bw, false, -1, out var format); - AssertPng(format, fullPath, ImageResources.dog); + AssertPng(format, fullPath, ImageResources.dog_bw); } [Fact] public void SaveSmallestFormat_SmallerPng() { - var bw = LoadImage(ImageResources.dog_bw_24bit); + var bw = LoadImage(ImageResources.dog_clustered_gray); var path = Path.Combine(FolderPath, "test"); - var fullPath = _helper.SaveSmallestFormat(path, bw, BitDepth.Color, false, -1, out var format); + var fullPath = ImageExportHelper.SaveSmallestFormat(path, bw, false, -1, out var format); - AssertPng(format, fullPath, ImageResources.dog_bw); + AssertPng(format, fullPath, ImageResources.dog_clustered_gray); } [Fact] @@ -73,7 +66,7 @@ public void SaveSmallestFormat_OriginalPng() var color = LoadImage(ImageResources.dog_png); var path = Path.Combine(FolderPath, "test"); - var fullPath = _helper.SaveSmallestFormat(path, color, BitDepth.Color, false, -1, out var format); + var fullPath = ImageExportHelper.SaveSmallestFormat(path, color, false, -1, out var format); AssertPng(format, fullPath, ImageResources.dog); } @@ -82,10 +75,10 @@ public void SaveSmallestFormat_OriginalPng() public void SaveSmallestFormat_SmallerJpeg() { var color = LoadImage(ImageResources.dog_png); - color.OriginalFileFormat = ImageFileFormat.Unspecified; + color.OriginalFileFormat = ImageFileFormat.Unknown; var path = Path.Combine(FolderPath, "test"); - var fullPath = _helper.SaveSmallestFormat(path, color, BitDepth.Color, false, -1, out var format); + var fullPath = ImageExportHelper.SaveSmallestFormat(path, color, false, -1, out var format); AssertJpeg(format, fullPath, ImageResources.dog); } @@ -96,7 +89,7 @@ public void SaveSmallestFormat_OriginalJpeg() var color = LoadImage(ImageResources.dog_bw_jpg); var path = Path.Combine(FolderPath, "test"); - var fullPath = _helper.SaveSmallestFormat(path, color, BitDepth.Color, false, -1, out var format); + var fullPath = ImageExportHelper.SaveSmallestFormat(path, color, false, -1, out var format); AssertJpeg(format, fullPath, ImageResources.dog_bw); } diff --git a/NAPS2.Sdk.Tests/Images/ImageSerializerTests.cs b/NAPS2.Sdk.Tests/Images/ImageSerializerTests.cs index 236ea5dd01..7b52b98f32 100644 --- a/NAPS2.Sdk.Tests/Images/ImageSerializerTests.cs +++ b/NAPS2.Sdk.Tests/Images/ImageSerializerTests.cs @@ -1,4 +1,4 @@ -using NAPS2.ImportExport.Pdf; +using NAPS2.Pdf; using NAPS2.Scan; using NAPS2.Sdk.Tests.Asserts; using NAPS2.Serialization; @@ -21,9 +21,9 @@ public void SerializesMetadata(StorageConfig config) using var sourceImage = ScanningContext.CreateProcessedImage( LoadImage(ImageResources.dog), // TODO: Use an actual grayscale image - BitDepth.Grayscale, true, -1, + PageSize.A4, new[] { new BrightnessTransform(300) }); var serializedImage = ImageSerializer.Serialize(sourceImage, new SerializeImageOptions()); @@ -32,7 +32,7 @@ public void SerializesMetadata(StorageConfig config) Assert.Single(destImage.TransformState.Transforms); Assert.Equal(300, Assert.IsType(destImage.TransformState.Transforms[0]).Brightness); Assert.True(destImage.Metadata.Lossless); - Assert.Equal(BitDepth.Grayscale, destImage.Metadata.BitDepth); + Assert.Equal(PageSize.A4, destImage.Metadata.PageSize); ImageAsserts.Similar(ImageResources.dog_b_p300, destImage); } @@ -41,9 +41,7 @@ public void SerializesMetadata(StorageConfig config) public void DeserializeToFileStorage(StorageConfig config) { config.Apply(this); - - using var destContext = new ScanningContext(TestImageContextFactory.Get(), - FileStorageManager.CreateFolder(Path.Combine(FolderPath, "dest"))); + using var destContext = CreateDestContextWithFileStorage(); using var sourceImage = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); var serializedImage = ImageSerializer.Serialize(sourceImage, new SerializeImageOptions()); @@ -60,7 +58,6 @@ public void DeserializeToFileStorage(StorageConfig config) public void DeserializeToMemoryStorage(StorageConfig config) { config.Apply(this); - using var destContext = new ScanningContext(TestImageContextFactory.Get()); using var sourceImage = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); @@ -77,9 +74,7 @@ public void DeserializeToMemoryStorage(StorageConfig config) public void ShareFileStorage() { new StorageConfig.File().Apply(this); - - using var destContext = new ScanningContext(TestImageContextFactory.Get(), - FileStorageManager.CreateFolder(Path.Combine(FolderPath, "dest"))); + using var destContext = CreateDestContextWithFileStorage(); using var sourceImage = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); var serializedImage = ImageSerializer.Serialize(sourceImage, new SerializeImageOptions()); @@ -103,9 +98,7 @@ public void ShareFileStorage() public void TransferOwnership() { new StorageConfig.File().Apply(this); - - using var destContext = new ScanningContext(TestImageContextFactory.Get(), - FileStorageManager.CreateFolder(Path.Combine(FolderPath, "dest"))); + using var destContext = CreateDestContextWithFileStorage(); using var sourceImage = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); var serializedImage = ImageSerializer.Serialize(sourceImage, new SerializeImageOptions @@ -129,9 +122,7 @@ public void TransferOwnership() public void CrossDevice() { new StorageConfig.File().Apply(this); - - using var destContext = new ScanningContext(TestImageContextFactory.Get(), - FileStorageManager.CreateFolder(Path.Combine(FolderPath, "dest"))); + using var destContext = CreateDestContextWithFileStorage(); using var sourceImage = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); var serializedImage = ImageSerializer.Serialize(sourceImage, new SerializeImageOptions @@ -149,9 +140,7 @@ public void CrossDevice() public void DisposeBeforeDeserialization() { new StorageConfig.File().Apply(this); - - using var destContext = new ScanningContext(TestImageContextFactory.Get(), - FileStorageManager.CreateFolder(Path.Combine(FolderPath, "dest"))); + using var destContext = CreateDestContextWithFileStorage(); using var sourceImage = CreateScannedImage(); var serializedImage = ImageSerializer.Serialize(sourceImage, new SerializeImageOptions()); @@ -165,9 +154,7 @@ public void DisposeBeforeDeserialization() public void ShareFileStorage_DisposeBeforeDeserialization() { new StorageConfig.File().Apply(this); - - using var destContext = new ScanningContext(TestImageContextFactory.Get(), - FileStorageManager.CreateFolder(Path.Combine(FolderPath, "dest"))); + using var destContext = CreateDestContextWithFileStorage(); using var sourceImage = CreateScannedImage(); var serializedImage = ImageSerializer.Serialize(sourceImage, new SerializeImageOptions()); @@ -184,9 +171,7 @@ public void ShareFileStorage_DisposeBeforeDeserialization() public void TransferOwnershipOfSharedFile() { new StorageConfig.File().Apply(this); - - using var destContext = new ScanningContext(TestImageContextFactory.Get(), - FileStorageManager.CreateFolder(Path.Combine(FolderPath, "dest"))); + using var destContext = CreateDestContextWithFileStorage(); using var sourceImage = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); var serializedImage = ImageSerializer.Serialize(sourceImage, new SerializeImageOptions()); @@ -273,13 +258,11 @@ public void CrossDevice_AndTransferOwnership() public async Task PdfDeserializeToFileStorage(StorageConfig config) { config.Apply(this); + using var destContext = CreateDestContextWithFileStorage(); var importPath = Path.Combine(FolderPath, "import.pdf"); File.WriteAllBytes(importPath, PdfResources.word_generated_pdf); - using var destContext = new ScanningContext(TestImageContextFactory.Get(new PdfiumPdfRenderer()), - FileStorageManager.CreateFolder(Path.Combine(FolderPath, "dest"))); - using var sourceImage = await new PdfImporter(ScanningContext).Import(importPath).FirstAsync(); var serializedImage = ImageSerializer.Serialize(sourceImage, new SerializeImageOptions()); using var destImage = ImageSerializer.Deserialize(destContext, serializedImage, new DeserializeImageOptions()); @@ -295,12 +278,11 @@ public async Task PdfDeserializeToFileStorage(StorageConfig config) public async Task PdfDeserializeToMemoryStorage(StorageConfig config) { config.Apply(this); + using var destContext = new ScanningContext(TestImageContextFactory.Get()); var importPath = Path.Combine(FolderPath, "import.pdf"); File.WriteAllBytes(importPath, PdfResources.word_generated_pdf); - using var destContext = new ScanningContext(TestImageContextFactory.Get(new PdfiumPdfRenderer())); - using var sourceImage = await new PdfImporter(ScanningContext).Import(importPath).FirstAsync(); var serializedImage = ImageSerializer.Serialize(sourceImage, new SerializeImageOptions()); using var destImage = ImageSerializer.Deserialize(destContext, serializedImage, new DeserializeImageOptions()); @@ -316,7 +298,6 @@ public async Task PdfDeserializeToMemoryStorage(StorageConfig config) public void IncludeThumbnail(StorageConfig config) { config.Apply(this); - using var destContext = new ScanningContext(TestImageContextFactory.Get()); using var sourceImage = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); @@ -356,4 +337,12 @@ public void IncludeThumbnail_InvalidTransformState() Assert.Null(destImage.PostProcessingData.Thumbnail); } + + private ScanningContext CreateDestContextWithFileStorage() + { + return new ScanningContext(TestImageContextFactory.Get()) + { + FileStorageManager = FileStorageManager.CreateFolder(Path.Combine(FolderPath, "dest")) + }; + } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/LoadSaveTests.cs b/NAPS2.Sdk.Tests/Images/LoadSaveTests.cs index 5c5484715d..675d65bc8a 100644 --- a/NAPS2.Sdk.Tests/Images/LoadSaveTests.cs +++ b/NAPS2.Sdk.Tests/Images/LoadSaveTests.cs @@ -1,6 +1,6 @@ using System.Globalization; -using Moq; using NAPS2.Sdk.Tests.Asserts; +using NSubstitute; using Xunit; namespace NAPS2.Sdk.Tests.Images; @@ -15,8 +15,10 @@ public class LoadSaveTests : ContextualTests [Theory] [MemberData(nameof(TestCases))] public void LoadFromFile(ImageFileFormat format, string ext, string resource, string[] compare, - ImagePixelFormat[] logicalPixelFormats, bool ignoreRes) + ImagePixelFormat[] logicalPixelFormats, bool ignoreRes, PlatformFactAttribute platforms = null) { + if (platforms is { DoSkip: true }) return; + var path = CopyResourceToFile(GetResource(resource), $"image{ext}"); if (!ImageContext.SupportsFormat(format)) @@ -27,15 +29,17 @@ public void LoadFromFile(ImageFileFormat format, string ext, string resource, st using var image = ImageContext.Load(path); Assert.Equal(format, image.OriginalFileFormat); - Assert.Equal(logicalPixelFormats[0], image.LogicalPixelFormat); + Assert.Equal(logicalPixelFormats[0], image.UpdateLogicalPixelFormat()); ImageAsserts.Similar(GetResource(compare[0]), image, ignoreResolution: ignoreRes); } [Theory] [MemberData(nameof(TestCases))] public void LoadFromStream(ImageFileFormat format, string ext, string resource, string[] compare, - ImagePixelFormat[] logicalPixelFormats, bool ignoreRes) + ImagePixelFormat[] logicalPixelFormats, bool ignoreRes, PlatformFactAttribute platforms = null) { + if (platforms is { DoSkip: true }) return; + var stream = new MemoryStream(GetResource(resource)); if (!ImageContext.SupportsFormat(format)) @@ -46,69 +50,75 @@ public void LoadFromStream(ImageFileFormat format, string ext, string resource, using var image = ImageContext.Load(stream); Assert.Equal(format, image.OriginalFileFormat); - Assert.Equal(logicalPixelFormats[0], image.LogicalPixelFormat); + Assert.Equal(logicalPixelFormats[0], image.UpdateLogicalPixelFormat()); ImageAsserts.Similar(GetResource(compare[0]), image, ignoreResolution: ignoreRes); } [Theory] [MemberData(nameof(TestCases))] public async Task LoadFramesFromFile(ImageFileFormat format, string ext, string resource, string[] compare, - ImagePixelFormat[] logicalPixelFormats, bool ignoreRes) + ImagePixelFormat[] logicalPixelFormats, bool ignoreRes, PlatformFactAttribute platforms = null) { + if (platforms is { DoSkip: true }) return; + var path = CopyResourceToFile(GetResource(resource), $"image{ext}"); - var progressMock = new Mock(); + var progressMock = Substitute.For(); if (!ImageContext.SupportsFormat(format)) { await Assert.ThrowsAsync(async () => - await ImageContext.LoadFrames(path, progressMock.Object).ToListAsync()); + await ImageContext.LoadFrames(path, progressMock).ToListAsync()); return; } - var images = await ImageContext.LoadFrames(path, progressMock.Object).ToListAsync(); + var images = await ImageContext.LoadFrames(path, progressMock).ToListAsync(); Assert.Equal(compare.Length, images.Count); for (int i = 0; i < images.Count; i++) { Assert.Equal(format, images[i].OriginalFileFormat); - Assert.Equal(logicalPixelFormats[i], images[i].LogicalPixelFormat); + Assert.Equal(logicalPixelFormats[i], images[i].UpdateLogicalPixelFormat()); ImageAsserts.Similar(GetResource(compare[i]), images[i], ignoreResolution: ignoreRes); - progressMock.Verify(x => x(i, images.Count)); + progressMock.Received()(i, images.Count); } - progressMock.Verify(x => x(images.Count, images.Count)); + progressMock.Received()(images.Count, images.Count); } [Theory] [MemberData(nameof(TestCases))] public async Task LoadFramesFromStream(ImageFileFormat format, string ext, string resource, string[] compare, - ImagePixelFormat[] logicalPixelFormats, bool ignoreRes) + ImagePixelFormat[] logicalPixelFormats, bool ignoreRes, PlatformFactAttribute platforms = null) { + if (platforms is { DoSkip: true }) return; + var stream = new MemoryStream(GetResource(resource)); - var progressMock = new Mock(); + var progressMock = Substitute.For(); if (!ImageContext.SupportsFormat(format)) { await Assert.ThrowsAsync(async () => - await ImageContext.LoadFrames(stream, progressMock.Object).ToListAsync()); + await ImageContext.LoadFrames(stream, progressMock).ToListAsync()); return; } - var images = await ImageContext.LoadFrames(stream, progressMock.Object).ToListAsync(); + var images = await ImageContext.LoadFrames(stream, progressMock).ToListAsync(); Assert.Equal(compare.Length, images.Count); for (int i = 0; i < images.Count; i++) { Assert.Equal(format, images[i].OriginalFileFormat); - Assert.Equal(logicalPixelFormats[i], images[i].LogicalPixelFormat); + Assert.Equal(logicalPixelFormats[i], images[i].UpdateLogicalPixelFormat()); ImageAsserts.Similar(GetResource(compare[i]), images[i], ignoreResolution: ignoreRes); - progressMock.Verify(x => x(i, images.Count)); + progressMock.Received()(i, images.Count); } - progressMock.Verify(x => x(images.Count, images.Count)); + progressMock.Received()(images.Count, images.Count); } [Theory] [MemberData(nameof(TestCases))] public void SaveToFile(ImageFileFormat format, string ext, string resource, string[] compare, - ImagePixelFormat[] logicalPixelFormats, bool ignoreRes) + ImagePixelFormat[] logicalPixelFormats, bool ignoreRes, PlatformFactAttribute platforms = null) { + if (platforms is { DoSkip: true }) return; + var path = Path.Combine(FolderPath, $"image{ext}"); var expected = LoadImage(GetResource(compare[0])); @@ -122,15 +132,17 @@ public void SaveToFile(ImageFileFormat format, string ext, string resource, stri var image2 = ImageContext.Load(path); Assert.Equal(format, image2.OriginalFileFormat); - Assert.Equal(logicalPixelFormats[0], image2.LogicalPixelFormat); + Assert.Equal(logicalPixelFormats[0], image2.UpdateLogicalPixelFormat()); ImageAsserts.Similar(expected, image2, ignoreResolution: ignoreRes); } [Theory] [MemberData(nameof(TestCases))] public void SaveToStream(ImageFileFormat format, string ext, string resource, string[] compare, - ImagePixelFormat[] logicalPixelFormats, bool ignoreRes) + ImagePixelFormat[] logicalPixelFormats, bool ignoreRes, PlatformFactAttribute platforms = null) { + if (platforms is { DoSkip: true }) return; + var stream = new MemoryStream(); var expected = LoadImage(GetResource(compare[0])); @@ -144,7 +156,7 @@ public void SaveToStream(ImageFileFormat format, string ext, string resource, st var image2 = ImageContext.Load(stream); Assert.Equal(format, image2.OriginalFileFormat); - Assert.Equal(logicalPixelFormats[0], image2.LogicalPixelFormat); + Assert.Equal(logicalPixelFormats[0], image2.UpdateLogicalPixelFormat()); ImageAsserts.Similar(expected, image2, ignoreResolution: ignoreRes); } @@ -169,115 +181,204 @@ public async Task LoadFramesFromWrongExtension() ImageAsserts.Similar(ImageResources.dog, images[0]); } + [Fact] + public void SetResolutionAndSaveJpeg() + { + var image = LoadImage(ImageResources.dog); + + image.SetResolution(300, 300); + var stream = image.SaveToMemoryStream(ImageFileFormat.Jpeg); + var image2 = ImageContext.Load(stream); + + Assert.Equal(300, image2.HorizontalResolution, 2); + Assert.Equal(300, image2.VerticalResolution, 2); + } + + [Fact] + public void SetResolutionAndSavePng() + { + var image = LoadImage(ImageResources.dog); + + image.SetResolution(300, 300); + var stream = image.SaveToMemoryStream(ImageFileFormat.Png); + var image2 = ImageContext.Load(stream); + + Assert.Equal(300, image2.HorizontalResolution, 2); + Assert.Equal(300, image2.VerticalResolution, 2); + } + + [Fact] + public void SavePngOptimizesBitDepth() + { + var image32Bpp = LoadImage(ImageResources.dog_bw).CopyWithPixelFormat(ImagePixelFormat.ARGB32); + var image24Bpp = LoadImage(ImageResources.dog_bw).CopyWithPixelFormat(ImagePixelFormat.RGB24); + var image8Bpp = LoadImage(ImageResources.dog_bw).CopyWithPixelFormat(ImagePixelFormat.Gray8); + var image1Bpp = LoadImage(ImageResources.dog_bw).CopyWithPixelFormat(ImagePixelFormat.BW1); + + var optimized32Bpp = GetSavedSize(image24Bpp, ImageFileFormat.Png); + var optimized24Bpp = GetSavedSize(image24Bpp, ImageFileFormat.Png); + var optimized8Bpp = GetSavedSize(image8Bpp, ImageFileFormat.Png); + var optimized1Bpp = GetSavedSize(image1Bpp, ImageFileFormat.Png); + + // All should be equal as since the logical pixel format is BW1, all should be converted to BW1 for saving. + Assert.Equal(optimized24Bpp, optimized32Bpp); + Assert.Equal(optimized24Bpp, optimized8Bpp); + Assert.Equal(optimized24Bpp, optimized1Bpp); + } + + // Gtk does not support saving with 1bpp/8bpp + [PlatformFact(exclude: PlatformFlags.GtkImage)] + public void SavePngWithUnoptimizedBitDepth() + { + var image32Bpp = LoadImage(ImageResources.dog_bw).CopyWithPixelFormat(ImagePixelFormat.ARGB32); + var image24Bpp = LoadImage(ImageResources.dog_bw).CopyWithPixelFormat(ImagePixelFormat.RGB24); + var image8Bpp = LoadImage(ImageResources.dog_bw).CopyWithPixelFormat(ImagePixelFormat.Gray8); + var image1Bpp = LoadImage(ImageResources.dog_bw).CopyWithPixelFormat(ImagePixelFormat.BW1); + + // Specifying a PixelFormatHint equal to the real pixel format prevents optimized saving. + + var optimized32Bpp = GetSavedSize(image32Bpp, ImageFileFormat.Png); + var unoptimized32Bpp = GetSavedSize(image32Bpp, ImageFileFormat.Png, + new ImageSaveOptions { PixelFormatHint = ImagePixelFormat.ARGB32 }); + + var optimized24Bpp = GetSavedSize(image24Bpp, ImageFileFormat.Png); + var unoptimized24Bpp = GetSavedSize(image24Bpp, ImageFileFormat.Png, + new ImageSaveOptions { PixelFormatHint = ImagePixelFormat.RGB24 }); + + var optimized8Bpp = GetSavedSize(image8Bpp, ImageFileFormat.Png); + var unoptimized8Bpp = GetSavedSize(image8Bpp, ImageFileFormat.Png, + new ImageSaveOptions { PixelFormatHint = ImagePixelFormat.Gray8 }); + + var optimized1Bpp = GetSavedSize(image1Bpp, ImageFileFormat.Png); + var unoptimized1Bpp = GetSavedSize(image1Bpp, ImageFileFormat.Png, + new ImageSaveOptions { PixelFormatHint = ImagePixelFormat.BW1 }); + + // All optimized values should be less than their unoptimized counterparts. + Assert.True(optimized32Bpp < unoptimized32Bpp); + Assert.True(optimized24Bpp < unoptimized24Bpp); + if (!CurrentPlatformFlags.HasAny(PlatformFlags.ImageSharpImage | PlatformFlags.WpfImage)) + { + Assert.True(optimized8Bpp < unoptimized8Bpp); + Assert.Equal(optimized1Bpp, unoptimized1Bpp); + + // Verify that 1bpp < 8bpp < 24bpp. 32bpp and 24bpp should be close but may vary so it isn't worth testing. + Assert.True(unoptimized1Bpp < unoptimized8Bpp); + Assert.True(unoptimized8Bpp < unoptimized24Bpp); + } + } + + [Fact] + public void PixelFormatHintDoesntLoseColor() + { + var original = LoadImage(ImageResources.dog); + + var stream = new MemoryStream(); + original.Save(stream, ImageFileFormat.Png, new ImageSaveOptions { PixelFormatHint = ImagePixelFormat.BW1 }); + + var copy = ImageContext.Load(stream); + ImageAsserts.Similar(ImageResources.dog, copy); + } + + private int GetSavedSize(IMemoryImage image, ImageFileFormat fileFormat, ImageSaveOptions options = null) + { + var stream = new MemoryStream(); + image.Save(stream, fileFormat, options); + return (int) stream.Length; + } + private static byte[] GetResource(string resource) => (byte[]) ImageResources.ResourceManager.GetObject(resource, CultureInfo.InvariantCulture); // TODO: Ignore resolution by default in the existing tests, but have separate tests/test cases for resolution - public static IEnumerable TestCases = new List - { - new object[] - { + public static IEnumerable TestCases = + [ + [ ImageFileFormat.Png, ".png", "dog_alpha", new[] { "dog_alpha" }, new[] { ImagePixelFormat.ARGB32 }, false - }, - new object[] - { + ], + [ ImageFileFormat.Png, ".png", "dog_png", new[] { "dog" }, new[] { ImagePixelFormat.RGB24 }, false - }, - new object[] - { + ], + [ ImageFileFormat.Png, ".png", "dog_gray_png", new[] { "dog_gray" }, new[] { ImagePixelFormat.Gray8 }, true - }, - new object[] - { + ], + [ ImageFileFormat.Png, ".png", "dog_gray_24bit_png", - new[] { "dog_gray" }, new[] { ImagePixelFormat.Gray8 }, false - }, - new object[] - { + new[] { "dog_gray" }, new[] { ImagePixelFormat.Gray8 }, false, + // TODO: Can we improve this for WPF? + new PlatformFactAttribute(exclude: PlatformFlags.WpfImage) + ], + [ ImageFileFormat.Png, ".png", "dog_bw", new[] { "dog_bw" }, new[] { ImagePixelFormat.BW1 }, false - }, - new object[] - { + ], + [ ImageFileFormat.Png, ".png", "dog_bw_24bit", new[] { "dog_bw" }, new[] { ImagePixelFormat.BW1 }, false - }, - new object[] - { + ], + [ ImageFileFormat.Jpeg, ".jpg", "dog", new[] { "dog" }, new[] { ImagePixelFormat.RGB24 }, false - }, - new object[] - { - ImageFileFormat.Jpeg, ".jpg", "dog_gray", - new[] { "dog_gray" }, new[] { ImagePixelFormat.Gray8 }, false - }, - new object[] - { + ], + [ + ImageFileFormat.Jpeg, ".jpg", "dog_gray_8bit", + new[] { "dog_gray" }, new[] { ImagePixelFormat.Gray8 }, true // Gtk fails to load resolution + ], + [ ImageFileFormat.Jpeg, ".jpg", "dog_bw_jpg", new[] { "dog_bw_jpg" }, new[] { ImagePixelFormat.Gray8 }, false - }, - new object[] - { + ], + [ ImageFileFormat.Jpeg2000, ".jp2", "dog_jp2", new[] { "dog" }, new[] { ImagePixelFormat.RGB24 }, false - }, - new object[] - { + ], + [ ImageFileFormat.Bmp, ".bmp", "dog_bmp", new[] { "dog" }, new[] { ImagePixelFormat.RGB24 }, true - }, - // TODO: Re-enable this for macOS - seems like a bug in 12.6, 12.5 was fine and apparently 13.0 works ok -#if !MAC - new object[] - { + ], + [ ImageFileFormat.Bmp, ".bmp", "dog_gray_bmp", new[] { "dog_gray" }, new[] { ImagePixelFormat.Gray8 }, true - }, - new object[] - { + ], + [ ImageFileFormat.Bmp, ".bmp", "dog_bw_bmp", new[] { "dog_bw" }, new[] { ImagePixelFormat.BW1 }, true - }, - new object[] - { + ], + [ ImageFileFormat.Bmp, ".bmp", "dog_bw_invertpal", new[] { "dog_bw" }, new[] { ImagePixelFormat.BW1 }, true - }, -#endif - new object[] - { - ImageFileFormat.Tiff, ".tiff", "dog_alpha_tiff", - new[] { "dog_alpha" }, new[] { ImagePixelFormat.ARGB32 }, false - }, - new object[] - { + ], + [ ImageFileFormat.Tiff, ".tiff", "dog_tiff", - new[] { "dog" }, new[] { ImagePixelFormat.RGB24 }, false - }, - new object[] - { + new[] { "dog" }, new[] { ImagePixelFormat.RGB24 }, true + ], + [ ImageFileFormat.Tiff, ".tiff", "dog_gray_tiff", new[] { "dog_gray" }, new[] { ImagePixelFormat.Gray8 }, true - }, - new object[] - { + ], + [ ImageFileFormat.Tiff, ".tiff", "dog_gray_24bit_tiff", - new[] { "dog_gray" }, new[] { ImagePixelFormat.Gray8 }, false - }, - new object[] - { + new[] { "dog_gray" }, new[] { ImagePixelFormat.Gray8 }, true, + // TODO: Can we improve this for WPF? + new PlatformFactAttribute(exclude: PlatformFlags.WpfImage) + ], + [ ImageFileFormat.Tiff, ".tiff", "dog_bw_tiff", - new[] { "dog_bw" }, new[] { ImagePixelFormat.BW1 }, false - }, - new object[] - { + new[] { "dog_bw" }, new[] { ImagePixelFormat.BW1 }, true + ], + // TODO: Any way to improve these cases for ImageSharp? + [ + ImageFileFormat.Tiff, ".tiff", "dog_alpha_tiff", + new[] { "dog_alpha" }, new[] { ImagePixelFormat.ARGB32 }, true, + new PlatformFactAttribute(exclude: PlatformFlags.ImageSharpImage) + ], + [ ImageFileFormat.Tiff, ".tiff", "animals_tiff", new[] { "dog", "dog_h_p300", "stock_cat" }, - new[] { ImagePixelFormat.RGB24, ImagePixelFormat.RGB24, ImagePixelFormat.RGB24 }, false - }, - }; + new[] { ImagePixelFormat.RGB24, ImagePixelFormat.RGB24, ImagePixelFormat.RGB24 }, true, + new PlatformFactAttribute(exclude: PlatformFlags.ImageSharpImage) + ], + ]; } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/MacImageTests.cs b/NAPS2.Sdk.Tests/Images/MacImageTests.cs index dc20f1cfda..e1f21fb407 100644 --- a/NAPS2.Sdk.Tests/Images/MacImageTests.cs +++ b/NAPS2.Sdk.Tests/Images/MacImageTests.cs @@ -9,8 +9,6 @@ namespace NAPS2.Sdk.Tests.Images; public class MacImageTests : ContextualTests { - private readonly ImageContext _imageContext = new MacImageContext(); - [Theory] [InlineData(ImagePixelFormat.ARGB32)] [InlineData(ImagePixelFormat.RGB24)] @@ -21,7 +19,7 @@ public void UsesCorrectPixelFormat(ImagePixelFormat pixelFormat) var nsImage = new NSImage(); var rep = MacBitmapHelper.CreateRep(100, 100, pixelFormat); nsImage.AddRepresentation(rep); - var image = new MacImage(_imageContext, nsImage); + var image = new MacImage(nsImage); Assert.Equal(pixelFormat, image.PixelFormat); Assert.Equal(rep.Handle, image.Rep.Handle); } @@ -30,7 +28,7 @@ public void UsesCorrectPixelFormat(ImagePixelFormat pixelFormat) public void ThrowsOnNoReps() { var nsImage = new NSImage(); - Assert.Throws(() => new MacImage(_imageContext, nsImage)); + Assert.Throws(() => new MacImage(nsImage)); } [Fact] @@ -39,7 +37,7 @@ public void ThrowsOnMultipleReps() var nsImage = new NSImage(); nsImage.AddRepresentation(MacBitmapHelper.CreateRep(100, 100, ImagePixelFormat.ARGB32)); nsImage.AddRepresentation(MacBitmapHelper.CreateRep(100, 100, ImagePixelFormat.ARGB32)); - Assert.Throws(() => new MacImage(_imageContext, nsImage)); + Assert.Throws(() => new MacImage(nsImage)); } [Theory] @@ -55,7 +53,7 @@ public void ConvertsUnexpectedColorSpace(ImagePixelFormat pixelFormat) : NSColorSpace.GenericGrayColorSpace; rep = rep.ConvertingToColorSpace(colorSpace, NSColorRenderingIntent.Default); nsImage.AddRepresentation(rep); - var image = new MacImage(_imageContext, nsImage); + var image = new MacImage(nsImage); Assert.NotEqual(rep.Handle, image.Rep.Handle); Assert.Equal(pixelFormat, image.PixelFormat); } @@ -66,7 +64,7 @@ public void ConvertsBlackColorSpace() var nsImage = new NSImage(); var rep = new NSBitmapImageRep(IntPtr.Zero, 100, 100, 1, 1, false, false, NSColorSpace.DeviceBlack, 13, 1); nsImage.AddRepresentation(rep); - var image = new MacImage(_imageContext, nsImage); + var image = new MacImage(nsImage); Assert.NotEqual(rep.Handle, image.Rep.Handle); Assert.Equal(ImagePixelFormat.Gray8, image.PixelFormat); } @@ -78,7 +76,7 @@ public void ConvertsUnsupportedPixelFormat() var nsImage = new NSImage(); var rep = Create64BitRepFromImage(referenceImage); nsImage.AddRepresentation(rep); - var image = new MacImage(_imageContext, nsImage); + var image = new MacImage(nsImage); Assert.NotEqual(rep.Handle, image.Rep.Handle); ImageAsserts.Similar(referenceImage, image); } diff --git a/NAPS2.Sdk.Tests/Images/MemoryImageTests.cs b/NAPS2.Sdk.Tests/Images/MemoryImageTests.cs index 9b5fef5b60..b21c1ccd1c 100644 --- a/NAPS2.Sdk.Tests/Images/MemoryImageTests.cs +++ b/NAPS2.Sdk.Tests/Images/MemoryImageTests.cs @@ -33,8 +33,8 @@ public void SaveWithQuality() var highQualityPath = Path.Combine(FolderPath, "highq.jpg"); var lowQualityPath = Path.Combine(FolderPath, "lowq.jpg"); - image.Save(highQualityPath, ImageFileFormat.Jpeg, 75); - image.Save(lowQualityPath, ImageFileFormat.Jpeg, 25); + image.Save(highQualityPath, ImageFileFormat.Jpeg, new ImageSaveOptions { Quality = 75 }); + image.Save(lowQualityPath, ImageFileFormat.Jpeg, new ImageSaveOptions { Quality = 25 }); var highQuality = TestImageContextFactory.Get().Load(highQualityPath); var lowQuality = TestImageContextFactory.Get().Load(lowQualityPath); @@ -66,8 +66,8 @@ public void SaveWithQualityToStream() var highQualityStream = new MemoryStream(); var lowQualityStream = new MemoryStream(); - image.Save(highQualityStream, ImageFileFormat.Jpeg, 75); - image.Save(lowQualityStream, ImageFileFormat.Jpeg, 25); + image.Save(highQualityStream, ImageFileFormat.Jpeg, new ImageSaveOptions { Quality = 75 }); + image.Save(lowQualityStream, ImageFileFormat.Jpeg, new ImageSaveOptions { Quality = 25 }); var highQuality = TestImageContextFactory.Get().Load(highQualityStream); var lowQuality = TestImageContextFactory.Get().Load(lowQualityStream); @@ -111,6 +111,6 @@ public void SaveWithUnspecifiedFormatToStream() var path = Path.Combine(FolderPath, "test.png"); using var stream = new FileStream(path, FileMode.CreateNew); - Assert.Throws(() => image.Save(stream, ImageFileFormat.Unspecified)); + Assert.Throws(() => image.Save(stream, ImageFileFormat.Unknown)); } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/PageSizeTests.cs b/NAPS2.Sdk.Tests/Images/PageSizeTests.cs new file mode 100644 index 0000000000..8533959797 --- /dev/null +++ b/NAPS2.Sdk.Tests/Images/PageSizeTests.cs @@ -0,0 +1,142 @@ +using Xunit; + +namespace NAPS2.Sdk.Tests.Images; + +public class PageSizeTests +{ + [Fact] + public void ParseInches() + { + var pageSize = PageSize.Parse("8.5x11 in"); + Assert.NotNull(pageSize); + Assert.Equal(8.5m, pageSize.Width); + Assert.Equal(11m, pageSize.Height); + Assert.Equal(PageSizeUnit.Inch, pageSize.Unit); + } + + [Fact] + public void ParseInchesNoSpace() + { + var pageSize = PageSize.Parse("8.5x11in"); + Assert.NotNull(pageSize); + Assert.Equal(8.5m, pageSize.Width); + Assert.Equal(11m, pageSize.Height); + Assert.Equal(PageSizeUnit.Inch, pageSize.Unit); + } + + [Fact] + public void ParseWellKnownLetter() + { + var pageSize = PageSize.Parse("Letter"); + Assert.NotNull(pageSize); + Assert.Equal(8.5m, pageSize.Width); + Assert.Equal(11m, pageSize.Height); + Assert.Equal(PageSizeUnit.Inch, pageSize.Unit); + } + + [Fact] + public void ParseWellKnownA4() + { + var pageSize = PageSize.Parse("a4"); + Assert.NotNull(pageSize); + Assert.Equal(210m, pageSize.Width); + Assert.Equal(297m, pageSize.Height); + Assert.Equal(PageSizeUnit.Millimetre, pageSize.Unit); + } + + [Fact] + public void ParseCentimetres() + { + var pageSize = PageSize.Parse("21x29.7 cm"); + Assert.NotNull(pageSize); + Assert.Equal(21m, pageSize.Width); + Assert.Equal(29.7m, pageSize.Height); + Assert.Equal(PageSizeUnit.Centimetre, pageSize.Unit); + } + + [Fact] + public void ParseMillimetres() + { + var pageSize = PageSize.Parse("210x297 mm"); + Assert.NotNull(pageSize); + Assert.Equal(210m, pageSize.Width); + Assert.Equal(297m, pageSize.Height); + Assert.Equal(PageSizeUnit.Millimetre, pageSize.Unit); + } + + [Fact] + public void ParseInvalid() + { + Assert.Null(PageSize.Parse("612x792 pt")); + Assert.Null(PageSize.Parse("8,5x11 in")); + Assert.Null(PageSize.Parse("8.5x11")); + Assert.Null(PageSize.Parse("8.5 in")); + Assert.Null(PageSize.Parse("8.5 11 in")); + Assert.Null(PageSize.Parse("")); + Assert.Null(PageSize.Parse(null)); + } + + [Fact] + public void InchConversions() + { + var pageSize = new PageSize(8.5m, 11m, PageSizeUnit.Inch); + Assert.Equal(8.5m, pageSize.WidthInInches); + Assert.Equal(11m, pageSize.HeightInInches); + Assert.Equal(8500, pageSize.WidthInThousandthsOfAnInch); + Assert.Equal(11000, pageSize.HeightInThousandthsOfAnInch); + Assert.Equal(215.9m, pageSize.WidthInMm); + Assert.Equal(279.4m, pageSize.HeightInMm); + } + + [Fact] + public void CentimetreConversions() + { + var pageSize = new PageSize(21m, 29.7m, PageSizeUnit.Centimetre); + Assert.Equal(8.2677m, pageSize.WidthInInches, 4); + Assert.Equal(11.6929m, pageSize.HeightInInches, 4); + Assert.Equal(210m, pageSize.WidthInMm); + Assert.Equal(297m, pageSize.HeightInMm); + Assert.Equal(8267, pageSize.WidthInThousandthsOfAnInch); + Assert.Equal(11692, pageSize.HeightInThousandthsOfAnInch); + } + + [Fact] + public void MillimetreConversions() + { + var pageSize = new PageSize(210m, 297m, PageSizeUnit.Millimetre); + Assert.Equal(8.2677m, pageSize.WidthInInches, 4); + Assert.Equal(11.6929m, pageSize.HeightInInches, 4); + Assert.Equal(210m, pageSize.WidthInMm); + Assert.Equal(297m, pageSize.HeightInMm); + Assert.Equal(8267, pageSize.WidthInThousandthsOfAnInch); + Assert.Equal(11692, pageSize.HeightInThousandthsOfAnInch); + } + + [Fact] + public void InchesToString() + { + var pageSize = new PageSize(8.5m, 11m, PageSizeUnit.Inch); + Assert.Equal("8.5x11 in", pageSize.ToString()); + } + + [Fact] + public void CentimetresToString() + { + var pageSize = new PageSize(21m, 29.7m, PageSizeUnit.Centimetre); + Assert.Equal("21x29.7 cm", pageSize.ToString()); + } + + [Fact] + public void MillimetresToString() + { + var pageSize = new PageSize(210m, 297m, PageSizeUnit.Millimetre); + Assert.Equal("210x297 mm", pageSize.ToString()); + } + + [Fact] + public void ThousandsToString() + { + var pageSize = new PageSize(21000m, 29700m, PageSizeUnit.Millimetre); + Assert.Equal("21000x29700 mm", pageSize.ToString()); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/ProcessedImageTests.cs b/NAPS2.Sdk.Tests/Images/ProcessedImageTests.cs index 95c2cf747e..1450625279 100644 --- a/NAPS2.Sdk.Tests/Images/ProcessedImageTests.cs +++ b/NAPS2.Sdk.Tests/Images/ProcessedImageTests.cs @@ -1,5 +1,5 @@ -using System.Collections.Immutable; -using Moq; +using NAPS2.Sdk.Tests.Asserts; +using NSubstitute; using Xunit; namespace NAPS2.Sdk.Tests.Images; @@ -11,21 +11,14 @@ public void Construct() { var storage = LoadImage(ImageResources.dog); - var metadata1 = new ImageMetadata(BitDepth.Color, false); - var postProcessingData1 = new PostProcessingData(); - var transformState1 = TransformState.Empty; - var image1 = new ProcessedImage(ImageContext, storage, metadata1, postProcessingData1, transformState1); - Assert.Equal(storage, image1.Storage); - Assert.Equal(BitDepth.Color, image1.Metadata.BitDepth); + var image1 = ScanningContext.CreateProcessedImage(storage); + ImageAsserts.Similar(storage, (IMemoryImage) image1.Storage, 0); Assert.False(image1.Metadata.Lossless); Assert.True(image1.TransformState.IsEmpty); - var metadata2 = new ImageMetadata(BitDepth.BlackAndWhite, true); - var postProcessingData2 = new PostProcessingData(); - var transformState2 = new TransformState(ImmutableList.Empty.Add(new CropTransform(0, 50, 0, 50))); - var image2 = new ProcessedImage(ImageContext, storage, metadata2, postProcessingData2, transformState2); - Assert.Equal(storage, image2.Storage); - Assert.Equal(BitDepth.BlackAndWhite, image2.Metadata.BitDepth); + var image2 = ScanningContext.CreateProcessedImage(storage, lossless: true, + transforms: [new CropTransform(0, 50, 0, 50)]); + ImageAsserts.Similar(storage, (IMemoryImage) image2.Storage, 0); Assert.True(image2.Metadata.Lossless); Assert.Single(image2.TransformState.Transforms); var cropTransform = Assert.IsType(image2.TransformState.Transforms[0]); @@ -38,52 +31,44 @@ public void Construct() [Fact] public void StorageDisposed() { - var storageMock = new Mock(); - var metadata = new ImageMetadata(BitDepth.Color, false); + var storageMock = Substitute.For(); + var image = ScanningContext.CreateProcessedImage(storageMock); - var image = new ProcessedImage( - ImageContext, storageMock.Object, metadata, new PostProcessingData(), TransformState.Empty); image.Dispose(); - storageMock.Verify(storage => storage.Dispose()); + storageMock.Received().Dispose(); } [Fact] public void StorageDisposedOnlyAfterAllClonesDisposed() { - var storageMock = new Mock(); - var metadata = new ImageMetadata(BitDepth.Color, false); - - var image = new ProcessedImage( - ImageContext, storageMock.Object, metadata, new PostProcessingData(), TransformState.Empty); + var storageMock = Substitute.For(); + var image = ScanningContext.CreateProcessedImage(storageMock); var image2 = image.Clone(); var image3 = image.Clone(); var image4 = image2.Clone(); image.Dispose(); - storageMock.VerifyNoOtherCalls(); + storageMock.DidNotReceive().Dispose(); image2.Dispose(); - storageMock.VerifyNoOtherCalls(); + storageMock.DidNotReceive().Dispose(); // Check extra calls on a single reference don't have an effect image3.Dispose(); image3.Dispose(); image3.Dispose(); - storageMock.VerifyNoOtherCalls(); + storageMock.DidNotReceive().Dispose(); image4.Dispose(); - storageMock.Verify(storage => storage.Dispose()); + storageMock.Received().Dispose(); } [Fact] public void TransformSimplification() { - var storageMock = new Mock(); - var metadata = new ImageMetadata(BitDepth.Color, false); - - var image = new ProcessedImage( - ImageContext, storageMock.Object, metadata, new PostProcessingData(), TransformState.Empty); + var storageMock = Substitute.For(); + var image = ScanningContext.CreateProcessedImage(storageMock); // 90deg transform var image2 = image.WithTransform(new RotationTransform(90)); @@ -109,19 +94,16 @@ public void TransformSimplification() image.Dispose(); image2.Dispose(); image3.Dispose(); - storageMock.VerifyNoOtherCalls(); + storageMock.DidNotReceive().Dispose(); image4.Dispose(); - storageMock.Verify(storage => storage.Dispose()); + storageMock.Received().Dispose(); } [Fact] public void MultipleTransforms() { - var storageMock = new Mock(); - var metadata = new ImageMetadata(BitDepth.Color, false); - - var image = new ProcessedImage( - ImageContext, storageMock.Object, metadata, new PostProcessingData(), TransformState.Empty); + var storageMock = Substitute.For(); + var image = ScanningContext.CreateProcessedImage(storageMock); // 90deg transform var image2 = image.WithTransform(new RotationTransform(90)); @@ -148,22 +130,19 @@ public void MultipleTransforms() image.Dispose(); image2.Dispose(); image3.Dispose(); - storageMock.VerifyNoOtherCalls(); + storageMock.DidNotReceive().Dispose(); image4.Dispose(); - storageMock.Verify(storage => storage.Dispose()); + storageMock.Received().Dispose(); } [Fact] public void CloneAfterDisposed() { - var storageMock = new Mock(); - var metadata = new ImageMetadata(BitDepth.Color, false); - - var image = new ProcessedImage( - ImageContext, storageMock.Object, metadata, new PostProcessingData(), TransformState.Empty); + var storageMock = Substitute.For(); + var image = ScanningContext.CreateProcessedImage(storageMock); image.Dispose(); - storageMock.Verify(storage => storage.Dispose()); + storageMock.Received().Dispose(); Assert.Throws(() => image.Clone()); } diff --git a/NAPS2.Sdk.Tests/Images/TiffWriterTests.cs b/NAPS2.Sdk.Tests/Images/TiffWriterTests.cs index 3bac28a15d..f7d0f08dbc 100644 --- a/NAPS2.Sdk.Tests/Images/TiffWriterTests.cs +++ b/NAPS2.Sdk.Tests/Images/TiffWriterTests.cs @@ -15,7 +15,7 @@ public TiffWriterTests() _tiffWriter = ImageContext.TiffWriter; } - [Fact] + [PlatformFact(exclude: PlatformFlags.ImageSharpImage)] public async Task SaveSinglePageTiffToFile() { var path = Path.Combine(FolderPath, "image.tiff"); @@ -25,7 +25,7 @@ public async Task SaveSinglePageTiffToFile() await AssertTiff(path, ImageResources.dog); } - [Fact] + [PlatformFact(exclude: PlatformFlags.ImageSharpImage)] public async Task SaveMultiPageTiffToFile() { var path = Path.Combine(FolderPath, "image.tiff"); @@ -40,7 +40,7 @@ public async Task SaveMultiPageTiffToFile() await AssertTiff(path, ImageResources.dog, ImageResources.dog_bw, ImageResources.stock_cat); } - [Fact] + [PlatformFact(exclude: PlatformFlags.ImageSharpImage)] public async Task SaveSinglePageTiffToStream() { var stream = new MemoryStream(); @@ -50,7 +50,7 @@ public async Task SaveSinglePageTiffToStream() await AssertTiff(stream, ImageResources.dog); } - [Fact] + [PlatformFact(exclude: PlatformFlags.ImageSharpImage)] public async Task SaveMultiPageTiffToStream() { var stream = new MemoryStream(); @@ -65,7 +65,7 @@ public async Task SaveMultiPageTiffToStream() await AssertTiff(stream, ImageResources.dog, ImageResources.dog_bw, ImageResources.stock_cat); } - [Fact] + [PlatformFact(exclude: PlatformFlags.ImageSharpImage)] public async Task SaveBlackAndWhiteTiff() { var path = Path.Combine(FolderPath, "image.tiff"); @@ -75,7 +75,7 @@ public async Task SaveBlackAndWhiteTiff() await AssertTiff(path, ImageResources.dog_bw); } - [Fact] + [PlatformFact(exclude: PlatformFlags.ImageSharpImage)] public async Task SaveColorTiffWithG4() { var path = Path.Combine(FolderPath, "image.tiff"); @@ -104,7 +104,7 @@ private static void DoAssertTiff(List actual, byte[][] expectedIma for (int i = 0; i < expectedImages.Length; i++) { Assert.Equal(ImageFileFormat.Tiff, actual[i].OriginalFileFormat); - ImageAsserts.Similar(expectedImages[i], actual[i]); + ImageAsserts.Similar(expectedImages[i], actual[i], ignoreResolution: true); } } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/TransformTests.cs b/NAPS2.Sdk.Tests/Images/TransformTests.cs index 83b552ee88..7727953e8d 100644 --- a/NAPS2.Sdk.Tests/Images/TransformTests.cs +++ b/NAPS2.Sdk.Tests/Images/TransformTests.cs @@ -15,225 +15,245 @@ public class TransformTests : ContextualTests [Fact] public void BrightnessNull() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog); - actual = actual.PerformTransform(new BrightnessTransform()); + var transformed = original.PerformTransform(new BrightnessTransform()); - ImageAsserts.Similar(expected, actual, ImageAsserts.NULL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.NULL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void BrightnessP300() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_b_p300); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_b_p300); - actual = actual.PerformTransform(new BrightnessTransform(300)); + var transformed = original.PerformTransform(new BrightnessTransform(300)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void BrightnessN300() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_b_n300); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_b_n300); - actual = actual.PerformTransform(new BrightnessTransform(-300)); + var transformed = original.PerformTransform(new BrightnessTransform(-300)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void ContrastNull() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog); - actual = actual.PerformTransform(new TrueContrastTransform()); + var transformed = original.PerformTransform(new TrueContrastTransform()); - ImageAsserts.Similar(expected, actual, ImageAsserts.NULL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.NULL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void ContrastP300() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_c_p300); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_c_p300); - actual = actual.PerformTransform(new TrueContrastTransform(300)); + var transformed = original.PerformTransform(new TrueContrastTransform(300)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void ContrastN300() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_c_n300); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_c_n300); - actual = actual.PerformTransform(new TrueContrastTransform(-300)); + var transformed = original.PerformTransform(new TrueContrastTransform(-300)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void HueNull() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog); - actual = actual.PerformTransform(new HueTransform()); + var transformed = original.PerformTransform(new HueTransform()); - ImageAsserts.Similar(expected, actual, ImageAsserts.NULL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.NULL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void HueP300() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_h_p300); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_h_p300); - actual = actual.PerformTransform(new HueTransform(300)); + var transformed = original.PerformTransform(new HueTransform(300)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void HueN300() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_h_n300); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_h_n300); - actual = actual.PerformTransform(new HueTransform(-300)); + var transformed = original.PerformTransform(new HueTransform(-300)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void SaturationNull() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog); - actual = actual.PerformTransform(new SaturationTransform()); + var transformed = original.PerformTransform(new SaturationTransform()); - ImageAsserts.Similar(expected, actual, ImageAsserts.NULL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.NULL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void SaturationP300() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_s_p300); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_s_p300); - actual = actual.PerformTransform(new SaturationTransform(300)); + var transformed = original.PerformTransform(new SaturationTransform(300)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void SaturationN300() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_s_n300); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_s_n300); - actual = actual.PerformTransform(new SaturationTransform(-300)); + var transformed = original.PerformTransform(new SaturationTransform(-300)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void SharpenNull() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog); - actual = actual.PerformTransform(new SharpenTransform()); + var transformed = original.PerformTransform(new SharpenTransform()); - ImageAsserts.Similar(expected, actual, ImageAsserts.NULL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.NULL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void SharpenP300() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_sh_p1000); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_sh_p1000); - actual = actual.PerformTransform(new SharpenTransform(1000)); + var transformed = original.PerformTransform(new SharpenTransform(1000)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void SharpenN300() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_sh_n1000); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_sh_n1000); - actual = actual.PerformTransform(new SharpenTransform(-1000)); + var transformed = original.PerformTransform(new SharpenTransform(-1000)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void RotationNull() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog); - actual = actual.PerformTransform(new RotationTransform()); + var transformed = original.PerformTransform(new RotationTransform()); - ImageAsserts.Similar(expected, actual, ImageAsserts.NULL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.NULL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void RotationP90() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_r_p90); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_r_p90); - actual = actual.PerformTransform(new RotationTransform(90)); + var transformed = original.PerformTransform(new RotationTransform(90)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void RotationP46() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_r_p46); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_r_p46); - actual = actual.PerformTransform(new RotationTransform(46)); + var transformed = original.PerformTransform(new RotationTransform(46)); - ImageAsserts.Similar(expected, actual, ImageAsserts.XPLAT_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.XPLAT_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void RotationN45() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_r_n45); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_r_n45); - actual = actual.PerformTransform(new RotationTransform(-45)); + var transformed = original.PerformTransform(new RotationTransform(-45)); // TODO: The mac rotated image looks way better than gdi, consider if we can improve the gdi end - ImageAsserts.Similar(expected, actual, ImageAsserts.XPLAT_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.XPLAT_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void Rotation180() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage actual2 = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_r_180); + var original = LoadImage(ImageResources.dog); + var transformed2 = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_r_180); - actual = actual.PerformTransform(new RotationTransform(180)); - actual2 = actual2.PerformTransform(new RotationTransform(-180)); + var transformed = original.PerformTransform(new RotationTransform(180)); + transformed2 = transformed2.PerformTransform(new RotationTransform(-180)); - ImageAsserts.Similar(actual2, actual, 0); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(transformed2, transformed, 0); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } // TODO: Add tests for rotating black and white images @@ -241,79 +261,96 @@ public void Rotation180() [Fact] public void ScaleNull() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog); - actual = actual.PerformTransform(new ScaleTransform()); + var transformed = original.PerformTransform(new ScaleTransform()); - ImageAsserts.Similar(expected, actual, ImageAsserts.NULL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.NULL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void Scale50Percent() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_sc_50pct); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_sc_50pct); - actual = actual.PerformTransform(new ScaleTransform(0.5)); + var transformed = original.PerformTransform(new ScaleTransform(0.5)); - ImageAsserts.Similar(expected, actual, ImageAsserts.XPLAT_RMSE_THRESHOLD, ignoreResolution: true); + ImageAsserts.Similar(expected, transformed, ImageAsserts.XPLAT_RMSE_THRESHOLD, ignoreResolution: true); + AssertOwnership(original, transformed); } [Theory] [MemberData(nameof(CommutativeGrayTransforms))] public void GrayTransformsAreCommutative(Transform transform) { - IMemoryImage original = LoadImage(ImageResources.dog); + var original = LoadImage(ImageResources.dog); - var actual = original.CopyWithPixelFormat(ImagePixelFormat.Gray8); - actual = actual.PerformTransform(transform); + var transformed = original.CopyWithPixelFormat(ImagePixelFormat.Gray8); + transformed = transformed.PerformTransform(transform); var expected = original.Clone(); expected = expected.PerformTransform(transform); expected = expected.CopyWithPixelFormat(ImagePixelFormat.Gray8); - ImageAsserts.Similar(expected, actual); + ImageAsserts.Similar(expected, transformed); } [Fact] public void Scale1000Percent() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_huge); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_huge); - actual = actual.PerformTransform(new ScaleTransform(10)); + var transformed = original.PerformTransform(new ScaleTransform(10)); - ImageAsserts.Similar(expected, actual, ImageAsserts.XPLAT_RMSE_THRESHOLD, ignoreResolution: true); + ImageAsserts.Similar(expected, transformed, ImageAsserts.XPLAT_RMSE_THRESHOLD, ignoreResolution: true); + AssertOwnership(original, transformed); + } + + [Fact] + public void Scale50PercentWithAlpha() + { + var original = LoadImage(ImageResources.dog_alpha); + var expected = LoadImage(ImageResources.dog_alpha_sc_50pct); + + var transformed = original.PerformTransform(new ScaleTransform(0.5)); + + ImageAsserts.Similar(expected, transformed, ImageAsserts.XPLAT_RMSE_THRESHOLD, ignoreResolution: true); + AssertOwnership(original, transformed); } [Fact] public void CropNull() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog); - actual = actual.PerformTransform(new CropTransform()); + var transformed = original.PerformTransform(new CropTransform()); - ImageAsserts.Similar(expected, actual, ImageAsserts.NULL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.NULL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void Crop() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_c_5_10_15_20); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_c_5_10_15_20); - actual = actual.PerformTransform(new CropTransform(10, 20, 15, 5)); + var transformed = original.PerformTransform(new CropTransform(10, 20, 15, 5)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void CropBlackWhiteBeforeAfter() { - IMemoryImage first = LoadImage(ImageResources.dog); - IMemoryImage second = LoadImage(ImageResources.dog); + var first = LoadImage(ImageResources.dog); + var second = LoadImage(ImageResources.dog); first = first.PerformTransform(new BlackWhiteTransform()); first = first.PerformTransform(new CropTransform(10, 20, 15, 5)); @@ -326,89 +363,145 @@ public void CropBlackWhiteBeforeAfter() [Fact] public void CropWithOriginal() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_c_5_10_15_20); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_c_5_10_15_20); - actual = actual.PerformTransform(new CropTransform(10, 20, 15, 5, actual.Width, actual.Height)); + var transformed = original.PerformTransform(new CropTransform(10, 20, 15, 5, original.Width, original.Height)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void CropWithDifferentOriginal() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_c_5_10_15_20); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_c_5_10_15_20); - actual = ImageContext.PerformTransform(actual, - new CropTransform(20, 40, 30, 10, actual.Width * 2, actual.Height * 2)); + var transformed = ImageContext.PerformTransform(original, + new CropTransform(20, 40, 30, 10, original.Width * 2, original.Height * 2)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void CropOutOfBounds() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog); - actual = actual.PerformTransform(new CropTransform(-1, -1, -1, -1)); + var transformed = original.PerformTransform(new CropTransform(-1, -1, -1, -1)); - ImageAsserts.Similar(expected, actual, ImageAsserts.GENERAL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void BlackWhite() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_bw); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_bw); - actual = actual.PerformTransform(new BlackWhiteTransform()); - Assert.Equal(ImagePixelFormat.BW1, actual.LogicalPixelFormat); + var transformed = original.PerformTransform(new BlackWhiteTransform()); + Assert.Equal(ImagePixelFormat.BW1, transformed.UpdateLogicalPixelFormat()); // TODO: There's no inherent reason this shouldn't be an exact match, unless I guess if // there's a slight pixel difference between png loading on mac/gdi - ImageAsserts.Similar(expected, actual, ImageAsserts.XPLAT_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.XPLAT_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void BlackWhiteP300() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_bw_p300); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_bw_p300); - actual = actual.PerformTransform(new BlackWhiteTransform(300)); - Assert.Equal(ImagePixelFormat.BW1, actual.LogicalPixelFormat); + var transformed = original.PerformTransform(new BlackWhiteTransform(300)); + Assert.Equal(ImagePixelFormat.BW1, transformed.UpdateLogicalPixelFormat()); - ImageAsserts.Similar(expected, actual, ImageAsserts.XPLAT_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.XPLAT_RMSE_THRESHOLD); + AssertOwnership(original, transformed); + } + + [Fact] + public void Grayscale() + { + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_gray); + + var transformed = original.PerformTransform(new GrayscaleTransform()); + Assert.Equal(ImagePixelFormat.Gray8, transformed.UpdateLogicalPixelFormat()); + + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void ColorBitDepth() { - IMemoryImage actual = LoadImage(ImageResources.dog_bw); - IMemoryImage expected = LoadImage(ImageResources.dog_bw_24bit); + var original = LoadImage(ImageResources.dog_bw); + var expected = LoadImage(ImageResources.dog_bw_24bit); - actual = actual.PerformTransform(new BlackWhiteTransform()); - actual = actual.PerformTransform(new ColorBitDepthTransform()); - Assert.Equal(ImagePixelFormat.RGB24, actual.PixelFormat); + var transformed = original.PerformTransform(new BlackWhiteTransform()); + transformed = transformed.PerformTransform(new ColorBitDepthTransform()); + Assert.Equal(ImagePixelFormat.RGB24, transformed.PixelFormat); - ImageAsserts.Similar(expected, actual, ImageAsserts.NULL_RMSE_THRESHOLD); + ImageAsserts.Similar(expected, transformed, ImageAsserts.NULL_RMSE_THRESHOLD); + AssertOwnership(original, transformed); } [Fact] public void Thumbnail() { - IMemoryImage actual = LoadImage(ImageResources.dog); - IMemoryImage expected = LoadImage(ImageResources.dog_thumb_256); + var original = LoadImage(ImageResources.dog); + var expected = LoadImage(ImageResources.dog_thumb_256); + + var transformed = original.PerformTransform(new ThumbnailTransform(256)); + + ImageAsserts.Similar(expected, transformed, ImageAsserts.XPLAT_RMSE_THRESHOLD, ignoreResolution: true); + AssertOwnership(original, transformed); + } + + [Fact] + public void Combine() + { + var first = LoadImage(ImageResources.dog); + var second = LoadImage(ImageResources.cat); + var expected = LoadImage(ImageResources.dog_cat_combined); - actual = actual.PerformTransform(new ThumbnailTransform(256)); + var transformed = MoreImageTransforms.Combine(first, second, CombineOrientation.Vertical); + Assert.Equal(ImagePixelFormat.RGB24, transformed.UpdateLogicalPixelFormat()); - ImageAsserts.Similar(expected, actual, ImageAsserts.XPLAT_RMSE_THRESHOLD, ignoreResolution: true); + ImageAsserts.Similar(expected, transformed, ImageAsserts.GENERAL_RMSE_THRESHOLD); } - public static IEnumerable CommutativeGrayTransforms = new List + [Fact] + public void CombineBlackAndWhite() { + var first = LoadImage(ImageResources.dog).PerformTransform(new BlackWhiteTransform()); + var second = LoadImage(ImageResources.cat).PerformTransform(new BlackWhiteTransform()); + var expected = LoadImage(ImageResources.dog_cat_combined_bw).PerformTransform(new BlackWhiteTransform()); + + var transformed = MoreImageTransforms.Combine(first, second, CombineOrientation.Vertical); + Assert.Equal(ImagePixelFormat.BW1, transformed.UpdateLogicalPixelFormat()); + + ImageAsserts.Similar(expected, transformed, ImageAsserts.XPLAT_RMSE_THRESHOLD); + } + + private void AssertOwnership(IMemoryImage original, IMemoryImage transformed) + { + // The contract for a transform is that either it returns the original image or it disposes the original and + // returns a copy. This check works in both cases, and tests what we really care about (that disposing the + // result cleans up everything). + Assert.False(IsDisposed(transformed)); + transformed.Dispose(); + Assert.True(IsDisposed(original)); + } + + public static IEnumerable CommutativeGrayTransforms = + [ // Note that hue and saturation aren't commutative with grayscale as the the grayscale transform weighs each // color channel differently new object[] { new BrightnessTransform(300) }, @@ -417,5 +510,5 @@ public void Thumbnail() new object[] { new RotationTransform(46) }, new object[] { new ThumbnailTransform() }, new object[] { new CropTransform(10, 10, 10, 10) } - }; + ]; } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/UiImageListTests.cs b/NAPS2.Sdk.Tests/Images/UiImageListTests.cs deleted file mode 100644 index 7f8d9b7deb..0000000000 --- a/NAPS2.Sdk.Tests/Images/UiImageListTests.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Xunit; - -namespace NAPS2.Sdk.Tests.Images; - -// TODO: Add tests -public class UiImageListTests -{ - [Fact] - public void Something() - { - } -} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/UiImageTests.cs b/NAPS2.Sdk.Tests/Images/UiImageTests.cs deleted file mode 100644 index 43a1df0b3a..0000000000 --- a/NAPS2.Sdk.Tests/Images/UiImageTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Xunit; - -namespace NAPS2.Sdk.Tests.Images; - -public class UiImageTests -{ - [Fact] - public void Something() - { - } -} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Images/UndoStackTests.cs b/NAPS2.Sdk.Tests/Images/UndoStackTests.cs deleted file mode 100644 index 2ad8b6c223..0000000000 --- a/NAPS2.Sdk.Tests/Images/UndoStackTests.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System.Collections.Immutable; -using Xunit; - -namespace NAPS2.Sdk.Tests.Images; - -public class UndoStackTests : ContextualTests -{ - [Fact] - public void MementoComparison() - { - var emptyList = ImmutableList.Empty; - - var emptyMemento = Memento.Empty; - Assert.Equal(new Memento(emptyList), emptyMemento); - - var image = CreateScannedImage(); - var snapshot1 = new Memento(emptyList.Add(image)); - Assert.NotEqual(emptyMemento, snapshot1); - Assert.Equal(new Memento(emptyList.Add(image)), snapshot1); - - var image2 = image.WithTransform(new BrightnessTransform(100)); - var snapshot2 = new Memento(emptyList.Add(image2)); - Assert.NotEqual(snapshot1, snapshot2); - Assert.Equal(new Memento(emptyList.Add(image2)), snapshot2); - } - - [Fact] - public void Initial_IsEmpty() - { - var stack = new UndoStack(10); - Assert.Equal(Memento.Empty, stack.Current); - } - - [Fact] - public void Initial_NoUndoOrRedo() - { - var stack = new UndoStack(10); - Assert.False(stack.Undo()); - Assert.False(stack.Redo()); - } - - [Fact] - public void PushUndoRedoUndo() - { - var emptyList = ImmutableList.Empty; - - var stack = new UndoStack(10); - var image = CreateScannedImage(); - Assert.True(stack.Push(new[] { image })); - Assert.Equal(new Memento(emptyList.Add(image)), stack.Current); - Assert.True(stack.Undo()); - Assert.Equal(Memento.Empty, stack.Current); - Assert.True(stack.Redo()); - Assert.Equal(new Memento(emptyList.Add(image)), stack.Current); - Assert.True(stack.Undo()); - Assert.Equal(Memento.Empty, stack.Current); - } - - [Fact] - public void PushSame() - { - var stack = new UndoStack(10); - Assert.False(stack.Push(Memento.Empty)); - Assert.False(stack.Undo()); - var imageArray = new[] { CreateScannedImage() }; - Assert.True(stack.Push(imageArray)); - Assert.False(stack.Push(imageArray)); - Assert.True(stack.Undo()); - Assert.Equal(Memento.Empty, stack.Current); - } - - [Fact] - public void StackLimit() - { - var stack = new UndoStack(4); - var images = Enumerable.Repeat(0, 10).Select(i => CreateScannedImage()).ToList(); - for (int i = 1; i <= 10; i++) - { - stack.Push(images.Take(i)); - } - Assert.Equal(new Memento(images.Take(10).ToImmutableList()), stack.Current); - for (int i = 9; i >= 7; i--) - { - Assert.True(stack.Undo()); - Assert.Equal(new Memento(images.Take(i).ToImmutableList()), stack.Current); - } - Assert.False(stack.Undo()); - for (int i = 8; i <= 10; i++) - { - Assert.True(stack.Redo()); - Assert.Equal(new Memento(images.Take(i).ToImmutableList()), stack.Current); - } - Assert.False(stack.Redo()); - } - - [Fact] - public void ClearUndo() - { - var stack = new UndoStack(10); - var images = Enumerable.Repeat(0, 5).Select(i => CreateScannedImage()).ToList(); - for (int i = 1; i <= 5; i++) - { - stack.Push(images.Take(i)); - } - stack.ClearUndo(); - Assert.False(stack.Undo()); - Assert.Equal(new Memento(images.Take(5).ToImmutableList()), stack.Current); - } - - [Fact] - public void ClearRedo() - { - var stack = new UndoStack(10); - var images = Enumerable.Repeat(0, 5).Select(i => CreateScannedImage()).ToList(); - for (int i = 1; i <= 5; i++) - { - stack.Push(images.Take(i)); - } - for (int i = 1; i <= 5; i++) - { - stack.Undo(); - } - stack.ClearRedo(); - Assert.False(stack.Redo()); - Assert.Equal(Memento.Empty, stack.Current); - } - - [Fact] - public void ClearBoth() - { - var stack = new UndoStack(10); - var images = Enumerable.Repeat(0, 5).Select(i => CreateScannedImage()).ToList(); - for (int i = 1; i <= 5; i++) - { - stack.Push(images.Take(i)); - } - for (int i = 1; i <= 2; i++) - { - stack.Undo(); - } - stack.ClearBoth(); - Assert.False(stack.Undo()); - Assert.False(stack.Redo()); - Assert.Equal(new Memento(images.Take(3).ToImmutableList()), stack.Current); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/ImportExport/FileImporterTests.cs b/NAPS2.Sdk.Tests/ImportExport/FileImporterTests.cs new file mode 100644 index 0000000000..fbf5900c8b --- /dev/null +++ b/NAPS2.Sdk.Tests/ImportExport/FileImporterTests.cs @@ -0,0 +1,156 @@ +using NAPS2.ImportExport; +using NAPS2.Pdf.Pdfium; +using Xunit; + +namespace NAPS2.Sdk.Tests.ImportExport; + +public class FileImporterTests : ContextualTests +{ + private readonly FileImporter _fileImporter; + + public FileImporterTests() + { + _fileImporter = new FileImporter(ScanningContext); + SetUpFileStorage(); + } + + [Fact] + public async Task ImportPngImage() + { + var filePath = CopyResourceToFile(ImageResources.skewed_bw, "image.png"); + + var source = _fileImporter.Import(filePath, new ImportParams()); + var result = await source.ToListAsync(); + + Assert.Single(result); + var storage = Assert.IsType(result[0].Storage); + Assert.Equal(".png", Path.GetExtension(storage.FullPath)); + } + + [Fact] + public async Task ImportPngStream() + { + var fileStream = new MemoryStream(ImageResources.skewed_bw); + + var source = _fileImporter.Import(fileStream, new ImportParams()); + var result = await source.ToListAsync(); + + Assert.Single(result); + var storage = Assert.IsType(result[0].Storage); + Assert.Equal(".png", Path.GetExtension(storage.FullPath)); + } + + [Fact] + public async Task ImportJpegImage() + { + var filePath = CopyResourceToFile(ImageResources.dog, "image.jpg"); + + var source = _fileImporter.Import(filePath, new ImportParams()); + var result = await source.ToListAsync(); + + Assert.Single(result); + var storage = Assert.IsType(result[0].Storage); + Assert.Equal(".jpg", Path.GetExtension(storage.FullPath)); + } + + [Fact] + public async Task ImportJpegStream() + { + var fileStream = new MemoryStream(ImageResources.dog); + + var source = _fileImporter.Import(fileStream, new ImportParams()); + var result = await source.ToListAsync(); + + Assert.Single(result); + var storage = Assert.IsType(result[0].Storage); + Assert.Equal(".jpg", Path.GetExtension(storage.FullPath)); + } + + [Fact] + public async Task ImportPdfFile() + { + var filePath = CopyResourceToFile(PdfResources.word_patcht_pdf, "word.pdf"); + + var source = _fileImporter.Import(filePath, new ImportParams()); + var result = await source.ToListAsync(); + + Assert.Single(result); + var storage = Assert.IsType(result[0].Storage); + Assert.Equal(".pdf", Path.GetExtension(storage.FullPath)); + } + + [Fact] + public async Task ImportPdfStream() + { + var fileStream = new MemoryStream(PdfResources.word_patcht_pdf); + + var source = _fileImporter.Import(fileStream, new ImportParams()); + var result = await source.ToListAsync(); + + Assert.Single(result); + var storage = Assert.IsType(result[0].Storage); + Assert.Equal(".pdf", Path.GetExtension(storage.FullPath)); + } + + [Fact] + public async Task ImportZipFile() + { + var filePath = CopyResourceToFile(BinaryResources.animals, "animals.zip"); + + var source = _fileImporter.Import(filePath, new ImportParams()); + var result = await source.ToListAsync(); + + Assert.Equal(2, result.Count); + var storage = Assert.IsType(result[0].Storage); + Assert.Equal(".jpg", Path.GetExtension(storage.FullPath)); + } + + [Fact] + public async Task ImportZipStream() + { + var fileStream = new MemoryStream(BinaryResources.animals); + + var source = _fileImporter.Import(fileStream, new ImportParams()); + var result = await source.ToListAsync(); + + Assert.Equal(2, result.Count); + var storage = Assert.IsType(result[0].Storage); + Assert.Equal(".jpg", Path.GetExtension(storage.FullPath)); + } + + [Fact] + public async Task ImportUnsupportedFile() + { + var filePath = CopyResourceToFile(BinaryResources.testcert, "something.crt"); + + await Assert.ThrowsAsync(async () => + await _fileImporter.Import(filePath, new ImportParams()).ToListAsync()); + } + + [Fact] + public async Task ImportUnsupportedStream() + { + var fileStream = new MemoryStream(BinaryResources.testcert); + + await Assert.ThrowsAsync(async () => + await _fileImporter.Import(fileStream, new ImportParams()).ToListAsync()); + } + + [Fact] + public async Task ImportImageWithPdfExtension() + { + var filePath = CopyResourceToFile(ImageResources.dog, "image.pdf"); + + await Assert.ThrowsAsync(async () => + await _fileImporter.Import(filePath, new ImportParams()).ToListAsync()); + } + + [Fact] + public async Task ImportPdfWithImageExtension() + { + var filePath = CopyResourceToFile(PdfResources.word_patcht_pdf, "pdf.jpg"); + + await Assert.ThrowsAsync(async () => + await _fileImporter.Import(filePath, new ImportParams()).ToListAsync()); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/ImportExport/ImageImporterTests.cs b/NAPS2.Sdk.Tests/ImportExport/ImageImporterTests.cs index 744f981ff1..a4e2a5b803 100644 --- a/NAPS2.Sdk.Tests/ImportExport/ImageImporterTests.cs +++ b/NAPS2.Sdk.Tests/ImportExport/ImageImporterTests.cs @@ -1,9 +1,9 @@ using System.Threading; -using Moq; using NAPS2.ImportExport; using NAPS2.ImportExport.Images; using NAPS2.Scan; using NAPS2.Sdk.Tests.Asserts; +using NSubstitute; using Xunit; namespace NAPS2.Sdk.Tests.ImportExport; @@ -15,8 +15,8 @@ public class ImageImporterTests : ContextualTests public ImageImporterTests() { - _imageImporter = new ImageImporter(ScanningContext, ImageContext, new ImportPostProcessor()); - ScanningContext.FileStorageManager = FileStorageManager.CreateFolder(Path.Combine(FolderPath, "recovery")); + _imageImporter = new ImageImporter(ScanningContext); + SetUpFileStorage(); } [Fact] @@ -32,9 +32,8 @@ public async Task ImportPngImage() Assert.True(File.Exists(storage.FullPath)); Assert.Equal(Path.Combine(FolderPath, "recovery"), Path.GetDirectoryName(storage.FullPath)); Assert.True(result[0].Metadata.Lossless); - Assert.Equal(BitDepth.Color, result[0].Metadata.BitDepth); Assert.Null(result[0].PostProcessingData.Thumbnail); - Assert.False(result[0].PostProcessingData.BarcodeDetection.IsAttempted); + Assert.False(result[0].PostProcessingData.Barcode.IsDetectionAttempted); Assert.True(result[0].TransformState.IsEmpty); result[0].Dispose(); @@ -54,16 +53,67 @@ public async Task ImportJpegImage() Assert.True(File.Exists(storage.FullPath)); Assert.Equal(Path.Combine(FolderPath, "recovery"), Path.GetDirectoryName(storage.FullPath)); Assert.False(result[0].Metadata.Lossless); - Assert.Equal(BitDepth.Color, result[0].Metadata.BitDepth); Assert.Null(result[0].PostProcessingData.Thumbnail); - Assert.False(result[0].PostProcessingData.BarcodeDetection.IsAttempted); + Assert.False(result[0].PostProcessingData.Barcode.IsDetectionAttempted); Assert.True(result[0].TransformState.IsEmpty); + // Verify no re-encode happens + var originalImage = ImageContext.Load(filePath); + var storageImage = ImageContext.Load(storage.FullPath); + ImageAsserts.Similar(originalImage, storageImage, 0); + result[0].Dispose(); Assert.False(File.Exists(storage.FullPath)); } [Fact] + public async Task ImportPngImageFromStream() + { + var fileStream = new MemoryStream(ImageResources.skewed_bw); + + var source = _imageImporter.Import(fileStream, new ImportParams()); + var result = await source.ToListAsync(); + + Assert.Single(result); + var storage = Assert.IsType(result[0].Storage); + Assert.True(File.Exists(storage.FullPath)); + Assert.Equal(Path.Combine(FolderPath, "recovery"), Path.GetDirectoryName(storage.FullPath)); + Assert.True(result[0].Metadata.Lossless); + Assert.Null(result[0].PostProcessingData.Thumbnail); + Assert.False(result[0].PostProcessingData.Barcode.IsDetectionAttempted); + Assert.True(result[0].TransformState.IsEmpty); + + result[0].Dispose(); + Assert.False(File.Exists(storage.FullPath)); + } + + [Fact] + public async Task ImportJpegImageFromStream() + { + var fileStream = new MemoryStream(ImageResources.dog); + + var source = _imageImporter.Import(fileStream, new ImportParams()); + var result = await source.ToListAsync(); + + Assert.Single(result); + var storage = Assert.IsType(result[0].Storage); + Assert.True(File.Exists(storage.FullPath)); + Assert.Equal(Path.Combine(FolderPath, "recovery"), Path.GetDirectoryName(storage.FullPath)); + Assert.False(result[0].Metadata.Lossless); + Assert.Null(result[0].PostProcessingData.Thumbnail); + Assert.False(result[0].PostProcessingData.Barcode.IsDetectionAttempted); + Assert.True(result[0].TransformState.IsEmpty); + + // Verify no re-encode happens + var originalImage = ImageContext.Load(fileStream); + var storageImage = ImageContext.Load(storage.FullPath); + ImageAsserts.Similar(originalImage, storageImage, 0); + + result[0].Dispose(); + Assert.False(File.Exists(storage.FullPath)); + } + + [PlatformFact(exclude: PlatformFlags.ImageSharpImage)] public async Task ImportTiffImage() { var filePath = CopyResourceToFile(ImageResources.animals_tiff, "image.tiff"); @@ -74,12 +124,10 @@ public async Task ImportTiffImage() Assert.Equal(3, result.Count); AssertUsesRecoveryStorage(result[0].Storage, "00001.jpg"); Assert.False(result[0].Metadata.Lossless); - Assert.Equal(BitDepth.Color, result[0].Metadata.BitDepth); ImageAsserts.Similar(ImageResources.dog, result[0]); AssertUsesRecoveryStorage(result[2].Storage, "00003.jpg"); Assert.False(result[2].Metadata.Lossless); - Assert.Equal(BitDepth.Color, result[2].Metadata.BitDepth); ImageAsserts.Similar(ImageResources.stock_cat, result[2]); result[0].Dispose(); @@ -102,46 +150,48 @@ public async Task ImportWithThumbnailGeneration() Assert.Equal(256, result[0].PostProcessingData.Thumbnail.Width); } - [Fact] + [Fact(Skip = "Flaky")] public async Task SingleFrameProgress() { var filePath = CopyResourceToFile(ImageResources.dog, "image.jpg"); - var progressMock = new Mock(); - var source = _imageImporter.Import(filePath, new ImportParams(), progressMock.Object); + var progressMock = Substitute.For(); + var source = _imageImporter.Import(filePath, new ImportParams(), progressMock); - progressMock.VerifyNoOtherCalls(); + progressMock.ReceivedCallsCount(0); await source.ToListAsync(); - progressMock.Verify(x => x(0, 1)); - progressMock.Verify(x => x(1, 1)); - progressMock.VerifyNoOtherCalls(); + progressMock.Received()(0, 1); + progressMock.Received()(1, 1); + progressMock.ReceivedCallsCount(2); } - [Fact] + // TODO: Why is this flaking on Linux? + [Fact(Skip = "Flaky")] public async Task MultiFrameProgress() { var filePath = CopyResourceToFile(ImageResources.animals_tiff, "image.tiff"); - var progressMock = new Mock(); - var source = _imageImporter.Import(filePath, new ImportParams(), progressMock.Object); + var progressMock = Substitute.For(); + var source = _imageImporter.Import(filePath, new ImportParams(), progressMock); var enumerator = source.GetAsyncEnumerator(); - progressMock.VerifyNoOtherCalls(); + progressMock.ReceivedCallsCount(0); Assert.True(await enumerator.MoveNextAsync()); - progressMock.Verify(x => x(0, 3)); - progressMock.Verify(x => x(1, 3)); - progressMock.VerifyNoOtherCalls(); + progressMock.Received()(0, 3); + progressMock.Received()(1, 3); + progressMock.ReceivedCallsCount(2); Assert.True(await enumerator.MoveNextAsync()); - progressMock.Verify(x => x(2, 3)); - progressMock.VerifyNoOtherCalls(); + progressMock.Received()(2, 3); + progressMock.ReceivedCallsCount(3); Assert.True(await enumerator.MoveNextAsync()); - progressMock.Verify(x => x(3, 3)); - progressMock.VerifyNoOtherCalls(); + progressMock.Received()(3, 3); + progressMock.ReceivedCallsCount(4); Assert.False(await enumerator.MoveNextAsync()); - progressMock.VerifyNoOtherCalls(); + progressMock.ReceivedCallsCount(5); } - [Fact] + // TODO: Why is this flaking (at least on Linux)? + [Fact(Skip = "Flaky")] public async Task SingleFrameCancellation() { var filePath = CopyResourceToFile(ImageResources.dog, "image.jpg"); @@ -154,8 +204,8 @@ public async Task SingleFrameCancellation() Assert.False(await enumerator.MoveNextAsync()); } - // This test doesn't work on Mac as the full file is loaded first, making per-frame loading instant - [PlatformFact(exclude: PlatformFlags.Mac)] + // This test doesn't work on Mac (and flaky on Linux) as the full file is loaded first, making enumeration instant + [Fact(Skip = "Flaky")] public async Task MultiFrameCancellation() { var filePath = CopyResourceToFile(ImageResources.animals_tiff, "image.tiff"); @@ -218,9 +268,9 @@ public async Task ImportWithBarcodeDetection() var result = await source.ToListAsync(); Assert.Single(result); - Assert.True(result[0].PostProcessingData.BarcodeDetection.IsAttempted); - Assert.True(result[0].PostProcessingData.BarcodeDetection.IsBarcodePresent); - Assert.True(result[0].PostProcessingData.BarcodeDetection.IsPatchT); - Assert.Equal("PATCHT", result[0].PostProcessingData.BarcodeDetection.DetectedText); + Assert.True(result[0].PostProcessingData.Barcode.IsDetectionAttempted); + Assert.True(result[0].PostProcessingData.Barcode.IsDetected); + Assert.True(result[0].PostProcessingData.Barcode.IsPatchT); + Assert.Equal("PATCHT", result[0].PostProcessingData.Barcode.DetectedText); } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/ImportExport/ImportPostProcessorTests.cs b/NAPS2.Sdk.Tests/ImportExport/ImportPostProcessorTests.cs index 3c4896589d..2483c57185 100644 --- a/NAPS2.Sdk.Tests/ImportExport/ImportPostProcessorTests.cs +++ b/NAPS2.Sdk.Tests/ImportExport/ImportPostProcessorTests.cs @@ -1,3 +1,4 @@ +using NAPS2.ImportExport; using NAPS2.ImportExport.Images; using NAPS2.Scan; using NAPS2.Sdk.Tests.Asserts; @@ -7,23 +8,16 @@ namespace NAPS2.Sdk.Tests.ImportExport; public class ImportPostProcessorTests : ContextualTests { - private readonly ImportPostProcessor _importPostProcessor; - - public ImportPostProcessorTests() - { - _importPostProcessor = new ImportPostProcessor(); - } - [Fact] public void NoPostProcessing() { using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); using var image2 = - _importPostProcessor.AddPostProcessingData(image, null, null, new BarcodeDetectionOptions(), false); + ImportPostProcessor.AddPostProcessingData(image, null, null, new BarcodeDetectionOptions(), false); Assert.Null(image2.PostProcessingData.Thumbnail); Assert.Null(image2.PostProcessingData.ThumbnailTransformState); - Assert.False(image2.PostProcessingData.BarcodeDetection.IsAttempted); + Assert.False(image2.PostProcessingData.Barcode.IsDetectionAttempted); Assert.False(IsDisposed(image2)); image2.Dispose(); Assert.False(IsDisposed(image)); @@ -34,7 +28,7 @@ public void DisposesOriginalImageWithNoPostProcessing() { using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); using var image2 = - _importPostProcessor.AddPostProcessingData(image, null, null, new BarcodeDetectionOptions(), true); + ImportPostProcessor.AddPostProcessingData(image, null, null, new BarcodeDetectionOptions(), true); Assert.False(IsDisposed(image2)); image2.Dispose(); @@ -46,7 +40,7 @@ public void ThumbnailRendering() { using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); using var image2 = - _importPostProcessor.AddPostProcessingData(image, null, 256, new BarcodeDetectionOptions(), false); + ImportPostProcessor.AddPostProcessingData(image, null, 256, new BarcodeDetectionOptions(), false); var actual = image2.PostProcessingData.Thumbnail; @@ -63,7 +57,7 @@ public void ThumbnailRenderingWithTransform() using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); using var image2 = image.WithTransform(new BrightnessTransform(300)); using var image3 = - _importPostProcessor.AddPostProcessingData(image2, null, 256, new BarcodeDetectionOptions(), false); + ImportPostProcessor.AddPostProcessingData(image2, null, 256, new BarcodeDetectionOptions(), false); var actual = image3.PostProcessingData.Thumbnail; @@ -83,7 +77,7 @@ public void ThumbnailRenderingWithPrerenderedImageAndDisposingOriginal() using var rendered = LoadImage(ImageResources.dog); using var image = ScanningContext.CreateProcessedImage(rendered); using var image2 = - _importPostProcessor.AddPostProcessingData(image, rendered, 256, new BarcodeDetectionOptions(), true); + ImportPostProcessor.AddPostProcessingData(image, rendered, 256, new BarcodeDetectionOptions(), true); var actual = image2.PostProcessingData.Thumbnail; @@ -103,9 +97,9 @@ public void BarcodeDetection() { using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.patcht)); var barcodeOptions = new BarcodeDetectionOptions { DetectBarcodes = true }; - using var image2 = _importPostProcessor.AddPostProcessingData(image, null, null, barcodeOptions, false); + using var image2 = ImportPostProcessor.AddPostProcessingData(image, null, null, barcodeOptions, false); - Assert.True(image2.PostProcessingData.BarcodeDetection.IsPatchT); + Assert.True(image2.PostProcessingData.Barcode.IsPatchT); } [Fact] @@ -114,8 +108,8 @@ public void BarcodeDetectionWithPrerenderedImage() using var rendered = LoadImage(ImageResources.patcht); using var image = ScanningContext.CreateProcessedImage(rendered); var barcodeOptions = new BarcodeDetectionOptions { DetectBarcodes = true }; - using var image2 = _importPostProcessor.AddPostProcessingData(image, rendered, null, barcodeOptions, false); + using var image2 = ImportPostProcessor.AddPostProcessingData(image, rendered, null, barcodeOptions, false); - Assert.True(image2.PostProcessingData.BarcodeDetection.IsPatchT); + Assert.True(image2.PostProcessingData.Barcode.IsPatchT); } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfATests.cs b/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfATests.cs deleted file mode 100644 index 0a88df7fd8..0000000000 --- a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfATests.cs +++ /dev/null @@ -1,35 +0,0 @@ -using NAPS2.ImportExport.Pdf; -using NAPS2.Sdk.Tests.Asserts; - -namespace NAPS2.Sdk.Tests.ImportExport.Pdf; - -// TODO: Validate with OCR output -// TODO: Maaaybe validate with external import? We certainly can't guarantee it, but maybe some cases can be verified for best effort -public class PdfATests : ContextualTests -{ - // Sadly the pdfa verifier library only supports windows/mac - [PlatformFact(exclude: PlatformFlags.Mac)] - public async Task Validate() - { - var pdfExporter = new PdfExporter(ScanningContext); - var testCases = new (PdfCompat pdfCompat, string profile, string fileName)[] - { - (PdfCompat.PdfA1B, "PDF/A-1B", "pdfa1b_test.pdf"), - (PdfCompat.PdfA2B, "PDF/A-2B", "pdfa2b_test.pdf"), - (PdfCompat.PdfA3B, "PDF/A-3B", "pdfa3b_test.pdf"), - (PdfCompat.PdfA3U, "PDF/A-3U", "pdfa3u_test.pdf") - }; - - var tasks = testCases.Select(testCase => - { - using var image = CreateScannedImage(); - var path = Path.Combine(FolderPath, testCase.fileName); - pdfExporter.Export(path, new[] { image }, new PdfExportParams - { - Compat = testCase.pdfCompat - }).Wait(); - return PdfAsserts.AssertCompliant(testCase.profile, path); - }).ToArray(); - await Task.WhenAll(tasks); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Util/SliceTests.cs b/NAPS2.Sdk.Tests/ImportExport/SliceTests.cs similarity index 97% rename from NAPS2.Sdk.Tests/Util/SliceTests.cs rename to NAPS2.Sdk.Tests/ImportExport/SliceTests.cs index ceac29e725..c2a9465836 100644 --- a/NAPS2.Sdk.Tests/Util/SliceTests.cs +++ b/NAPS2.Sdk.Tests/ImportExport/SliceTests.cs @@ -1,6 +1,7 @@ -using Xunit; +using NAPS2.ImportExport; +using Xunit; -namespace NAPS2.Sdk.Tests.Util; +namespace NAPS2.Sdk.Tests.ImportExport; public class SliceTests { diff --git a/NAPS2.Sdk.Tests/Mocks/MockScanBridge.cs b/NAPS2.Sdk.Tests/Mocks/MockScanBridge.cs new file mode 100644 index 0000000000..7c5a027b7d --- /dev/null +++ b/NAPS2.Sdk.Tests/Mocks/MockScanBridge.cs @@ -0,0 +1,61 @@ +using System.Threading; +using NAPS2.Scan; +using NAPS2.Scan.Internal; + +namespace NAPS2.Sdk.Tests.Mocks; + +internal class MockScanBridge : IScanBridge +{ + public List MockDevices { get; set; } = []; + + public List MockOutput { get; set; } = []; + + public List ProgressReports { get; set; } = []; + + public Exception Error { get; set; } + + public ScanOptions LastOptions { get; private set; } + + public Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback) + { + LastOptions = options; + return Task.Run(() => + { + if (Error != null) + { + throw Error; + } + + foreach (var device in MockDevices) + { + callback(device); + } + }); + } + + public Task GetCaps(ScanOptions options, CancellationToken cancelToken) + { + return null!; + } + + public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) + { + LastOptions = options; + return Task.Run(() => + { + foreach (var img in MockOutput) + { + scanEvents.PageStart(); + foreach (var progress in ProgressReports) + { + scanEvents.PageProgress(progress); + } + callback(img.Clone(), new PostProcessingContext()); + } + if (Error != null) + { + throw Error; + } + }); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Mocks/ScanDriverFactoryBuilder.cs b/NAPS2.Sdk.Tests/Mocks/ScanDriverFactoryBuilder.cs index d015e06a49..a55a978066 100644 --- a/NAPS2.Sdk.Tests/Mocks/ScanDriverFactoryBuilder.cs +++ b/NAPS2.Sdk.Tests/Mocks/ScanDriverFactoryBuilder.cs @@ -1,20 +1,20 @@ using System.Threading; -using Moq; using NAPS2.Scan; using NAPS2.Scan.Internal; +using NSubstitute; namespace NAPS2.Sdk.Tests.Mocks; public class ScanDriverFactoryBuilder { private readonly StubScanDriver _scanDriver; - private readonly Mock _scanDriverFactory; + private readonly IScanDriverFactory _scanDriverFactory; public ScanDriverFactoryBuilder() { _scanDriver = new StubScanDriver(); - _scanDriverFactory = new Mock(); - _scanDriverFactory.Setup(x => x.Create(It.IsAny())).Returns(_scanDriver); + _scanDriverFactory = Substitute.For(); + _scanDriverFactory.Create(Arg.Any()).Returns(_scanDriver); } public ScanDriverFactoryBuilder WithDeviceList(params ScanDevice[] devices) @@ -31,12 +31,12 @@ public ScanDriverFactoryBuilder WithScannedImages(params byte[][] images) internal IScanDriverFactory Build() { - return _scanDriverFactory.Object; + return _scanDriverFactory; } private class StubScanDriver : IScanDriver { - private readonly Queue> _scans = new Queue>(); + private readonly Queue> _scans = new(); public List DeviceList { get; set; } @@ -45,9 +45,18 @@ public void AddScanResult(List images) _scans.Enqueue(images); } - public Task> GetDeviceList(ScanOptions options) + public Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback) { - return Task.FromResult(DeviceList ?? throw new NotSupportedException()); + foreach (var device in DeviceList) + { + callback(device); + } + return Task.CompletedTask; + } + + public Task GetCaps(ScanOptions options, CancellationToken cancelToken) + { + return Task.FromResult(null); } public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) diff --git a/NAPS2.Sdk.Tests/Mocks/StubScanBridge.cs b/NAPS2.Sdk.Tests/Mocks/StubScanBridge.cs deleted file mode 100644 index cb65ff7c51..0000000000 --- a/NAPS2.Sdk.Tests/Mocks/StubScanBridge.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Threading; -using NAPS2.Scan; -using NAPS2.Scan.Internal; - -namespace NAPS2.Sdk.Tests.Mocks; - -internal class StubScanBridge : IScanBridge -{ - public List MockDevices { get; set; } = new(); - - public List MockOutput { get; set; } = new(); - - public Exception Error { get; set; } - - public Task> GetDeviceList(ScanOptions options) - { - return Task.Run(() => - { - if (Error != null) - { - throw Error; - } - - return MockDevices; - }); - } - - public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) - { - return Task.Run(() => - { - foreach (var img in MockOutput) - { - callback(img, new PostProcessingContext()); - } - if (Error != null) - { - throw Error; - } - }); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj b/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj index f41d6a9081..8931f330a8 100644 --- a/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj +++ b/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj @@ -1,8 +1,8 @@  - net6;net462 - net6 + net9-windows + net9 true true @@ -12,28 +12,28 @@ - - - ..\NAPS2.Setup\lib\PdfSharpCore.dll - - + + + - - - + + + + + - - + + - + <_Parameter1>NAPS2.Lib.Tests diff --git a/NAPS2.Sdk.Tests/NetworkFactAttribute.cs b/NAPS2.Sdk.Tests/NetworkFactAttribute.cs new file mode 100644 index 0000000000..6270f7c1d4 --- /dev/null +++ b/NAPS2.Sdk.Tests/NetworkFactAttribute.cs @@ -0,0 +1,14 @@ +using Xunit; + +namespace NAPS2.Sdk.Tests; + +public class NetworkFactAttribute : FactAttribute +{ + public NetworkFactAttribute() + { + if (Environment.GetEnvironmentVariable("NAPS2_TEST_NONETWORK") == "1") + { + Skip = "Running without network access, skipping ESCL tests"; + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Ocr/OcrRequestQueueTests.cs b/NAPS2.Sdk.Tests/Ocr/OcrRequestQueueTests.cs index b8ec2be327..2ee4363004 100644 --- a/NAPS2.Sdk.Tests/Ocr/OcrRequestQueueTests.cs +++ b/NAPS2.Sdk.Tests/Ocr/OcrRequestQueueTests.cs @@ -1,7 +1,7 @@ using System.Collections.Immutable; using System.Threading; -using Moq; using NAPS2.Ocr; +using NSubstitute; using Xunit; namespace NAPS2.Sdk.Tests.Ocr; @@ -9,7 +9,7 @@ namespace NAPS2.Sdk.Tests.Ocr; public class OcrRequestQueueTests : ContextualTests { private readonly OcrRequestQueue _ocrRequestQueue; - private readonly Mock _mockEngine; + private readonly IOcrEngine _mockEngine; private readonly ProcessedImage _image; private readonly string _tempPath; private readonly OcrParams _ocrParams; @@ -18,7 +18,7 @@ public class OcrRequestQueueTests : ContextualTests public OcrRequestQueueTests() { - _mockEngine = new Mock(MockBehavior.Strict); + _mockEngine = Substitute.For(); _ocrRequestQueue = new OcrRequestQueue { WorkerCount = 4 @@ -34,13 +34,14 @@ public OcrRequestQueueTests() [Fact] public async Task Enqueue() { - _mockEngine.Setup(x => x.ProcessImage(_tempPath, _ocrParams, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, _tempPath, _ocrParams, Arg.Any()) .Returns(_expectedResultTask); var ocrResult = await DoEnqueueForeground(_image, _tempPath, _ocrParams); Assert.Equal(_expectedResult, ocrResult); Assert.False(File.Exists(_tempPath)); + _mockEngine.ReceivedCallsCount(1); } [Fact] @@ -48,7 +49,7 @@ public async Task EnqueueTwiceReturnsCached() { var tempPath1 = CreateTempFile(); var tempPath2 = CreateTempFile(); - _mockEngine.Setup(x => x.ProcessImage(tempPath1, _ocrParams, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, tempPath1, _ocrParams, Arg.Any()) .Returns(_expectedResultTask); await DoEnqueueForeground(_image, tempPath1, _ocrParams); @@ -63,8 +64,8 @@ public async Task EnqueueTwiceReturnsCached() Assert.False(File.Exists(tempPath2)); // Verify only a single engine call - _mockEngine.Verify(x => x.ProcessImage(tempPath1, _ocrParams, It.IsAny())); - _mockEngine.VerifyNoOtherCalls(); + _ = _mockEngine.Received().ProcessImage(ScanningContext, tempPath1, _ocrParams, Arg.Any()); + _mockEngine.ReceivedCallsCount(1); } [Fact] @@ -72,7 +73,7 @@ public async Task EnqueueSimultaneous() { var tempPath1 = CreateTempFile(); var tempPath2 = CreateTempFile(); - _mockEngine.Setup(x => x.ProcessImage(tempPath1, _ocrParams, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, tempPath1, _ocrParams, Arg.Any()) .Returns(_expectedResultTask); var ocrResult1Task = DoEnqueueForeground(_image, tempPath1, _ocrParams); @@ -87,8 +88,8 @@ public async Task EnqueueSimultaneous() Assert.False(File.Exists(tempPath2)); // Verify only a single engine call - _mockEngine.Verify(x => x.ProcessImage(tempPath1, _ocrParams, It.IsAny())); - _mockEngine.VerifyNoOtherCalls(); + _ = _mockEngine.Received().ProcessImage(ScanningContext, tempPath1, _ocrParams, Arg.Any()); + _mockEngine.ReceivedCallsCount(1); } [Fact] @@ -97,13 +98,13 @@ public async Task EnqueueWithDifferentImagesReturnsDifferentResult() var image1 = CreateScannedImage(); var tempPath1 = CreateTempFile(); var expectedResult1 = CreateOcrResult(); - _mockEngine.Setup(x => x.ProcessImage(tempPath1, _ocrParams, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, tempPath1, _ocrParams, Arg.Any()) .Returns(Task.FromResult(expectedResult1)); var image2 = CreateScannedImage(); var tempPath2 = CreateTempFile(); var expectedResult2 = CreateOcrResult(); - _mockEngine.Setup(x => x.ProcessImage(tempPath2, _ocrParams, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, tempPath2, _ocrParams, Arg.Any()) .Returns(Task.FromResult(expectedResult2)); var ocrResult1 = await DoEnqueueForeground(image1, tempPath1, _ocrParams); @@ -115,9 +116,9 @@ public async Task EnqueueWithDifferentImagesReturnsDifferentResult() Assert.False(File.Exists(tempPath2)); // Verify two engine calls - _mockEngine.Verify(x => x.ProcessImage(tempPath1, _ocrParams, It.IsAny())); - _mockEngine.Verify(x => x.ProcessImage(tempPath2, _ocrParams, It.IsAny())); - _mockEngine.VerifyNoOtherCalls(); + _ = _mockEngine.Received().ProcessImage(ScanningContext, tempPath1, _ocrParams, Arg.Any()); + _ = _mockEngine.Received().ProcessImage(ScanningContext, tempPath2, _ocrParams, Arg.Any()); + _mockEngine.ReceivedCallsCount(2); } [Fact] @@ -131,13 +132,13 @@ public async Task EnqueueWithDifferentParamsReturnsDifferentResult() var expectedResult2 = CreateOcrResult(); var expectedResult3 = CreateOcrResult(); var expectedResult4 = CreateOcrResult(); - _mockEngine.Setup(x => x.ProcessImage(It.IsAny(), ocrParams1, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, Arg.Any(), ocrParams1, Arg.Any()) .Returns(Task.FromResult(expectedResult1)); - _mockEngine.Setup(x => x.ProcessImage(It.IsAny(), ocrParams2, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, Arg.Any(), ocrParams2, Arg.Any()) .Returns(Task.FromResult(expectedResult2)); - _mockEngine.Setup(x => x.ProcessImage(It.IsAny(), ocrParams3, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, Arg.Any(), ocrParams3, Arg.Any()) .Returns(Task.FromResult(expectedResult3)); - _mockEngine.Setup(x => x.ProcessImage(It.IsAny(), ocrParams4, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, Arg.Any(), ocrParams4, Arg.Any()) .Returns(Task.FromResult(expectedResult4)); var ocrResult1 = await DoEnqueueForeground(_image, CreateTempFile(), ocrParams1); @@ -150,24 +151,24 @@ public async Task EnqueueWithDifferentParamsReturnsDifferentResult() Assert.Equal(expectedResult4, ocrResult4); // Verify distinct engine calls - _mockEngine.Verify(x => x.ProcessImage(It.IsAny(), ocrParams1, It.IsAny())); - _mockEngine.Verify(x => x.ProcessImage(It.IsAny(), ocrParams2, It.IsAny())); - _mockEngine.Verify(x => x.ProcessImage(It.IsAny(), ocrParams3, It.IsAny())); - _mockEngine.Verify(x => x.ProcessImage(It.IsAny(), ocrParams4, It.IsAny())); - _mockEngine.VerifyNoOtherCalls(); + _ = _mockEngine.Received().ProcessImage(ScanningContext, Arg.Any(), ocrParams1, Arg.Any()); + _ = _mockEngine.Received().ProcessImage(ScanningContext, Arg.Any(), ocrParams2, Arg.Any()); + _ = _mockEngine.Received().ProcessImage(ScanningContext, Arg.Any(), ocrParams3, Arg.Any()); + _ = _mockEngine.Received().ProcessImage(ScanningContext, Arg.Any(), ocrParams4, Arg.Any()); + _mockEngine.ReceivedCallsCount(4); } [Fact] public async Task EnqueueWithEngineError() { - _mockEngine.Setup(x => x.ProcessImage(_tempPath, _ocrParams, It.IsAny())) - .Throws(); + _mockEngine.When(x => x.ProcessImage(ScanningContext, _tempPath, _ocrParams, Arg.Any())) + .Do(_ => throw new Exception()); var ocrResult = await DoEnqueueForeground(_image, _tempPath, _ocrParams); Assert.Null(ocrResult); - _mockEngine.Verify(x => x.ProcessImage(_tempPath, _ocrParams, It.IsAny())); - _mockEngine.VerifyNoOtherCalls(); + _ = _mockEngine.Received().ProcessImage(ScanningContext, _tempPath, _ocrParams, Arg.Any()); + _mockEngine.ReceivedCallsCount(1); } [Fact] @@ -175,9 +176,9 @@ public async Task EnqueueTwiceWithTransientEngineError() { var tempPath1 = CreateTempFile(); var tempPath2 = CreateTempFile(); - _mockEngine.Setup(x => x.ProcessImage(tempPath1, _ocrParams, It.IsAny())) - .Throws(); - _mockEngine.Setup(x => x.ProcessImage(tempPath2, _ocrParams, It.IsAny())) + _mockEngine.When(x => x.ProcessImage(ScanningContext, tempPath1, _ocrParams, Arg.Any())) + .Do(_ => throw new Exception()); + _mockEngine.ProcessImage(ScanningContext, tempPath2, _ocrParams, Arg.Any()) .Returns(_expectedResultTask); var ocrResult1 = await DoEnqueueForeground(_image, tempPath1, _ocrParams); @@ -185,12 +186,12 @@ public async Task EnqueueTwiceWithTransientEngineError() Assert.Null(ocrResult1); Assert.Equal(_expectedResult, ocrResult2); - _mockEngine.Verify(x => x.ProcessImage(tempPath1, _ocrParams, It.IsAny())); - _mockEngine.Verify(x => x.ProcessImage(tempPath2, _ocrParams, It.IsAny())); - _mockEngine.VerifyNoOtherCalls(); + _ = _mockEngine.Received().ProcessImage(ScanningContext, tempPath1, _ocrParams, Arg.Any()); + _ = _mockEngine.Received().ProcessImage(ScanningContext, tempPath2, _ocrParams, Arg.Any()); + _mockEngine.ReceivedCallsCount(2); } - [Fact] + [Fact(Skip = "flaky")] public async Task ImmediateCancel() { var cts = new CancellationTokenSource(); @@ -199,7 +200,7 @@ public async Task ImmediateCancel() var ocrResult = await DoEnqueueForeground(_image, _tempPath, _ocrParams, cts.Token); Assert.Null(ocrResult); - _mockEngine.VerifyNoOtherCalls(); + _mockEngine.ReceivedCallsCount(0); } [Fact] @@ -209,19 +210,19 @@ public async Task CancelDuringProcessing() var cancelledAtEngineStart = false; var cancelledAtEngineEnd = false; var cts = new CancellationTokenSource(); - _mockEngine.Setup(x => x.ProcessImage(_tempPath, _ocrParams, It.IsAny())).Returns( - new InvocationFunc(invocation => + _mockEngine.ProcessImage(ScanningContext, _tempPath, _ocrParams, Arg.Any()).Returns( + x => { mockEngineTask = Task.Run(() => { - var cancelToken = (CancellationToken) invocation.Arguments[2]; + var cancelToken = (CancellationToken) x[3]; cancelledAtEngineStart = cancelToken.IsCancellationRequested; cts.Cancel(); cancelledAtEngineEnd = cancelToken.IsCancellationRequested; return (OcrResult) null; }); return mockEngineTask; - })); + }); var ocrResult = await DoEnqueueForeground(_image, _tempPath, _ocrParams, cts.Token); await mockEngineTask; @@ -229,17 +230,17 @@ public async Task CancelDuringProcessing() Assert.Null(ocrResult); Assert.False(cancelledAtEngineStart); Assert.True(cancelledAtEngineEnd); - _mockEngine.Verify(x => x.ProcessImage(_tempPath, _ocrParams, It.IsAny())); - _mockEngine.VerifyNoOtherCalls(); + _ = _mockEngine.Received().ProcessImage(ScanningContext, _tempPath, _ocrParams, Arg.Any()); + _mockEngine.ReceivedCallsCount(1); } - [Fact] + [Fact(Skip = "flaky")] public async Task CancelOnceWithTwoReferences() { var tempPath1 = CreateTempFile(); var tempPath2 = CreateTempFile(); - _mockEngine.Setup(x => x.ProcessImage(tempPath1, _ocrParams, It.IsAny())) - .Returns(() => Task.Run(async () => + _mockEngine.ProcessImage(ScanningContext, tempPath1, _ocrParams, Arg.Any()) + .Returns(_ => Task.Run(async () => { await Task.Delay(200); return _expectedResult; @@ -256,11 +257,11 @@ public async Task CancelOnceWithTwoReferences() var ocrResult2 = await ocrResult2Task; Assert.Null(ocrResult1); Assert.Equal(_expectedResult, ocrResult2); - _mockEngine.Verify(x => x.ProcessImage(tempPath1, _ocrParams, It.IsAny())); - _mockEngine.VerifyNoOtherCalls(); + _ = _mockEngine.Received().ProcessImage(ScanningContext, tempPath1, _ocrParams, Arg.Any()); + _mockEngine.ReceivedCallsCount(1); } - [Fact] + [Fact(Skip = "flaky")] public async Task CancelTwiceWithTwoReferences() { // Delay the workers so we can cancel before processing starts @@ -276,14 +277,14 @@ public async Task CancelTwiceWithTwoReferences() Assert.Null(ocrResult1); Assert.Null(ocrResult2); await Task.Delay(200); - _mockEngine.VerifyNoOtherCalls(); + _mockEngine.ReceivedCallsCount(0); } // TODO: Deflake [Fact(Skip = "flaky")] public async Task ForegroundPrioritized() { - _mockEngine.Setup(x => x.ProcessImage(_tempPath, _ocrParams, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, _tempPath, _ocrParams, Arg.Any()) .Returns(_expectedResultTask); _ocrRequestQueue.WorkerAddedLatency = 50; @@ -301,10 +302,11 @@ public async Task ForegroundPrioritized() await Task.WhenAll(foregroundTasks); } - [Fact] + // This seems to cause some crash (task/thread starvation related?) on Linux + [PlatformFact(exclude: PlatformFlags.Linux)] public async Task StressTest() { - _mockEngine.Setup(x => x.ProcessImage(It.IsAny(), _ocrParams, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, Arg.Any(), _ocrParams, Arg.Any()) .Returns(_expectedResultTask); var tasks = EnqueueMany(OcrPriority.Foreground, 1000); @@ -320,42 +322,42 @@ public async Task StressTest() [Fact] public async Task HasCachedResult() { - _mockEngine.Setup(x => x.ProcessImage(_tempPath, _ocrParams, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, _tempPath, _ocrParams, Arg.Any()) .Returns(_expectedResultTask); await DoEnqueueForeground(_image, _tempPath, _ocrParams); var ocrParams2 = new OcrParams("fra", OcrMode.Fast, 10); - Assert.True(_ocrRequestQueue.HasCachedResult(_mockEngine.Object, _image, _ocrParams)); - Assert.False(_ocrRequestQueue.HasCachedResult(_mockEngine.Object, _image, ocrParams2)); - Assert.False(_ocrRequestQueue.HasCachedResult(_mockEngine.Object, CreateScannedImage(), _ocrParams)); - Assert.False(_ocrRequestQueue.HasCachedResult(new Mock().Object, _image, _ocrParams)); + Assert.True(_ocrRequestQueue.HasCachedResult(_mockEngine, _image, _ocrParams)); + Assert.False(_ocrRequestQueue.HasCachedResult(_mockEngine, _image, ocrParams2)); + Assert.False(_ocrRequestQueue.HasCachedResult(_mockEngine, CreateScannedImage(), _ocrParams)); + Assert.False(_ocrRequestQueue.HasCachedResult(Substitute.For(), _image, _ocrParams)); } - [Fact] + [Fact(Skip = "flaky")] public async Task HasCachedResult_WhileProcessing() { - _mockEngine.Setup(x => x.ProcessImage(_tempPath, _ocrParams, It.IsAny())) + _mockEngine.ProcessImage(ScanningContext, _tempPath, _ocrParams, Arg.Any()) .Returns(_expectedResultTask); _ocrRequestQueue.WorkerAddedLatency = 50; var queuedTask = DoEnqueueForeground(_image, _tempPath, _ocrParams); - Assert.False(_ocrRequestQueue.HasCachedResult(_mockEngine.Object, _image, _ocrParams)); + Assert.False(_ocrRequestQueue.HasCachedResult(_mockEngine, _image, _ocrParams)); await queuedTask; - Assert.True(_ocrRequestQueue.HasCachedResult(_mockEngine.Object, _image, _ocrParams)); + Assert.True(_ocrRequestQueue.HasCachedResult(_mockEngine, _image, _ocrParams)); } [Fact] public async Task HasCachedResult_WithError() { - _mockEngine.Setup(x => x.ProcessImage(_tempPath, _ocrParams, It.IsAny())) - .Throws(); + _mockEngine.When(x => x.ProcessImage(ScanningContext, _tempPath, _ocrParams, Arg.Any())) + .Do(_ => throw new Exception()); await DoEnqueueForeground(_image, _tempPath, _ocrParams); - Assert.False(_ocrRequestQueue.HasCachedResult(_mockEngine.Object, _image, _ocrParams)); + Assert.False(_ocrRequestQueue.HasCachedResult(_mockEngine, _image, _ocrParams)); } private List> EnqueueMany(OcrPriority priority, int count) @@ -373,8 +375,8 @@ private string CreateTempFile() private static OcrResult CreateOcrResult() { - var uniqueElement = new OcrResultElement(Guid.NewGuid().ToString(), "eng", false, (0, 0, 1, 1)); - return new OcrResult((0, 0, 1, 1), ImmutableList.Empty.Add(uniqueElement)); + var uniqueElement = new OcrResultElement(Guid.NewGuid().ToString(), "eng", false, (0, 0, 1, 1), 0, 10, ImmutableList.Empty); + return new OcrResult((0, 0, 1, 1), ImmutableList.Create(uniqueElement), ImmutableList.Create(uniqueElement)); } private static OcrParams CreateOcrParams() @@ -392,7 +394,8 @@ private Task DoEnqueue(OcrPriority priority, ProcessedImage image, st CancellationToken cancellationToken = default) { return _ocrRequestQueue.Enqueue( - _mockEngine.Object, + ScanningContext, + _mockEngine, image, tempPath, ocrParams, diff --git a/NAPS2.Sdk.Tests/Ocr/TesseractOcrEngineTests.cs b/NAPS2.Sdk.Tests/Ocr/TesseractOcrEngineTests.cs index 63ddce4347..4f86ae0dcd 100644 --- a/NAPS2.Sdk.Tests/Ocr/TesseractOcrEngineTests.cs +++ b/NAPS2.Sdk.Tests/Ocr/TesseractOcrEngineTests.cs @@ -1,6 +1,7 @@ using System.Threading; using NAPS2.Ocr; using Xunit; +using Xunit.Abstractions; namespace NAPS2.Sdk.Tests.Ocr; @@ -10,7 +11,8 @@ public class TesseractOcrEngineTests : ContextualTests private readonly string _testImagePath; private readonly string _testImagePathHebrew; - public TesseractOcrEngineTests() + public TesseractOcrEngineTests(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) { SetUpOcr(); _testImagePath = CopyResourceToFile(BinaryResources.ocr_test, "ocr_test.jpg"); @@ -21,66 +23,67 @@ public TesseractOcrEngineTests() [Fact] public async Task ProcessEnglishImage() { - var result = await _engine.ProcessImage(_testImagePath, new OcrParams("eng", OcrMode.Fast, 0), CancellationToken.None); + var ocrParams = new OcrParams("eng", OcrMode.Fast, 0); + var result = await _engine.ProcessImage(ScanningContext, _testImagePath, ocrParams, CancellationToken.None); Assert.NotNull(result); - Assert.NotEmpty(result.Elements); - foreach (var element in result.Elements) + Assert.NotEmpty(result.Words); + foreach (var element in result.Words) { Assert.Equal("eng", element.LanguageCode); Assert.False(element.RightToLeft); } - Assert.Equal("ADVERTISEMENT.", result.Elements[0].Text); - Assert.InRange(result.Elements[0].Bounds.x, 139, 149); - Assert.InRange(result.Elements[0].Bounds.y, 26, 36); - Assert.InRange(result.Elements[0].Bounds.w, 237, 247); - Assert.InRange(result.Elements[0].Bounds.h, 17, 27); + Assert.Equal("ADVERTISEMENT.", result.Words[0].Text); + Assert.InRange(result.Words[0].Bounds.x, 139, 149); + Assert.InRange(result.Words[0].Bounds.y, 26, 36); + Assert.InRange(result.Words[0].Bounds.w, 237, 247); + Assert.InRange(result.Words[0].Bounds.h, 17, 27); } [Fact] public async Task ProcessHebrewImage() { - var result = await _engine.ProcessImage(_testImagePathHebrew, new OcrParams("heb", OcrMode.Fast, 0), CancellationToken.None); + var result = await _engine.ProcessImage(ScanningContext, _testImagePathHebrew, new OcrParams("heb", OcrMode.Fast, 0), CancellationToken.None); Assert.NotNull(result); - Assert.NotEmpty(result.Elements); - foreach (var element in result.Elements) + Assert.NotEmpty(result.Words); + foreach (var element in result.Words) { Assert.Equal("heb", element.LanguageCode); Assert.True(element.RightToLeft); } - Assert.Equal("הקדמת", result.Elements[0].Text); + Assert.Equal("הקדמת", result.Words[0].Text); } - [Fact] + [Fact(Skip = "flaky")] public async Task ImmediateCancel() { CancellationTokenSource cts = new CancellationTokenSource(); cts.Cancel(); - var result = await _engine.ProcessImage(_testImagePath, new OcrParams("eng", OcrMode.Fast, 0), cts.Token); + var result = await _engine.ProcessImage(ScanningContext, _testImagePath, new OcrParams("eng", OcrMode.Fast, 0), cts.Token); Assert.Null(result); } - [Fact] + [Fact(Skip = "flaky")] public async Task CancelWhileProcessing() { CancellationTokenSource cts = new CancellationTokenSource(); - cts.CancelAfter(50); - var result = await _engine.ProcessImage(_testImagePath, new OcrParams("eng", OcrMode.Fast, 0), cts.Token); + cts.CancelAfter(20); + var result = await _engine.ProcessImage(ScanningContext, _testImagePath, new OcrParams("eng", OcrMode.Fast, 0), cts.Token); Assert.Null(result); } - [Fact] + [Fact(Skip = "flaky")] public async Task Timeout() { var timeout = 0.1; - var result = await _engine.ProcessImage(_testImagePath, new OcrParams("eng", OcrMode.Fast, timeout), CancellationToken.None); + var result = await _engine.ProcessImage(ScanningContext, _testImagePath, new OcrParams("eng", OcrMode.Fast, timeout), CancellationToken.None); Assert.Null(result); } [Fact] public async Task NoTimeout() { - var timeout = 10; - var result = await _engine.ProcessImage(_testImagePath, new OcrParams("eng", OcrMode.Fast, timeout), CancellationToken.None); + var timeout = 60; + var result = await _engine.ProcessImage(ScanningContext, _testImagePath, new OcrParams("eng", OcrMode.Fast, timeout), CancellationToken.None); Assert.NotNull(result); } @@ -92,8 +95,8 @@ public async Task Mode() CopyResourceToFile(BinaryResources.heb_traineddata, Path.Combine(FolderPath, "fast"), "eng.traineddata"); var mode = OcrMode.Best; - var result = await _engine.ProcessImage(_testImagePath, new OcrParams("eng", mode, 0), CancellationToken.None); + var result = await _engine.ProcessImage(ScanningContext, _testImagePath, new OcrParams("eng", mode, 0), CancellationToken.None); Assert.NotNull(result); - Assert.Equal("ADVERTISEMENT.", result.Elements[0].Text); + Assert.Equal("ADVERTISEMENT.", result.Words[0].Text); } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Pdf/JpegFormatHelperTests.cs b/NAPS2.Sdk.Tests/Pdf/JpegFormatHelperTests.cs new file mode 100644 index 0000000000..3dd07af1fa --- /dev/null +++ b/NAPS2.Sdk.Tests/Pdf/JpegFormatHelperTests.cs @@ -0,0 +1,47 @@ +using NAPS2.Pdf; +using Xunit; + +namespace NAPS2.Sdk.Tests.Pdf; + +public class JpegFormatHelperTests +{ + [Fact] + public void Jfif() + { + var header = JpegFormatHelper.ReadHeader(new MemoryStream(ImageResources.dog)); + Assert.NotNull(header); + Assert.Equal(788, header.Width); + Assert.Equal(525, header.Height); + Assert.Equal(3, header.NumComponents); + Assert.Equal(72, header.HorizontalDpi); + Assert.Equal(72, header.VerticalDpi); + Assert.True(header.HasJfifHeader); + } + + [Fact] + public void JfifGrey() + { + var header = JpegFormatHelper.ReadHeader(new MemoryStream(ImageResources.dog_gray_8bit)); + Assert.NotNull(header); + Assert.Equal(788, header.Width); + Assert.Equal(525, header.Height); + Assert.Equal(1, header.NumComponents); + Assert.Equal(72, header.HorizontalDpi); + Assert.Equal(72, header.VerticalDpi); + Assert.True(header.HasJfifHeader); + } + + [Fact] + public void Exif() + { + var header = JpegFormatHelper.ReadHeader(new MemoryStream(ImageResources.dog_exif)); + Assert.NotNull(header); + Assert.Equal(788, header.Width); + Assert.Equal(525, header.Height); + Assert.Equal(3, header.NumComponents); + Assert.Equal(72, header.HorizontalDpi); + Assert.Equal(72, header.VerticalDpi); + Assert.False(header.HasJfifHeader); + Assert.True(header.HasExifHeader); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/ImportExport/Pdf/OcrTestConfig.cs b/NAPS2.Sdk.Tests/Pdf/OcrTestConfig.cs similarity index 68% rename from NAPS2.Sdk.Tests/ImportExport/Pdf/OcrTestConfig.cs rename to NAPS2.Sdk.Tests/Pdf/OcrTestConfig.cs index 735a636c9a..52d3d16bd7 100644 --- a/NAPS2.Sdk.Tests/ImportExport/Pdf/OcrTestConfig.cs +++ b/NAPS2.Sdk.Tests/Pdf/OcrTestConfig.cs @@ -1,5 +1,5 @@ using NAPS2.Ocr; -namespace NAPS2.Sdk.Tests.ImportExport.Pdf; +namespace NAPS2.Sdk.Tests.Pdf; public record OcrTestConfig(StorageConfig StorageConfig, OcrParams OcrParams); \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/ImportExport/Pdf/OcrTestData.cs b/NAPS2.Sdk.Tests/Pdf/OcrTestData.cs similarity index 76% rename from NAPS2.Sdk.Tests/ImportExport/Pdf/OcrTestData.cs rename to NAPS2.Sdk.Tests/Pdf/OcrTestData.cs index 7ed637cc34..5d2a26d546 100644 --- a/NAPS2.Sdk.Tests/ImportExport/Pdf/OcrTestData.cs +++ b/NAPS2.Sdk.Tests/Pdf/OcrTestData.cs @@ -1,7 +1,7 @@ using System.Collections; using NAPS2.Ocr; -namespace NAPS2.Sdk.Tests.ImportExport.Pdf; +namespace NAPS2.Sdk.Tests.Pdf; public class OcrTestData : IEnumerable { @@ -9,8 +9,8 @@ public IEnumerator GetEnumerator() { yield return new object[] { new OcrTestConfig(new StorageConfig.Memory(), null) }; yield return new object[] { new OcrTestConfig(new StorageConfig.File(), null) }; - yield return new object[] { new OcrTestConfig(new StorageConfig.Memory(), new OcrParams("eng", OcrMode.Default, 10)) }; - yield return new object[] { new OcrTestConfig(new StorageConfig.File(), new OcrParams("eng", OcrMode.Default, 10)) }; + yield return new object[] { new OcrTestConfig(new StorageConfig.Memory(), new OcrParams("eng", OcrMode.Default, 60)) }; + yield return new object[] { new OcrTestConfig(new StorageConfig.File(), new OcrParams("eng", OcrMode.Default, 60)) }; } IEnumerator IEnumerable.GetEnumerator() diff --git a/NAPS2.Sdk.Tests/Pdf/PdfATests.cs b/NAPS2.Sdk.Tests/Pdf/PdfATests.cs new file mode 100644 index 0000000000..2accbae136 --- /dev/null +++ b/NAPS2.Sdk.Tests/Pdf/PdfATests.cs @@ -0,0 +1,76 @@ +using NAPS2.Ocr; +using NAPS2.Pdf; +using NAPS2.Sdk.Tests.Asserts; +using Xunit; + +namespace NAPS2.Sdk.Tests.Pdf; + +public class PdfATests : ContextualTests +{ + private readonly PdfExporter _pdfExporter; + private readonly string _path; + private readonly string _importPath; + + public PdfATests() + { + _pdfExporter = new PdfExporter(ScanningContext); + _path = Path.Combine(FolderPath, "test.pdf"); + _importPath = CopyResourceToFile(PdfResources.word_patcht_pdf, "word.pdf"); + } + + // Sadly the pdfa verifier library only supports windows/linux + [PlatformTheory(exclude: PlatformFlags.Mac)] + [MemberData(nameof(TestCases))] + public async Task Validate(PdfCompat pdfCompat, string profile, int version) + { + await _pdfExporter.Export(_path, new[] { CreateScannedImage() }, new PdfExportParams + { + Compat = pdfCompat + }); + + PdfAsserts.AssertVersion(version, _path); + await PdfAsserts.AssertCompliant(profile, _path); + } + + [PlatformTheory(exclude: PlatformFlags.Mac)] + [MemberData(nameof(TestCases))] + public async Task ValidateWithOcr(PdfCompat pdfCompat, string profile, int version) + { + SetUpFakeOcr(ifNoMatch: "hello world"); + + await _pdfExporter.Export(_path, new[] { CreateScannedImage() }, new PdfExportParams + { + Compat = pdfCompat + }, new OcrParams("eng")); + + PdfAsserts.AssertVersion(version, _path); + await PdfAsserts.AssertCompliant(profile, _path); + } + + [PlatformTheory(exclude: PlatformFlags.Mac)] + [MemberData(nameof(TestCases))] + public async Task ValidateWithPdfium(PdfCompat pdfCompat, string profile, int version) + { + var images = await new PdfImporter(ScanningContext).Import(_importPath).ToListAsync(); + + await _pdfExporter.Export(_path, images, new PdfExportParams + { + Compat = pdfCompat + }); + + PdfAsserts.AssertVersion(version, _path); + await PdfAsserts.AssertCompliant(profile, _path); + } + + // Note that we don't have a Pdfium OCR test as we fail compliance due to the way Pdfium embeds fonts, which isn't + // practical to fix. + + public static IEnumerable TestCases = + [ + [PdfCompat.Default, "", 14], + [PdfCompat.PdfA1B, "PDF/A-1B", 14], + [PdfCompat.PdfA2B, "PDF/A-2B", 17], + [PdfCompat.PdfA3B, "PDF/A-3B", 17], + [PdfCompat.PdfA3U, "PDF/A-3U", 17] + ]; +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfBenchmarkTests.cs b/NAPS2.Sdk.Tests/Pdf/PdfBenchmarkTests.cs similarity index 63% rename from NAPS2.Sdk.Tests/ImportExport/Pdf/PdfBenchmarkTests.cs rename to NAPS2.Sdk.Tests/Pdf/PdfBenchmarkTests.cs index 6a8f773830..615a376cf8 100644 --- a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfBenchmarkTests.cs +++ b/NAPS2.Sdk.Tests/Pdf/PdfBenchmarkTests.cs @@ -1,7 +1,7 @@ -using NAPS2.ImportExport.Pdf; +using NAPS2.Pdf; using Xunit; -namespace NAPS2.Sdk.Tests.ImportExport.Pdf; +namespace NAPS2.Sdk.Tests.Pdf; public class PdfBenchmarkTests : ContextualTests { @@ -9,13 +9,12 @@ public class PdfBenchmarkTests : ContextualTests public async Task PdfSharpExport300() { var filePath = Path.Combine(FolderPath, "test"); - using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog), BitDepth.Color, - false, -1, Enumerable.Empty()); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); var pdfExporter = new PdfExporter(ScanningContext); for (int i = 0; i < 300; i++) { - await pdfExporter.Export(filePath + i + ".pdf", new[] { image }, new PdfExportParams()); + await pdfExporter.Export(filePath + i + ".pdf", [image]); } } @@ -23,35 +22,32 @@ public async Task PdfSharpExport300() public async Task PdfSharpExportHuge() { var filePath = Path.Combine(FolderPath, "test"); - using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_huge), - BitDepth.Color, false, -1, Enumerable.Empty()); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_huge)); var pdfExporter = new PdfExporter(ScanningContext); - await pdfExporter.Export(filePath + ".pdf", new[] { image }, new PdfExportParams()); + await pdfExporter.Export(filePath + ".pdf", [image]); } [BenchmarkFact] public async Task PdfSharpExportHugePng() { var filePath = Path.Combine(FolderPath, "test"); - using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_huge_png), - BitDepth.Color, true, -1, Enumerable.Empty()); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_huge_png)); var pdfExporter = new PdfExporter(ScanningContext); - await pdfExporter.Export(filePath + ".pdf", new[] { image }, new PdfExportParams()); + await pdfExporter.Export(filePath + ".pdf", [image]); } [BenchmarkFact] public async Task PdfiumExport300() { var filePath = Path.Combine(FolderPath, "test"); - using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog), BitDepth.Color, - false, -1, Enumerable.Empty()); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); var pdfExporter = new PdfiumPdfExporter(ScanningContext); for (int i = 0; i < 300; i++) { - await pdfExporter.Export(filePath + i + ".pdf", new[] { image }, new PdfExportParams()); + await pdfExporter.Export(filePath + i + ".pdf", [image]); } } @@ -59,28 +55,26 @@ public async Task PdfiumExport300() public async Task PdfiumExportHuge() { var filePath = Path.Combine(FolderPath, "test"); - using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_huge), - BitDepth.Color, false, -1, Enumerable.Empty()); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_huge)); var pdfExporter = new PdfiumPdfExporter(ScanningContext); - await pdfExporter.Export(filePath + ".pdf", new[] { image }, new PdfExportParams()); + await pdfExporter.Export(filePath + ".pdf", [image]); } [BenchmarkFact] public async Task PdfiumExportHugePng() { var filePath = Path.Combine(FolderPath, "test"); - using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_huge_png), - BitDepth.Color, true, -1, Enumerable.Empty()); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_huge_png)); var pdfExporter = new PdfiumPdfExporter(ScanningContext); - await pdfExporter.Export(filePath + ".pdf", new[] { image }, new PdfExportParams()); + await pdfExporter.Export(filePath + ".pdf", [image]); } [BenchmarkFact] public async Task Import300Naps2() { - ScanningContext.FileStorageManager = FileStorageManager.CreateFolder("recovery"); + SetUpFileStorage(); var filePath = CopyResourceToFile(PdfResources.image_pdf, "test.pdf"); var pdfExporter = new PdfImporter(ScanningContext); @@ -90,10 +84,23 @@ public async Task Import300Naps2() } } + [BenchmarkFact] + public async Task Import300Naps2Bw() + { + SetUpFileStorage(); + var filePath = CopyResourceToFile(PdfResources.image_pdf_bw, "test.pdf"); + + var pdfExporter = new PdfImporter(ScanningContext); + for (int i = 0; i < 300; i++) + { + await pdfExporter.Import(filePath).ToListAsync(); + } + } + [BenchmarkFact] public async Task Import300NonNaps2() { - ScanningContext.FileStorageManager = FileStorageManager.CreateFolder("recovery"); + SetUpFileStorage(); var filePath = CopyResourceToFile(PdfResources.word_generated_pdf, "test.pdf"); var pdfExporter = new PdfImporter(ScanningContext); diff --git a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfExportTests.cs b/NAPS2.Sdk.Tests/Pdf/PdfExportTests.cs similarity index 52% rename from NAPS2.Sdk.Tests/ImportExport/Pdf/PdfExportTests.cs rename to NAPS2.Sdk.Tests/Pdf/PdfExportTests.cs index 33d2eb16d7..e3ccd6ec5e 100644 --- a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfExportTests.cs +++ b/NAPS2.Sdk.Tests/Pdf/PdfExportTests.cs @@ -1,10 +1,10 @@ using System.Threading; -using Moq; -using NAPS2.ImportExport.Pdf; +using NAPS2.Pdf; using NAPS2.Sdk.Tests.Asserts; +using NSubstitute; using Xunit; -namespace NAPS2.Sdk.Tests.ImportExport.Pdf; +namespace NAPS2.Sdk.Tests.Pdf; public class PdfExporterTests : ContextualTests { @@ -24,12 +24,75 @@ public async Task ExportJpegImage(StorageConfig storageConfig) var filePath = Path.Combine(FolderPath, "test.pdf"); using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); - await _exporter.Export(filePath, new[] { image }, new PdfExportParams()); + await _exporter.Export(filePath, [image]); PdfAsserts.AssertImages(filePath, ImageResources.dog); PdfAsserts.AssertImageFilter(filePath, 0, "DCTDecode"); } + [Theory] + [ClassData(typeof(StorageAwareTestData))] + public async Task ExportJpegImageToStream(StorageConfig storageConfig) + { + storageConfig.Apply(this); + + var filePath = Path.Combine(FolderPath, "test.pdf"); + var fileStream = File.OpenWrite(filePath); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); + + await _exporter.Export(fileStream, [image]); + fileStream.Close(); + + PdfAsserts.AssertImages(filePath, ImageResources.dog); + PdfAsserts.AssertImageFilter(filePath, 0, "DCTDecode"); + } + + [Theory] + [ClassData(typeof(StorageAwareTestData))] + public async Task ExportJpegImageToNonexistentFolder(StorageConfig storageConfig) + { + storageConfig.Apply(this); + + var filePath = Path.Combine(FolderPath, "blah", "test.pdf"); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); + + await _exporter.Export(filePath, [image]); + + PdfAsserts.AssertImages(filePath, ImageResources.dog); + PdfAsserts.AssertImageFilter(filePath, 0, "DCTDecode"); + } + + [Theory] + [ClassData(typeof(StorageAwareTestData))] + public async Task ExportGrayJpegImage(StorageConfig storageConfig) + { + storageConfig.Apply(this); + + var filePath = Path.Combine(FolderPath, "test.pdf"); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_gray_8bit) + .PerformTransform(new GrayscaleTransform())); + + await _exporter.Export(filePath, [image]); + + PdfAsserts.AssertImages(filePath, ImageResources.dog_gray); + PdfAsserts.AssertImageFilter(filePath, 0, "DCTDecode"); + } + + [Fact] + public async Task ExportJpegWithoutEncoding() + { + SetUpFileStorage(); + + var filePath = Path.Combine(FolderPath, "test.pdf"); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); + + await _exporter.Export(filePath, [image]); + + var renderer = new PdfiumPdfRenderer(); + var pdfImage = renderer.RenderPage(ImageContext, filePath, PdfRenderSize.Default); + ImageAsserts.Similar(image.Render(), pdfImage, 0); + } + [Theory] [ClassData(typeof(StorageAwareTestData))] public async Task ExportPngImage(StorageConfig storageConfig) @@ -37,15 +100,30 @@ public async Task ExportPngImage(StorageConfig storageConfig) storageConfig.Apply(this); var filePath = Path.Combine(FolderPath, "test.pdf"); - using var image = ScanningContext.CreateProcessedImage( - LoadImage(ImageResources.dog_png), BitDepth.Color, true, -1); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_png), lossless: true); - await _exporter.Export(filePath, new[] { image }, new PdfExportParams()); + await _exporter.Export(filePath, [image]); PdfAsserts.AssertImages(filePath, ImageResources.dog); PdfAsserts.AssertImageFilter(filePath, 0, "FlateDecode"); } + [Theory] + [ClassData(typeof(StorageAwareTestData))] + public async Task ExportUnalignedPngImage(StorageConfig storageConfig) + { + storageConfig.Apply(this); + + var filePath = Path.Combine(FolderPath, "test.pdf"); + // Width is 99 (not divisible by 4) + var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_99w), lossless: true); + + await _exporter.Export(filePath, [image]); + + PdfAsserts.AssertImages(filePath, ImageResources.dog_99w); + PdfAsserts.AssertImageFilter(filePath, 0, "FlateDecode"); + } + [Theory] [ClassData(typeof(StorageAwareTestData))] public async Task ExportAlphaImage(StorageConfig storageConfig) @@ -53,10 +131,9 @@ public async Task ExportAlphaImage(StorageConfig storageConfig) storageConfig.Apply(this); var filePath = Path.Combine(FolderPath, "test.pdf"); - using var image = ScanningContext.CreateProcessedImage( - LoadImage(ImageResources.dog_alpha), BitDepth.Color, false, -1); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_alpha)); - await _exporter.Export(filePath, new[] { image }, new PdfExportParams()); + await _exporter.Export(filePath, [image]); // TODO: This assert is broken as pdfium rendering doesn't work for images with masks yet // PdfAsserts.AssertImages(filePath, ImageResources.dog_alpha); @@ -70,10 +147,9 @@ public async Task ExportMaskedImage(StorageConfig storageConfig) storageConfig.Apply(this); var filePath = Path.Combine(FolderPath, "test.pdf"); - using var image = ScanningContext.CreateProcessedImage( - LoadImage(ImageResources.dog_mask), BitDepth.Color, false, -1); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_mask)); - await _exporter.Export(filePath, new[] { image }, new PdfExportParams()); + await _exporter.Export(filePath, [image]); // TODO: This assert is broken as pdfium rendering doesn't work for images with masks yet // PdfAsserts.AssertImages(filePath, ImageResources.dog_alpha); @@ -90,12 +166,30 @@ public async Task ExportBlackAndWhiteImage(StorageConfig storageConfig) var storageImage = LoadImage(ImageResources.dog_bw); using var image = ScanningContext.CreateProcessedImage(storageImage); - await _exporter.Export(filePath, new[] { image }, new PdfExportParams()); + await _exporter.Export(filePath, [image]); PdfAsserts.AssertImages(filePath, ImageResources.dog_bw); PdfAsserts.AssertImageFilter(filePath, 0, "CCITTFaxDecode"); } + [Theory] + [ClassData(typeof(StorageAwareTestData))] + public async Task ExportBlackAndWhiteAlternatingPixels(StorageConfig storageConfig) + { + storageConfig.Apply(this); + + var filePath = Path.Combine(FolderPath, "test.pdf"); + var storageImage = LoadImage(ImageResources.bw_alternating); + using var image = ScanningContext.CreateProcessedImage(storageImage); + + await _exporter.Export(filePath, [image]); + + PdfAsserts.AssertImages(filePath, ImageResources.bw_alternating); + // Alternating black & white pixels is the worst case for CCITT encoding (i.e. the encoded size is bigger than + // unencoded), therefore PDFSharp should choose FlateDecode (PNG encoding) instead + PdfAsserts.AssertImageFilter(filePath, 0, "FlateDecode"); + } + [Theory] [ClassData(typeof(StorageAwareTestData))] public async Task ExportGrayImage(StorageConfig storageConfig) @@ -106,7 +200,7 @@ public async Task ExportGrayImage(StorageConfig storageConfig) var storageImage = LoadImage(ImageResources.dog_clustered_gray); using var image = ScanningContext.CreateProcessedImage(storageImage); - await _exporter.Export(filePath, new[] { image }, new PdfExportParams()); + await _exporter.Export(filePath, [image]); PdfAsserts.AssertImages(filePath, ImageResources.dog_clustered_gray); PdfAsserts.AssertImageFilter(filePath, 0, "FlateDecode"); @@ -120,9 +214,9 @@ public async Task ExportBlackAndWhiteImageByMetadata(StorageConfig storageConfig var filePath = Path.Combine(FolderPath, "test.pdf"); using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog_bw_24bit), - BitDepth.BlackAndWhite, true, -1); + true, -1, null); - await _exporter.Export(filePath, new[] { image }, new PdfExportParams()); + await _exporter.Export(filePath, [image]); PdfAsserts.AssertImages(filePath, ImageResources.dog_bw); PdfAsserts.AssertImageFilter(filePath, 0, "CCITTFaxDecode"); @@ -144,7 +238,7 @@ public async Task ExportMetadata(StorageConfig storageConfig) Subject = "subject" }; - await _exporter.Export(filePath, new[] { image }, new PdfExportParams { Metadata = metadata }); + await _exporter.Export(filePath, [image], new PdfExportParams { Metadata = metadata }); PdfAsserts.AssertMetadata(metadata with { Creator = "NAPS2" }, filePath, "world"); // TODO: We should also test embedded dates etc. somewhere, no tests for that yet @@ -166,12 +260,26 @@ public async Task ExportUnicodeMetadata(StorageConfig storageConfig) Subject = "נושא" }; - await _exporter.Export(filePath, new[] { image }, new PdfExportParams { Metadata = metadata }); + await _exporter.Export(filePath, [image], new PdfExportParams { Metadata = metadata }); PdfAsserts.AssertMetadata(metadata with { Creator = "NAPS2" }, filePath, "world"); // TODO: We should also test embedded dates etc. somewhere, no tests for that yet } + [Theory] + [ClassData(typeof(StorageAwareTestData))] + public async Task ExportUnicodePath(StorageConfig storageConfig) + { + storageConfig.Apply(this); + + var filePath = Path.Combine(FolderPath, "מְחַבֵּר.pdf"); + using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); + + await _exporter.Export(filePath, [image]); + + PdfAsserts.AssertImages(filePath, ImageResources.dog); + } + [Theory] [ClassData(typeof(StorageAwareTestData))] public async Task ExportEncrypted(StorageConfig storageConfig) @@ -181,7 +289,7 @@ public async Task ExportEncrypted(StorageConfig storageConfig) var filePath = Path.Combine(FolderPath, "test.pdf"); using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); - await _exporter.Export(filePath, new[] { image }, new PdfExportParams + await _exporter.Export(filePath, [image], new PdfExportParams { Encryption = new() { @@ -210,7 +318,7 @@ public async Task ExportMetadataEncrypted(StorageConfig storageConfig) Subject = "subject" }; - await _exporter.Export(filePath, new[] { image }, new PdfExportParams + await _exporter.Export(filePath, [image], new PdfExportParams { Encryption = new() { @@ -241,7 +349,7 @@ public async Task ExportUnicodeMetadataEncrypted(StorageConfig storageConfig) Subject = "נושא" }; - await _exporter.Export(filePath, new[] { image }, new PdfExportParams + await _exporter.Export(filePath, [image], new PdfExportParams { Encryption = new() { @@ -261,20 +369,21 @@ public async Task ExportUnicodeMetadataEncrypted(StorageConfig storageConfig) public async Task ExportProgress(OcrTestConfig config) { config.StorageConfig.Apply(this); + SetUpFakeOcr(); - var progressMock = new Mock(); + var progressMock = Substitute.For(); var filePath = Path.Combine(FolderPath, "test.pdf"); var images = new[] { CreateScannedImage(), CreateScannedImage(), CreateScannedImage() }; - var result = await _exporter.Export(filePath, images, new PdfExportParams(), config.OcrParams, - progress: progressMock.Object); + var result = await _exporter.Export(filePath, images, ocrParams: config.OcrParams, + progress: progressMock); Assert.True(result); - progressMock.Verify(x => x(0, 3)); - progressMock.Verify(x => x(1, 3)); - progressMock.Verify(x => x(2, 3)); - progressMock.Verify(x => x(3, 3)); - progressMock.VerifyNoOtherCalls(); + progressMock.Received()(0, 3); + progressMock.Received()(1, 3); + progressMock.Received()(2, 3); + progressMock.Received()(3, 3); + progressMock.ReceivedCallsCount(4); } [Theory] @@ -282,24 +391,59 @@ public async Task ExportProgress(OcrTestConfig config) public async Task ExportCancellation(OcrTestConfig config) { config.StorageConfig.Apply(this); + SetUpFakeOcr(); - var progressMock = new Mock(); + var progressMock = Substitute.For(); var cts = new CancellationTokenSource(); void Progress(int current, int total) { - progressMock.Object(current, total); + progressMock(current, total); if (current == 1) cts.Cancel(); } var filePath = Path.Combine(FolderPath, "test.pdf"); var images = new[] { CreateScannedImage(), CreateScannedImage(), CreateScannedImage() }; - var result = await _exporter.Export(filePath, images, new PdfExportParams(), config.OcrParams, + var result = await _exporter.Export(filePath, images, ocrParams: config.OcrParams, progress: new ProgressHandler(Progress, cts.Token)); Assert.False(result); - progressMock.Verify(x => x(0, 3)); - progressMock.Verify(x => x(1, 3)); - progressMock.VerifyNoOtherCalls(); + progressMock.Received()(0, 3); + progressMock.Received()(1, 3); + progressMock.ReceivedCallsCount(2); + } + + [Theory] + [ClassData(typeof(StorageAwareTestData))] + public async Task ExportWithPageSize(StorageConfig storageConfig) + { + storageConfig.Apply(this); + + var filePath = Path.Combine(FolderPath, "test.pdf"); + var sourceImage = ImageContext.Create(850, 1100, ImagePixelFormat.RGB24); + sourceImage.SetResolution(99.4f, 99.4f); + using var image = ScanningContext.CreateProcessedImage(sourceImage, false, -1, PageSize.Letter); + + await _exporter.Export(filePath, [image]); + + // If the resolution is close to the actual page size, we should correct to that page size with high precision + PdfAsserts.AssertPageSize(PageSize.Letter, 3, filePath); + } + + [Theory] + [ClassData(typeof(StorageAwareTestData))] + public async Task ExportWithMismatchedPageSize(StorageConfig storageConfig) + { + storageConfig.Apply(this); + + var filePath = Path.Combine(FolderPath, "test.pdf"); + var sourceImage = ImageContext.Create(850, 1100, ImagePixelFormat.RGB24); + sourceImage.SetResolution(98, 98); + using var image = ScanningContext.CreateProcessedImage(sourceImage, false, -1, PageSize.Letter); + + await _exporter.Export(filePath, [image]); + + // If the page size is too far off, we should ignore it and go by the actual resolution (precision is less important here) + PdfAsserts.AssertPageSize(new PageSize(8.7m, 11.2m, PageSizeUnit.Inch), 1, filePath); } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Pdf/PdfFontTests.cs b/NAPS2.Sdk.Tests/Pdf/PdfFontTests.cs new file mode 100644 index 0000000000..a5d9d91459 --- /dev/null +++ b/NAPS2.Sdk.Tests/Pdf/PdfFontTests.cs @@ -0,0 +1,118 @@ +using Microsoft.Extensions.Logging; +using NAPS2.Ocr; +using NAPS2.Pdf; +using NAPS2.Sdk.Tests.Asserts; +using PdfSharpCore.Utils; +using Xunit; +using Xunit.Abstractions; +using Alphabet = NAPS2.Pdf.PdfFontPicker.Alphabet; + +namespace NAPS2.Sdk.Tests.Pdf; + +// As we use the same data for multiple methods, some parameters may be unused +#pragma warning disable xUnit1026 + +public class PdfFontTests : ContextualTests +{ + private readonly PdfImporter _importer; + private readonly PdfExporter _exporter; + private readonly string _exportPath; + private readonly string _pdfiumImportPath; + + public PdfFontTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + _importer = new PdfImporter(ScanningContext); + _exporter = new PdfExporter(ScanningContext); + _exportPath = Path.Combine(FolderPath, "test.pdf"); + _pdfiumImportPath = Path.Combine(FolderPath, "import_ocr.pdf"); + File.WriteAllBytes(_pdfiumImportPath, PdfResources.word_ocr_test); + } + + [Fact] + public void CheckAvailableFonts() + { + foreach (var font in FontResolver.InstalledFonts.OrderBy(x => x.Key)) + { + ScanningContext.Logger.LogDebug($"Font: {font.Key}"); + } + } + + [Theory] + [MemberData(nameof(AlphabetTestCases))] + internal void MapLanguageCodeToAlphabet(Alphabet alphabet, string langCode, string text, bool rtl) + { + Assert.Equal(alphabet, PdfFontPicker.MapLanguageCodeToAlphabet(langCode)); + } + + [Theory] + [MemberData(nameof(AlphabetTestCases))] + internal async Task ExportAlphabetsWithPdfSharp(Alphabet alphabet, string langCode, string text, bool rtl) + { + SetUpFakeOcr(ifNoMatch: text, delay: 0); + + using var image = CreateScannedImage(); + await _exporter.Export(_exportPath, [image], ocrParams: new OcrParams(langCode)); + + if (rtl) + { + text = new string(text.Reverse().ToArray()); + } + PdfAsserts.AssertContainsTextOnce(text, _exportPath); + // Rough verification that a font subset is used instead of embedding the whole font + Assert.InRange(new FileInfo(_exportPath).Length, 1, 500_000); + } + + [Theory] + [MemberData(nameof(AlphabetTestCases))] + internal async Task ExportAlphabetsWithPdfium(Alphabet alphabet, string langCode, string text, bool rtl) + { + SetUpFakeOcr(ifNoMatch: text, delay: 0); + + var images = await _importer.Import(_pdfiumImportPath).ToListAsync(); + await _exporter.Export(_exportPath, images, ocrParams: new OcrParams(langCode)); + + if (rtl) + { + text = new string(text.Reverse().ToArray()); + } + PdfAsserts.AssertContainsTextOnce(text, _exportPath); + // Rough verification that a font subset is used instead of embedding the whole font + // TODO: It seems like Pdfium fonts are bigger than PdfSharp - maybe not compressed? Can we improve that? + Assert.InRange(new FileInfo(_exportPath).Length, 1, 700_000); + } + + public static IEnumerable AlphabetTestCases = + [ + new object[] { Alphabet.Latin, "eng", "Hello world", false }, + new object[] { Alphabet.Cyrillic, "rus", "Привет, мир", false }, + new object[] { Alphabet.Greek, "ell", "Γειά σου Κόσμε", false }, + new object[] { Alphabet.Hebrew, "heb", "שלום עולם", true }, + new object[] { Alphabet.Arabic, "ara", "مرحبا بالعالم", true }, + new object[] { Alphabet.Armenian, "hye", "Բարեւ աշխարհ", false }, + new object[] { Alphabet.Bengali, "ben", "ওহে বিশ্ব", false }, + new object[] { Alphabet.CanadianAboriginal, "iku", "ᐃᓄᒃᑎᑐᑦ", false }, + new object[] { Alphabet.Cherokee, "chr", "ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ", false }, + new object[] { Alphabet.Devanagari, "hin", "ह\u0948ल\u094b वर\u094dल\u094dड", false }, + new object[] { Alphabet.Ethiopic, "amh", "ሰላም ልዑል", false }, + new object[] { Alphabet.Georgian, "kat", "Გამარჯობა მსოფლიო", false }, + new object[] { Alphabet.Gujarati, "guj", "હ\u0ac7લ\u0acb વર\u0acdલ\u0acdડ", false }, + new object[] { Alphabet.Gurmukhi, "pan", "ਸਤ\u0a3f ਸ\u0a4dਰ\u0a40 ਅਕ\u0a3eਲ ਦ\u0a41ਨ\u0a3fਆ", false }, + new object[] { Alphabet.Kannada, "kan", "ಹಲ\u0ccbವರ\u0ccdಲ\u0ccdಡ\u0ccd", false }, + new object[] { Alphabet.Khmer, "khm", "ស\u17bdស\u17d2ត\u17b8\u200bព\u17b7ភពល\u17c4ក", false }, + new object[] { Alphabet.Lao, "lao", "ສະ\u200bບາຍ\u200bດ\u0eb5\u200bຊາວ\u200bໂລກ", false }, + new object[] { Alphabet.Malayalam, "mal", "ഹല\u0d47\u0d3e വ\u0d47ൾഡ\u0d4d", false }, + new object[] { Alphabet.Myanmar, "mya", "မင\u103a\u1039ဂလ\u102cပ\u102bကမ\u1039ဘ\u102cလ\u1031\u102cက", false }, + new object[] { Alphabet.Sinhala, "sin", "හ\u0dd9ල\u0dddවර\u0dcaල\u0dcaඩ\u0dca", false }, + // Not running by default as it requires a supplemental font on Windows + // new object[] { Alphabet.Syriac, "syr", "ܐܘ ܢ\u0733ܫܐ ܟ\u0737ܬܠ\u0736ܗ", true }, + new object[] { Alphabet.Tamil, "tam", "வணக\u0bcdகம\u0bcdஉலகம\u0bcd", false }, + new object[] { Alphabet.Telugu, "tel", "హల\u0c4bవరల\u0c4dడ\u0c4d", false }, + new object[] { Alphabet.Thaana, "div", "ހ\u07acލ\u07afދ\u07aaނ\u07a8ޔ\u07ac", true }, + new object[] { Alphabet.Thai, "tha", "สว\u0e31สด\u0e35ชาวโลก", false }, + new object[] { Alphabet.Tibetan, "bod", "བ\u0f7cད་ས\u0f90ད་", false }, + new object[] { Alphabet.ChineseSimplified, "chi_sim", "你好复杂的世界", false }, + new object[] { Alphabet.ChineseTraditional, "chi_tra", "你好複雜的世界", false }, + new object[] { Alphabet.Japanese, "jpn", "こんにちは世界", false }, + new object[] { Alphabet.Korean, "kor", "안녕하세요 세상", false }, + ]; +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfImportExportTests.cs b/NAPS2.Sdk.Tests/Pdf/PdfImportExportTests.cs similarity index 57% rename from NAPS2.Sdk.Tests/ImportExport/Pdf/PdfImportExportTests.cs rename to NAPS2.Sdk.Tests/Pdf/PdfImportExportTests.cs index ca04696f60..a2465ce691 100644 --- a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfImportExportTests.cs +++ b/NAPS2.Sdk.Tests/Pdf/PdfImportExportTests.cs @@ -1,9 +1,10 @@ -using NAPS2.ImportExport.Pdf; -using NAPS2.Ocr; +using NAPS2.ImportExport; +using NAPS2.Pdf; using NAPS2.Sdk.Tests.Asserts; using Xunit; +using Xunit.Abstractions; -namespace NAPS2.Sdk.Tests.ImportExport.Pdf; +namespace NAPS2.Sdk.Tests.Pdf; public class PdfImportExportTests : ContextualTests { @@ -12,7 +13,8 @@ public class PdfImportExportTests : ContextualTests private readonly string _importPath; private readonly string _exportPath; - public PdfImportExportTests() + public PdfImportExportTests(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) { _importer = new PdfImporter(ScanningContext); _exporter = new PdfExporter(ScanningContext); @@ -25,19 +27,52 @@ public PdfImportExportTests() public async Task ImportExport(OcrTestConfig config) { config.StorageConfig.Apply(this); + SetUpFakeOcr(); var images = await _importer.Import(_importPath).ToListAsync(); Assert.Equal(2, images.Count); - await _exporter.Export(_exportPath, images, new PdfExportParams(), config.OcrParams); + await _exporter.Export(_exportPath, images, ocrParams: config.OcrParams); PdfAsserts.AssertImages(_exportPath, PdfResources.word_p1, PdfResources.word_p2); } + [Theory] + [ClassData(typeof(OcrTestData))] + public async Task ImportExportToStream(OcrTestConfig config) + { + config.StorageConfig.Apply(this); + SetUpFakeOcr(); + + var fileStream = File.OpenWrite(_exportPath); + var images = await _importer.Import(_importPath).ToListAsync(); + Assert.Equal(2, images.Count); + await _exporter.Export(fileStream, images, ocrParams: config.OcrParams); + fileStream.Close(); + + PdfAsserts.AssertImages(_exportPath, PdfResources.word_p1, PdfResources.word_p2); + } + + [Theory] + [ClassData(typeof(OcrTestData))] + public async Task ImportExportToNonexistentFolder(OcrTestConfig config) + { + config.StorageConfig.Apply(this); + SetUpFakeOcr(); + + var exportPath = Path.Combine(FolderPath, "blah", "export.pdf"); + var images = await _importer.Import(_importPath).ToListAsync(); + Assert.Equal(2, images.Count); + await _exporter.Export(exportPath, images, ocrParams: config.OcrParams); + + PdfAsserts.AssertImages(exportPath, PdfResources.word_p1, PdfResources.word_p2); + } + [Theory] [ClassData(typeof(OcrTestData))] public async Task ImportInsertExport(OcrTestConfig config) { config.StorageConfig.Apply(this); + SetUpFakeOcr(); var images = await _importer.Import(_importPath).ToListAsync(); Assert.Equal(2, images.Count); @@ -49,7 +84,7 @@ public async Task ImportInsertExport(OcrTestConfig config) toInsert, images[1] }; - await _exporter.Export(_exportPath, newImages, new PdfExportParams(), config.OcrParams); + await _exporter.Export(_exportPath, newImages, ocrParams: config.OcrParams); PdfAsserts.AssertImages(_exportPath, PdfResources.word_p1, ImageResources.dog, PdfResources.word_p2); } @@ -59,6 +94,7 @@ public async Task ImportInsertExport(OcrTestConfig config) public async Task ImportTransformExport(OcrTestConfig config) { config.StorageConfig.Apply(this); + SetUpFakeOcr(); var images = await _importer.Import(_importPath).ToListAsync(); Assert.Equal(2, images.Count); @@ -71,7 +107,7 @@ public async Task ImportTransformExport(OcrTestConfig config) ImageAsserts.Similar(PdfResources.word_p1_rotated, newImages[0], ignoreResolution: true); ImageAsserts.Similar(PdfResources.word_p2_bw, newImages[1], ignoreResolution: true); - await _exporter.Export(_exportPath, newImages, new PdfExportParams(), config.OcrParams); + await _exporter.Export(_exportPath, newImages, ocrParams: config.OcrParams); PdfAsserts.AssertImages(_exportPath, PdfResources.word_p1_rotated, PdfResources.word_p2_bw); } @@ -80,7 +116,12 @@ public async Task ImportTransformExport(OcrTestConfig config) public async Task ImportExportOcrablePdf(OcrTestConfig config) { config.StorageConfig.Apply(this); - SetUpOcr(); + SetUpFakeOcr(new() + { + { LoadImage(PdfResources.word_p1), "Page one."}, + { LoadImage(PdfResources.word_p2), "Page two."}, + { LoadImage(PdfResources.word_patcht_p1), "Sized for printing unscaled"} + }); var importPathForOcr = Path.Combine(FolderPath, "import_ocr.pdf"); File.WriteAllBytes(importPathForOcr, PdfResources.word_patcht_pdf); @@ -93,7 +134,7 @@ public async Task ImportExportOcrablePdf(OcrTestConfig config) var allImages = images.Concat(imagesForOcr).ToList(); - await _exporter.Export(_exportPath, allImages, new PdfExportParams(), config.OcrParams); + await _exporter.Export(_exportPath, allImages, ocrParams: config.OcrParams); PdfAsserts.AssertImages(_exportPath, PdfResources.word_p1, PdfResources.word_p2, PdfResources.word_patcht_p1); PdfAsserts.AssertContainsTextOnce("Page one.", _exportPath); if (config.OcrParams != null) @@ -106,11 +147,30 @@ public async Task ImportExportOcrablePdf(OcrTestConfig config) } } + [Theory] + [ClassData(typeof(OcrTestData))] + public async Task ImportExportPdfWithOcrText(OcrTestConfig config) + { + config.StorageConfig.Apply(this); + SetUpFakeOcr(new() + { + { LoadImage(ImageResources.ocr_test), "ADVERTISEMENT."} + }); + + File.WriteAllBytes(_importPath, PdfResources.ocr_test_output); + + var images = await _importer.Import(_importPath).ToListAsync(); + await _exporter.Export(_exportPath, images, ocrParams: config.OcrParams); + + PdfAsserts.AssertContainsTextOnce("ADVERTISEMENT.", _exportPath); + } + [Theory] [ClassData(typeof(OcrTestData))] public async Task ImportExportEncrypted(OcrTestConfig config) { config.StorageConfig.Apply(this); + SetUpFakeOcr(); var images = await _importer.Import(_importPath).ToListAsync(); Assert.Equal(2, images.Count); @@ -146,8 +206,15 @@ public async Task ImportVariousAndExport(OcrTestConfig config) images.Add(ScanningContext.CreateProcessedImage(LoadImage(ImageResources.ocr_test))); Assert.Equal(5, images.Count); - SetUpOcr(); - await _exporter.Export(_exportPath, images, new PdfExportParams(), config.OcrParams); + SetUpFakeOcr(new() + { + { LoadImage(PdfResources.word_p1), "Page one."}, + { LoadImage(PdfResources.word_p2), "Page two."}, + { LoadImage(PdfResources.word_patcht_p1), "Sized for printing unscaled"}, + { LoadImage(ImageResources.dog), ""}, + { LoadImage(ImageResources.ocr_test), "ADVERTISEMENT."}, + }); + await _exporter.Export(_exportPath, images, ocrParams: config.OcrParams); PdfAsserts.AssertImages(_exportPath, PdfResources.word_p1, @@ -169,4 +236,36 @@ public async Task ImportVariousAndExport(OcrTestConfig config) PdfAsserts.AssertDoesNotContainText("Sized for printing unscaled", _exportPath); } } + + [Fact] + public async Task ImportJpegExportWithoutEncoding() + { + SetUpFileStorage(); + + var path = CopyResourceToFile(ImageResources.dog, "image.jpg"); + var images = await new ImageImporter(ScanningContext).Import(path).ToListAsync(); + Assert.Single(images); + + await _exporter.Export(_exportPath, images); + + var renderer = new PdfiumPdfRenderer(); + var pdfImage = renderer.RenderPage(ImageContext, _exportPath, PdfRenderSize.Default); + ImageAsserts.Similar(ImageResources.dog, pdfImage, 0); + } + + [Fact] + public async Task ImportExifJpegExportWithoutEncoding() + { + SetUpFileStorage(); + + var path = CopyResourceToFile(ImageResources.dog_exif, "image.jpg"); + var images = await new ImageImporter(ScanningContext).Import(path).ToListAsync(); + Assert.Single(images); + + await _exporter.Export(_exportPath, images); + + var renderer = new PdfiumPdfRenderer(); + var pdfImage = renderer.RenderPage(ImageContext, _exportPath, PdfRenderSize.Default); + ImageAsserts.Similar(ImageResources.dog_exif, pdfImage, 0, ignoreResolution: true); + } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfImportTests.cs b/NAPS2.Sdk.Tests/Pdf/PdfImportTests.cs similarity index 68% rename from NAPS2.Sdk.Tests/ImportExport/Pdf/PdfImportTests.cs rename to NAPS2.Sdk.Tests/Pdf/PdfImportTests.cs index a43c009420..67cedfd8e9 100644 --- a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfImportTests.cs +++ b/NAPS2.Sdk.Tests/Pdf/PdfImportTests.cs @@ -1,11 +1,11 @@ -using Moq; using NAPS2.ImportExport; -using NAPS2.ImportExport.Pdf; -using NAPS2.ImportExport.Pdf.Pdfium; +using NAPS2.Pdf; +using NAPS2.Pdf.Pdfium; using NAPS2.Sdk.Tests.Asserts; +using NSubstitute; using Xunit; -namespace NAPS2.Sdk.Tests.ImportExport.Pdf; +namespace NAPS2.Sdk.Tests.Pdf; // TODO: MemoryStorage tests are a lot slower than FileStorage, why? // TODO: Add an import test for 1bit png (not ccitt) @@ -35,6 +35,23 @@ public async Task ImportNonNaps2Pdf(StorageConfig storageConfig) ImageAsserts.Similar(PdfResources.word_p2, images[1], ignoreResolution: true); } + [Theory] + [ClassData(typeof(StorageAwareTestData))] + public async Task ImportNonNaps2PdfFromStream(StorageConfig storageConfig) + { + storageConfig.Apply(this); + + var importStream = new MemoryStream(PdfResources.word_generated_pdf); + var images = await _importer.Import(importStream).ToListAsync(); + + Assert.Equal(2, images.Count); + storageConfig.AssertPdfStorage(images[0].Storage); + storageConfig.AssertPdfStorage(images[1].Storage); + // TODO: Why is the expected resolution weird? + ImageAsserts.Similar(PdfResources.word_p1, images[0], ignoreResolution: true); + ImageAsserts.Similar(PdfResources.word_p2, images[1], ignoreResolution: true); + } + [Theory] [ClassData(typeof(StorageAwareTestData))] public async Task ImportNaps2Pdf(StorageConfig storageConfig) @@ -49,6 +66,34 @@ public async Task ImportNaps2Pdf(StorageConfig storageConfig) ImageAsserts.Similar(ImageResources.dog, images[0]); } + [Theory] + [ClassData(typeof(StorageAwareTestData))] + public async Task ImportNaps2PdfFromStream(StorageConfig storageConfig) + { + storageConfig.Apply(this); + + var importStream = new MemoryStream(PdfResources.image_pdf); + var images = await _importer.Import(importStream).ToListAsync(); + + Assert.Single(images); + storageConfig.AssertJpegStorage(images[0].Storage); + ImageAsserts.Similar(ImageResources.dog, images[0]); + } + + [Theory] + [ClassData(typeof(StorageAwareTestData))] + public async Task ImportNaps2PdfWithUnicodePath(StorageConfig storageConfig) + { + storageConfig.Apply(this); + + var importPath = CopyResourceToFile(PdfResources.image_pdf, "מְחַבֵּר.pdf"); + var images = await _importer.Import(importPath).ToListAsync(); + + Assert.Single(images); + storageConfig.AssertJpegStorage(images[0].Storage); + ImageAsserts.Similar(ImageResources.dog, images[0]); + } + [Theory] [ClassData(typeof(StorageAwareTestData))] public async Task ImportNaps2PngPdf(StorageConfig storageConfig) @@ -96,10 +141,13 @@ public async Task ImportEncryptedWithPasswordProvider(StorageConfig storageConfi { storageConfig.Apply(this); - var passwordProvider = new Mock(); - var password = "hello"; - passwordProvider.Setup(x => x.ProvidePassword(It.IsAny(), It.IsAny(), out password)).Returns(true); - var importer = new PdfImporter(ScanningContext, passwordProvider.Object); + var passwordProvider = Substitute.For(); + passwordProvider.ProvidePassword(Arg.Any(), Arg.Any(), out Arg.Any()).Returns(x => + { + x[2] = "hello"; + return true; + }); + var importer = new PdfImporter(ScanningContext, passwordProvider); var importPath = CopyResourceToFile(PdfResources.encrypted_pdf, "import.pdf"); var images = await importer.Import(importPath).ToListAsync(); diff --git a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfiumPdfExporterTests.cs b/NAPS2.Sdk.Tests/Pdf/PdfiumPdfExporterTests.cs similarity index 74% rename from NAPS2.Sdk.Tests/ImportExport/Pdf/PdfiumPdfExporterTests.cs rename to NAPS2.Sdk.Tests/Pdf/PdfiumPdfExporterTests.cs index 4bd11bda98..3ddf05dc7c 100644 --- a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfiumPdfExporterTests.cs +++ b/NAPS2.Sdk.Tests/Pdf/PdfiumPdfExporterTests.cs @@ -1,8 +1,8 @@ -using NAPS2.ImportExport.Pdf; +using NAPS2.Pdf; using NAPS2.Sdk.Tests.Asserts; using Xunit; -namespace NAPS2.Sdk.Tests.ImportExport.Pdf; +namespace NAPS2.Sdk.Tests.Pdf; public class PdfiumPdfExporterTests : ContextualTests { @@ -13,7 +13,7 @@ public async Task ExportSingleImage() using var image = ScanningContext.CreateProcessedImage(LoadImage(ImageResources.dog)); var pdfExporter = new PdfiumPdfExporter(ScanningContext); - await pdfExporter.Export(filePath, new[] { image }, new PdfExportParams()); + await pdfExporter.Export(filePath, new[] { image }); PdfAsserts.AssertImages(filePath, ImageResources.dog); } diff --git a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfiumPdfRendererTests.cs b/NAPS2.Sdk.Tests/Pdf/PdfiumPdfRendererTests.cs similarity index 52% rename from NAPS2.Sdk.Tests/ImportExport/Pdf/PdfiumPdfRendererTests.cs rename to NAPS2.Sdk.Tests/Pdf/PdfiumPdfRendererTests.cs index 7961e23dc2..4e7f8e5823 100644 --- a/NAPS2.Sdk.Tests/ImportExport/Pdf/PdfiumPdfRendererTests.cs +++ b/NAPS2.Sdk.Tests/Pdf/PdfiumPdfRendererTests.cs @@ -1,8 +1,8 @@ -using NAPS2.ImportExport.Pdf; +using NAPS2.Pdf; using NAPS2.Sdk.Tests.Asserts; using Xunit; -namespace NAPS2.Sdk.Tests.ImportExport.Pdf; +namespace NAPS2.Sdk.Tests.Pdf; public class PdfiumPdfRendererTests : ContextualTests { @@ -41,4 +41,39 @@ public void RenderImageWithTextPdf() // This also verifies that the renderer gets the actual image dpi (72) ImageAsserts.Similar(ImageResources.ocr_test, images[0], ignoreResolution: true); } + + [Fact] + public void RenderCmykImagePdf() + { + var path = CopyResourceToFile(PdfResources.image_pdf_cmyk, "test.pdf"); + + var images = new PdfiumPdfRenderer().Render(ImageContext, path, PdfRenderSize.Default).ToList(); + + Assert.Single(images); + // This also verifies that the renderer gets the actual image dpi (72) + ImageAsserts.Similar(ImageResources.dog, images[0]); + } + + [Fact] + public void RenderBlackAndWhiteImagePdf() + { + var path = CopyResourceToFile(PdfResources.image_pdf_bw, "test.pdf"); + + var images = new PdfiumPdfRenderer().Render(ImageContext, path, PdfRenderSize.Default).ToList(); + + Assert.Single(images); + // This also verifies that the renderer gets the actual image dpi (72) + ImageAsserts.Similar(ImageResources.dog_bw, images[0]); + } + + [Fact] + public void RenderFormsAndAnnotations() + { + var path = CopyResourceToFile(PdfResources.filled_form_annotated, "test.pdf"); + + var images = new PdfiumPdfRenderer().Render(ImageContext, path, PdfRenderSize.Default).ToList(); + + Assert.Single(images); + ImageAsserts.Similar(ImageResources.filled_form_annotated, images[0], ImageAsserts.XPLAT_RMSE_THRESHOLD); + } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/PdfResources.Designer.cs b/NAPS2.Sdk.Tests/PdfResources.Designer.cs index b096faa607..4d5c42c128 100644 --- a/NAPS2.Sdk.Tests/PdfResources.Designer.cs +++ b/NAPS2.Sdk.Tests/PdfResources.Designer.cs @@ -11,32 +11,46 @@ namespace NAPS2.Sdk.Tests { using System; - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [System.Diagnostics.DebuggerNonUserCodeAttribute()] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class PdfResources { - private static System.Resources.ResourceManager resourceMan; + private static global::System.Resources.ResourceManager resourceMan; - private static System.Globalization.CultureInfo resourceCulture; + private static global::System.Globalization.CultureInfo resourceCulture; - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal PdfResources() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Resources.ResourceManager ResourceManager { + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { get { - if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("NAPS2.Sdk.Tests.PdfResources", typeof(PdfResources).Assembly); + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NAPS2.Sdk.Tests.PdfResources", typeof(PdfResources).Assembly); resourceMan = temp; } return resourceMan; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Globalization.CultureInfo Culture { + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -45,48 +59,69 @@ internal static System.Globalization.CultureInfo Culture { } } - internal static byte[] word_generated_pdf { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] encrypted_pdf { get { - object obj = ResourceManager.GetObject("word_generated_pdf", resourceCulture); + object obj = ResourceManager.GetObject("encrypted_pdf", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] word_p1 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] filled_form_annotated { get { - object obj = ResourceManager.GetObject("word_p1", resourceCulture); + object obj = ResourceManager.GetObject("filled_form_annotated", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] word_p2 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] image_pdf { get { - object obj = ResourceManager.GetObject("word_p2", resourceCulture); + object obj = ResourceManager.GetObject("image_pdf", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] word_p1_rotated { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] image_pdf_bw { get { - object obj = ResourceManager.GetObject("word_p1_rotated", resourceCulture); + object obj = ResourceManager.GetObject("image_pdf_bw", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] word_p2_bw { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] image_pdf_cmyk { get { - object obj = ResourceManager.GetObject("word_p2_bw", resourceCulture); + object obj = ResourceManager.GetObject("image_pdf_cmyk", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] image_pdf { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] image_pdf_png { get { - object obj = ResourceManager.GetObject("image_pdf", resourceCulture); + object obj = ResourceManager.GetObject("image_pdf_png", resourceCulture); return ((byte[])(obj)); } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// internal static byte[] image_with_text_pdf { get { object obj = ResourceManager.GetObject("image_with_text_pdf", resourceCulture); @@ -94,37 +129,92 @@ internal static byte[] image_with_text_pdf { } } - internal static byte[] image_pdf_bw { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] ocr_test_output { get { - object obj = ResourceManager.GetObject("image_pdf_bw", resourceCulture); + object obj = ResourceManager.GetObject("ocr_test_output", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] word_patcht_pdf { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] word_generated_pdf { get { - object obj = ResourceManager.GetObject("word_patcht_pdf", resourceCulture); + object obj = ResourceManager.GetObject("word_generated_pdf", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] word_patcht_p1 { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] word_ocr_test { get { - object obj = ResourceManager.GetObject("word_patcht_p1", resourceCulture); + object obj = ResourceManager.GetObject("word_ocr_test", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] encrypted_pdf { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] word_p1 { get { - object obj = ResourceManager.GetObject("encrypted_pdf", resourceCulture); + object obj = ResourceManager.GetObject("word_p1", resourceCulture); return ((byte[])(obj)); } } - internal static byte[] image_pdf_png { + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] word_p1_rotated { get { - object obj = ResourceManager.GetObject("image_pdf_png", resourceCulture); + object obj = ResourceManager.GetObject("word_p1_rotated", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] word_p2 { + get { + object obj = ResourceManager.GetObject("word_p2", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] word_p2_bw { + get { + object obj = ResourceManager.GetObject("word_p2_bw", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] word_patcht_p1 { + get { + object obj = ResourceManager.GetObject("word_patcht_p1", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] word_patcht_pdf { + get { + object obj = ResourceManager.GetObject("word_patcht_pdf", resourceCulture); return ((byte[])(obj)); } } diff --git a/NAPS2.Sdk.Tests/PdfResources.resx b/NAPS2.Sdk.Tests/PdfResources.resx index 30a8218ad7..36279b6aaf 100644 --- a/NAPS2.Sdk.Tests/PdfResources.resx +++ b/NAPS2.Sdk.Tests/PdfResources.resx @@ -133,6 +133,9 @@ Resources\word_p2_bw.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Resources\word_ocr_test.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Resources\image_pdf.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -142,6 +145,9 @@ Resources\image_pdf_bw.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Resources\image_pdf_cmyk.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Resources\word_patcht_pdf.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -154,4 +160,10 @@ Resources\image_pdf_png.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Resources\ocr_test_output.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\filled_form_annotated.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/PlatformFactAttribute.cs b/NAPS2.Sdk.Tests/PlatformFactAttribute.cs index 437762a79c..d956851f80 100644 --- a/NAPS2.Sdk.Tests/PlatformFactAttribute.cs +++ b/NAPS2.Sdk.Tests/PlatformFactAttribute.cs @@ -7,39 +7,15 @@ public sealed class PlatformFactAttribute : FactAttribute { public PlatformFactAttribute(PlatformFlags include = PlatformFlags.None, PlatformFlags exclude = PlatformFlags.None) { - if (include != PlatformFlags.None && (GetPlatform() & include) != include) + if (include != PlatformFlags.None && (CurrentPlatformFlags.Get() & include) != include) { - Skip = $"Only runs on platform: {include}"; + Skip = $"Only runs on platform(s): {include}"; } - if (exclude != PlatformFlags.None && (GetPlatform() & exclude) != PlatformFlags.None) + if (exclude != PlatformFlags.None && (CurrentPlatformFlags.Get() & exclude) != PlatformFlags.None) { - Skip = $"Doesn't runs on platform: {exclude}"; + Skip = $"Doesn't run on platform(s): {exclude}"; } } - private PlatformFlags GetPlatform() - { - var p = PlatformFlags.None; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - p |= PlatformFlags.Windows; - } - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - p |= PlatformFlags.Mac; - } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - p |= PlatformFlags.Linux; - } - if (RuntimeInformation.ProcessArchitecture == Architecture.X64) - { - p |= PlatformFlags.X64; - } - if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) - { - p |= PlatformFlags.Arm64; - } - return p; - } + public bool DoSkip => !string.IsNullOrEmpty(Skip); } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/PlatformFlags.cs b/NAPS2.Sdk.Tests/PlatformFlags.cs index f4796cde62..382b11170c 100644 --- a/NAPS2.Sdk.Tests/PlatformFlags.cs +++ b/NAPS2.Sdk.Tests/PlatformFlags.cs @@ -8,5 +8,10 @@ public enum PlatformFlags Mac = 2, Linux = 4, X64 = 8, - Arm64 = 16 + Arm64 = 16, + ImageSharpImage = 32, + WpfImage = 64, + GdiImage = 128, + GtkImage = 256, + MacImage = 512 } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/PlatformTheoryAttribute.cs b/NAPS2.Sdk.Tests/PlatformTheoryAttribute.cs new file mode 100644 index 0000000000..14269af065 --- /dev/null +++ b/NAPS2.Sdk.Tests/PlatformTheoryAttribute.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; +using Xunit; + +namespace NAPS2.Sdk.Tests; + +public sealed class PlatformTheoryAttribute : TheoryAttribute +{ + public PlatformTheoryAttribute(PlatformFlags include = PlatformFlags.None, PlatformFlags exclude = PlatformFlags.None) + { + if (include != PlatformFlags.None && (CurrentPlatformFlags.Get() & include) != include) + { + Skip = $"Only runs on platform(s): {include}"; + } + if (exclude != PlatformFlags.None && (CurrentPlatformFlags.Get() & exclude) != PlatformFlags.None) + { + Skip = $"Doesn't run on platform(s): {exclude}"; + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Remoting/FallbackScanServerTests.cs b/NAPS2.Sdk.Tests/Remoting/FallbackScanServerTests.cs new file mode 100644 index 0000000000..726067f5b6 --- /dev/null +++ b/NAPS2.Sdk.Tests/Remoting/FallbackScanServerTests.cs @@ -0,0 +1,48 @@ +using System.Net.Http; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using NAPS2.Escl; +using NAPS2.Scan; +using NAPS2.Sdk.Tests.Asserts; +using Xunit; +using Xunit.Abstractions; + +namespace NAPS2.Sdk.Tests.Remoting; + +public class FallbackScanServerTests(ITestOutputHelper testOutputHelper) : ScanServerTestsBase(testOutputHelper, + EsclSecurityPolicy.None, X509CertificateLoader.LoadPkcs12(BinaryResources.testcert, null)) +{ + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanFallbackFromHttpsToHttp() + { + _bridge.MockOutput = CreateScannedImages(ImageResources.dog); + var images = await _client.Scan(new ScanOptions + { + Device = _clientDevice, + EsclOptions = + { + // This policy makes sure HTTPS will fail due to an untrusted certificate, which simulates the case + // where we're failing due to the server only supporting obsolete TLS versions. + SecurityPolicy = EsclSecurityPolicy.ClientRequireTrustedCertificate + } + }).ToListAsync(); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanPreventedByTrustedCertificateSecurityPolicy() + { + var scanResult = _client.Scan(new ScanOptions + { + Device = _clientDevice, + EsclOptions = + { + SecurityPolicy = EsclSecurityPolicy.RequireTrustedCertificate + } + }); + var exception = await Assert.ThrowsAsync(async () => await scanResult.ToListAsync()); + Assert.True(exception.InnerException is AuthenticationException || + exception.InnerException?.InnerException is AuthenticationException); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Remoting/ManualIpScanServerTests.cs b/NAPS2.Sdk.Tests/Remoting/ManualIpScanServerTests.cs new file mode 100644 index 0000000000..fe6506a1ea --- /dev/null +++ b/NAPS2.Sdk.Tests/Remoting/ManualIpScanServerTests.cs @@ -0,0 +1,42 @@ +using System.Security.Cryptography.X509Certificates; +using NAPS2.Escl; +using NAPS2.Scan; +using NAPS2.Sdk.Tests.Asserts; +using Xunit; +using Xunit.Abstractions; + +namespace NAPS2.Sdk.Tests.Remoting; + +public class ManualIpScanServerTests(ITestOutputHelper testOutputHelper) : ScanServerTestsBase(testOutputHelper, + EsclSecurityPolicy.None, X509CertificateLoader.LoadPkcs12(BinaryResources.testcert, null)) +{ + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanHttpIpv4() + { + var httpPort = _server.GetDevicePorts(_serverDevice, _serverDisplayName).port; + var httpDevice = new ScanDevice(Driver.Escl, $"http://127.0.0.1:{httpPort}/eSCL", _serverDisplayName); + + _bridge.MockOutput = CreateScannedImages(ImageResources.dog); + var images = await _client.Scan(new ScanOptions + { + Device = httpDevice + }).ToListAsync(); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanHttpsIpv4() + { + var httpsPort = _server.GetDevicePorts(_serverDevice, _serverDisplayName).tlsPort; + var httpsDevice = new ScanDevice(Driver.Escl, $"https://127.0.0.1:{httpsPort}/eSCL", _serverDisplayName); + + _bridge.MockOutput = CreateScannedImages(ImageResources.dog); + var images = await _client.Scan(new ScanOptions + { + Device = httpsDevice + }).ToListAsync(); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Remoting/ScanServerTests.cs b/NAPS2.Sdk.Tests/Remoting/ScanServerTests.cs new file mode 100644 index 0000000000..2b90da7b58 --- /dev/null +++ b/NAPS2.Sdk.Tests/Remoting/ScanServerTests.cs @@ -0,0 +1,272 @@ +using NAPS2.Escl; +using NAPS2.Scan; +using NAPS2.Scan.Exceptions; +using NAPS2.Sdk.Tests.Asserts; +using NSubstitute; +using Xunit; +using Xunit.Abstractions; + +namespace NAPS2.Sdk.Tests.Remoting; + +public class ScanServerTests(ITestOutputHelper testOutputHelper) + : ScanServerTestsBase(testOutputHelper, EsclSecurityPolicy.ServerDisableHttps) +{ + [NetworkFact(Timeout = TIMEOUT)] + public async Task FindDevice() + { + Assert.True(await TryFindClientDevice()); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task Scan() + { + _bridge.MockOutput = CreateScannedImages(ImageResources.dog); + var images = await _client.Scan(new ScanOptions + { + Device = _clientDevice + }).ToListAsync(); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanMultiplePages() + { + _bridge.MockOutput = + CreateScannedImages(ImageResources.dog, ImageResources.dog_h_n300, ImageResources.dog_h_p300); + var images = await _client.Scan(new ScanOptions + { + Device = _clientDevice, + PaperSource = PaperSource.Feeder + }).ToListAsync(); + Assert.Equal(3, images.Count); + ImageAsserts.Similar(ImageResources.dog, images[0]); + ImageAsserts.Similar(ImageResources.dog_h_n300, images[1]); + ImageAsserts.Similar(ImageResources.dog_h_p300, images[2]); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanWithCorrectOptions() + { + _bridge.MockOutput = CreateScannedImages(ImageResources.dog); + var images = await _client.Scan(new ScanOptions + { + Device = _clientDevice, + BitDepth = BitDepth.Color, + Dpi = 100, + PaperSource = PaperSource.Flatbed, + PageSize = PageSize.Letter, + PageAlign = HorizontalAlign.Right + }).ToListAsync(); + + var opts = _bridge.LastOptions; + Assert.Equal(BitDepth.Color, opts.BitDepth); + Assert.Equal(100, opts.Dpi); + Assert.Equal(PaperSource.Flatbed, opts.PaperSource); + Assert.Equal(PageSize.Letter, opts.PageSize); + Assert.Equal(HorizontalAlign.Right, opts.PageAlign); + Assert.Equal(75, opts.Quality); + Assert.False(opts.MaxQuality); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + + _bridge.MockOutput = CreateScannedImages(ImageResources.dog_gray); + images = await _client.Scan(new ScanOptions + { + Device = _clientDevice, + BitDepth = BitDepth.Grayscale, + Dpi = 300, + PaperSource = PaperSource.Feeder, + PageSize = PageSize.Legal, + PageAlign = HorizontalAlign.Center, + Quality = 0, + MaxQuality = true + }).ToListAsync(); + + opts = _bridge.LastOptions; + Assert.Equal(BitDepth.Grayscale, opts.BitDepth); + Assert.Equal(300, opts.Dpi); + Assert.Equal(PaperSource.Feeder, opts.PaperSource); + Assert.Equal(PageSize.Legal, opts.PageSize); + Assert.Equal(HorizontalAlign.Center, opts.PageAlign); + Assert.Equal(0, opts.Quality); + Assert.True(opts.MaxQuality); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog_gray, images[0]); + + _bridge.MockOutput = CreateScannedImages(ImageResources.dog_bw); + images = await _client.Scan(new ScanOptions + { + Device = _clientDevice, + BitDepth = BitDepth.BlackAndWhite, + Dpi = 4800, + PaperSource = PaperSource.Duplex, + PageSize = PageSize.A3, + PageAlign = HorizontalAlign.Left, + Quality = 100 + }).ToListAsync(); + + opts = _bridge.LastOptions; + Assert.Equal(BitDepth.BlackAndWhite, opts.BitDepth); + Assert.Equal(4800, opts.Dpi); + Assert.Equal(PaperSource.Duplex, opts.PaperSource); + Assert.Equal(PageSize.A3.WidthInMm, opts.PageSize!.WidthInMm, 1); + Assert.Equal(PageSize.A3.HeightInMm, opts.PageSize!.HeightInMm, 1); + Assert.Equal(HorizontalAlign.Left, opts.PageAlign); + Assert.Equal(100, opts.Quality); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog_bw, images[0]); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanWithError() + { + _bridge.Error = new DeviceFeederEmptyException(); + + await Assert.ThrowsAsync(async () => await _client.Scan(new ScanOptions + { + Device = _clientDevice, + PaperSource = PaperSource.Feeder + }).ToListAsync()); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanWithErrorAfterPage() + { + _bridge.MockOutput = CreateScannedImages(ImageResources.dog); + _bridge.Error = new DeviceException(SdkResources.DevicePaperJam); + + await using var enumerator = _client.Scan(new ScanOptions + { + Device = _clientDevice, + PaperSource = PaperSource.Feeder + }).GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + ImageAsserts.Similar(ImageResources.dog, enumerator.Current); + + var exception = await Assert.ThrowsAsync(async () => await enumerator.MoveNextAsync()); + Assert.Equal(SdkResources.DevicePaperJam, exception.Message); + } + + [NetworkFact(Timeout = TIMEOUT, Skip = "Flaky")] + public async Task ScanProgress() + { + _bridge.MockOutput = CreateScannedImages(ImageResources.dog, ImageResources.dog); + _bridge.ProgressReports = [0.5]; + + var pageStartMock = Substitute.For>(); + var pageProgressMock = Substitute.For>(); + _client.PageStart += pageStartMock; + _client.PageProgress += pageProgressMock; + + await _client.Scan(new ScanOptions + { + Device = _clientDevice, + PaperSource = PaperSource.Feeder + }).ToListAsync(); + + pageStartMock.Received()(Arg.Any(), Arg.Is(args => args.PageNumber == 1)); + // TODO: This flaked and we only got the second one - why? Can we fix it? + pageProgressMock.Received()(Arg.Any(), + Arg.Is(args => args.PageNumber == 1 && args.Progress == 0.5)); + pageStartMock.Received()(Arg.Any(), Arg.Is(args => args.PageNumber == 2)); + pageProgressMock.Received()(Arg.Any(), + Arg.Is(args => args.PageNumber == 2 && args.Progress == 0.5)); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanPreventedByHttpsSecurityPolicy() + { + var scanResult = _client.Scan(new ScanOptions + { + Device = _clientDevice, + EsclOptions = + { + SecurityPolicy = EsclSecurityPolicy.RequireHttps + } + }); + await Assert.ThrowsAsync(async () => await scanResult.ToListAsync()); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanWithIpInId() + { + _bridge.MockOutput = CreateScannedImages(ImageResources.dog); + await UseServerPort(12145); + + var device = new ScanDevice(Driver.Escl, "http://127.0.0.1:12145/eSCL", _serverDisplayName); + var images = await _client.Scan(new ScanOptions { Device = device }).ToListAsync(); + + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanWithCorrectConnectionUri() + { + _bridge.MockOutput = CreateScannedImages(ImageResources.dog); + var mockHandler = Substitute.For>(); + _client.DeviceUriChanged += mockHandler; + await UseServerPort(12146); + + var device = new ScanDevice(Driver.Escl, "bad_uuid", _serverDisplayName, + ConnectionUri: "http://127.0.0.1:12146/eSCL"); + var images = await _client.Scan(new ScanOptions { Device = device }).ToListAsync(); + + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + mockHandler.DidNotReceive()(Arg.Any(), Arg.Any()); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanWithIncorrectConnectionUri() + { + _bridge.MockOutput = CreateScannedImages(ImageResources.dog); + var mockHandler = Substitute.For>(); + _client.DeviceUriChanged += mockHandler; + await UseServerPort(12147); + + var device = new ScanDevice(Driver.Escl, _uuid, _serverDisplayName, + ConnectionUri: "http://127.0.0.1:31233/eSCL"); + var images = await _client.Scan(new ScanOptions { Device = device }).ToListAsync(); + + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + mockHandler.Received()(Arg.Any(), + Arg.Is(args => args.ConnectionUri.EndsWith(":12147/eSCL"))); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanWithOfflineDevice() + { + var device = new ScanDevice(Driver.Escl, "bad_uuid", _serverDisplayName); + await Assert.ThrowsAsync(async () => await _client.Scan(new ScanOptions + { + Device = device, + EsclOptions = { SearchTimeout = 1000 } + }).ToListAsync()); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanWithOfflineDeviceAndIncorrectConnectionUri() + { + var device = new ScanDevice(Driver.Escl, "bad_uuid", _serverDisplayName, + ConnectionUri: "http://127.0.0.1:31233/eSCL"); + await Assert.ThrowsAsync(async () => await _client.Scan(new ScanOptions + { + Device = device, + EsclOptions = { SearchTimeout = 1000 } + }).ToListAsync()); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanWithOfflineDeviceAndIpInId() + { + var device = new ScanDevice(Driver.Escl, "http://127.0.0.1:31233/eSCL", _serverDisplayName); + await Assert.ThrowsAsync(async () => await _client.Scan(new ScanOptions + { + Device = device + }).ToListAsync()); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Remoting/ScanServerTestsBase.cs b/NAPS2.Sdk.Tests/Remoting/ScanServerTestsBase.cs new file mode 100644 index 0000000000..3b1f86301c --- /dev/null +++ b/NAPS2.Sdk.Tests/Remoting/ScanServerTestsBase.cs @@ -0,0 +1,75 @@ +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using Microsoft.Extensions.Logging; +using NAPS2.Escl; +using NAPS2.Escl.Server; +using NAPS2.Remoting.Server; +using NAPS2.Scan; +using NAPS2.Scan.Internal; +using NAPS2.Sdk.Tests.Mocks; +using NSubstitute; +using Xunit; +using Xunit.Abstractions; + +namespace NAPS2.Sdk.Tests.Remoting; + +public class ScanServerTestsBase : ContextualTests, IAsyncLifetime +{ + protected const int TIMEOUT = 60_000; + + protected readonly ScanServer _server; + protected readonly ScanDevice _serverDevice; + protected readonly string _serverDisplayName; + private protected readonly MockScanBridge _bridge; + protected readonly ScanController _client; + protected readonly string _uuid; + protected readonly ScanDevice _clientDevice; + + public ScanServerTestsBase(ITestOutputHelper testOutputHelper, + EsclSecurityPolicy securityPolicy = EsclSecurityPolicy.None, + X509Certificate2 certificate = null) : base(testOutputHelper) + { + _server = new ScanServer(ScanningContext, new EsclServer()); + + // Set up a server connecting to a mock scan backend + _bridge = new MockScanBridge(); + var scanBridgeFactory = Substitute.For(); + scanBridgeFactory.Create(Arg.Any()).Returns(_bridge); + _server.ScanControllerFactory = () => new ScanController(ScanningContext, scanBridgeFactory); + _server.SecurityPolicy = securityPolicy; + _server.Certificate = certificate; + + // Initialize the server with a single device with a unique ID for the test + _serverDisplayName = $"testName-{Guid.NewGuid()}"; + ScanningContext.Logger.LogDebug("Display name: {Name}", _serverDisplayName); + _serverDevice = new ScanDevice(ScanOptionsValidator.SystemDefaultDriver, "testID", "testName"); + _server.RegisterDevice(_serverDevice, _serverDisplayName); + + // Set up a client ScanController for scanning through EsclScanDriver -> network -> ScanServer + _client = new ScanController(ScanningContext); + _uuid = new ScanServerDevice { Device = _serverDevice, Name = _serverDisplayName }.GetUuid(_server.InstanceId); + _clientDevice = new ScanDevice(Driver.Escl, _uuid, _serverDisplayName); + } + + public Task InitializeAsync() => _server.Start(); + + public Task DisposeAsync() => _server.Stop(); + + protected async Task TryFindClientDevice() + { + var cts = new CancellationTokenSource(); + // The device name is suffixed with the IP so we just check the prefix matches + bool found = await _client.GetDevices(Driver.Escl, cts.Token) + .AnyAsync(device => device.Name.StartsWith(_clientDevice.Name) && device.ID == _clientDevice.ID); + cts.Cancel(); + return found; + } + + protected async Task UseServerPort(int port) + { + _server.UnregisterDevice(_serverDevice, _serverDisplayName); + await _server.Stop(); + _server.RegisterDevice(_serverDevice, _serverDisplayName, port); + await _server.Start(); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Remoting/SelfSignedCertScanServerTests.cs b/NAPS2.Sdk.Tests/Remoting/SelfSignedCertScanServerTests.cs new file mode 100644 index 0000000000..0605b27aa1 --- /dev/null +++ b/NAPS2.Sdk.Tests/Remoting/SelfSignedCertScanServerTests.cs @@ -0,0 +1,51 @@ +using System.Net.Http; +using System.Security.Authentication; +using NAPS2.Escl; +using NAPS2.Scan; +using NAPS2.Sdk.Tests.Asserts; +using Xunit; +using Xunit.Abstractions; + +namespace NAPS2.Sdk.Tests.Remoting; + +public class SelfSignedCertScanServerTests(ITestOutputHelper testOutputHelper) + : ScanServerTestsBase(testOutputHelper, EsclSecurityPolicy.RequireHttps) +{ + [NetworkFact(Timeout = TIMEOUT)] + public async Task FindDevice() + { + Assert.True(await TryFindClientDevice()); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task Scan() + { + _bridge.MockOutput = CreateScannedImages(ImageResources.dog); + var images = await _client.Scan(new ScanOptions + { + Device = _clientDevice, + EsclOptions = + { + SecurityPolicy = EsclSecurityPolicy.RequireHttps + } + }).ToListAsync(); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanPreventedByTrustedCertificateSecurityPolicy() + { + var scanResult = _client.Scan(new ScanOptions + { + Device = _clientDevice, + EsclOptions = + { + SecurityPolicy = EsclSecurityPolicy.RequireTrustedCertificate + } + }); + var exception = await Assert.ThrowsAsync(async () => await scanResult.ToListAsync()); + Assert.True(exception.InnerException is AuthenticationException || + exception.InnerException?.InnerException is AuthenticationException); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Remoting/TlsScanServerTests.cs b/NAPS2.Sdk.Tests/Remoting/TlsScanServerTests.cs new file mode 100644 index 0000000000..c9ace71fc9 --- /dev/null +++ b/NAPS2.Sdk.Tests/Remoting/TlsScanServerTests.cs @@ -0,0 +1,52 @@ +using System.Net.Http; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using NAPS2.Escl; +using NAPS2.Scan; +using NAPS2.Sdk.Tests.Asserts; +using Xunit; +using Xunit.Abstractions; + +namespace NAPS2.Sdk.Tests.Remoting; + +public class TlsScanServerTests(ITestOutputHelper testOutputHelper) : ScanServerTestsBase(testOutputHelper, + EsclSecurityPolicy.RequireHttps, X509CertificateLoader.LoadPkcs12(BinaryResources.testcert, null)) +{ + [NetworkFact(Timeout = TIMEOUT)] + public async Task FindDevice() + { + Assert.True(await TryFindClientDevice()); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task Scan() + { + _bridge.MockOutput = CreateScannedImages(ImageResources.dog); + var images = await _client.Scan(new ScanOptions + { + Device = _clientDevice, + EsclOptions = + { + SecurityPolicy = EsclSecurityPolicy.RequireHttps + } + }).ToListAsync(); + Assert.Single(images); + ImageAsserts.Similar(ImageResources.dog, images[0]); + } + + [NetworkFact(Timeout = TIMEOUT)] + public async Task ScanPreventedByTrustedCertificateSecurityPolicy() + { + var scanResult = _client.Scan(new ScanOptions + { + Device = _clientDevice, + EsclOptions = + { + SecurityPolicy = EsclSecurityPolicy.RequireTrustedCertificate + } + }); + var exception = await Assert.ThrowsAsync(async () => await scanResult.ToListAsync()); + Assert.True(exception.InnerException is AuthenticationException || + exception.InnerException?.InnerException is AuthenticationException); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Resources/blank1.jpg b/NAPS2.Sdk.Tests/Resources/blank1.jpg new file mode 100644 index 0000000000..0beb85d990 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/blank1.jpg differ diff --git a/NAPS2.Sdk.Tests/Resources/blank2.jpg b/NAPS2.Sdk.Tests/Resources/blank2.jpg new file mode 100644 index 0000000000..bde31ac216 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/blank2.jpg differ diff --git a/NAPS2.Sdk.Tests/Resources/bw_alternating.png b/NAPS2.Sdk.Tests/Resources/bw_alternating.png new file mode 100644 index 0000000000..f7ea1ca360 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/bw_alternating.png differ diff --git a/NAPS2.Sdk.Tests/Resources/cat.jpg b/NAPS2.Sdk.Tests/Resources/cat.jpg new file mode 100644 index 0000000000..ec7d3f404d Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/cat.jpg differ diff --git a/NAPS2.Sdk.Tests/Resources/dog_99w.png b/NAPS2.Sdk.Tests/Resources/dog_99w.png new file mode 100644 index 0000000000..5937f815ef Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/dog_99w.png differ diff --git a/NAPS2.Sdk.Tests/Resources/dog_alpha_sc_50pct.png b/NAPS2.Sdk.Tests/Resources/dog_alpha_sc_50pct.png new file mode 100644 index 0000000000..b9257cbd1b Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/dog_alpha_sc_50pct.png differ diff --git a/NAPS2.Sdk.Tests/Resources/dog_cat_combined.jpg b/NAPS2.Sdk.Tests/Resources/dog_cat_combined.jpg new file mode 100644 index 0000000000..980b8897c4 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/dog_cat_combined.jpg differ diff --git a/NAPS2.Sdk.Tests/Resources/dog_cat_combined_bw.jpg b/NAPS2.Sdk.Tests/Resources/dog_cat_combined_bw.jpg new file mode 100644 index 0000000000..9f1007a5a7 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/dog_cat_combined_bw.jpg differ diff --git a/NAPS2.Sdk.Tests/Resources/dog_exif.jpg b/NAPS2.Sdk.Tests/Resources/dog_exif.jpg new file mode 100644 index 0000000000..d7423932b5 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/dog_exif.jpg differ diff --git a/NAPS2.Sdk.Tests/Resources/dog_gray_8bit.jpg b/NAPS2.Sdk.Tests/Resources/dog_gray_8bit.jpg new file mode 100644 index 0000000000..121fe06b73 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/dog_gray_8bit.jpg differ diff --git a/NAPS2.Sdk.Tests/Resources/filled_form_annotated.pdf b/NAPS2.Sdk.Tests/Resources/filled_form_annotated.pdf new file mode 100644 index 0000000000..a3effa983a Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/filled_form_annotated.pdf differ diff --git a/NAPS2.Sdk.Tests/Resources/filled_form_annotated.png b/NAPS2.Sdk.Tests/Resources/filled_form_annotated.png new file mode 100644 index 0000000000..22d453f1a3 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/filled_form_annotated.png differ diff --git a/NAPS2.Sdk.Tests/Resources/image_pdf_cmyk.pdf b/NAPS2.Sdk.Tests/Resources/image_pdf_cmyk.pdf new file mode 100644 index 0000000000..16fb65cc75 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/image_pdf_cmyk.pdf differ diff --git a/NAPS2.Sdk.Tests/Resources/notblank.jpg b/NAPS2.Sdk.Tests/Resources/notblank.jpg new file mode 100644 index 0000000000..f92b910a93 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/notblank.jpg differ diff --git a/NAPS2.Sdk.Tests/Resources/ocr_test_output.pdf b/NAPS2.Sdk.Tests/Resources/ocr_test_output.pdf new file mode 100644 index 0000000000..d6637fc29c Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/ocr_test_output.pdf differ diff --git a/NAPS2.Sdk.Tests/Resources/patcht_cropped_bl.jpg b/NAPS2.Sdk.Tests/Resources/patcht_cropped_bl.jpg new file mode 100644 index 0000000000..c9c3093c25 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/patcht_cropped_bl.jpg differ diff --git a/NAPS2.Sdk.Tests/Resources/patcht_cropped_br.jpg b/NAPS2.Sdk.Tests/Resources/patcht_cropped_br.jpg new file mode 100644 index 0000000000..ab4377c007 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/patcht_cropped_br.jpg differ diff --git a/NAPS2.Sdk.Tests/Resources/testcert.crt b/NAPS2.Sdk.Tests/Resources/testcert.crt new file mode 100644 index 0000000000..4a5235e175 --- /dev/null +++ b/NAPS2.Sdk.Tests/Resources/testcert.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDEzCCAfsCFBbfet3dXy6Z774903bxJvhounivMA0GCSqGSIb3DQEBCwUAMEUx +CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl +cm5ldCBXaWRnaXRzIFB0eSBMdGQwIBcNMjQwMzI3MDI1MDQxWhgPMjEyNDAzMDMw +MjUwNDFaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD +VQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDBIPIafttfxb8cwSQEld5YRd7SX1AD14p6wBZ32qfY+NkG +/lKKhhDK2mFucO+6prhllYrutrXt9F8myyap7RbRGFLytZahgOLK9BajIUQCXKHA +6VHehCdZHVdMVTzZk+VGsgctvCxX1pIGWeiR42uiPu6T7FijGHZrpUNIMheAATi3 +4LSVmDY+jxHRvBpDXVBdsoHzIrfYUA+GVGVpTpbzmQmooMH0c5bj3SNidiy7Mhx0 +EuuwPSATQ6E2aG6ckhn9tjzTIJWA+3RvoUU9zqHkAj2J4+xXYl09TzqsRAwZ0w+r +JNMQ1hOKualIyMnnQP74a4skZKJg+D3+R6R9qjshAgMBAAEwDQYJKoZIhvcNAQEL +BQADggEBAA9nzTygpYaAbCBI+pfscOAnF2kKn8tAyCy7R2LbEa2zPFV+2ZJUCFZt +E47jvpzFVrhMbd1sgmxup2P3Reeff718YIMFB3HAEDXmCUHd+Jh2HnoUfcNQVoUv +HSIskPpWK0PueZxRbPA72uTBpEQcwZ06kPREMEmiKkoWh9db2tMpjdiF0ci8XZdg +2qCMgJUrTVw3wtIufSPu8LWklnHM8T2uHtNQlppxSE5a0Sa9IU12dTWaA96GCO+X +AdQm7PvVSdaocRKhrsnJ5pxtvJFSYuqP2bMxstagkfqPpOJYO6gp/efBq+vqfJg1 +VTgLVJgjTFNwEdOytQJ9ZPrlpBjupyI= +-----END CERTIFICATE----- diff --git a/NAPS2.Sdk.Tests/Resources/testcert.csr b/NAPS2.Sdk.Tests/Resources/testcert.csr new file mode 100644 index 0000000000..e8ee897dd6 --- /dev/null +++ b/NAPS2.Sdk.Tests/Resources/testcert.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx +ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAMEg8hp+21/FvxzBJASV3lhF3tJfUAPXinrAFnfa +p9j42Qb+UoqGEMraYW5w77qmuGWViu62te30XybLJqntFtEYUvK1lqGA4sr0FqMh +RAJcocDpUd6EJ1kdV0xVPNmT5UayBy28LFfWkgZZ6JHja6I+7pPsWKMYdmulQ0gy +F4ABOLfgtJWYNj6PEdG8GkNdUF2ygfMit9hQD4ZUZWlOlvOZCaigwfRzluPdI2J2 +LLsyHHQS67A9IBNDoTZobpySGf22PNMglYD7dG+hRT3OoeQCPYnj7FdiXT1POqxE +DBnTD6sk0xDWE4q5qUjIyedA/vhriyRkomD4Pf5HpH2qOyECAwEAAaAAMA0GCSqG +SIb3DQEBCwUAA4IBAQB7eR0vqyWCuf0EUSBYYngHfewJM/dBUR+C+ZRloEwYBkwU +ma06L/3uSV50+L81x2ZbOi93Ee6WrukdYMq0r82LlizHDAVeWz6FkuDCobVyWnbX +QvoUbvPAHvBmw172Zkzs7pGCbq3h0gejqzMOT6lVnZOMsHRXDVVvM7afatSNMf6w +EnIpbil4bQ9XQoj4bF1f81d28E9O4w4saB7WLDvbjukeQC81qRhXu7FXAsLP9ZA1 +Pq0wuqCIMmfF6BVh9reZ8nVR9RtrFGSOT6+rVgztjuuFETq7p83xawdABQwYTE1M +icvlO9gXI1Gey4CkS9uTGjrH1JU5zLOHNL0RwDWe +-----END CERTIFICATE REQUEST----- diff --git a/NAPS2.Sdk.Tests/Resources/testcert.key b/NAPS2.Sdk.Tests/Resources/testcert.key new file mode 100644 index 0000000000..125a320de2 --- /dev/null +++ b/NAPS2.Sdk.Tests/Resources/testcert.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDBIPIafttfxb8c +wSQEld5YRd7SX1AD14p6wBZ32qfY+NkG/lKKhhDK2mFucO+6prhllYrutrXt9F8m +yyap7RbRGFLytZahgOLK9BajIUQCXKHA6VHehCdZHVdMVTzZk+VGsgctvCxX1pIG +WeiR42uiPu6T7FijGHZrpUNIMheAATi34LSVmDY+jxHRvBpDXVBdsoHzIrfYUA+G +VGVpTpbzmQmooMH0c5bj3SNidiy7Mhx0EuuwPSATQ6E2aG6ckhn9tjzTIJWA+3Rv +oUU9zqHkAj2J4+xXYl09TzqsRAwZ0w+rJNMQ1hOKualIyMnnQP74a4skZKJg+D3+ +R6R9qjshAgMBAAECggEAEjYtHlqADVP0ZZ3A673GLcTI8kWSogodQN4EQGEaGte8 +f3BUEEP8KWTWczerI4q9MLcdVs1b8ohswJe/mZ6F3EnS6Jg/EBO7TzAdQlzMsPxT +NIHL+pOzsi+WH9iZ2Fqd8ECxdJqeA9p0Aq1PxRIRAEe277QF17tiz1vSMGio1qUc +4c59mMXArApDNWLmphqsG6scQf5JyY8HHdjA/m4kfltiimlhId0Z7vJ7IuCndI7w +tr3kj5meJFqmmzl05U0a47WXiF4bpbZ/Io6Hk5Zu/40xgqBKVByPIv7nuwbJj5bA +ev0wd5+iVNErFKIw+xCI+wln0imQSuyQXtBjz3Cw0QKBgQDmaYbkVxLjmxRHHR/3 +GmgET73QFmjlkL6L/truOfxbrLDBx8nDEp3vhq1u9+qou0ODYbU5dPdMgWNULpEH +M9aj+4LxeTOoAr2tZkVANiCJNeZDNfQt95puH822skXIz6Y8DLAfZ55DOIlmwhN/ +4+gdGHeql5MCUwdW33VI1UcZWwKBgQDWk3jSwpt3WUrFavQez22AJtezeTPywyTK +FFI+Yo6W4tUF22ALgaN00yUDkAA8CZvc7/WAoGwyGbbBGw89Ct2WHq4bN+PQVo0c +U4WvxlAKLq5osFH2Mm0p9KkA/zDxAZZfOnjd4TgQbTe4bU3T/FM/LeWCSX0vMgb4 +NnwWZ2nqMwKBgQDJjvKzeQBLHvQkKXQ3A2COtPsEtzXX7EDj0nPOBeeegni1a4Iy +JW0Hhbbd5f3e0MIEgkq4Envq7xznHT09IbnYBULM3gu0I4Gt2FMoErFvljjx/pa2 +R21OfH/GHDkzq4Jt8WN4dXpar3By9b99Fu+L1EWKc8HkPKGk+yFsLzZdFQKBgQDU +VHvj+sTCli5CKnLFJjdR753UsCPynp4CBZfYucglkPKA6DMjT7ZCvUlMPCuvPUbp +mt3R2W0XKpDIh5FNsznP+i4JKwYYu/zIwfFxHYlIeicF2yxPtliFgt/V57AzXIHD +W+YMkXfb8WeI7Uhtc6ugwjbw9O2WTSfOaIPj25NYNwKBgQCRH/OqDClcO9IKkiUU +yl5XtXWLqobGsuSIivesQ2k8+83yLoYY8IaUr1IBYlQQAwvCRTvzXqQtkOL9uTi7 +xzY6+wGphQvJjEweMggBbY4XRdfip7XdZU9CDDog1GyaUgfqR0zov6FLHVf0/EVA +4OrmR6DYSJhAZSgpahZGlZEuNw== +-----END PRIVATE KEY----- diff --git a/NAPS2.Sdk.Tests/Resources/testcert.pfx b/NAPS2.Sdk.Tests/Resources/testcert.pfx new file mode 100644 index 0000000000..022a670b64 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/testcert.pfx differ diff --git a/NAPS2.Sdk.Tests/Resources/word_ocr_test.pdf b/NAPS2.Sdk.Tests/Resources/word_ocr_test.pdf new file mode 100644 index 0000000000..d9e84ff183 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/word_ocr_test.pdf differ diff --git a/NAPS2.Sdk.Tests/Scan/DpiCapsTests.cs b/NAPS2.Sdk.Tests/Scan/DpiCapsTests.cs new file mode 100644 index 0000000000..aceddd978a --- /dev/null +++ b/NAPS2.Sdk.Tests/Scan/DpiCapsTests.cs @@ -0,0 +1,20 @@ +using NAPS2.Scan; +using Xunit; + +namespace NAPS2.Sdk.Tests.Scan; + +public class DpiCapsTests +{ + [Fact] + public void TestCommonValues() + { + var fullRange = DpiCaps.ForRange(50, 10000, 50); + Assert.Equal(fullRange.CommonValues, [50, 100, 150, 200, 300, 400, 600, 800, 1200, 2400, 4800, 10000]); + + var partialRange = DpiCaps.ForRange(300, 1200, 50); + Assert.Equal(partialRange.CommonValues, [300, 400, 600, 800, 1200]); + + var mismatchRange = DpiCaps.ForRange(240, 990, 50); + Assert.Equal(mismatchRange.CommonValues, [240, 340, 440, 640, 840, 990]); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Scan/PageSizeCapsTests.cs b/NAPS2.Sdk.Tests/Scan/PageSizeCapsTests.cs new file mode 100644 index 0000000000..83fbd7c287 --- /dev/null +++ b/NAPS2.Sdk.Tests/Scan/PageSizeCapsTests.cs @@ -0,0 +1,29 @@ +using NAPS2.Scan; +using Xunit; + +namespace NAPS2.Sdk.Tests.Scan; + +public class PageSizeCapsTests +{ + [Fact] + public void Fits() + { + var letter = new PageSizeCaps { ScanArea = PageSize.Letter }; + var legal = new PageSizeCaps { ScanArea = PageSize.Legal }; + var a4 = new PageSizeCaps { ScanArea = PageSize.A4 }; + // 11.6 inch height is slightly too small, so this tests the 1% margin + var a4AndLetter = new PageSizeCaps { ScanArea = new PageSize(8.5m, 11.6m, PageSizeUnit.Inch) }; + + Assert.True(letter.Fits(PageSize.Letter)); + Assert.False(letter.Fits(PageSize.Legal)); + + Assert.True(legal.Fits(PageSize.Letter)); + Assert.True(legal.Fits(PageSize.Legal)); + + Assert.True(a4.Fits(PageSize.A4)); + Assert.False(a4.Fits(PageSize.Letter)); + + Assert.True(a4AndLetter.Fits(PageSize.A4)); + Assert.True(a4AndLetter.Fits(PageSize.Letter)); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Scan/PerSourceCapsTests.cs b/NAPS2.Sdk.Tests/Scan/PerSourceCapsTests.cs new file mode 100644 index 0000000000..f84bf49e5e --- /dev/null +++ b/NAPS2.Sdk.Tests/Scan/PerSourceCapsTests.cs @@ -0,0 +1,50 @@ +using NAPS2.Scan; +using Xunit; + +namespace NAPS2.Sdk.Tests.Scan; + +public class PerSourceCapsTests +{ + [Fact] + public void UnionAllEmpty() + { + var emptyCaps = new PerSourceCaps(); + var union = PerSourceCaps.UnionAll([emptyCaps]); + Assert.Null(union.DpiCaps); + Assert.Null(union.BitDepthCaps); + Assert.Null(union.PageSizeCaps); + } + + [Fact] + public void UnionAll() + { + var emptyCaps = new PerSourceCaps(); + var emptySubCaps = new PerSourceCaps + { + DpiCaps = new DpiCaps(), + BitDepthCaps = new BitDepthCaps(), + PageSizeCaps = new PageSizeCaps() + }; + var caps1 = new PerSourceCaps + { + DpiCaps = new DpiCaps { Values = [100, 150] }, + BitDepthCaps = new BitDepthCaps { SupportsColor = true }, + PageSizeCaps = new PageSizeCaps { ScanArea = PageSize.Letter } + }; + var caps2 = new PerSourceCaps + { + DpiCaps = new DpiCaps { Values = [50, 100] }, + BitDepthCaps = new BitDepthCaps { SupportsGrayscale = true }, + PageSizeCaps = new PageSizeCaps { ScanArea = PageSize.A4 } + }; + + var union = PerSourceCaps.UnionAll([emptyCaps, emptySubCaps, caps1, caps2]); + + Assert.Equal([50, 100, 150], union.DpiCaps!.Values); + Assert.True(union.BitDepthCaps!.SupportsColor); + Assert.True(union.BitDepthCaps!.SupportsGrayscale); + Assert.False(union.BitDepthCaps!.SupportsBlackAndWhite); + Assert.Equal(8.5m, union.PageSizeCaps!.ScanArea!.WidthInInches); + Assert.Equal(297m, union.PageSizeCaps!.ScanArea!.HeightInMm, 3); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Scan/RemotePostProcessorTests.cs b/NAPS2.Sdk.Tests/Scan/RemotePostProcessorTests.cs new file mode 100644 index 0000000000..aa2dfb03d4 --- /dev/null +++ b/NAPS2.Sdk.Tests/Scan/RemotePostProcessorTests.cs @@ -0,0 +1,214 @@ +using NAPS2.Scan; +using NAPS2.Scan.Internal; +using NAPS2.Sdk.Tests.Asserts; +using Xunit; + +namespace NAPS2.Sdk.Tests.Scan; + +public class RemotePostProcessorTests : ContextualTests +{ + // TODO: Add more tests + + private readonly RemotePostProcessor _remotePostProcessor; + + public RemotePostProcessorTests() + { + _remotePostProcessor = new RemotePostProcessor(ScanningContext); + } + + [Fact] + public void Blank() + { + var image = LoadImage(ImageResources.blank1); + var options = new ScanOptions + { + ExcludeBlankPages = true + }; + var result = _remotePostProcessor.PostProcess(image, options, new PostProcessingContext()); + Assert.Null(result); + } + + [Fact] + public void NotBlank() + { + var image = LoadImage(ImageResources.notblank); + var options = new ScanOptions + { + ExcludeBlankPages = true + }; + var result = _remotePostProcessor.PostProcess(image, options, new PostProcessingContext()); + Assert.NotNull(result); + } + + [Fact] + public void Brightness() + { + var image = LoadImage(ImageResources.dog); + var options = new ScanOptions + { + Brightness = 300, + ThumbnailSize = 256 + }; + var result = _remotePostProcessor.PostProcess(image, options, new PostProcessingContext()); + Assert.Single(result.TransformState.Transforms); + Assert.Equal(300, Assert.IsType(result.TransformState.Transforms[0]).Brightness); + ImageAsserts.Similar(ImageResources.dog_b_p300_thumb_256, result.PostProcessingData.Thumbnail, ignoreResolution: true, rmseThreshold: ImageAsserts.XPLAT_RMSE_THRESHOLD); + } + + [Fact] + public void AutoDeskew() + { + var image = LoadImage(ImageResources.skewed); + var options = new ScanOptions + { + AutoDeskew = true + }; + var result = _remotePostProcessor.PostProcess(image, options, new PostProcessingContext()); + ImageAsserts.Similar(ImageResources.deskewed, result, ImageAsserts.XL_RMSE_THRESHOLD); + } + + [Fact] + public void Rotate() + { + var image = LoadImage(ImageResources.dog); + var options = new ScanOptions + { + RotateDegrees = 90 + }; + var result = _remotePostProcessor.PostProcess(image, options, new PostProcessingContext()); + ImageAsserts.Similar(ImageResources.dog_r_p90, result, ImageAsserts.XPLAT_RMSE_THRESHOLD); + } + + [Fact] + public void CropToPageSize_BothPortrait() + { + var image = LoadImage(ImageResources.patcht); + var options = new ScanOptions + { + CropToPageSize = true, + PageSize = new PageSize(8m, 10m, PageSizeUnit.Inch) + }; + var result = _remotePostProcessor.PostProcess(image, options, new PostProcessingContext()); + ImageAsserts.Similar(ImageResources.patcht_cropped_br, result); + } + + [Fact] + public void CropToPageSize_SizeLandscape() + { + var image = LoadImage(ImageResources.patcht); + var options = new ScanOptions + { + CropToPageSize = true, + PageSize = new PageSize(10m, 8m, PageSizeUnit.Inch) + }; + var result = _remotePostProcessor.PostProcess(image, options, new PostProcessingContext()); + ImageAsserts.Similar(ImageResources.patcht_cropped_br, result); + } + + [Fact] + public void CropToPageSize_ImageLandscape() + { + var image = LoadImage(ImageResources.patcht).PerformTransform(new RotationTransform(-90)); + var options = new ScanOptions + { + CropToPageSize = true, + PageSize = new PageSize(8m, 10m, PageSizeUnit.Inch) + }; + var result = _remotePostProcessor.PostProcess(image, options, new PostProcessingContext()); + ImageAsserts.Similar(ImageResources.patcht_cropped_bl, result!.Render().PerformTransform(new RotationTransform(90))); + } + + [Fact] + public void CropToPageSize_BothLandscape() + { + var image = LoadImage(ImageResources.patcht).PerformTransform(new RotationTransform(-90)); + var options = new ScanOptions + { + CropToPageSize = true, + PageSize = new PageSize(10m, 8m, PageSizeUnit.Inch) + }; + var result = _remotePostProcessor.PostProcess(image, options, new PostProcessingContext()); + ImageAsserts.Similar(ImageResources.patcht_cropped_bl, result!.Render().PerformTransform(new RotationTransform(90))); + } + + // Only Linux can have zero resolution images + [PlatformFact(include: PlatformFlags.GtkImage)] + public void CropToPageSize_NoResolution() + { + var image = LoadImage(ImageResources.patcht); + image.SetResolution(0, 0); + var options = new ScanOptions + { + CropToPageSize = true, + PageSize = new PageSize(8m, 10m, PageSizeUnit.Inch) + }; + var result = _remotePostProcessor.PostProcess(image, options, new PostProcessingContext()); + ImageAsserts.Similar(ImageResources.patcht, result, ignoreResolution: true); + } + + [Fact] + public void PageSize() + { + var image = LoadImage(ImageResources.dog); + var options = new ScanOptions + { + PageSize = NAPS2.Images.PageSize.A4 + }; + var result = _remotePostProcessor.PostProcess(image, options, new PostProcessingContext()); + Assert.NotNull(result?.Metadata.PageSize); + Assert.Equal(210m, result.Metadata.PageSize.Width); + Assert.Equal(297m, result.Metadata.PageSize.Height); + Assert.Equal(PageSizeUnit.Millimetre, result.Metadata.PageSize.Unit); + } + + [Fact] + public void NoFlipDuplexed() + { + var image1 = LoadImage(ImageResources.dog); + var image2 = LoadImage(ImageResources.dog); + var options = new ScanOptions + { + PaperSource = PaperSource.Duplex + }; + var result1 = _remotePostProcessor.PostProcess(image1, options, new PostProcessingContext + { + PageNumber = 1 + })!; + var result2 = _remotePostProcessor.PostProcess(image2, options, new PostProcessingContext + { + PageNumber = 2 + })!; + Assert.Equal(1, result1.PostProcessingData.PageNumber); + Assert.Equal(PageSide.Front, result1.PostProcessingData.PageSide); + ImageAsserts.Similar(ImageResources.dog, result1); + Assert.Equal(2, result2.PostProcessingData.PageNumber); + Assert.Equal(PageSide.Back, result2.PostProcessingData.PageSide); + ImageAsserts.Similar(ImageResources.dog, result2); + } + + [Fact] + public void FlipDuplexed() + { + var image1 = LoadImage(ImageResources.dog); + var image2 = LoadImage(ImageResources.dog); + var options = new ScanOptions + { + PaperSource = PaperSource.Duplex, + FlipDuplexedPages = true + }; + var result1 = _remotePostProcessor.PostProcess(image1, options, new PostProcessingContext + { + PageNumber = 1 + })!; + var result2 = _remotePostProcessor.PostProcess(image2, options, new PostProcessingContext + { + PageNumber = 2 + })!; + Assert.Equal(1, result1.PostProcessingData.PageNumber); + Assert.Equal(PageSide.Front, result1.PostProcessingData.PageSide); + ImageAsserts.Similar(ImageResources.dog, result1); + Assert.Equal(2, result2.PostProcessingData.PageNumber); + Assert.Equal(PageSide.Back, result2.PostProcessingData.PageSide); + ImageAsserts.Similar(ImageResources.dog, result2.WithTransform(new RotationTransform(180))); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Scan/RemoteScanControllerTests.cs b/NAPS2.Sdk.Tests/Scan/RemoteScanControllerTests.cs index bfc0f7055f..7e0375af3a 100644 --- a/NAPS2.Sdk.Tests/Scan/RemoteScanControllerTests.cs +++ b/NAPS2.Sdk.Tests/Scan/RemoteScanControllerTests.cs @@ -1,8 +1,8 @@ using System.Threading; -using Moq; using NAPS2.Scan; using NAPS2.Scan.Internal; using NAPS2.Sdk.Tests.Asserts; +using NSubstitute; using Xunit; using IScanDriver = NAPS2.Scan.Internal.IScanDriver; using IScanDriverFactory = NAPS2.Scan.Internal.IScanDriverFactory; @@ -12,45 +12,56 @@ namespace NAPS2.Sdk.Tests.Scan; public class RemoteScanControllerTests : ContextualTests { [Fact] - public async Task GetDeviceList() + public async Task GetDevices() { - var device = new ScanDevice("test_id1", "test_name1"); - var wiaDevice = new ScanDevice("WIA-test_id2", "test_name2"); - var scanDriver = new Mock(); - scanDriver.Setup(x => x.GetDeviceList(It.IsAny())) - .ReturnsAsync(new List { device, wiaDevice }); - var controller = CreateControllerWithMockDriver(scanDriver.Object); + var scanDriver = Substitute.For(); + scanDriver.GetDevices(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(x => + { + var options = (ScanOptions) x[0]; + var callback = (Action) x[2]; + callback(new ScanDevice(options.Driver, "test_id1", "test_name1")); + callback(new ScanDevice(options.Driver, "WIA-test_id2", "test_name2")); + return Task.CompletedTask; + }); + var controller = CreateControllerWithMockDriver(scanDriver); - var deviceList = await controller.GetDeviceList(new ScanOptions { Driver = Driver.Wia }); + var deviceList = new List(); + await controller.GetDevices(new ScanOptions { Driver = Driver.Wia }, CancellationToken.None, deviceList.Add); Assert.Equal(2, deviceList.Count); Assert.Equal("test_id1", deviceList[0].ID); Assert.Equal("WIA-test_id2", deviceList[1].ID); - deviceList = await controller.GetDeviceList(new ScanOptions { Driver = Driver.Twain }); + deviceList.Clear(); + await controller.GetDevices(new ScanOptions { Driver = Driver.Twain }, CancellationToken.None, deviceList.Add); Assert.Single(deviceList); Assert.Equal("test_id1", deviceList[0].ID); - deviceList = await controller.GetDeviceList(new ScanOptions - { Driver = Driver.Twain, TwainOptions = { IncludeWiaDevices = true } }); + deviceList.Clear(); + await controller.GetDevices( + new ScanOptions { Driver = Driver.Twain, TwainOptions = { IncludeWiaDevices = true } }, + CancellationToken.None, + deviceList.Add); Assert.Equal(2, deviceList.Count); } [Fact] public async Task ScanAndDeskew() { - var scanDriver = new Mock(); - scanDriver.Setup(x => x.Scan(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny>())).Returns(new InvocationFunc( - ctx => + var scanDriver = Substitute.For(); + scanDriver.Scan(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any>()).ReturnsForAnyArgs( + x => { - var callback = (Action) ctx.Arguments[3]; + var callback = (Action) x[3]; var image = LoadImage(ImageResources.skewed); callback(image); return Task.FromResult(true); - })); - var controller = CreateControllerWithMockDriver(scanDriver.Object); + }); + var controller = CreateControllerWithMockDriver(scanDriver); var images = new List(); + void Callback(ProcessedImage image, PostProcessingContext context) { images.Add(image); @@ -61,16 +72,16 @@ void Callback(ProcessedImage image, PostProcessingContext context) AutoDeskew = true }; await controller.Scan(options, CancellationToken.None, ScanEvents.Stub, Callback); - + Assert.Single(images); - ImageAsserts.Similar(ImageResources.deskewed, images[0], ImageAsserts.XPLAT_RMSE_THRESHOLD); + ImageAsserts.Similar(ImageResources.deskewed, images[0], ImageAsserts.XL_RMSE_THRESHOLD); } private RemoteScanController CreateControllerWithMockDriver(IScanDriver scanDriver) { - var scanDriverFactory = new Mock(); - scanDriverFactory.Setup(x => x.Create(It.IsAny())).Returns(scanDriver); - var controller = new RemoteScanController(scanDriverFactory.Object, new RemotePostProcessor(ScanningContext)); + var scanDriverFactory = Substitute.For(); + scanDriverFactory.Create(Arg.Any()).Returns(scanDriver); + var controller = new RemoteScanController(scanDriverFactory, new RemotePostProcessor(ScanningContext)); return controller; } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Scan/SaneScanDriverOptionTests.cs b/NAPS2.Sdk.Tests/Scan/SaneScanDriverOptionTests.cs new file mode 100644 index 0000000000..ce470bf247 --- /dev/null +++ b/NAPS2.Sdk.Tests/Scan/SaneScanDriverOptionTests.cs @@ -0,0 +1,299 @@ +using NAPS2.Scan; +using NAPS2.Scan.Internal.Sane; +using NAPS2.Scan.Internal.Sane.Native; +using Xunit; + +namespace NAPS2.Sdk.Tests.Scan; + +public class SaneScanDriverOptionTests : ContextualTests +{ + private readonly SaneScanDriver _driver; + + public SaneScanDriverOptionTests() + { + _driver = new SaneScanDriver(ScanningContext); + } + + // TODO: More tests + + [Fact] + public void SetOptions_Flatbed() + { + var device = new DeviceOptionsMock([ + SaneOption.CreateStringListForTesting(1, SaneOptionNames.SOURCE, ["Flatbed", "ADF", "Duplex"]) + ]); + var options = new ScanOptions + { + PaperSource = PaperSource.Flatbed + }; + + var optionData = _driver.SetOptions(device, options); + + Assert.False(optionData.IsFeeder); + Assert.Equal("Flatbed", device.GetValue(1)); + VerifyCapPaperSources(device, true, true, true); + } + + [Fact] + public void GetSaneCaps() + { + var device = new DeviceOptionsMock([ + SaneOption.CreateFixedForTesting(1, SaneOptionNames.TOP_LEFT_X, + new SaneRange { Min = 0, Max = 100, Quant = 1 }), + SaneOption.CreateFixedForTesting(2, SaneOptionNames.TOP_LEFT_Y, + new SaneRange { Min = 0, Max = 100, Quant = 1 }), + SaneOption.CreateFixedForTesting(3, SaneOptionNames.BOT_RIGHT_X, + new SaneRange { Min = 0, Max = 100, Quant = 1 }), + SaneOption.CreateFixedForTesting(4, SaneOptionNames.BOT_RIGHT_Y, + new SaneRange { Min = 0, Max = 100, Quant = 1 }), + SaneOption.CreateStringListForTesting(5, SaneOptionNames.SOURCE, ["Flatbed"]), + SaneOption.CreateWordListForTesting(6, SaneOptionNames.RESOLUTION, [100, 200, 300]), + SaneOption.CreateStringListForTesting(7, SaneOptionNames.MODE, ["Gray", "Color"]) + ]); + + var caps = _driver.GetSaneCaps(device, "pixma"); + + Assert.Equal("pixma", caps.MetadataCaps?.DriverSubtype); + VerifyCapPaperSources(device, true, false, false); + var flatbedCaps = caps.FlatbedCaps; + Assert.NotNull(flatbedCaps); + Assert.Equal(100, flatbedCaps.PageSizeCaps?.ScanArea?.Width); + Assert.Equal(100, flatbedCaps.PageSizeCaps?.ScanArea?.Height); + Assert.Equal(PageSizeUnit.Millimetre, flatbedCaps.PageSizeCaps?.ScanArea?.Unit); + Assert.Equal([100, 200, 300], flatbedCaps.DpiCaps?.Values); + Assert.Equal(true, flatbedCaps.BitDepthCaps?.SupportsColor); + Assert.Equal(true, flatbedCaps.BitDepthCaps?.SupportsGrayscale); + Assert.Equal(false, flatbedCaps.BitDepthCaps?.SupportsBlackAndWhite); + } + + [Fact] + public void SetOptions_Feeder() + { + var device = new DeviceOptionsMock([ + SaneOption.CreateStringListForTesting(1, SaneOptionNames.SOURCE, ["Flatbed", "ADF", "Duplex"]) + ]); + var options = new ScanOptions { PaperSource = PaperSource.Feeder }; + + var optionData = _driver.SetOptions(device, options); + + Assert.True(optionData.IsFeeder); + Assert.Equal("ADF", device.GetValue(1)); + VerifyCapPaperSources(device, true, true, true); + } + + [Fact] + public void SetOptions_FeederWithDuplexMatch() + { + var device = new DeviceOptionsMock([ + SaneOption.CreateStringListForTesting(1, SaneOptionNames.SOURCE, ["Flatbed", "ADF Duplex", "ADF"]) + ]); + var options = new ScanOptions { PaperSource = PaperSource.Feeder }; + + var optionData = _driver.SetOptions(device, options); + + Assert.True(optionData.IsFeeder); + Assert.Equal("ADF", device.GetValue(1)); + VerifyCapPaperSources(device, true, true, true); + } + + [Fact] + public void SetOptions_Duplex() + { + var device = new DeviceOptionsMock([ + SaneOption.CreateStringListForTesting(1, SaneOptionNames.SOURCE, ["Flatbed", "ADF", "Duplex"]) + ]); + var options = new ScanOptions { PaperSource = PaperSource.Duplex }; + + var optionData = _driver.SetOptions(device, options); + + Assert.True(optionData.IsFeeder); + Assert.Equal("Duplex", device.GetValue(1)); + VerifyCapPaperSources(device, true, true, true); + } + + [Fact] + public void SetOptions_DuplexWithAdfMode() + { + var device = new DeviceOptionsMock([ + SaneOption.CreateStringListForTesting(1, SaneOptionNames.SOURCE, ["Flatbed", "ADF"]), + SaneOption.CreateStringListForTesting(2, SaneOptionNames.ADF_MODE1, ["Simplex", "Duplex"]) + ]); + var options = new ScanOptions { PaperSource = PaperSource.Duplex }; + + var optionData = _driver.SetOptions(device, options); + + Assert.True(optionData.IsFeeder); + Assert.Equal("ADF", device.GetValue(1)); + Assert.Equal("Duplex", device.GetValue(2)); + VerifyCapPaperSources(device, true, true, true); + } + + [Fact] + public void SetOptions_AutoWithFlatbed() + { + var device = new DeviceOptionsMock([ + SaneOption.CreateStringListForTesting(1, SaneOptionNames.SOURCE, ["Flatbed", "ADF", "Duplex"]) + ]); + var options = new ScanOptions { PaperSource = PaperSource.Auto }; + + var optionData = _driver.SetOptions(device, options); + + Assert.False(optionData.IsFeeder); + Assert.Equal("Flatbed", device.GetValue(1)); + VerifyCapPaperSources(device, true, true, true); + } + + [Fact] + public void SetOptions_AutoWithNoFlatbed() + { + var device = new DeviceOptionsMock([ + SaneOption.CreateStringListForTesting(1, SaneOptionNames.SOURCE, ["ADF", "Duplex"]) + ]); + var options = new ScanOptions { PaperSource = PaperSource.Auto }; + + var optionData = _driver.SetOptions(device, options); + + Assert.True(optionData.IsFeeder); + Assert.Equal("ADF", device.GetValue(1)); + VerifyCapPaperSources(device, false, true, true); + } + + [Fact] + public void SetOptions_DuplexWithPartialMatch() + { + var device = new DeviceOptionsMock([ + SaneOption.CreateStringListForTesting(1, SaneOptionNames.SOURCE, + ["Feeder(left aligned)", "Feeder(left aligned,Duplex)"]) + ]); + var options = new ScanOptions { PaperSource = PaperSource.Duplex }; + + var optionData = _driver.SetOptions(device, options); + + Assert.True(optionData.IsFeeder); + Assert.Equal("Feeder(left aligned,Duplex)", device.GetValue(1)); + VerifyCapPaperSources(device, false, true, true); + } + + [Fact] + public void SetOptions_DuplexBoolean() + { + // Settings from Epson WF-3520 with epsonscan2 backend + var device = new DeviceOptionsMock([ + SaneOption.CreateStringListForTesting(1, SaneOptionNames.SOURCE, + ["Auto", "Flatbed", "ADF", "ADF Front"]), + SaneOption.CreateBooleanForTesting(2, SaneOptionNames.DUPLEX) + ]); + var options = new ScanOptions { PaperSource = PaperSource.Duplex }; + + var optionData = _driver.SetOptions(device, options); + + Assert.True(optionData.IsFeeder); + Assert.Equal("ADF", device.GetValue(1)); + Assert.Equal(true, device.GetValue(2)); + VerifyCapPaperSources(device, true, true, true); + } + + [Fact] + public void SetOptions_DuplicateOptions() + { + var device = new DeviceOptionsMock([ + SaneOption.CreateStringListForTesting(1, SaneOptionNames.SOURCE, ["Flatbed", "ADF", "Duplex"]), + SaneOption.CreateStringListForTesting(2, SaneOptionNames.SOURCE, ["Flatbed", "ADF", "Duplex"]) + ]); + var options = new ScanOptions + { + PaperSource = PaperSource.Flatbed + }; + + var optionData = _driver.SetOptions(device, options); + + Assert.False(optionData.IsFeeder); + Assert.Equal("Flatbed", device.GetValue(1)); + VerifyCapPaperSources(device, true, true, true); + } + + [Fact] + public void SetOptions_NoGrayErrorDiffusion() + { + var device = new DeviceOptionsMock([ + SaneOption.CreateStringListForTesting(1, SaneOptionNames.MODE, ["Gray[Error Diffusion]", "True Gray"]) + ]); + var options = new ScanOptions + { + BitDepth = BitDepth.Grayscale + }; + + var optionData = _driver.SetOptions(device, options); + + Assert.Equal("True Gray", optionData.Mode); + } + + [Fact] + public void SetOptions_KeyValue() + { + var device = new DeviceOptionsMock([ + SaneOption.CreateStringListForTesting(1, SaneOptionNames.MODE, ["Lineart", "Halftone", "Gray", "Color"]) + ]); + var options = new ScanOptions + { + KeyValueOptions = { ["mode"] = "Halftone" } + }; + + _driver.SetOptions(device, options); + + Assert.Equal("Halftone", device.GetValue(1)); + } + + private class DeviceOptionsMock : ISaneDevice + { + private readonly IEnumerable _options; + private readonly Dictionary _values; + + public DeviceOptionsMock(IEnumerable options, Dictionary defaultValues = null) + { + _options = options; + _values = defaultValues ?? new(); + } + + public object GetValue(int index) => _values[index]; + + public void Cancel() => throw new NotSupportedException(); + public void Start() => throw new NotSupportedException(); + public SaneReadParameters GetParameters() => throw new NotSupportedException(); + public bool Read(byte[] buffer, out int len) => throw new NotSupportedException(); + + public IEnumerable GetOptions() => _options; + + public void SetOption(SaneOption option, bool value, out SaneOptionSetInfo info) + { + _values[option.Index] = value; + info = SaneOptionSetInfo.None; + } + + public void SetOption(SaneOption option, double value, out SaneOptionSetInfo info) + { + _values[option.Index] = value; + info = SaneOptionSetInfo.None; + } + + public void SetOption(SaneOption option, string value, out SaneOptionSetInfo info) + { + _values[option.Index] = value; + info = SaneOptionSetInfo.None; + } + + public void GetOption(SaneOption option, out double value) + { + value = (double) _values[option.Index]; + } + } + + private void VerifyCapPaperSources(DeviceOptionsMock device, bool flatbed, bool feeder, bool duplex) + { + var caps = _driver.GetSaneCaps(device, ""); + Assert.NotNull(caps.PaperSourceCaps); + Assert.Equal(flatbed, caps.PaperSourceCaps.SupportsFlatbed); + Assert.Equal(feeder, caps.PaperSourceCaps.SupportsFeeder); + Assert.Equal(duplex, caps.PaperSourceCaps.SupportsDuplex); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Scan/SaneScanDriverTests.cs b/NAPS2.Sdk.Tests/Scan/SaneScanDriverTests.cs new file mode 100644 index 0000000000..fa340b3fca --- /dev/null +++ b/NAPS2.Sdk.Tests/Scan/SaneScanDriverTests.cs @@ -0,0 +1,212 @@ +using NAPS2.Scan.Internal; +using NAPS2.Scan.Internal.Sane; +using NAPS2.Scan.Internal.Sane.Native; +using NAPS2.Sdk.Tests.Asserts; +using NSubstitute; +using Xunit; + +namespace NAPS2.Sdk.Tests.Scan; + +public class SaneScanDriverTests : ContextualTests +{ + private static readonly int[] ExpectedData = Enumerable.Range(0, 64).Concat(Enumerable.Range(0, 16)).ToArray(); + + private readonly SaneScanDriver _driver; + + public SaneScanDriverTests() + { + _driver = new SaneScanDriver(ScanningContext); + } + + [Fact] + public void ScanFrame_KnownFrameSize() + { + var deviceMock = Substitute.For(); + var eventsMock = Substitute.For(); + SetupDeviceMock(deviceMock); + + var result = _driver.ScanFrame(deviceMock, eventsMock, 0, out var p); + + Assert.NotNull(result); + Assert.Equal(ExpectedData.Length, result.Length); + Assert.Equal(ExpectedData, result.ToArray().Select(x => (int) x)); + deviceMock.Received().Start(); + eventsMock.Received().PageStart(); + eventsMock.Received().PageProgress(0); + eventsMock.Received().PageProgress(Arg.Is(x => x - 64.0 / 80.0 < 0.01)); + eventsMock.Received().PageProgress(1); + eventsMock.ReceivedCallsCount(4); + } + + [Fact] + public void ScanFrame_UnknownFrameSize() + { + var deviceMock = Substitute.For(); + var eventsMock = Substitute.For(); + SetupDeviceMock(deviceMock, -1); + + var result = _driver.ScanFrame(deviceMock, eventsMock, 0, out var p); + + Assert.NotNull(result); + Assert.Equal(ExpectedData.Length, result.Length); + Assert.Equal(ExpectedData, result.ToArray().Select(x => (int) x)); + Assert.Equal(p.Lines, 8); + deviceMock.Received().Start(); + eventsMock.Received().PageStart(); + eventsMock.ReceivedCallsCount(1); + } + + [Fact] + public void ScanFrame_NoRead_ReturnsNull() + { + var deviceMock = Substitute.For(); + var eventsMock = Substitute.For(); + SetupDeviceMock(deviceMock); + deviceMock.Read(Arg.Any(), out Arg.Any()) + .Returns(x => + { + x[1] = 0; + return false; + }); + + var result = _driver.ScanFrame(deviceMock, eventsMock, 0, out var p); + + Assert.Null(result); + deviceMock.Received().Start(); + eventsMock.Received().PageStart(); + eventsMock.Received().PageProgress(0); + eventsMock.ReceivedCallsCount(2); + } + + [Fact] + public void ScanPage_KnownFrameSize() + { + var deviceMock = Substitute.For(); + var eventsMock = Substitute.For(); + SetupDeviceMock(deviceMock); + + var result = _driver.ScanPage(deviceMock, eventsMock, new SaneScanDriver.OptionData()); + + Assert.NotNull(result); + Assert.Equal(3, result.Width); + Assert.Equal(8, result.Height); + ImageAsserts.PixelColors(result, new() + { + { (0, 0), (0, 1, 2) }, + { (2, 2), (26, 27, 28) } + }); + } + + [Fact] + public void ScanPage_UnknownFrameSize() + { + var deviceMock = Substitute.For(); + var eventsMock = Substitute.For(); + SetupDeviceMock(deviceMock, -1); + + var result = _driver.ScanPage(deviceMock, eventsMock, new SaneScanDriver.OptionData()); + + Assert.NotNull(result); + Assert.Equal(3, result.Width); + Assert.Equal(8, result.Height); + ImageAsserts.PixelColors(result, new() + { + { (0, 0), (0, 1, 2) }, + { (2, 2), (26, 27, 28) } + }); + } + + [Fact] + public void ScanPage_TooSmallFrameSize() + { + var deviceMock = Substitute.For(); + var eventsMock = Substitute.For(); + SetupDeviceMock(deviceMock, 6); + + var result = _driver.ScanPage(deviceMock, eventsMock, new SaneScanDriver.OptionData()); + + Assert.NotNull(result); + Assert.Equal(3, result.Width); + Assert.Equal(8, result.Height); + ImageAsserts.PixelColors(result, new() + { + { (0, 0), (0, 1, 2) }, + { (2, 2), (26, 27, 28) } + }); + } + + [Fact] + public void ScanPage_TooBigFrameSize() + { + var deviceMock = Substitute.For(); + var eventsMock = Substitute.For(); + SetupDeviceMock(deviceMock, 10); + + var result = _driver.ScanPage(deviceMock, eventsMock, new SaneScanDriver.OptionData()); + + Assert.NotNull(result); + Assert.Equal(3, result.Width); + Assert.Equal(8, result.Height); + ImageAsserts.PixelColors(result, new() + { + { (0, 0), (0, 1, 2) }, + { (2, 2), (26, 27, 28) } + }); + } + + [Fact] + public void ScanPage_NoRead_ReturnsNull() + { + var deviceMock = Substitute.For(); + var eventsMock = Substitute.For(); + SetupDeviceMock(deviceMock); + deviceMock.Read(Arg.Any(), out Arg.Any()) + .Returns(x => + { + x[1] = 0; + return false; + }); + + var result = _driver.ScanPage(deviceMock, eventsMock, new SaneScanDriver.OptionData()); + + Assert.Null(result); + } + + private static void SetupDeviceMock(ISaneDevice deviceMock, int? lines = null) + { + deviceMock.GetParameters().Returns(new SaneReadParameters + { + Depth = 8, + Frame = SaneFrameType.Rgb, + Lines = lines ?? 8, + BytesPerLine = 10, + PixelsPerLine = 3 + }); + deviceMock.Read(Arg.Any(), out Arg.Any()) + .Returns(x => + { + var buffer = (byte[]) x[0]; + for (int i = 0; i < 64; i++) + { + buffer[i] = (byte) i; + } + x[1] = 64; + return true; + }, + x => + { + var buffer = (byte[]) x[0]; + for (int i = 0; i < 16; i++) + { + buffer[i] = (byte) i; + } + x[1] = 16; + return true; + }, + x => + { + x[1] = 0; + return false; + }); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Scan/ScanErrorHandling.cs b/NAPS2.Sdk.Tests/Scan/ScanErrorHandling.cs index 6fe7f00d71..dd6735c592 100644 --- a/NAPS2.Sdk.Tests/Scan/ScanErrorHandling.cs +++ b/NAPS2.Sdk.Tests/Scan/ScanErrorHandling.cs @@ -1,7 +1,7 @@ -using Moq; using NAPS2.Scan; using NAPS2.Scan.Internal; using NAPS2.Sdk.Tests.Mocks; +using NSubstitute; using Xunit; namespace NAPS2.Sdk.Tests.Scan; @@ -11,11 +11,11 @@ public class ScanErrorHandling : ContextualTests [Fact] public void Scan_InvalidOptions() { - var localPostProcessor = new Mock(); - var bridgeFactory = new Mock(); + var localPostProcessor = Substitute.For(); + var bridgeFactory = Substitute.For(); var controller = - new ScanController(ScanningContext, localPostProcessor.Object, new ScanOptionsValidator(), - bridgeFactory.Object); + new ScanController(ScanningContext, localPostProcessor, new ScanOptionsValidator(), + bridgeFactory); var invalidOptions = new ScanOptions { Dpi = -1 }; Assert.Throws(() => controller.Scan(invalidOptions)); @@ -24,90 +24,89 @@ public void Scan_InvalidOptions() [Fact] public async Task GetDeviceList_InvalidOptions() { - var localPostProcessor = new Mock(); - var bridgeFactory = new Mock(); + var localPostProcessor = Substitute.For(); + var bridgeFactory = Substitute.For(); var controller = - new ScanController(ScanningContext, localPostProcessor.Object, new ScanOptionsValidator(), - bridgeFactory.Object); + new ScanController(ScanningContext, localPostProcessor, new ScanOptionsValidator(), + bridgeFactory); var invalidOptions = new ScanOptions { Dpi = -1 }; await Assert.ThrowsAsync(() => controller.GetDeviceList(invalidOptions)); } [Fact] - public async void Scan_CreateScanBridge() + public async Task Scan_CreateScanBridge() { - var localPostProcessor = new Mock(); - var bridgeFactory = new Mock(); + var localPostProcessor = Substitute.For(); + var bridgeFactory = Substitute.For(); var controller = - new ScanController(ScanningContext, localPostProcessor.Object, new ScanOptionsValidator(), - bridgeFactory.Object); - controller.PropagateErrors = true; + new ScanController(ScanningContext, localPostProcessor, new ScanOptionsValidator(), + bridgeFactory); - bridgeFactory.Setup(factory => factory.Create(It.IsAny())).Throws(); - var source = controller.Scan(new ScanOptions { Device = new ScanDevice("foo", "bar") }); + bridgeFactory.When(factory => factory.Create(Arg.Any())) + .Do(_ => throw new InvalidOperationException()); + var source = controller.Scan(new ScanOptions { Device = new ScanDevice(Driver.Default, "foo", "bar") }); await Assert.ThrowsAsync(async () => await source.ToListAsync()); } [Fact] public async Task GetDeviceList_CreateScanBridge() { - var localPostProcessor = new Mock(); - var bridgeFactory = new Mock(); + var localPostProcessor = Substitute.For(); + var bridgeFactory = Substitute.For(); var controller = - new ScanController(ScanningContext, localPostProcessor.Object, new ScanOptionsValidator(), - bridgeFactory.Object); + new ScanController(ScanningContext, localPostProcessor, new ScanOptionsValidator(), + bridgeFactory); - bridgeFactory.Setup(factory => factory.Create(It.IsAny())).Throws(); + bridgeFactory.When(factory => factory.Create(Arg.Any())) + .Do(_ => throw new InvalidOperationException()); await Assert.ThrowsAsync(() => controller.GetDeviceList(new ScanOptions())); } [Fact] public async Task GetDeviceList_BridgeGetDeviceList() { - var localPostProcessor = new Mock(); - var bridge = new StubScanBridge { Error = new InvalidOperationException() }; - var bridgeFactory = new Mock(); + var localPostProcessor = Substitute.For(); + var bridge = new MockScanBridge { Error = new InvalidOperationException() }; + var bridgeFactory = Substitute.For(); var controller = - new ScanController(ScanningContext, localPostProcessor.Object, new ScanOptionsValidator(), - bridgeFactory.Object); + new ScanController(ScanningContext, localPostProcessor, new ScanOptionsValidator(), + bridgeFactory); - bridgeFactory.Setup(factory => factory.Create(It.IsAny())).Returns(bridge); + bridgeFactory.Create(Arg.Any()).Returns(bridge); await Assert.ThrowsAsync(() => controller.GetDeviceList(new ScanOptions())); } [Fact] - public async void Scan_LocalPostProcess() + public async Task Scan_LocalPostProcess() { - var localPostProcessor = new Mock(); - var bridge = new StubScanBridge { MockOutput = new List { CreateScannedImage() } }; - var bridgeFactory = new Mock(); + var localPostProcessor = Substitute.For(); + var bridge = new MockScanBridge { MockOutput = [CreateScannedImage()] }; + var bridgeFactory = Substitute.For(); var controller = - new ScanController(ScanningContext, localPostProcessor.Object, new ScanOptionsValidator(), - bridgeFactory.Object); - controller.PropagateErrors = true; - - bridgeFactory.Setup(factory => factory.Create(It.IsAny())).Returns(bridge); - localPostProcessor.Setup(pp => - pp.PostProcess(It.IsAny(), It.IsAny(), It.IsAny())) - .Throws(); - var source = controller.Scan(new ScanOptions { Device = new ScanDevice("foo", "bar") }); + new ScanController(ScanningContext, localPostProcessor, new ScanOptionsValidator(), + bridgeFactory); + + bridgeFactory.Create(Arg.Any()).Returns(bridge); + localPostProcessor.When(pp => + pp.PostProcess(Arg.Any(), Arg.Any(), Arg.Any())) + .Do(_ => throw new InvalidOperationException()); + var source = controller.Scan(new ScanOptions { Device = new ScanDevice(Driver.Default, "foo", "bar") }); await Assert.ThrowsAsync(async () => await source.ToListAsync()); } [Fact] - public async void Scan_BridgeScan() + public async Task Scan_BridgeScan() { - var localPostProcessor = new Mock(); - var bridge = new StubScanBridge { Error = new InvalidOperationException() }; - var bridgeFactory = new Mock(); + var localPostProcessor = Substitute.For(); + var bridge = new MockScanBridge { Error = new InvalidOperationException() }; + var bridgeFactory = Substitute.For(); var controller = - new ScanController(ScanningContext, localPostProcessor.Object, new ScanOptionsValidator(), - bridgeFactory.Object); - controller.PropagateErrors = true; + new ScanController(ScanningContext, localPostProcessor, new ScanOptionsValidator(), + bridgeFactory); - bridgeFactory.Setup(factory => factory.Create(It.IsAny())).Returns(bridge); - var source = controller.Scan(new ScanOptions { Device = new ScanDevice("foo", "bar") }); + bridgeFactory.Create(Arg.Any()).Returns(bridge); + var source = controller.Scan(new ScanOptions { Device = new ScanDevice(Driver.Default, "foo", "bar") }); await Assert.ThrowsAsync(async () => await source.ToListAsync()); } diff --git a/NAPS2.Sdk.Tests/Scan/ScanningContextTests.cs b/NAPS2.Sdk.Tests/Scan/ScanningContextTests.cs index 72e2d2e350..7b446234d0 100644 --- a/NAPS2.Sdk.Tests/Scan/ScanningContextTests.cs +++ b/NAPS2.Sdk.Tests/Scan/ScanningContextTests.cs @@ -1,5 +1,5 @@ -using Moq; using NAPS2.Scan; +using NSubstitute; using Xunit; namespace NAPS2.Sdk.Tests.Scan; @@ -10,39 +10,39 @@ public class ScanningContextTests : ContextualTests public void CreateAndNormallyDisposeImages() { var context = new ScanningContext(TestImageContextFactory.Get()); - var storage1 = new Mock(); - var storage2 = new Mock(); - var image1 = context.CreateProcessedImage(storage1.Object); - var image2 = context.CreateProcessedImage(storage2.Object); + var storage1 = Substitute.For(); + var storage2 = Substitute.For(); + var image1 = context.CreateProcessedImage(storage1); + var image2 = context.CreateProcessedImage(storage2); - storage1.VerifyNoOtherCalls(); + storage1.DidNotReceive().Dispose(); image1.Dispose(); - storage1.Verify(x => x.Dispose()); - storage2.VerifyNoOtherCalls(); + storage1.Received().Dispose(); + storage2.DidNotReceive().Dispose(); image2.Dispose(); - storage2.Verify(x => x.Dispose()); + storage2.Received().Dispose(); } [Fact] public void CreateAndDisposeImagesByDisposingContext() { var context = new ScanningContext(TestImageContextFactory.Get()); - var storage1 = new Mock(); - var storage2 = new Mock(); + var storage1 = Substitute.For(); + var storage2 = Substitute.For(); // Create a simple image - context.CreateProcessedImage(storage1.Object); + context.CreateProcessedImage(storage1); // Create an image, clone it a couple times, and dispose the original // The underling storage shouldn't be disposed yet due to refcounting // This tests the interaction between refcounting and context disposal - var imageToClone = context.CreateProcessedImage(storage2.Object); + var imageToClone = context.CreateProcessedImage(storage2); imageToClone.Clone(); imageToClone.Clone(); imageToClone.Dispose(); - storage1.VerifyNoOtherCalls(); - storage2.VerifyNoOtherCalls(); + storage1.DidNotReceive().Dispose(); + storage2.DidNotReceive().Dispose(); context.Dispose(); - storage1.Verify(x => x.Dispose()); - storage2.Verify(x => x.Dispose()); + storage1.Received().Dispose(); + storage2.Received().Dispose(); } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Scan/TwainImageProcessorTests.cs b/NAPS2.Sdk.Tests/Scan/TwainImageProcessorTests.cs new file mode 100644 index 0000000000..e04b7a1574 --- /dev/null +++ b/NAPS2.Sdk.Tests/Scan/TwainImageProcessorTests.cs @@ -0,0 +1,417 @@ +using Google.Protobuf; +using NAPS2.Remoting.Worker; +using NAPS2.Scan; +using NAPS2.Scan.Internal; +using NAPS2.Scan.Internal.Twain; +using NAPS2.Sdk.Tests.Asserts; +using NSubstitute; +using NTwain.Data; +using Xunit; + +namespace NAPS2.Sdk.Tests.Scan; + +public class TwainImageProcessorTests : ContextualTests +{ + private static readonly (int, int, int) RED = (0xFF, 0, 0); + private static readonly (int, int, int) GREEN = (0, 0xFF, 0); + private static readonly (int, int, int) BLUE = (0, 0, 0xFF); + private static readonly (int, int, int) WHITE = (0xFF, 0xFF, 0xFF); + private static readonly (int, int, int) BLACK = (0, 0, 0); + private static readonly (int, int, int) GRAY = (0x80, 0x80, 0x80); + private static readonly (int, int, int) LIGHT_GRAY = (0xD3, 0xD3, 0xD3); + + private readonly IScanEvents _scanEvents; + private readonly Action _callback; + private readonly TwainImageProcessor _processor; + private readonly List _images; + + public TwainImageProcessorTests() + { + _scanEvents = Substitute.For(); + _callback = Substitute.For>(); + + _images = []; + _callback.When(x => x(Arg.Any())) + .Do(x => _images.Add((IMemoryImage) x[0])); + + _processor = new TwainImageProcessor(ScanningContext, new ScanOptions(), _scanEvents, _callback); + } + + [Fact] + public void SingleImageSingleBuffer() + { + _processor.PageStart(new TwainPageStart + { + ImageData = CreateColorImageData(2, 2) + }); + _processor.MemoryBufferTransferred(new TwainMemoryBuffer + { + Buffer = ByteString.CopyFrom( + 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0x00, + 0x00, 0x00, + 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, + 0x00, 0x00), + Columns = 2, + Rows = 2, + BytesPerRow = 8, + XOffset = 0, + YOffset = 0 + }); + _processor.Flush(); + + Assert.Single(_images); + ImageAsserts.PixelColors(_images[0], new() + { + { (0, 0), RED }, + { (1, 0), GREEN }, + { (0, 1), BLUE }, + { (1, 1), WHITE }, + }); + } + + [Fact] + public void SingleImageTwoBuffers() + { + _processor.PageStart(new TwainPageStart + { + ImageData = CreateColorImageData(2, 2) + }); + _processor.MemoryBufferTransferred(new TwainMemoryBuffer + { + Buffer = ByteString.CopyFrom( + 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0x00, + 0x00, 0x00), + Columns = 2, + Rows = 1, + BytesPerRow = 8, + XOffset = 0, + YOffset = 0 + }); + _processor.MemoryBufferTransferred(new TwainMemoryBuffer + { + Buffer = ByteString.CopyFrom( + 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, + 0x00, 0x00), + Columns = 2, + Rows = 1, + BytesPerRow = 8, + XOffset = 0, + YOffset = 1 + }); + _processor.Flush(); + + Assert.Single(_images); + ImageAsserts.PixelColors(_images[0], new() + { + { (0, 0), RED }, + { (1, 0), GREEN }, + { (0, 1), BLUE }, + { (1, 1), WHITE }, + }); + } + + [Fact] + public void MultipleImages() + { + _processor.PageStart(new TwainPageStart + { + ImageData = CreateColorImageData(2, 2) + }); + _processor.MemoryBufferTransferred(new TwainMemoryBuffer + { + Buffer = ByteString.CopyFrom( + 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0x00, + 0x00, 0x00, + 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, + 0x00, 0x00), + Columns = 2, + Rows = 2, + BytesPerRow = 8, + XOffset = 0, + YOffset = 0 + }); + _processor.PageStart(new TwainPageStart + { + ImageData = CreateColorImageData(2, 2) + }); + _processor.MemoryBufferTransferred(new TwainMemoryBuffer + { + Buffer = ByteString.CopyFrom( + 0x00, 0x00, 0x00, + 0x80, 0x80, 0x80, + 0x00, 0x00, + 0xD3, 0xD3, 0xD3, + 0xFF, 0xFF, 0xFF, + 0x00, 0x00), + Columns = 2, + Rows = 2, + BytesPerRow = 8, + XOffset = 0, + YOffset = 0 + }); + _processor.Flush(); + + Assert.Equal(2, _images.Count); + ImageAsserts.PixelColors(_images[0], new() + { + { (0, 0), RED }, + { (1, 0), GREEN }, + { (0, 1), BLUE }, + { (1, 1), WHITE }, + }); + ImageAsserts.PixelColors(_images[1], new() + { + { (0, 0), BLACK }, + { (1, 0), GRAY }, + { (0, 1), LIGHT_GRAY }, + { (1, 1), WHITE }, + }); + } + + [Fact] + public void SingleImageTooSmall() + { + _processor.PageStart(new TwainPageStart + { + ImageData = CreateColorImageData(3, 3) + }); + _processor.MemoryBufferTransferred(new TwainMemoryBuffer + { + Buffer = ByteString.CopyFrom( + 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0x00, + 0x00, 0x00, + 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, + 0x00, 0x00), + Columns = 2, + Rows = 2, + BytesPerRow = 8, + XOffset = 0, + YOffset = 0 + }); + _processor.Flush(); + + Assert.Single(_images); + Assert.Equal(2, _images[0].Width); + Assert.Equal(2, _images[0].Height); + ImageAsserts.PixelColors(_images[0], new() + { + { (0, 0), RED }, + { (1, 0), GREEN }, + { (0, 1), BLUE }, + { (1, 1), WHITE }, + }); + } + + [Fact] + public void SingleImageTooBig() + { + _processor.PageStart(new TwainPageStart + { + ImageData = CreateColorImageData(1, 1) + }); + _processor.MemoryBufferTransferred(new TwainMemoryBuffer + { + Buffer = ByteString.CopyFrom( + 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0x00, + 0x00, 0x00, + 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, + 0x00, 0x00), + Columns = 2, + Rows = 2, + BytesPerRow = 8, + XOffset = 0, + YOffset = 0 + }); + _processor.Flush(); + + Assert.Single(_images); + Assert.Equal(2, _images[0].Width); + Assert.Equal(2, _images[0].Height); + ImageAsserts.PixelColors(_images[0], new() + { + { (0, 0), RED }, + { (1, 0), GREEN }, + { (0, 1), BLUE }, + { (1, 1), WHITE }, + }); + } + + [Fact] + public void MultipleImagesWrongSize() + { + _processor.PageStart(new TwainPageStart + { + ImageData = CreateColorImageData(3, 3) + }); + _processor.MemoryBufferTransferred(new TwainMemoryBuffer + { + Buffer = ByteString.CopyFrom( + 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0x00, + 0x00, 0x00, + 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, + 0x00, 0x00), + Columns = 2, + Rows = 2, + BytesPerRow = 8, + XOffset = 0, + YOffset = 0 + }); + _processor.PageStart(new TwainPageStart + { + ImageData = CreateColorImageData(1, 1) + }); + _processor.MemoryBufferTransferred(new TwainMemoryBuffer + { + Buffer = ByteString.CopyFrom( + 0x00, 0x00, 0x00, + 0x80, 0x80, 0x80, + 0x00, 0x00, + 0xD3, 0xD3, 0xD3, + 0xFF, 0xFF, 0xFF, + 0x00, 0x00), + Columns = 2, + Rows = 2, + BytesPerRow = 8, + XOffset = 0, + YOffset = 0 + }); + _processor.Flush(); + + Assert.Equal(2, _images.Count); + Assert.Equal(2, _images[0].Width); + Assert.Equal(2, _images[0].Height); + ImageAsserts.PixelColors(_images[0], new() + { + { (0, 0), RED }, + { (1, 0), GREEN }, + { (0, 1), BLUE }, + { (1, 1), WHITE }, + }); + Assert.Equal(2, _images[1].Width); + Assert.Equal(2, _images[1].Height); + ImageAsserts.PixelColors(_images[1], new() + { + { (0, 0), BLACK }, + { (1, 0), GRAY }, + { (0, 1), LIGHT_GRAY }, + { (1, 1), WHITE }, + }); + } + + [Fact] + public void DisposeFlushesWithFullImage() + { + _processor.PageStart(new TwainPageStart + { + ImageData = CreateColorImageData(2, 2) + }); + _processor.MemoryBufferTransferred(new TwainMemoryBuffer + { + Buffer = ByteString.CopyFrom( + 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0x00, + 0x00, 0x00, + 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, + 0x00, 0x00), + Columns = 2, + Rows = 2, + BytesPerRow = 8, + XOffset = 0, + YOffset = 0 + }); + _processor.Dispose(); + + Assert.Single(_images); + ImageAsserts.PixelColors(_images[0], new() + { + { (0, 0), RED }, + { (1, 0), GREEN }, + { (0, 1), BLUE }, + { (1, 1), WHITE }, + }); + } + + [Fact] + public void DisposeDoesntFlushWithPartialImage() + { + _processor.PageStart(new TwainPageStart + { + ImageData = CreateColorImageData(2, 2) + }); + _processor.MemoryBufferTransferred(new TwainMemoryBuffer + { + Buffer = ByteString.CopyFrom( + 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0x00, + 0x00, 0x00), + Columns = 2, + Rows = 1, + BytesPerRow = 8, + XOffset = 0, + YOffset = 0 + }); + _processor.Dispose(); + + Assert.Empty(_images); + } + + [Fact] + public void ZeroWidthAndHeightInImageData() + { + _processor.PageStart(new TwainPageStart + { + ImageData = CreateColorImageData(0, 0) + }); + _processor.MemoryBufferTransferred(new TwainMemoryBuffer + { + Buffer = ByteString.CopyFrom( + 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0x00, + 0x00, 0x00, + 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, + 0x00, 0x00), + Columns = 2, + Rows = 2, + BytesPerRow = 8, + XOffset = 0, + YOffset = 0 + }); + _processor.Flush(); + + Assert.Single(_images); + ImageAsserts.PixelColors(_images[0], new() + { + { (0, 0), RED }, + { (1, 0), GREEN }, + { (0, 1), BLUE }, + { (1, 1), WHITE }, + }); + } + + private static TwainImageData CreateColorImageData(int width, int height) + { + return new TwainImageData + { + Height = height, + Width = width, + PixelType = (int) PixelType.RGB, + BitsPerPixel = 24, + BitsPerSample = { 8, 8, 8 }, + SamplesPerPixel = 3 + }; + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Scan/TwainMemoryBufferReaderTests.cs b/NAPS2.Sdk.Tests/Scan/TwainMemoryBufferReaderTests.cs index 2cc00c1e33..307f07c07e 100644 --- a/NAPS2.Sdk.Tests/Scan/TwainMemoryBufferReaderTests.cs +++ b/NAPS2.Sdk.Tests/Scan/TwainMemoryBufferReaderTests.cs @@ -339,7 +339,7 @@ public void Invalid24BitSamples() Width = 2, PixelType = (int) PixelType.RGB, BitsPerPixel = 24, - BitsPerSample = { 8, 7, 9 }, + BitsPerSample = { 7, 8, 9 }, SamplesPerPixel = 3 }; var image = Create24BitImage(2, 2); diff --git a/NAPS2.Sdk.Tests/Serialization/XmlSerializerTests.cs b/NAPS2.Sdk.Tests/Serialization/XmlSerializerTests.cs index a0efff9611..b8086584da 100644 --- a/NAPS2.Sdk.Tests/Serialization/XmlSerializerTests.cs +++ b/NAPS2.Sdk.Tests/Serialization/XmlSerializerTests.cs @@ -1,4 +1,6 @@ using System.Collections.Immutable; +using System.Globalization; +using System.Threading; using NAPS2.Serialization; using Xunit; using XmlElementAttribute = System.Xml.Serialization.XmlElementAttribute; @@ -30,6 +32,21 @@ public void SerializePoco() Assert.Equal(42, copy.Int); } + [Fact] + public void DeserializeWithUnknownElement() + { + var serializer = new XmlSerializer(); + + var doc = new XDocument(new XElement("Poco", + new XElement("Str", "Hello world"), + new XElement("Int", "42"), + new XElement("doesnotexist", "foobar"))); + + var poco = serializer.DeserializeFromXDocument(doc); + Assert.Equal("Hello world", poco!.Str); + Assert.Equal(42, poco.Int); + } + [Fact] public void SerializePrivateSetter() { @@ -163,6 +180,59 @@ public void SerializeUnknownType() Assert.Equal("A", copy[1]); } + [Fact] + public void SerializeNumbers() + { + var original = new Numbers + { + Int = -1_000_000, + Double = -1.2345e20, + Decimal = -1_000.2345m + }; + var serializer = new XmlSerializer(); + var doc = serializer.SerializeToXDocument(original); + + Assert.NotNull(doc.Root); + Assert.Equal("Numbers", doc.Root.Name); + Assert.Equal(3, doc.Root.Elements().Count()); + + var copy = serializer.DeserializeFromXDocument(doc); + Assert.Equal(original.Int, copy.Int); + Assert.Equal(original.Double, copy.Double); + Assert.Equal(original.Decimal, copy.Decimal); + } + + [Fact] + public void SerializeNumbersWithLocale() + { + var originalCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR"); + try + { + var original = new Numbers + { + Int = -1_000_000, + Double = -1.2345e20, + Decimal = -1_000.2345m + }; + var serializer = new XmlSerializer(); + var doc = serializer.SerializeToXDocument(original); + + Assert.NotNull(doc.Root); + Assert.Equal("Numbers", doc.Root.Name); + Assert.Equal(3, doc.Root.Elements().Count()); + + var copy = serializer.DeserializeFromXDocument(doc); + Assert.Equal(original.Int, copy.Int); + Assert.Equal(original.Double, copy.Double); + Assert.Equal(original.Decimal, copy.Decimal); + } + finally + { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + [Fact] public void SerializeList() { @@ -190,7 +260,8 @@ public void SerializeImmutableList() [Fact] public void SerializeImmutableHashSet() { - VerifySerializeCollection(ImmutableHashSet.Create(new Poco { Str = "Hello" }, new Poco { Str = "World" }), true); + VerifySerializeCollection(ImmutableHashSet.Create(new Poco { Str = "Hello" }, new Poco { Str = "World" }), + true); } private void VerifySerializeCollection(T original, bool unordered = false) where T : IEnumerable @@ -305,7 +376,6 @@ public XmlSerializerControl(Type[] knownTypes) } // TODO: Custom serialization - // TODO: Ordering public class NestedPoco { @@ -334,6 +404,15 @@ public class PocoTypes : CustomXmlTypes protected override Type[] GetKnownTypes() => new[] { typeof(PocoSubtype) }; } + public class Numbers + { + public int Int { get; set; } + + public double Double { get; set; } + + public decimal Decimal { get; set; } + } + public class PrivateSetter { public PrivateSetter() @@ -357,10 +436,10 @@ public class OrderedProps { [XmlElement(Order = 1)] public string A { get; set; } - + [XmlElement(Order = 4)] public string B { get; set; } - + [XmlElement(Order = 2)] public string C { get; set; } diff --git a/NAPS2.Sdk.Tests/StorageConfig.cs b/NAPS2.Sdk.Tests/StorageConfig.cs index fa193b9f71..1720e833dd 100644 --- a/NAPS2.Sdk.Tests/StorageConfig.cs +++ b/NAPS2.Sdk.Tests/StorageConfig.cs @@ -33,8 +33,7 @@ public class File : StorageConfig { public override void Apply(ContextualTests ctx) { - var recoveryPath = Path.Combine(ctx.FolderPath, "recovery"); - ctx.ScanningContext.FileStorageManager = FileStorageManager.CreateFolder(recoveryPath); + ctx.SetUpFileStorage(); } public override void AssertPdfStorage(IImageStorage storage) diff --git a/NAPS2.Sdk.Tests/TestExtensions.cs b/NAPS2.Sdk.Tests/TestExtensions.cs new file mode 100644 index 0000000000..b563cd4493 --- /dev/null +++ b/NAPS2.Sdk.Tests/TestExtensions.cs @@ -0,0 +1,12 @@ +using NSubstitute; +using Xunit; + +namespace NAPS2.Sdk.Tests; + +public static class TestExtensions +{ + public static void ReceivedCallsCount(this T substitute, int count) where T : class + { + Assert.Equal(count, substitute.ReceivedCalls().Count()); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/TestImageContextFactory.cs b/NAPS2.Sdk.Tests/TestImageContextFactory.cs index 90d34d6b35..fe0c56da7e 100644 --- a/NAPS2.Sdk.Tests/TestImageContextFactory.cs +++ b/NAPS2.Sdk.Tests/TestImageContextFactory.cs @@ -2,17 +2,29 @@ namespace NAPS2.Sdk.Tests; public static class TestImageContextFactory { - public static ImageContext Get(IPdfRenderer pdfRenderer = null) + public static ImageContext Get() { + return Environment.GetEnvironmentVariable("NAPS2_TEST_IMAGES") switch + { +#if WINDOWS + "gdi" => new NAPS2.Images.Gdi.GdiImageContext(), + "wpf" => new NAPS2.Images.Wpf.WpfImageContext(), +#endif + "is" or "imagesharp" => new NAPS2.Images.ImageSharp.ImageSharpImageContext(), #if MAC - return new NAPS2.Images.Mac.MacImageContext(pdfRenderer); + "mac" => new NAPS2.Images.Mac.MacImageContext(), +#endif +#if LINUX + "gtk" or "gdk" or "linux" => new NAPS2.Images.Gtk.GtkImageContext(), +#endif + _ => +#if MAC + new NAPS2.Images.Mac.MacImageContext() #elif LINUX - return new NAPS2.Images.Gtk.GtkImageContext(pdfRenderer); + new NAPS2.Images.Gtk.GtkImageContext() #else -#if NET6_0_OR_GREATER - if (!OperatingSystem.IsWindowsVersionAtLeast(7)) throw new InvalidOperationException(); -#endif - return new NAPS2.Images.Gdi.GdiImageContext(pdfRenderer); + new NAPS2.Images.Gdi.GdiImageContext() #endif + }; } } \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Util/IsExternalInit.cs b/NAPS2.Sdk.Tests/Util/IsExternalInit.cs deleted file mode 100644 index 0993ddec14..0000000000 --- a/NAPS2.Sdk.Tests/Util/IsExternalInit.cs +++ /dev/null @@ -1,5 +0,0 @@ -// https://sergiopedri.medium.com/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb -// ReSharper disable once CheckNamespace -namespace System.Runtime.CompilerServices; - -internal static class IsExternalInit {} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Util/TimedCacheTests.cs b/NAPS2.Sdk.Tests/Util/TimedCacheTests.cs new file mode 100644 index 0000000000..cac4412616 --- /dev/null +++ b/NAPS2.Sdk.Tests/Util/TimedCacheTests.cs @@ -0,0 +1,122 @@ +using Xunit; + +namespace NAPS2.Sdk.Tests.Util; + +public class TimedCacheTests +{ + private readonly TimedCache _cache; + private DateTime _now = DateTime.MinValue; + private char _nextItemName = 'a'; + + public TimedCacheTests() + { + _cache = new() { NowSource = () => _now }; + } + + [Fact] + public void CachesItem() + { + var itemRef1 = _cache.UseCacheItem("a", CreateItem); + var item1 = itemRef1.Value; + var itemRef2 = _cache.UseCacheItem("a", CreateItem); + var item2 = itemRef2.Value; + + Assert.Equal(item1, item2); + } + + [Fact] + public void PreservesItemWithinExpiry() + { + var itemRef1 = _cache.UseCacheItem("a", CreateItem); + var item1 = itemRef1.Value; + + itemRef1.Dispose(); + _now += TimeSpan.FromSeconds(1); + _cache.ExpireItems(); + + Assert.False(item1.IsDisposed); + + _now += TimeSpan.FromSeconds(5); + _cache.ExpireItems(); + + Assert.True(item1.IsDisposed); + } + + [Fact] + public void PreservesItemWithActiveReferences() + { + var itemRef1 = _cache.UseCacheItem("a", CreateItem); + var item1 = itemRef1.Value; + var itemRef2 = _cache.UseCacheItem("a", CreateItem); + var item2 = itemRef2.Value; + Assert.Equal(item1, item2); + + itemRef1.Dispose(); + _now += TimeSpan.FromSeconds(10); + _cache.ExpireItems(); + + Assert.False(item1.IsDisposed); + + itemRef2.Dispose(); + _now += TimeSpan.FromSeconds(10); + _cache.ExpireItems(); + + Assert.True(item1.IsDisposed); + } + + [Fact] + public void MultipleItems() + { + var itemRef1 = _cache.UseCacheItem("a", CreateItem); + var item1 = itemRef1.Value; + var itemRef2 = _cache.UseCacheItem("b", CreateItem); + var item2 = itemRef2.Value; + + Assert.Equal("a", item1.Name); + Assert.Equal("b", item2.Name); + + itemRef1.Dispose(); + _now += TimeSpan.FromSeconds(10); + _cache.ExpireItems(); + + Assert.True(item1.IsDisposed); + Assert.False(item2.IsDisposed); + + itemRef2.Dispose(); + _now += TimeSpan.FromSeconds(10); + _cache.ExpireItems(); + + Assert.True(item1.IsDisposed); + Assert.True(item2.IsDisposed); + } + + [Fact] + public void ActiveReferenceRefreshesExpiry() + { + var itemRef1 = _cache.UseCacheItem("a", CreateItem); + var item1 = itemRef1.Value; + + _now += TimeSpan.FromSeconds(10); + itemRef1.Dispose(); + _cache.ExpireItems(); + + Assert.False(item1.IsDisposed); + + _now += TimeSpan.FromSeconds(10); + _cache.ExpireItems(); + + Assert.True(item1.IsDisposed); + } + + private MyItem CreateItem() + { + return new MyItem { Name = (_nextItemName++).ToString() }; + } + + private class MyItem : IDisposable + { + public required string Name { get; set; } + public bool IsDisposed { get; private set; } + public void Dispose() => IsDisposed = true; + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Worker/WorkerChannelTests.cs b/NAPS2.Sdk.Tests/Worker/WorkerChannelTests.cs index 7e2e9ea3ea..815b97f35b 100644 --- a/NAPS2.Sdk.Tests/Worker/WorkerChannelTests.cs +++ b/NAPS2.Sdk.Tests/Worker/WorkerChannelTests.cs @@ -1,6 +1,5 @@ using System.Threading; using GrpcDotNetNamedPipes; -using Moq; using NAPS2.ImportExport.Email.Mapi; using NAPS2.ImportExport.Images; using NAPS2.Remoting.Worker; @@ -8,22 +7,21 @@ using NAPS2.Scan.Exceptions; using NAPS2.Scan.Internal; using NAPS2.Scan.Internal.Twain; +using NSubstitute; using Xunit; namespace NAPS2.Sdk.Tests.Worker; -// TODO: Should these tests really be disabled on non-windows? Not sure if we still need the worker for mac/linux -// Although in that case we really need to test GrpcDotNetNamedPipes for mac/linux. public class WorkerChannelTests : ContextualTests { private Channel Start(IRemoteScanController remoteScanController = null, ThumbnailRenderer thumbnailRenderer = null, - IMapiWrapper mapiWrapper = null, ITwainSessionController twainSessionController = null) + IMapiWrapper mapiWrapper = null, ITwainController twainController = null) { string pipeName = $"WorkerNamedPipeTests.{Path.GetRandomFileName()}"; NamedPipeServer server = new NamedPipeServer(pipeName); WorkerService.BindService(server.ServiceBinder, new WorkerServiceImpl(ScanningContext, remoteScanController, thumbnailRenderer, mapiWrapper, - twainSessionController, new ImportPostProcessor())); + twainController)); server.Start(); var client = new WorkerServiceAdapter(new NamedPipeChannel(".", pipeName)); return new Channel @@ -33,18 +31,7 @@ private Channel Start(IRemoteScanController remoteScanController = null, Thumbna }; } - // TODO: Move this to a client/server test - // [PlatformFact(include: Platform.Windows)] - // public void SslCreds() - // { - // var (cert, privateKey) = SslHelper.GenerateRootCertificate(); - // var serverCreds = RemotingHelper.GetServerCreds(cert, privateKey); - // var clientCreds = RemotingHelper.GetClientCreds(cert, privateKey); - // using var channel = Start(serverCreds: serverCreds, clientCreds: clientCreds); - // channel.Client.Init(null); - // } - - [PlatformFact(include: PlatformFlags.Windows)] + [Fact] public void Init() { using var channel = Start(); @@ -52,41 +39,48 @@ public void Init() Assert.StartsWith(@"C:\Somewhere", ScanningContext.FileStorageManager.NextFilePath()); } - [PlatformFact(include: PlatformFlags.Windows)] + [Fact] public void Wia10NativeUi() { // TODO: This is not testable yet // channel.Client.Wia10NativeUI(...); } - [PlatformFact(include: PlatformFlags.Windows)] - public async Task GetDeviceList() + [Fact] + public async Task GetDevices() { - var remoteScanController = new Mock(); - using var channel = Start(remoteScanController.Object); - remoteScanController - .Setup(rsc => rsc.GetDeviceList(It.IsAny())) - .ReturnsAsync(new List { new ScanDevice("test_id", "test_name") }); + var remoteScanController = Substitute.For(); + using var channel = Start(remoteScanController); + remoteScanController.GetDevices(Arg.Any(), Arg.Any(), + Arg.Any>()) + .Returns(x => + { + var callback = (Action) x[2]; + callback(new ScanDevice(Driver.Wia, "test_id", "test_name")); + return Task.CompletedTask; + }); - var deviceList = await channel.Client.GetDeviceList(new ScanOptions()); + var deviceList = new List(); + await channel.Client.GetDevices(new ScanOptions(), CancellationToken.None, deviceList.Add); Assert.Single(deviceList); Assert.Equal("test_id", deviceList[0].ID); Assert.Equal("test_name", deviceList[0].Name); - remoteScanController.Verify(rsc => rsc.GetDeviceList(It.IsAny())); - remoteScanController.VerifyNoOtherCalls(); + _ = remoteScanController.Received().GetDevices(Arg.Any(), Arg.Any(), + Arg.Any>()); + remoteScanController.ReceivedCallsCount(1); } - [PlatformFact(include: PlatformFlags.Windows)] + [Fact] public async Task ScanWithMemoryStorage() { await ScanInternalTest(); } - [PlatformFact(include: PlatformFlags.Windows)] + [Fact] public async Task ScanWithFileStorage() { - ScanningContext.FileStorageManager = FileStorageManager.CreateFolder(Path.Combine(FolderPath, "recovery")); + SetUpFileStorage(); await ScanInternalTest(); } @@ -94,11 +88,11 @@ private async Task ScanInternalTest() { var remoteScanController = new MockRemoteScanController { - Images = new List - { + Images = + [ CreateScannedImage(), CreateScannedImage() - } + ] }; using var channel = Start(remoteScanController); @@ -114,16 +108,16 @@ await channel.Client.Scan( // TODO: Verify that thumbnails are set correctly (with and without revertible transforms) } - [PlatformFact(include: PlatformFlags.Windows)] + [Fact] public async Task ScanException() { var remoteScanController = new MockRemoteScanController { - Images = new List - { + Images = + [ CreateScannedImage(), CreateScannedImage() - }, + ], Exception = new DeviceException("Test error") }; using var channel = Start(remoteScanController); @@ -137,41 +131,47 @@ public async Task ScanException() Assert.Contains("Test error", ex.Message); } - [PlatformFact(include: PlatformFlags.Windows)] + [Fact] public async Task TwainScan() { - var twainEvents = new Mock(); - var sessionController = new Mock(); + var twainEvents = Substitute.For(); + var twainController = Substitute.For(); - sessionController.Setup(x => - x.StartScan(It.IsAny(), It.IsAny(), It.IsAny())).Returns( - new InvocationFunc(ctx => - { - var serverTwainEvents = (ITwainEvents) ctx.Arguments[1]; - serverTwainEvents.PageStart(new TwainPageStart()); - serverTwainEvents.MemoryBufferTransferred(new TwainMemoryBuffer()); - serverTwainEvents.PageStart(new TwainPageStart()); - serverTwainEvents.NativeImageTransferred(new TwainNativeImage()); - return Task.CompletedTask; - })); - - using var channel = Start(twainSessionController: sessionController.Object); - await channel.Client.TwainScan(new ScanOptions(), CancellationToken.None, twainEvents.Object); - - twainEvents.Verify(x => x.PageStart(It.IsAny())); - twainEvents.Verify(x => x.MemoryBufferTransferred(It.IsAny())); - twainEvents.Verify(x => x.PageStart(It.IsAny())); - twainEvents.Verify(x => x.NativeImageTransferred(It.IsAny())); - twainEvents.VerifyNoOtherCalls(); + twainController.StartScan(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns( + x => + { + var serverTwainEvents = (ITwainEvents) x[1]; + serverTwainEvents.PageStart(new TwainPageStart()); + serverTwainEvents.MemoryBufferTransferred(new TwainMemoryBuffer()); + serverTwainEvents.PageStart(new TwainPageStart()); + serverTwainEvents.NativeImageTransferred(new TwainNativeImage()); + return Task.CompletedTask; + }); + + using var channel = Start(twainController: twainController); + await channel.Client.TwainScan(new ScanOptions(), CancellationToken.None, twainEvents); + + twainEvents.Received().PageStart(Arg.Any()); + twainEvents.Received().MemoryBufferTransferred(Arg.Any()); + twainEvents.Received().PageStart(Arg.Any()); + twainEvents.Received().NativeImageTransferred(Arg.Any()); + twainEvents.ReceivedCallsCount(4); } private class MockRemoteScanController : IRemoteScanController { - public List Images { get; set; } = new(); + public List Images { get; set; } = []; public Exception Exception { get; set; } - public Task> GetDeviceList(ScanOptions options) => throw new NotSupportedException(); + public Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback) => + throw new NotSupportedException(); + + public Task GetCaps(ScanOptions options, CancellationToken cancelToken) + { + return null!; + } public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) diff --git a/NAPS2.Sdk.Worker.Build/.gitignore b/NAPS2.Sdk.Worker.Build/.gitignore new file mode 100644 index 0000000000..50815a5c0e --- /dev/null +++ b/NAPS2.Sdk.Worker.Build/.gitignore @@ -0,0 +1,32 @@ +Thumbs.db +*.obj +*.exe +*.pdb +*.user +*.aps +*.pch +*.vspscc +*_i.c +*_p.c +*.ncb +*.suo +*.sln.docstates +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +[Bb]in +[Dd]ebug*/ +*.lib +*.sbr +obj/ +[Rr]elease*/ +_ReSharper*/ +[Tt]est[Rr]esult* +*.vssscc +$tf*/ +publish/ +bin/ +temp/ \ No newline at end of file diff --git a/NAPS2.Sdk.Worker.Build/LICENSE b/NAPS2.Sdk.Worker.Build/LICENSE new file mode 100644 index 0000000000..ad8e231775 --- /dev/null +++ b/NAPS2.Sdk.Worker.Build/LICENSE @@ -0,0 +1,518 @@ +NAPS2.Sdk.Worker +https://www.github.com/cyanfish/naps2/ + +Copyright 2009-2025 NAPS2 Contributors + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/NAPS2.Sdk.Worker.Build/NAPS2.Sdk.Worker.Build.csproj b/NAPS2.Sdk.Worker.Build/NAPS2.Sdk.Worker.Build.csproj new file mode 100644 index 0000000000..475705f738 --- /dev/null +++ b/NAPS2.Sdk.Worker.Build/NAPS2.Sdk.Worker.Build.csproj @@ -0,0 +1,33 @@ + + + + net9 + WinExe + true + NAPS2.Sdk.Worker + NAPS2.Worker + + x86 + + NAPS2.Sdk.Worker + NAPS2.Sdk.Worker + + true + true + true + partial + win-x86 + + + + + + + + + + + + + + diff --git a/NAPS2.Sdk.Worker.Build/Program.cs b/NAPS2.Sdk.Worker.Build/Program.cs new file mode 100644 index 0000000000..45c6707529 --- /dev/null +++ b/NAPS2.Sdk.Worker.Build/Program.cs @@ -0,0 +1,15 @@ +using NAPS2.Images.Gdi; +using NAPS2.Remoting.Worker; +using NAPS2.Scan; + +namespace NAPS2.Sdk.Worker; + +[System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] +public class Program +{ + public static async Task Main() + { + var scanningContext = new ScanningContext(new GdiImageContext()); + await WorkerServer.Run(scanningContext); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Worker.Win32/.gitignore b/NAPS2.Sdk.Worker.Win32/.gitignore new file mode 100644 index 0000000000..50815a5c0e --- /dev/null +++ b/NAPS2.Sdk.Worker.Win32/.gitignore @@ -0,0 +1,32 @@ +Thumbs.db +*.obj +*.exe +*.pdb +*.user +*.aps +*.pch +*.vspscc +*_i.c +*_p.c +*.ncb +*.suo +*.sln.docstates +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +[Bb]in +[Dd]ebug*/ +*.lib +*.sbr +obj/ +[Rr]elease*/ +_ReSharper*/ +[Tt]est[Rr]esult* +*.vssscc +$tf*/ +publish/ +bin/ +temp/ \ No newline at end of file diff --git a/NAPS2.Sdk.Worker.Win32/LICENSE b/NAPS2.Sdk.Worker.Win32/LICENSE new file mode 100644 index 0000000000..ad8e231775 --- /dev/null +++ b/NAPS2.Sdk.Worker.Win32/LICENSE @@ -0,0 +1,518 @@ +NAPS2.Sdk.Worker +https://www.github.com/cyanfish/naps2/ + +Copyright 2009-2025 NAPS2 Contributors + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/NAPS2.Sdk.Worker.Win32/NAPS2.Sdk.Worker.Win32.csproj b/NAPS2.Sdk.Worker.Win32/NAPS2.Sdk.Worker.Win32.csproj new file mode 100644 index 0000000000..b1bcffbcdc --- /dev/null +++ b/NAPS2.Sdk.Worker.Win32/NAPS2.Sdk.Worker.Win32.csproj @@ -0,0 +1,27 @@ + + + + net462;net6;net8 + NAPS2.Sdk.Worker + NAPS2.Sdk.Worker.Win32 + + NAPS2.Sdk.Worker.Win32 + NAPS2.Sdk.Worker.Win32 + Windows 32-bit (x86) worker process for NAPS2.Sdk + naps2 + + + + + + + + + + lib/NAPS2.Worker.exe + true + true + contentFiles + + + diff --git a/NAPS2.Sdk.Worker.Win32/NAPS2.Sdk.Worker.Win32.targets b/NAPS2.Sdk.Worker.Win32/NAPS2.Sdk.Worker.Win32.targets new file mode 100644 index 0000000000..f831e5d72e --- /dev/null +++ b/NAPS2.Sdk.Worker.Win32/NAPS2.Sdk.Worker.Win32.targets @@ -0,0 +1,11 @@ + + + + + NAPS2.Worker.exe + PreserveNewest + + + + diff --git a/NAPS2.Sdk/Dependencies/DownloadFormat.cs b/NAPS2.Sdk/Dependencies/DownloadFormat.cs deleted file mode 100644 index e5ba348695..0000000000 --- a/NAPS2.Sdk/Dependencies/DownloadFormat.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.IO.Compression; - -namespace NAPS2.Dependencies; - -public abstract class DownloadFormat -{ - public static DownloadFormat Gzip = new GzipDownloadFormat(); - - public static DownloadFormat Zip = new ZipDownloadFormat(); - - public abstract string Prepare(string tempFilePath); - - private class GzipDownloadFormat : DownloadFormat - { - public override string Prepare(string tempFilePath) - { - if (!tempFilePath.EndsWith(".gz", StringComparison.InvariantCultureIgnoreCase)) - { - throw new ArgumentException(); - } - var pathWithoutGz = tempFilePath.Substring(0, tempFilePath.Length - 3); - Extract(tempFilePath, pathWithoutGz); - return pathWithoutGz; - } - - private static void Extract(string sourcePath, string destPath) - { - using FileStream inFile = new FileInfo(sourcePath).OpenRead(); - using FileStream outFile = File.Create(destPath); - using GZipStream decompress = new GZipStream(inFile, CompressionMode.Decompress); - decompress.CopyTo(outFile); - } - } - - private class ZipDownloadFormat : DownloadFormat - { - public override string Prepare(string tempFilePath) - { - if (!tempFilePath.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase)) - { - throw new ArgumentException(); - } - - var tempDir = Path.GetDirectoryName(tempFilePath) ?? throw new ArgumentNullException(); - ZipFile.ExtractToDirectory(tempFilePath, tempDir); - File.Delete(tempFilePath); - return tempDir; - } - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Images/Barcode.cs b/NAPS2.Sdk/Images/Barcode.cs new file mode 100644 index 0000000000..bac4ba060b --- /dev/null +++ b/NAPS2.Sdk/Images/Barcode.cs @@ -0,0 +1,18 @@ +namespace NAPS2.Images; + +/// +/// A wrapper around the ZXing library that detects patch-t and other barcodes. +/// http://www.alliancegroup.co.uk/patch-codes.htm +/// +public record Barcode(bool IsDetectionAttempted, bool IsDetected, string? DetectedText) +{ + private const string PATCH_T_TEXT = "PATCHT"; + + public static readonly Barcode NoDetection = new(false, false, null); + + private Barcode() : this(false, false, null) + { + } + + public bool IsPatchT => DetectedText == PATCH_T_TEXT; +} \ No newline at end of file diff --git a/NAPS2.Sdk/Images/BarcodeDetection.cs b/NAPS2.Sdk/Images/BarcodeDetection.cs deleted file mode 100644 index 6e0e64047c..0000000000 --- a/NAPS2.Sdk/Images/BarcodeDetection.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace NAPS2.Images; - -/// -/// A wrapper around the ZXing library that detects patch-t and other barcodes. -/// http://www.alliancegroup.co.uk/patch-codes.htm -/// -public class BarcodeDetection -{ - private const string PATCH_T_TEXT = "PATCHT"; - - public static readonly BarcodeDetection NotAttempted = new BarcodeDetection(false, false, null); - - public BarcodeDetection(bool isAttempted, bool isBarcodePresent, string? detectedText) - { - IsAttempted = isAttempted; - IsBarcodePresent = isBarcodePresent; - DetectedText = detectedText; - } - - private BarcodeDetection() - { - } - - public string? DetectedText { get; } - - public bool IsAttempted { get; } - - public bool IsBarcodePresent { get; } - - public bool IsPatchT => DetectedText == PATCH_T_TEXT; -} \ No newline at end of file diff --git a/NAPS2.Sdk/Images/BlankDetector.cs b/NAPS2.Sdk/Images/BlankDetector.cs deleted file mode 100644 index 5708c39386..0000000000 --- a/NAPS2.Sdk/Images/BlankDetector.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Runtime.InteropServices; - -namespace NAPS2.Images; - -// TODO: Add tests -public static class BlankDetector -{ - // If the pixel value (0-255) >= white_threshold, then it counts as a white pixel. - private const int WHITE_THRESHOLD_MIN = 1; - private const int WHITE_THRESHOLD_MAX = 255; - // If the fraction of non-white pixels > coverage_threshold, then it counts as a non-blank page. - private const double COVERAGE_THRESHOLD_MIN = 0.00; - private const double COVERAGE_THRESHOLD_MAX = 0.01; - - public static bool IsBlank(IMemoryImage image, int whiteThresholdNorm, int coverageThresholdNorm) - { - if (image.PixelFormat == ImagePixelFormat.BW1) - { - using var bitmap2 = image.PerformTransform(new ColorBitDepthTransform()); - return IsBlankRGB(bitmap2, whiteThresholdNorm, coverageThresholdNorm); - } - if (image.PixelFormat != ImagePixelFormat.RGB24) - { - return false; - } - return IsBlankRGB(image, whiteThresholdNorm, coverageThresholdNorm); - } - - private static bool IsBlankRGB(IMemoryImage image, int whiteThresholdNorm, int coverageThresholdNorm) - { - var whiteThreshold = (int)Math.Round(WHITE_THRESHOLD_MIN + (whiteThresholdNorm / 100.0) * (WHITE_THRESHOLD_MAX - WHITE_THRESHOLD_MIN)); - var coverageThreshold = COVERAGE_THRESHOLD_MIN + (coverageThresholdNorm / 100.0) * (COVERAGE_THRESHOLD_MAX - COVERAGE_THRESHOLD_MIN); - - long totalPixels = image.Width * image.Height; - long matchPixels = 0; - - // TODO: Wrap this in a bitwise op - using var lockState = image.Lock(LockMode.ReadOnly, out var data); - var bytes = new byte[data.stride * image.Height]; - Marshal.Copy(data.safePtr, bytes, 0, bytes.Length); - lockState.Dispose(); - for (int x = 0; x < image.Width; x++) - { - for (int y = 0; y < image.Height; y++) - { - int b = bytes[data.stride * y + x * 3]; - int g = bytes[data.stride * y + x * 3 + 1]; - int r = bytes[data.stride * y + x * 3 + 2]; - // Use standard values for grayscale conversion to weight the RGB values - int luma = r * 299 + g * 587 + b * 114; - if (luma < whiteThreshold * 1000) - { - matchPixels++; - } - } - } - - var coverage = matchPixels / (double)totalPixels; - return coverage < coverageThreshold; - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Images/Deskewer.cs b/NAPS2.Sdk/Images/Deskewer.cs index 458c948c2b..e7c0d92660 100644 --- a/NAPS2.Sdk/Images/Deskewer.cs +++ b/NAPS2.Sdk/Images/Deskewer.cs @@ -2,7 +2,7 @@ namespace NAPS2.Images; -public abstract class Deskewer +internal static class Deskewer { // Conceptually, Hough Line deskewing works like this: // diff --git a/NAPS2.Sdk/Images/IProcessedImageOwner.cs b/NAPS2.Sdk/Images/IProcessedImageOwner.cs index 6a09b64619..9a0faa7487 100644 --- a/NAPS2.Sdk/Images/IProcessedImageOwner.cs +++ b/NAPS2.Sdk/Images/IProcessedImageOwner.cs @@ -1,6 +1,6 @@ namespace NAPS2.Images; -public interface IProcessedImageOwner +internal interface IProcessedImageOwner { void Register(IDisposable internalDisposable); void Unregister(IDisposable internalDisposable); diff --git a/NAPS2.Sdk/Images/ImageExportFormat.cs b/NAPS2.Sdk/Images/ImageExportFormat.cs index 93bc8a135f..b162d95d47 100644 --- a/NAPS2.Sdk/Images/ImageExportFormat.cs +++ b/NAPS2.Sdk/Images/ImageExportFormat.cs @@ -1,3 +1,3 @@ namespace NAPS2.Images; -public record ImageExportFormat(ImageFileFormat FileFormat, ImagePixelFormat PixelFormat); \ No newline at end of file +internal record ImageExportFormat(ImageFileFormat FileFormat, ImagePixelFormat PixelFormat); \ No newline at end of file diff --git a/NAPS2.Sdk/Images/ImageExportHelper.cs b/NAPS2.Sdk/Images/ImageExportHelper.cs index aac96f6c65..9921c24881 100644 --- a/NAPS2.Sdk/Images/ImageExportHelper.cs +++ b/NAPS2.Sdk/Images/ImageExportHelper.cs @@ -1,12 +1,12 @@ namespace NAPS2.Images; -public class ImageExportHelper +internal static class ImageExportHelper { - public string SaveSmallestFormat(string pathWithoutExtension, IMemoryImage image, BitDepth bitDepth, + public static string SaveSmallestFormat(string pathWithoutExtension, IMemoryImage image, bool lossless, int quality, out ImageFileFormat imageFileFormat) { // TODO: Should we save directly to the file? - var memoryStream = SaveSmallestFormatToMemoryStream(image, bitDepth, lossless, quality, out imageFileFormat); + var memoryStream = SaveSmallestFormatToMemoryStream(image, lossless, quality, out imageFileFormat); var ext = imageFileFormat == ImageFileFormat.Png ? ".png" : ".jpg"; var path = pathWithoutExtension + ext; using var fileStream = new FileStream(path, FileMode.Create); @@ -14,28 +14,23 @@ public string SaveSmallestFormat(string pathWithoutExtension, IMemoryImage image return path; } - public MemoryStream SaveSmallestFormatToMemoryStream(IMemoryImage image, BitDepth bitDepth, bool lossless, + public static MemoryStream SaveSmallestFormatToMemoryStream(IMemoryImage image, bool lossless, int quality, out ImageFileFormat imageFileFormat) { - var exportFormat = GetExportFormat(image, bitDepth, lossless); + var exportFormat = GetExportFormat(image, lossless); if (exportFormat.FileFormat == ImageFileFormat.Png) { imageFileFormat = ImageFileFormat.Png; - if (exportFormat.PixelFormat == ImagePixelFormat.BW1 && image.LogicalPixelFormat != ImagePixelFormat.BW1) - { - using var bwImage = image.Clone().PerformTransform(new BlackWhiteTransform()); - return bwImage.SaveToMemoryStream(ImageFileFormat.Png); - } return image.SaveToMemoryStream(ImageFileFormat.Png); } if (exportFormat.FileFormat == ImageFileFormat.Jpeg) { imageFileFormat = ImageFileFormat.Jpeg; - return image.SaveToMemoryStream(ImageFileFormat.Jpeg, quality); + return image.SaveToMemoryStream(ImageFileFormat.Jpeg, new ImageSaveOptions { Quality = quality }); } // Save as PNG/JPEG depending on which is smaller var pngEncoded = image.SaveToMemoryStream(ImageFileFormat.Png); - var jpegEncoded = image.SaveToMemoryStream(ImageFileFormat.Jpeg, quality); + var jpegEncoded = image.SaveToMemoryStream(ImageFileFormat.Jpeg, new ImageSaveOptions { Quality = quality }); if (pngEncoded.Length <= jpegEncoded.Length) { // Probably a black and white image (e.g. from native WIA, where bitDepth is unknown), which PNG compresses well vs. JPEG @@ -47,24 +42,17 @@ public MemoryStream SaveSmallestFormatToMemoryStream(IMemoryImage image, BitDept return jpegEncoded; } - public ImageExportFormat GetExportFormat(IMemoryImage image, BitDepth bitDepth, bool lossless) + public static ImageExportFormat GetExportFormat(IMemoryImage image, bool lossless) { + image.UpdateLogicalPixelFormat(); // Store the image in as little space as possible if (image.LogicalPixelFormat == ImagePixelFormat.BW1) { - // Already encoded as 1-bit + // Already 1-bit return new ImageExportFormat(ImageFileFormat.Png, ImagePixelFormat.BW1); } - if (bitDepth == BitDepth.BlackAndWhite) - { - // Convert to a 1-bit bitmap before saving to help compression - // This is lossless and takes up minimal storage (best of both worlds), so highQuality is irrelevant - // Note that if a black and white image comes from native WIA, bitDepth is unknown, - // so the image will be png-encoded below instead of using a 1-bit bitmap - return new ImageExportFormat(ImageFileFormat.Png, ImagePixelFormat.BW1); - } - // TODO: Also for ARGB32? Or is OriginalFileFormat enough if we populate that more consistently? - if (lossless || image.OriginalFileFormat == ImageFileFormat.Png) + if (lossless || image.LogicalPixelFormat == ImagePixelFormat.ARGB32 || + image.OriginalFileFormat == ImageFileFormat.Png) { // Store as PNG // Lossless, but some images (color/grayscale) take up lots of storage @@ -77,6 +65,6 @@ public ImageExportFormat GetExportFormat(IMemoryImage image, BitDepth bitDepth, return new ImageExportFormat(ImageFileFormat.Jpeg, ImagePixelFormat.RGB24); } // No inherent preference for Jpeg or Png, the caller can decide - return new ImageExportFormat(ImageFileFormat.Unspecified, ImagePixelFormat.RGB24); + return new ImageExportFormat(ImageFileFormat.Unknown, ImagePixelFormat.RGB24); } } \ No newline at end of file diff --git a/NAPS2.Sdk/Images/ImageMetadata.cs b/NAPS2.Sdk/Images/ImageMetadata.cs index 9a1f5b103a..b085dc1682 100644 --- a/NAPS2.Sdk/Images/ImageMetadata.cs +++ b/NAPS2.Sdk/Images/ImageMetadata.cs @@ -1,9 +1,12 @@ namespace NAPS2.Images; -public record ImageMetadata(BitDepth BitDepth, bool Lossless) +/// +/// Represents additional information about a scanned image (quality, page size). +/// +public record ImageMetadata(bool Lossless, PageSize? PageSize) { /// /// A default set of metadata suitable for test images. Real use cases should be explicit and not use this default value. /// - public static readonly ImageMetadata DefaultForTesting = new(BitDepth.Color, false); + internal static readonly ImageMetadata DefaultForTesting = new(false, null); } diff --git a/NAPS2.Sdk/Images/Memento.cs b/NAPS2.Sdk/Images/Memento.cs deleted file mode 100644 index 33b470847c..0000000000 --- a/NAPS2.Sdk/Images/Memento.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Immutable; - -namespace NAPS2.Images; - -/// -/// A memento represents a snapshot of the current set of images in the application to allow undo/redo. -/// https://en.wikipedia.org/wiki/Memento_pattern -/// -/// Mementos are equal if their contents are equal. This allows no-ops to be identified so that you don't hit Ctrl+Z and -/// have nothing happen. -/// -/// When you create a memento with a list of ProcessedImage instances, the memento takes ownership of those instances. -/// When the memento is disposed they are disposed. -/// -public record Memento(ImmutableList Images) : IDisposable -{ - public static readonly Memento Empty = new(ImmutableList.Create()); - - // TODO: Do we need to implement equality better at the RenderableImage level? - public virtual bool Equals(Memento? other) - { - if (other == null) - { - return false; - } - - return ObjectHelpers.ListEquals(Images, other.Images); - } - - public override int GetHashCode() - { - return ObjectHelpers.ListHashCode(Images); - } - - public void Dispose() - { - foreach (var image in Images) - { - image.Dispose(); - } - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Images/PageSide.cs b/NAPS2.Sdk/Images/PageSide.cs new file mode 100644 index 0000000000..ebf0426d41 --- /dev/null +++ b/NAPS2.Sdk/Images/PageSide.cs @@ -0,0 +1,8 @@ +namespace NAPS2.Images; + +public enum PageSide +{ + Unknown, + Front, + Back +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/PageSize.cs b/NAPS2.Sdk/Images/PageSize.cs similarity index 60% rename from NAPS2.Sdk/Scan/PageSize.cs rename to NAPS2.Sdk/Images/PageSize.cs index 52eb1d22b8..dbfb979a94 100644 --- a/NAPS2.Sdk/Scan/PageSize.cs +++ b/NAPS2.Sdk/Images/PageSize.cs @@ -1,7 +1,10 @@ using System.Globalization; -namespace NAPS2.Scan; +namespace NAPS2.Images; +/// +/// Represents a page size, e.g. US Letter (8.5 x 11 in) or A4 (210 x 297 mm). +/// public record PageSize { public static PageSize Letter = new("8.5", "11", PageSizeUnit.Inch); @@ -18,6 +21,54 @@ public record PageSize public static PageSize B4 = new("250", "353", PageSizeUnit.Millimetre); + public static PageSize? Parse(string? size) + { + if (size == null) + return null; + + var wellKnownSize = size.ToLowerInvariant() switch + { + "letter" => Letter, + "legal" => Legal, + "a5" => A5, + "a4" => A4, + "a3" => A3, + "b5" => B5, + "b4" => B4, + _ => null + }; + if (wellKnownSize != null) + { + return wellKnownSize; + } + + var parts = size.Split(' '); + if (parts.Length == 1 && size.Length > 2) + { + // If there's no space separating the unit, assume the last 2 characters are the unit + parts = [size.Substring(0, size.Length - 2), size.Substring(size.Length - 2, 2)]; + } + if (parts.Length != 2) + return null; + var dims = parts[0].Split('x'); + if (dims.Length != 2) + return null; + if (!decimal.TryParse(dims[0], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var width)) + return null; + if (!decimal.TryParse(dims[1], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var height)) + return null; + var unit = parts[1] switch + { + "mm" => PageSizeUnit.Millimetre, + "cm" => PageSizeUnit.Centimetre, + "in" => PageSizeUnit.Inch, + _ => (PageSizeUnit?) null + }; + if (unit == null) + return null; + return new PageSize(width, height, unit.Value); + } + protected PageSize() { } @@ -117,4 +168,16 @@ public decimal HeightInInches } public int HeightInThousandthsOfAnInch => (int)(HeightInInches * 1000); + + public override string ToString() + { + var unit = Unit switch + { + PageSizeUnit.Centimetre => "cm", + PageSizeUnit.Millimetre => "mm", + PageSizeUnit.Inch => "in", + _ => throw new InvalidOperationException("Invalid page size unit") + }; + return $"{Width:0.#####}x{Height:0.#####} {unit}"; + } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/PageSizeUnit.cs b/NAPS2.Sdk/Images/PageSizeUnit.cs similarity index 74% rename from NAPS2.Sdk/Scan/PageSizeUnit.cs rename to NAPS2.Sdk/Images/PageSizeUnit.cs index f6ccc39e17..87070262c4 100644 --- a/NAPS2.Sdk/Scan/PageSizeUnit.cs +++ b/NAPS2.Sdk/Images/PageSizeUnit.cs @@ -1,4 +1,4 @@ -namespace NAPS2.Scan; +namespace NAPS2.Images; public enum PageSizeUnit { diff --git a/NAPS2.Sdk/Images/PostProcessingData.cs b/NAPS2.Sdk/Images/PostProcessingData.cs index 03202156dc..be30a077d9 100644 --- a/NAPS2.Sdk/Images/PostProcessingData.cs +++ b/NAPS2.Sdk/Images/PostProcessingData.cs @@ -2,12 +2,19 @@ namespace NAPS2.Images; -// TODO: We need to use the thumbnail when appropriate. Although we do need to consider thumbnail invalidation. -// TODO: We need to do OCR cancellation when the image is deleted or whatever. -public record PostProcessingData(IMemoryImage? Thumbnail, TransformState? ThumbnailTransformState, - BarcodeDetection BarcodeDetection, CancellationTokenSource? OcrCts) +/// +/// Represents information about an image obtained during post-processing (e.g. thumbnail image, barcode). +/// +public record PostProcessingData( + IMemoryImage? Thumbnail, + TransformState? ThumbnailTransformState, + int PageNumber, + PageSide PageSide, + Barcode Barcode, + CancellationTokenSource? OcrCts, + string? OriginalFilePath) { - public PostProcessingData() : this(null, null, BarcodeDetection.NotAttempted, null) + public PostProcessingData() : this(null, null, 0, PageSide.Unknown, Barcode.NoDetection, null, null) { } } \ No newline at end of file diff --git a/NAPS2.Sdk/Images/ProcessedImage.cs b/NAPS2.Sdk/Images/ProcessedImage.cs index bb6242972a..ce0bb362c0 100644 --- a/NAPS2.Sdk/Images/ProcessedImage.cs +++ b/NAPS2.Sdk/Images/ProcessedImage.cs @@ -1,3 +1,5 @@ +using NAPS2.Pdf; + namespace NAPS2.Images; /// @@ -9,33 +11,21 @@ namespace NAPS2.Images; /// reference with Clone() that will need to be disposed, and the underlying image storage will only be disposed once /// all related instances are disposed (or the parent ScanningContext is disposed). /// -public class ProcessedImage : IRenderableImage, IDisposable, IEquatable +public class ProcessedImage : IRenderableImage, IPdfRendererProvider, IDisposable, IEquatable { private readonly RefCount.Token _token; private bool _disposed; internal ProcessedImage(ImageContext imageContext, IImageStorage storage, ImageMetadata metadata, - PostProcessingData postProcessingData, TransformState transformState, RefCount refCount) + PostProcessingData postProcessingData, TransformState transformState, RefCount storageRefCount) { ImageContext = imageContext; Storage = storage; Metadata = metadata; PostProcessingData = postProcessingData; TransformState = transformState; - _token = refCount.NewToken(); - } - - public ProcessedImage(ImageContext imageContext, IImageStorage storage, ImageMetadata metadata, - PostProcessingData postProcessingData, TransformState transformState, IProcessedImageOwner? owner = null) - { - ImageContext = imageContext; - Storage = storage; - Metadata = metadata; - PostProcessingData = postProcessingData; - TransformState = transformState; - var internalDisposer = new InternalDisposer(this, owner); - var refCount = new RefCount(internalDisposer); - _token = refCount.NewToken(); + StorageRefCount = storageRefCount; + _token = StorageRefCount.NewToken(); } public ImageContext ImageContext { get; } @@ -43,6 +33,8 @@ public ProcessedImage(ImageContext imageContext, IImageStorage storage, ImageMet // TODO: Consider having two copies of the image on disk - one before transforms, one after. public IImageStorage Storage { get; } + internal RefCount StorageRefCount { get; } + public ImageMetadata Metadata { get; } public PostProcessingData PostProcessingData { get; } @@ -54,12 +46,22 @@ public ProcessedImage(ImageContext imageContext, IImageStorage storage, ImageMet /// appended to the TransformState. All instances will need to be disposed before the underlying image storage is /// disposed. /// - /// - /// - public ProcessedImage WithTransform(Transform transform, bool disposeSelf = false) + public ProcessedImage WithTransform(Transform transform, bool disposeSelf = false) => + WithTransformState(TransformState.AddOrSimplify(transform), disposeSelf); + + /// + /// Creates a new ProcessedImage instance with the same underlying image storage/metadata and no transforms. All + /// instances will need to be disposed before the underlying image storage is disposed. + /// + public ProcessedImage WithNoTransforms(bool disposeSelf = false) => + WithTransformState(TransformState.Empty, disposeSelf); + + /// + /// Creates a new ProcessedImage instance with the same underlying image storage/metadata and a new transform + /// state. All instances will need to be disposed before the underlying image storage is disposed. + /// + public ProcessedImage WithTransformState(TransformState newTransformState, bool disposeSelf = false) { - // TODO: Should metadata update for some transforms? - var newTransformState = TransformState.AddOrSimplify(transform); var result = new ProcessedImage(ImageContext, Storage, Metadata, PostProcessingData, newTransformState, _token.RefCount); if (disposeSelf) @@ -70,17 +72,6 @@ public ProcessedImage WithTransform(Transform transform, bool disposeSelf = fals return result; } - /// - /// Creates a new ProcessedImage instance with the same underlying image storage/metadata and no transforms. All - /// instances will need to be disposed before the underlying image storage is disposed. - /// - /// - public ProcessedImage WithNoTransforms() - { - return new ProcessedImage( - ImageContext, Storage, Metadata, PostProcessingData, TransformState.Empty, _token.RefCount); - } - public ProcessedImage WithPostProcessingData(PostProcessingData postProcessingData, bool disposeSelf) { var result = @@ -151,14 +142,16 @@ public void Dispose() } } - private class InternalDisposer : IDisposable + internal class InternalDisposer : IDisposable { - private readonly ProcessedImage _processedImage; + private readonly IImageStorage _storage; + private readonly PostProcessingData _postProcessingData; private bool _disposed; - public InternalDisposer(ProcessedImage processedImage, IProcessedImageOwner? owner) + public InternalDisposer(IImageStorage storage, PostProcessingData postProcessingData, IProcessedImageOwner? owner) { - _processedImage = processedImage; + _storage = storage; + _postProcessingData = postProcessingData; owner?.Register(this); } @@ -174,9 +167,9 @@ public void Dispose() _disposed = true; } - _processedImage.Storage.Dispose(); - _processedImage.PostProcessingData.Thumbnail?.Dispose(); - _processedImage.PostProcessingData.OcrCts?.Cancel(); + _storage.Dispose(); + _postProcessingData.Thumbnail?.Dispose(); + _postProcessingData.OcrCts?.Cancel(); } } @@ -189,4 +182,6 @@ public void Dispose() /// at any moment. /// public record WeakReference(ProcessedImage ProcessedImage); + + IPdfRenderer IPdfRendererProvider.PdfRenderer => new PdfiumPdfRenderer(); } \ No newline at end of file diff --git a/NAPS2.Sdk/Images/ThumbnailRenderer.cs b/NAPS2.Sdk/Images/ThumbnailRenderer.cs index c17112489e..db958cec6f 100644 --- a/NAPS2.Sdk/Images/ThumbnailRenderer.cs +++ b/NAPS2.Sdk/Images/ThumbnailRenderer.cs @@ -1,10 +1,13 @@ namespace NAPS2.Images; // TODO: Use this in more places, i.e. ImportPostProcessor +/// +/// Renders a ProcessedImage to a thumbnail of a given size. +/// public class ThumbnailRenderer { private const int OVERSAMPLE = 3; - + private readonly ImageContext _imageContext; public ThumbnailRenderer(ImageContext imageContext) @@ -12,21 +15,27 @@ public ThumbnailRenderer(ImageContext imageContext) _imageContext = imageContext; } - public IMemoryImage Render(ProcessedImage processedImage, int outputSize) + public Task Render(ProcessedImage processedImage, int outputSize) { - var image = _imageContext.RenderFromStorage(processedImage.Storage); - var transformList = processedImage.TransformState.Transforms; - if (!processedImage.TransformState.IsEmpty) + // On Mac it's really important this runs as a task, as Mac memory management depends on autorelease pools which + // in Xamarin operate at the task level. Otherwise a long-running thumbnail render thread will leak memory like + // a sieve. + return Task.Run(() => { - // When we have additional transformations, performing them on a large original image may be quite slow. - // On the other hand, scaling the image to the thumbnail size first can result in transforms losing detail. - // As a middle ground we scale to an "oversampled" size first. - double oversampledSize = outputSize * OVERSAMPLE; - double scaleFactor = Math.Min(oversampledSize / image.Height, oversampledSize / image.Width); - scaleFactor = Math.Min(scaleFactor, 1); - transformList = transformList.Insert(0, new ScaleTransform(scaleFactor)); - } - transformList = transformList.Add(new ThumbnailTransform(outputSize)); - return _imageContext.PerformAllTransforms(image, transformList); + var image = _imageContext.RenderWithoutTransforms(processedImage); + var transformList = processedImage.TransformState.Transforms; + if (!processedImage.TransformState.IsEmpty) + { + // When we have additional transformations, performing them on a large original image may be quite slow. + // On the other hand, scaling the image to the thumbnail size first can result in transforms losing detail. + // As a middle ground we scale to an "oversampled" size first. + double oversampledSize = outputSize * OVERSAMPLE; + double scaleFactor = Math.Min(oversampledSize / image.Height, oversampledSize / image.Width); + scaleFactor = Math.Min(scaleFactor, 1); + transformList = transformList.Insert(0, new ScaleTransform(scaleFactor)); + } + transformList = transformList.Add(new ThumbnailTransform(outputSize)); + return _imageContext.PerformAllTransforms(image, transformList); + }); } } \ No newline at end of file diff --git a/NAPS2.Sdk/Images/UndoStack.cs b/NAPS2.Sdk/Images/UndoStack.cs deleted file mode 100644 index d8a01219cc..0000000000 --- a/NAPS2.Sdk/Images/UndoStack.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Collections.Immutable; - -namespace NAPS2.Images; - -// TODO: Actually implement undo in the UI -public class UndoStack : IDisposable -{ - private readonly int _maxLength; - private readonly LinkedList _stack; - private LinkedListNode _current; - - public UndoStack(int maxLength) - { - _maxLength = maxLength; - _stack = new LinkedList(); - _stack.AddFirst(Memento.Empty); - _current = _stack.First!; - } - - public Memento Current => _current.Value; - - public bool Push(IEnumerable images) - { - return Push(new Memento(images.ToImmutableList())); - } - - public bool Push(Memento memento) - { - if (_stack.First!.Value == memento) - { - return false; - } - ClearRedo(); - _stack.AddFirst(memento); - _current = _stack.First; - Trim(); - return true; - } - - private void Trim() - { - while (_stack.Count > _maxLength && _stack.Last != _current) - { - _stack.Last!.Value.Dispose(); - _stack.RemoveLast(); - } - } - - public void ClearRedo() - { - while (_stack.First != _current) - { - _stack.First!.Value.Dispose(); - _stack.RemoveFirst(); - } - } - - public void ClearUndo() - { - while (_stack.Last != _current) - { - _stack.Last!.Value.Dispose(); - _stack.RemoveLast(); - } - } - - public void ClearBoth() - { - ClearRedo(); - ClearUndo(); - } - - public bool Undo() - { - if (_current.Next != null) - { - _current = _current.Next; - return true; - } - return false; - } - - public bool Redo() - { - if (_current.Previous != null) - { - _current = _current.Previous; - return true; - } - return false; - } - - public void Dispose() - { - foreach (var memento in _stack) - { - memento.Dispose(); - } - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Email/EmailAttachment.cs b/NAPS2.Sdk/ImportExport/Email/EmailAttachment.cs index 0562dea940..0aefbb964a 100644 --- a/NAPS2.Sdk/ImportExport/Email/EmailAttachment.cs +++ b/NAPS2.Sdk/ImportExport/Email/EmailAttachment.cs @@ -3,6 +3,15 @@ /// /// Represents an attachment for an EmailMessage. /// -/// The path of the source file to be attached. -/// The name of the attachment (usually the source file name). -public record EmailAttachment(string FilePath, string AttachmentName); \ No newline at end of file +internal record EmailAttachment +{ + /// + /// The path of the source file to be attached. + /// + public required string FilePath { get; init; } + + /// + /// The name of the attachment (usually the source file name). + /// + public required string AttachmentName { get; init; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Email/EmailMessage.cs b/NAPS2.Sdk/ImportExport/Email/EmailMessage.cs index d680f48068..523c85976b 100644 --- a/NAPS2.Sdk/ImportExport/Email/EmailMessage.cs +++ b/NAPS2.Sdk/ImportExport/Email/EmailMessage.cs @@ -1,20 +1,14 @@ namespace NAPS2.ImportExport.Email; -public class EmailMessage +internal class EmailMessage { - public EmailMessage() - { - Recipients = new List(); - Attachments = new List(); - } - public string? Subject { get; set; } public string? BodyText { get; set; } - public List Recipients { get; set; } + public List Recipients { get; set; } = []; - public List Attachments { get; set; } + public List Attachments { get; set; } = []; /// /// Gets or sets a value indicating whether the email should be sent automatically without prompting the user to make changes first. diff --git a/NAPS2.Sdk/ImportExport/Email/EmailRecipient.cs b/NAPS2.Sdk/ImportExport/Email/EmailRecipient.cs index bf8738f4da..71a92f39ec 100644 --- a/NAPS2.Sdk/ImportExport/Email/EmailRecipient.cs +++ b/NAPS2.Sdk/ImportExport/Email/EmailRecipient.cs @@ -1,6 +1,6 @@ namespace NAPS2.ImportExport.Email; -public class EmailRecipient +internal class EmailRecipient { public static IEnumerable FromText(EmailRecipientType recipType, string? recipText) { diff --git a/NAPS2.Sdk/ImportExport/Email/EmailRecipientType.cs b/NAPS2.Sdk/ImportExport/Email/EmailRecipientType.cs index e3ea1cda67..791fdaa502 100644 --- a/NAPS2.Sdk/ImportExport/Email/EmailRecipientType.cs +++ b/NAPS2.Sdk/ImportExport/Email/EmailRecipientType.cs @@ -1,6 +1,6 @@ namespace NAPS2.ImportExport.Email; -public enum EmailRecipientType +internal enum EmailRecipientType { To, Cc, diff --git a/NAPS2.Sdk/ImportExport/Email/IEmailProvider.cs b/NAPS2.Sdk/ImportExport/Email/IEmailProvider.cs index 2fd17aa41c..0793b45985 100644 --- a/NAPS2.Sdk/ImportExport/Email/IEmailProvider.cs +++ b/NAPS2.Sdk/ImportExport/Email/IEmailProvider.cs @@ -1,6 +1,8 @@ namespace NAPS2.ImportExport.Email; -public interface IEmailProvider +internal interface IEmailProvider { Task SendEmail(EmailMessage emailMessage, ProgressHandler progress = default); + bool ShowInList { get; } + bool CanSelectInList { get; } } \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Email/Mapi/IMapiWrapper.cs b/NAPS2.Sdk/ImportExport/Email/Mapi/IMapiWrapper.cs index c3cfbd356f..0e57aff581 100644 --- a/NAPS2.Sdk/ImportExport/Email/Mapi/IMapiWrapper.cs +++ b/NAPS2.Sdk/ImportExport/Email/Mapi/IMapiWrapper.cs @@ -1,6 +1,6 @@ namespace NAPS2.ImportExport.Email.Mapi; -public interface IMapiWrapper +internal interface IMapiWrapper { bool CanLoadClient(string? clientName); diff --git a/NAPS2.Sdk/ImportExport/Email/Mapi/MapiDispatcher.cs b/NAPS2.Sdk/ImportExport/Email/Mapi/MapiDispatcher.cs index 2e7176d406..feebf2432a 100644 --- a/NAPS2.Sdk/ImportExport/Email/Mapi/MapiDispatcher.cs +++ b/NAPS2.Sdk/ImportExport/Email/Mapi/MapiDispatcher.cs @@ -1,28 +1,17 @@ +using NAPS2.Remoting.Worker; using NAPS2.Scan; namespace NAPS2.ImportExport.Email.Mapi; -public class MapiDispatcher +internal class MapiDispatcher { private readonly ScanningContext _scanningContext; - private readonly IMapiWrapper _mapiWrapper; -#if NET6_0_OR_GREATER - [System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif public MapiDispatcher(ScanningContext scanningContext) - : this(scanningContext, new MapiWrapper(new SystemEmailClients())) - { - } - - public MapiDispatcher(ScanningContext scanningContext, IMapiWrapper mapiWrapper) { _scanningContext = scanningContext; - _mapiWrapper = mapiWrapper; } - private bool UseWorker => Environment.Is64BitProcess; - /// /// Sends an email described by the given message object. /// @@ -31,20 +20,38 @@ public MapiDispatcher(ScanningContext scanningContext, IMapiWrapper mapiWrapper) /// The MAPI return code. public async Task SendEmail(string? clientName, EmailMessage message) { + // We always run MAPI in a worker as it can cause weird changes to the application state. + if (_scanningContext.WorkerFactory == null) + { + // TODO: Maybe allow non-worker use for SDK? + throw new InvalidOperationException( + "ScanningContext must have a worker set up to use MAPI."); + } #if NET6_0_OR_GREATER if (!OperatingSystem.IsWindowsVersionAtLeast(7)) throw new InvalidOperationException("Windows-only"); #endif - // TODO: We should always do this in a worker (64 or 32 bit). Specifically, loading the outlook library does something weird to WinForms such that "new Eto.Forms.TextArea()" errors with a missing office dll. - if (UseWorker && !_mapiWrapper.CanLoadClient(clientName)) + + if (Environment.Is64BitProcess) { - if (_scanningContext.WorkerFactory == null) + // Try 64-bit first + using var worker1 = _scanningContext.CreateWorker(WorkerType.Native)!; + if (await worker1.Service.CanLoadMapi(clientName)) { - throw new InvalidOperationException( - "ScanningContext.WorkerFactory must be set to use MAPI from a 64-bit process."); + return await worker1.Service.SendMapiEmail(clientName, message); } - using var worker = _scanningContext.WorkerFactory.Create(); - return await worker.Service.SendMapiEmail(clientName, message); + worker1.Dispose(); } - return await _mapiWrapper.SendEmail(clientName, message); + + if (PlatformCompat.System.SupportsWinX86Worker) + { + // If 64-bit failed, try 32-bit + using var worker2 = _scanningContext.CreateWorker(WorkerType.WinX86)!; + if (await worker2.Service.CanLoadMapi(clientName)) + { + return await worker2.Service.SendMapiEmail(clientName, message); + } + } + + throw new Exception($"Could not load MAPI dll: {clientName}"); } } \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Email/Mapi/MapiSendMailReturnCode.cs b/NAPS2.Sdk/ImportExport/Email/Mapi/MapiSendMailReturnCode.cs index cdd1a97402..314a41e5f1 100644 --- a/NAPS2.Sdk/ImportExport/Email/Mapi/MapiSendMailReturnCode.cs +++ b/NAPS2.Sdk/ImportExport/Email/Mapi/MapiSendMailReturnCode.cs @@ -2,7 +2,7 @@ // Documented at: // http://msdn.microsoft.com/en-us/library/windows/desktop/hh707275%28v=vs.85%29.aspx#MAPI_FORCE_UNICODE -public enum MapiSendMailReturnCode +internal enum MapiSendMailReturnCode { Success = 0, UserAbort = 1, diff --git a/NAPS2.Sdk/ImportExport/Email/Mapi/MapiWrapper.cs b/NAPS2.Sdk/ImportExport/Email/Mapi/MapiWrapper.cs index b827d27961..2687f951b1 100644 --- a/NAPS2.Sdk/ImportExport/Email/Mapi/MapiWrapper.cs +++ b/NAPS2.Sdk/ImportExport/Email/Mapi/MapiWrapper.cs @@ -1,26 +1,30 @@ -using NAPS2.Unmanaged; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using Microsoft.Win32; +using NAPS2.Platform.Windows; +using NAPS2.Unmanaged; namespace NAPS2.ImportExport.Email.Mapi; -#if NET6_0_OR_GREATER [System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif -public class MapiWrapper : IMapiWrapper +internal class MapiWrapper : IMapiWrapper { - private readonly SystemEmailClients _systemEmailClients; + private const string DEFAULT_MAPI_DLL = "mapi32.dll"; - public MapiWrapper(SystemEmailClients systemEmailClients) + private readonly ILogger _logger; + + public MapiWrapper(ILogger logger) { - _systemEmailClients = systemEmailClients; + _logger = logger; } - public bool CanLoadClient(string? clientName) => _systemEmailClients.GetLibrary(clientName) != IntPtr.Zero; + public bool CanLoadClient(string? clientName) => GetLibrary(clientName) != IntPtr.Zero; public Task SendEmail(string? clientName, EmailMessage message) { return Task.Run(() => { - var (mapiSendMail, mapiSendMailW) = _systemEmailClients.GetDelegate(clientName, out bool unicode); + var (mapiSendMail, mapiSendMailW) = GetDelegate(clientName, out bool unicode); // Determine the flags used to send the message var flags = MapiSendMailFlags.None; @@ -34,11 +38,12 @@ public Task SendEmail(string? clientName, EmailMessage m flags |= MapiSendMailFlags.LogonUI; } - return unicode ? SendMailW(mapiSendMailW!, message, flags) : SendMail(mapiSendMail!, message, flags); + return Invoker.Current.InvokeGet(() => + unicode ? SendMailW(mapiSendMailW!, message, flags) : SendMail(mapiSendMail!, message, flags)); }); } - private static MapiSendMailReturnCode SendMail(SystemEmailClients.MapiSendMailDelegate mapiSendMail, EmailMessage message, MapiSendMailFlags flags) + private static MapiSendMailReturnCode SendMail(MapiSendMailDelegate mapiSendMail, EmailMessage message, MapiSendMailFlags flags) { using var files = UnmanagedTypes.CopyOf(GetFiles(message)); using var recips = UnmanagedTypes.CopyOf(GetRecips(message)); @@ -57,7 +62,7 @@ private static MapiSendMailReturnCode SendMail(SystemEmailClients.MapiSendMailDe return mapiSendMail(IntPtr.Zero, IntPtr.Zero, mapiMessage, flags, 0); } - private static MapiSendMailReturnCode SendMailW(SystemEmailClients.MapiSendMailDelegateW mapiSendMailW, EmailMessage message, MapiSendMailFlags flags) + private static MapiSendMailReturnCode SendMailW(MapiSendMailDelegateW mapiSendMailW, EmailMessage message, MapiSendMailFlags flags) { using var files = UnmanagedTypes.CopyOf(GetFilesW(message)); using var recips = UnmanagedTypes.CopyOf(GetRecipsW(message)); @@ -119,4 +124,58 @@ private static MapiFileDescW[] GetFilesW(EmailMessage message) name = attachment.AttachmentName }).ToArray(); } + + internal IntPtr GetLibrary(string? clientName) + { + var dllPath = GetDllPath(clientName); + return Win32.LoadLibrary(dllPath); + } + + internal (MapiSendMailDelegate?, MapiSendMailDelegateW?) GetDelegate(string? clientName, out bool unicode) + { + _logger.LogDebug($"Using MAPI client {clientName ?? ""}"); + var dllPath = GetDllPath(clientName); + _logger.LogDebug($"Loading MAPI DLL {dllPath}"); + var module = Win32.LoadLibrary(dllPath); + if (module == IntPtr.Zero) + { + throw new Exception($"Could not load dll for email: {dllPath}"); + } + var addr = Win32.GetProcAddress(module, "MAPISendMailW"); + if (addr != IntPtr.Zero) + { + _logger.LogDebug("Using unicode function MAPISendMailW"); + unicode = true; + return (null, (MapiSendMailDelegateW)Marshal.GetDelegateForFunctionPointer(addr, typeof(MapiSendMailDelegateW))); + } + addr = Win32.GetProcAddress(module, "MAPISendMail"); + if (addr != IntPtr.Zero) + { + _logger.LogDebug("Using ansi function MAPISendMail"); + unicode = false; + return ((MapiSendMailDelegate)Marshal.GetDelegateForFunctionPointer(addr, typeof(MapiSendMailDelegate)), null); + } + throw new Exception($"Could not find an entry point in dll for email: {dllPath}"); + } + + private string GetDllPath(string? clientName) + { + if (string.IsNullOrEmpty(clientName) || clientName == GetDefaultName()) + { + return DEFAULT_MAPI_DLL; + } + using var clientKey = Registry.LocalMachine.OpenSubKey($@"SOFTWARE\Clients\Mail\{clientName}"); + return clientKey?.GetValue("DllPathEx")?.ToString() ?? clientKey?.GetValue("DllPath")?.ToString() ?? DEFAULT_MAPI_DLL; + } + + private string? GetDefaultName() + { + using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Clients\Mail", false); + return key?.GetValue(null)?.ToString(); + } + + // MAPISendMail is documented at: + // http://msdn.microsoft.com/en-us/library/windows/desktop/dd296721%28v=vs.85%29.aspx + internal delegate MapiSendMailReturnCode MapiSendMailDelegate(IntPtr session, IntPtr hwnd, MapiMessage message, MapiSendMailFlags flags, int reserved); + internal delegate MapiSendMailReturnCode MapiSendMailDelegateW(IntPtr session, IntPtr hwnd, MapiMessageW message, MapiSendMailFlags flags, int reserved); } \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Email/Mapi/StubMapiWrapper.cs b/NAPS2.Sdk/ImportExport/Email/Mapi/StubMapiWrapper.cs index a70dd8a156..6f4d9a3286 100644 --- a/NAPS2.Sdk/ImportExport/Email/Mapi/StubMapiWrapper.cs +++ b/NAPS2.Sdk/ImportExport/Email/Mapi/StubMapiWrapper.cs @@ -1,6 +1,6 @@ namespace NAPS2.ImportExport.Email.Mapi; -public class StubMapiWrapper : IMapiWrapper +internal class StubMapiWrapper : IMapiWrapper { public bool CanLoadClient(string? clientName) => throw new NotSupportedException(); diff --git a/NAPS2.Sdk/ImportExport/Email/Mapi/SystemEmailClients.cs b/NAPS2.Sdk/ImportExport/Email/Mapi/SystemEmailClients.cs deleted file mode 100644 index 731a2e63d2..0000000000 --- a/NAPS2.Sdk/ImportExport/Email/Mapi/SystemEmailClients.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Runtime.InteropServices; -using Microsoft.Win32; -using NAPS2.Platform.Windows; - -namespace NAPS2.ImportExport.Email.Mapi; - -#if NET6_0_OR_GREATER -[System.Runtime.Versioning.SupportedOSPlatform("windows7.0")] -#endif -public class SystemEmailClients -{ - private const string DEFAULT_MAPI_DLL = "mapi32.dll"; - - public string? GetDefaultName() - { - using var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Clients\Mail", false); - return key?.GetValue(null)?.ToString(); - } - - public string[] GetNames() - { - // TODO: Swallow errors - using var clientList = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Clients\Mail", false); - return clientList?.GetSubKeyNames().Where(clientName => - { - using var clientKey = Registry.LocalMachine.OpenSubKey($@"SOFTWARE\Clients\Mail\{clientName}"); - return clientKey?.GetValue("DllPath") != null; - }).ToArray() ?? Array.Empty(); - } - - public string? GetExePath(string clientName) - { - using var command = Registry.LocalMachine.OpenSubKey($@"SOFTWARE\Clients\Mail\{clientName}\shell\open\command", false); - string commandText = command?.GetValue(null)?.ToString() ?? ""; - if (!commandText.StartsWith("\"", StringComparison.InvariantCulture)) - { - return null; - } - return commandText.Substring(1, commandText.IndexOf("\"", 1, StringComparison.InvariantCulture) - 1); - - } - - internal IntPtr GetLibrary(string? clientName) - { - var dllPath = GetDllPath(clientName); - return Win32.LoadLibrary(dllPath); - } - - internal (MapiSendMailDelegate?, MapiSendMailDelegateW?) GetDelegate(string? clientName, out bool unicode) - { - var dllPath = GetDllPath(clientName); - var module = Win32.LoadLibrary(dllPath); - if (module == IntPtr.Zero) - { - throw new Exception($"Could not load dll for email: {dllPath}"); - } - var addr = Win32.GetProcAddress(module, "MAPISendMailW"); - if (addr != IntPtr.Zero) - { - unicode = true; - return (null, (MapiSendMailDelegateW)Marshal.GetDelegateForFunctionPointer(addr, typeof(MapiSendMailDelegateW))); - } - addr = Win32.GetProcAddress(module, "MAPISendMail"); - if (addr != IntPtr.Zero) - { - unicode = false; - return ((MapiSendMailDelegate)Marshal.GetDelegateForFunctionPointer(addr, typeof(MapiSendMailDelegate)), null); - } - throw new Exception($"Could not find an entry point in dll for email: {dllPath}"); - } - - private static string GetDllPath(string? clientName) - { - if (string.IsNullOrEmpty(clientName)) - { - return DEFAULT_MAPI_DLL; - } - using var clientKey = Registry.LocalMachine.OpenSubKey($@"SOFTWARE\Clients\Mail\{clientName}"); - return clientKey?.GetValue("DllPathEx")?.ToString() ?? clientKey?.GetValue("DllPath")?.ToString() ?? DEFAULT_MAPI_DLL; - } - - // MAPISendMail is documented at: - // http://msdn.microsoft.com/en-us/library/windows/desktop/dd296721%28v=vs.85%29.aspx - internal delegate MapiSendMailReturnCode MapiSendMailDelegate(IntPtr session, IntPtr hwnd, MapiMessage message, MapiSendMailFlags flags, int reserved); - internal delegate MapiSendMailReturnCode MapiSendMailDelegateW(IntPtr session, IntPtr hwnd, MapiMessageW message, MapiSendMailFlags flags, int reserved); -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/FileImporter.cs b/NAPS2.Sdk/ImportExport/FileImporter.cs new file mode 100644 index 0000000000..daf66be390 --- /dev/null +++ b/NAPS2.Sdk/ImportExport/FileImporter.cs @@ -0,0 +1,107 @@ +using System.IO.Compression; +using NAPS2.Pdf; +using NAPS2.Scan; + +namespace NAPS2.ImportExport; + +/// +/// Imports PDF or image files. +/// +public class FileImporter +{ + private readonly PdfImporter _pdfImporter; + private readonly ImageImporter _imageImporter; + + public FileImporter(ScanningContext scanningContext) + : this(new PdfImporter(scanningContext), new ImageImporter(scanningContext)) + { + } + + public FileImporter(PdfImporter pdfImporter, ImageImporter imageImporter) + { + _pdfImporter = pdfImporter; + _imageImporter = imageImporter; + } + + + public IAsyncEnumerable Import(string filePath, ImportParams? importParams = null, + ProgressHandler progress = default) => + Import(new InputPathOrStream(filePath, null, null), importParams, progress); + + public IAsyncEnumerable Import(Stream stream, ImportParams? importParams = null, + ProgressHandler progress = default) => + Import(new InputPathOrStream(null, stream, null), importParams, progress); + + internal IAsyncEnumerable Import(InputPathOrStream input, ImportParams? importParams = null, + ProgressHandler progress = default, bool skipUnsupported = false) + { + if (Path.GetExtension(input.FileName).ToLowerInvariant() == ".pdf") + { + return _pdfImporter.Import(input, importParams, progress); + } + if (Path.GetExtension(input.FileName).ToLowerInvariant() is ".zip" or ".cbz") // cbz = comic book zip + { + return ImportZip(input, importParams, progress); + } + if (ImageContext.GetFileFormatFromExtension(input.FileName) != ImageFileFormat.Unknown) + { + return _imageImporter.Import(input, importParams, progress); + } + + // If we couldn't infer if it's a PDF from the extension, we will try and read the file itself + var firstBytes = new byte[8]; + if (input.Stream != null) + { + input.Stream.Seek(0, SeekOrigin.Begin); + input.Stream.Read(firstBytes, 0, 8); + input.Stream.Seek(0, SeekOrigin.Begin); + } + else + { + using var stream = new FileStream(input.FilePath!, FileMode.Open, FileAccess.Read); + stream.Seek(0, SeekOrigin.Begin); + stream.Read(firstBytes, 0, 8); + stream.Seek(0, SeekOrigin.Begin); + } + + // PDFs begin with "%PDF", possibly with a UTF-8 BOM first + if (firstBytes is [0x25, 0x50, 0x44, 0x46, ..] or [0xEF, 0xBB, 0xBF, 0x25, 0x50, 0x44, 0x46, ..]) + { + return _pdfImporter.Import(input, importParams, progress); + } + if (firstBytes is [0x50, 0x4b, 0x03, 0x04, ..]) + { + return ImportZip(input, importParams, progress); + } + + // If we're recursively importing a zip file, we should ignore any entries that aren't supported formats + // rather than trying to import and throwing an exception instead. + if (skipUnsupported && ImageContext.GetFileFormatFromFirstBytes(firstBytes) == ImageFileFormat.Unknown) + { + return AsyncProducers.Empty(); + } + + return _imageImporter.Import(input, importParams, progress); + } + + private async IAsyncEnumerable ImportZip(InputPathOrStream input, ImportParams? importParams, + ProgressHandler progress) + { + using var zip = input.Stream != null ? new ZipArchive(input.Stream) : ZipFile.OpenRead(input.FilePath!); + int n = 0; + var fileEntries = zip.Entries.Where(entry => entry.Length > 0).ToList(); + progress.Report(n++, fileEntries.Count); + foreach (var entry in fileEntries) + { + using var entryStream = entry.Open(); + var memoryStream = new MemoryStream(); + entryStream.CopyTo(memoryStream); + await foreach (var image in Import(new InputPathOrStream(null, memoryStream, entry.Name), importParams, + progress.CancelToken, skipUnsupported: true)) + { + yield return image; + } + progress.Report(n++, fileEntries.Count); + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/IScannedImageImporter.cs b/NAPS2.Sdk/ImportExport/IScannedImageImporter.cs deleted file mode 100644 index 09da6aff8c..0000000000 --- a/NAPS2.Sdk/ImportExport/IScannedImageImporter.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace NAPS2.ImportExport; - -public interface IScannedImageImporter -{ - IAsyncEnumerable Import(string filePath, ImportParams importParams, ProgressHandler progress = default); -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/ImageImporter.cs b/NAPS2.Sdk/ImportExport/ImageImporter.cs new file mode 100644 index 0000000000..6051c63c08 --- /dev/null +++ b/NAPS2.Sdk/ImportExport/ImageImporter.cs @@ -0,0 +1,113 @@ +using Microsoft.Extensions.Logging; +using NAPS2.Scan; + +namespace NAPS2.ImportExport; + +/// +/// Imports image files. +/// +public class ImageImporter +{ + private readonly ScanningContext _scanningContext; + + public ImageImporter(ScanningContext scanningContext) + { + _scanningContext = scanningContext; + } + + public IAsyncEnumerable Import(string filePath, ImportParams? importParams = null, + ProgressHandler progress = default) => + Import(new InputPathOrStream(filePath, null, null), importParams, progress); + + public IAsyncEnumerable Import(Stream stream, ImportParams? importParams = null, + ProgressHandler progress = default) => + Import(new InputPathOrStream(null, stream, null), importParams, progress); + + internal IAsyncEnumerable Import(InputPathOrStream input, ImportParams? importParams = null, + ProgressHandler progress = default) + { + importParams ??= new ImportParams(); + return AsyncProducers.RunProducer(async produceImage => + { + if (progress.IsCancellationRequested) return; + + int frameCount = 0; + try + { + var callback = new ProgressCallback((current, max) => + { + frameCount = max; + if (current == 0) + { + progress.Report(0, frameCount); + } + }); + var toImport = input.Stream != null + ? _scanningContext.ImageContext.LoadFrames(input.Stream, callback) + : _scanningContext.ImageContext.LoadFrames(input.FilePath!, callback); + + int i = 0; + await foreach (var frame in toImport) + { + using (frame) + { + if (progress.IsCancellationRequested) return; + + bool lossless = frame.OriginalFileFormat is ImageFileFormat.Bmp or ImageFileFormat.Png; + var image = _scanningContext.CreateProcessedImage( + frame.OriginalFileFormat == ImageFileFormat.Jpeg && + (input.Stream == null || input.Stream.CanSeek) + ? CreateJpegStorageWithoutReEncoding(input, frame) + : frame, + lossless, + -1, + null); + image = ImportPostProcessor.AddPostProcessingData( + image, + frame, + importParams.ThumbnailSize, + importParams.BarcodeDetectionOptions, + true); + if (input.FilePath != null) + { + image = image.WithPostProcessingData( + image.PostProcessingData with { OriginalFilePath = input.FilePath }, true); + } + + progress.Report(++i, frameCount); + produceImage(image); + } + } + } + catch (Exception e) + { + if (input.FilePath != null) + { + _scanningContext.Logger.LogError(e, "Error importing image: {FilePath}", input.FilePath); + } + else + { + _scanningContext.Logger.LogError(e, "Error importing image"); + } + // Handle and notify the user outside the method so that errors importing multiple files can be aggregated + throw; + } + }); + } + + private IImageStorage CreateJpegStorageWithoutReEncoding(InputPathOrStream input, IMemoryImage loadedImage) + { + if (_scanningContext.FileStorageManager == null) + { + return loadedImage; + } + var storagePath = _scanningContext.FileStorageManager.NextFilePath() + ".jpg"; + if (input.Stream != null) + { + // TODO: Technically we don't know if the stream we were given started at 0 + input.Stream.Position = 0; + } + input.CopyToFile(storagePath); + return new ImageFileStorage(storagePath); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Images/IImageImporter.cs b/NAPS2.Sdk/ImportExport/Images/IImageImporter.cs deleted file mode 100644 index 7312be179a..0000000000 --- a/NAPS2.Sdk/ImportExport/Images/IImageImporter.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace NAPS2.ImportExport.Images; - -public interface IImageImporter : IScannedImageImporter -{ -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Images/ImageImporter.cs b/NAPS2.Sdk/ImportExport/Images/ImageImporter.cs deleted file mode 100644 index 04abb746ee..0000000000 --- a/NAPS2.Sdk/ImportExport/Images/ImageImporter.cs +++ /dev/null @@ -1,72 +0,0 @@ -using NAPS2.Scan; - -namespace NAPS2.ImportExport.Images; - -public class ImageImporter : IImageImporter -{ - private readonly ScanningContext _scanningContext; - private readonly ImageContext _imageContext; - private readonly ImportPostProcessor _importPostProcessor; - - public ImageImporter(ScanningContext scanningContext, ImageContext imageContext, - ImportPostProcessor importPostProcessor) - { - _scanningContext = scanningContext; - _imageContext = imageContext; - _importPostProcessor = importPostProcessor; - } - - public IAsyncEnumerable Import(string filePath, ImportParams importParams, - ProgressHandler progress = default) - { - return AsyncProducers.RunProducer(async produceImage => - { - if (progress.IsCancellationRequested) return; - - int frameCount = 0; - try - { - var toImport = - _imageContext.LoadFrames(filePath, new ProgressCallback((current, max) => - { - frameCount = max; - if (current == 0) - { - progress.Report(0, frameCount); - } - })); - - int i = 0; - await foreach (var frame in toImport) - { - using (frame) - { - if (progress.IsCancellationRequested) return; - - bool lossless = frame.OriginalFileFormat is ImageFileFormat.Bmp or ImageFileFormat.Png; - var image = _scanningContext.CreateProcessedImage( - frame, - BitDepth.Color, - lossless, - -1); - image = _importPostProcessor.AddPostProcessingData( - image, - frame, - importParams.ThumbnailSize, - importParams.BarcodeDetectionOptions, - true); - - progress.Report(++i, frameCount); - produceImage(image); - } - } - } - catch (Exception e) - { - Log.ErrorException("Error importing image: " + filePath, e); - // Handle and notify the user outside the method so that errors importing multiple files can be aggregated - throw; - } - }); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Images/ImportPostProcessor.cs b/NAPS2.Sdk/ImportExport/Images/ImportPostProcessor.cs deleted file mode 100644 index a428ca28f4..0000000000 --- a/NAPS2.Sdk/ImportExport/Images/ImportPostProcessor.cs +++ /dev/null @@ -1,30 +0,0 @@ -using NAPS2.Scan; - -namespace NAPS2.ImportExport.Images; - -// TODO: Maybe make this static (and ImportExportHelper, ImageClipboard, etc.) now that ImageContext is on the image -public class ImportPostProcessor -{ - public ProcessedImage AddPostProcessingData(ProcessedImage image, IMemoryImage? rendered, int? thumbnailSize, - BarcodeDetectionOptions barcodeDetectionOptions, bool disposeOriginalImage) - { - if (!thumbnailSize.HasValue && !barcodeDetectionOptions.DetectBarcodes) - { - // This is a bit weird, but technically "disposeOriginalImage" doesn't mean we're actually disposing it, - // just that the caller releases ownership of it (and takes ownership of the return value). - return disposeOriginalImage ? image : image.Clone(); - } - - using var actualRendered = rendered == null ? image.Render() : rendered.Clone(); - var barcodeDetection = BarcodeDetector.Detect(actualRendered, barcodeDetectionOptions); - var thumbnail = thumbnailSize.HasValue - ? actualRendered.PerformTransform(new ThumbnailTransform(thumbnailSize.Value)) - : null; - return image.WithPostProcessingData(image.PostProcessingData with - { - Thumbnail = thumbnail, - ThumbnailTransformState = image.TransformState, - BarcodeDetection = barcodeDetection - }, disposeOriginalImage); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/ImportParams.cs b/NAPS2.Sdk/ImportExport/ImportParams.cs index 0be74d7907..b80cbd78d8 100644 --- a/NAPS2.Sdk/ImportExport/ImportParams.cs +++ b/NAPS2.Sdk/ImportExport/ImportParams.cs @@ -2,6 +2,9 @@ namespace NAPS2.ImportExport; +/// +/// Additional parameters for importing files (e.g. PDF password, barcode detection, thumbnail rendering). +/// public class ImportParams { public ImportParams() diff --git a/NAPS2.Sdk/ImportExport/ImportPostProcessor.cs b/NAPS2.Sdk/ImportExport/ImportPostProcessor.cs new file mode 100644 index 0000000000..cd25815e0c --- /dev/null +++ b/NAPS2.Sdk/ImportExport/ImportPostProcessor.cs @@ -0,0 +1,38 @@ +using NAPS2.Scan; + +namespace NAPS2.ImportExport; + +internal static class ImportPostProcessor +{ + public static ProcessedImage AddPostProcessingData(ProcessedImage image, IMemoryImage? rendered, int? thumbnailSize, + BarcodeDetectionOptions barcodeDetectionOptions, bool disposeOriginalImage) + { + if (!thumbnailSize.HasValue && !barcodeDetectionOptions.DetectBarcodes) + { + // This is a bit weird, but technically "disposeOriginalImage" doesn't mean we're actually disposing it, + // just that the caller releases ownership of it (and takes ownership of the return value). + return disposeOriginalImage ? image : image.Clone(); + } + + var actualRendered = rendered == null ? image.Render() : rendered.Clone(); + try + { + var barcodeDetection = BarcodeDetector.Detect(actualRendered, barcodeDetectionOptions); + var thumbnail = thumbnailSize.HasValue + ? actualRendered.PerformTransform(new ThumbnailTransform(thumbnailSize.Value)) + : null; + if (thumbnail == null) actualRendered.Dispose(); + return image.WithPostProcessingData(image.PostProcessingData with + { + Thumbnail = thumbnail, + ThumbnailTransformState = image.TransformState, + Barcode = barcodeDetection + }, disposeOriginalImage); + } + catch (Exception) + { + actualRendered.Dispose(); + throw; + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/InputPathOrStream.cs b/NAPS2.Sdk/ImportExport/InputPathOrStream.cs new file mode 100644 index 0000000000..dc8370fc10 --- /dev/null +++ b/NAPS2.Sdk/ImportExport/InputPathOrStream.cs @@ -0,0 +1,43 @@ +using NAPS2.Pdf.Pdfium; + +namespace NAPS2.ImportExport; + +internal record InputPathOrStream(string? FilePath, Stream? Stream, string? StreamFileName) +{ + public string FileName => Stream != null + ? StreamFileName ?? "" + : Path.GetFileName(FilePath)!; + + public void CopyToFile(string outputPath) + { + if (Stream != null) + { + using var outputStream = new FileStream(outputPath, FileMode.Create); + Stream.CopyTo(outputStream); + } + else + { + File.Copy(FilePath!, outputPath); + } + } + + public void CopyToStream(Stream outputStream) + { + if (Stream != null) + { + Stream.CopyTo(outputStream); + } + else + { + using var inputStream = new FileStream(FilePath!, FileMode.Open); + inputStream.CopyTo(outputStream); + } + } + + public PdfDocument LoadPdfDoc(string? password) + { + return Stream != null + ? PdfDocument.Load(Stream, password) + : PdfDocument.Load(FilePath!, password); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/OutputPathOrStream.cs b/NAPS2.Sdk/ImportExport/OutputPathOrStream.cs new file mode 100644 index 0000000000..94aca12363 --- /dev/null +++ b/NAPS2.Sdk/ImportExport/OutputPathOrStream.cs @@ -0,0 +1,33 @@ +using NAPS2.Pdf.Pdfium; + +namespace NAPS2.ImportExport; + +internal record OutputPathOrStream(string? Path, Stream? Stream) +{ + public void CopyFromStream(MemoryStream inputStream) + { + if (Stream != null) + { + inputStream.CopyTo(Stream); + } + else + { + FileSystemHelper.EnsureParentDirExists(Path!); + using var fileStream = new FileStream(Path!, FileMode.Create); + inputStream.CopyTo(fileStream); + } + } + + public void SavePdfDoc(PdfDocument doc) + { + if (Stream != null) + { + doc.Save(Stream); + } + else + { + FileSystemHelper.EnsureParentDirExists(Path!); + doc.Save(Path!); + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/CcittReader.cs b/NAPS2.Sdk/ImportExport/Pdf/CcittReader.cs deleted file mode 100644 index 1a757986ec..0000000000 --- a/NAPS2.Sdk/ImportExport/Pdf/CcittReader.cs +++ /dev/null @@ -1,79 +0,0 @@ -using NAPS2.ImportExport.Pdf.Pdfium; - -namespace NAPS2.ImportExport.Pdf; - -public static class CcittReader -{ - private static readonly byte[] TiffBeforeDataLen = { 0x49, 0x49, 0x2A, 0x00 }; - private static readonly byte[] TiffBeforeData = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - private static readonly byte[] TiffBeforeWidth = { 0x07, 0x00, 0x00, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00 }; - private static readonly byte[] TiffBeforeHeight = { 0x01, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00 }; - private static readonly byte[] TiffBeforeBits = { 0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00 }; - - private static readonly byte[] TiffBeforeRealLen = - { - 0x03, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x11, 0x01, 0x04, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x15, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x17, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00 - }; - - private static readonly byte[] TiffTrailer = { 0x00, 0x00, 0x00, 0x00 }; - - // Sample full tiff LEN------------------- DATA------------------ WIDTH----------------- HEIGHT---------------- BITS PER COMP--------- REALLEN--------------- - // { 0x49, 0x49, 0x2A, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x99, 0x99, 0x99, 0x99, 0x07, 0x00, 0x00, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x77, 0x77, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x88, 0x88, 0x00, 0x00, 0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x11, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x15, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x17, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - - public static IMemoryImage LoadRawCcitt(ImageContext imageContext, byte[] buffer, PdfImageMetadata metadata) - { - // We don't have easy access to a standalone CCITT G4 decoder, so we'll make use of the .NET TIFF decoder - // by constructing a valid TIFF file "manually" and directly injecting the bytestream - var stream = new MemoryStream(); - Write(stream, TiffBeforeDataLen); - // The bytestream is 2-padded, so we may need to append an extra zero byte - if (buffer.Length % 2 == 1) - { - Write(stream, buffer.Length + 0x11); - } - else - { - Write(stream, buffer.Length + 0x10); - } - - Write(stream, TiffBeforeData); - Write(stream, buffer); - if (buffer.Length % 2 == 1) - { - Write(stream, new byte[] { 0x00 }); - } - - Write(stream, TiffBeforeWidth); - Write(stream, metadata.Width); - Write(stream, TiffBeforeHeight); - Write(stream, metadata.Height); - Write(stream, TiffBeforeBits); - Write(stream, 1); // bits per component - Write(stream, TiffBeforeRealLen); - Write(stream, buffer.Length); - Write(stream, TiffTrailer); - stream.Seek(0, SeekOrigin.Begin); - - // TODO: If we need a TIFF hint for loading, it should go here. - return imageContext.Load(stream); - } - - private static void Write(MemoryStream stream, byte[] bytes) - { - stream.Write(bytes, 0, bytes.Length); - } - - private static void Write(MemoryStream stream, int value) - { - byte[] bytes = BitConverter.GetBytes(value); - if (!BitConverter.IsLittleEndian) - { - Array.Reverse(bytes); - } - - Debug.Assert(bytes.Length == 4); - stream.Write(bytes, 0, bytes.Length); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/IPdfExporter.cs b/NAPS2.Sdk/ImportExport/Pdf/IPdfExporter.cs deleted file mode 100644 index fc4a916b27..0000000000 --- a/NAPS2.Sdk/ImportExport/Pdf/IPdfExporter.cs +++ /dev/null @@ -1,9 +0,0 @@ -using NAPS2.Ocr; - -namespace NAPS2.ImportExport.Pdf; - -public interface IPdfExporter -{ - Task Export(string path, ICollection images, PdfExportParams exportParams, - OcrParams? ocrParams = null, ProgressHandler progress = default); -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/IPdfImporter.cs b/NAPS2.Sdk/ImportExport/Pdf/IPdfImporter.cs deleted file mode 100644 index f2ca5d077d..0000000000 --- a/NAPS2.Sdk/ImportExport/Pdf/IPdfImporter.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace NAPS2.ImportExport.Pdf; - -public interface IPdfImporter : IScannedImageImporter -{ -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/IPdfPasswordProvider.cs b/NAPS2.Sdk/ImportExport/Pdf/IPdfPasswordProvider.cs deleted file mode 100644 index 053b2ede01..0000000000 --- a/NAPS2.Sdk/ImportExport/Pdf/IPdfPasswordProvider.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace NAPS2.ImportExport.Pdf; - -public interface IPdfPasswordProvider -{ - bool ProvidePassword(string fileName, int attemptCount, out string password); -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfCompat.cs b/NAPS2.Sdk/ImportExport/Pdf/PdfCompat.cs deleted file mode 100644 index 8b36e0ac4b..0000000000 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfCompat.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace NAPS2.ImportExport.Pdf; - -public enum PdfCompat -{ - Default, - PdfA1B, - PdfA2B, - PdfA3B, - PdfA3U -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfExporter.cs b/NAPS2.Sdk/ImportExport/Pdf/PdfExporter.cs deleted file mode 100644 index 5863f0bf5c..0000000000 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfExporter.cs +++ /dev/null @@ -1,568 +0,0 @@ -using System.Globalization; -using System.Runtime.InteropServices; -using System.Threading; -using NAPS2.Images.Bitwise; -using NAPS2.ImportExport.Pdf.Pdfium; -using NAPS2.Ocr; -using NAPS2.Scan; -using PdfSharpCore.Drawing; -using PdfSharpCore.Drawing.Layout; -using PdfSharpCore.Pdf; -using PdfSharpCore.Pdf.IO; -using PdfSharpCore.Pdf.Security; -using PdfDocument = PdfSharpCore.Pdf.PdfDocument; -using PdfPage = PdfSharpCore.Pdf.PdfPage; - -namespace NAPS2.ImportExport.Pdf; - -public class PdfExporter : IPdfExporter -{ - private readonly ScanningContext _scanningContext; - - public PdfExporter(ScanningContext scanningContext) - { - _scanningContext = scanningContext; - } - - public async Task Export(string path, ICollection images, - PdfExportParams exportParams, OcrParams? ocrParams = null, ProgressHandler progress = default) - { - return await Task.Run(async () => - { - // The current iteration of PDF export is fairly complicated. We do a hybrid export using both PdfSharp - // and Pdfium. "Simple" exports just use PdfSharp. If we have imported PDF pages that are stored as PDFs - // (i.e. they weren't generated by NAPS2 and can't be extracted to a single image), we use Pdfium to: - // 1. Check if the pages have text (and therefore are ineligible for OCR). - // 2. Add those "passthrough" pages to the final PDF file (under certain conditions). - // For context PdfSharp has a number of bugs with handling arbitrary PDFs, so using Pdfium for exporting - // those PDF pages lets us avoid those bugs (given we also use Pdfium for importing). - // - // Export is also complicated by the fact that we may or may not have OCR enabled, and when it is enabled, - // we want to parallelize and pipeline the different operations (image rendering, OCR, PDF saving) to - // maximize performance. - // - // It would be simpler if we could use Pdfium for everything, but it doesn't support a lot of features we - // need, e.g. configuring interpolation, encryption, PDF/A, etc. - - var document = InitializeDocument(exportParams); - - // TODO: Consider storing text from imported image-based pages in PostProcessingData so it can be saved even - // when not exporting with OCR (assuming no transforms). - var ocrEngine = GetOcrEngine(ocrParams); - - var imagePages = new List(); - var pdfPages = new List(); - - int currentProgress = 0; - - void IncrementProgress() - { - Interlocked.Increment(ref currentProgress); - progress.Report(currentProgress, images.Count); - } - - progress.Report(0, images.Count); - - try - { - int pageIndex = 0; - foreach (var image in images) - { - var pageState = new PageExportState( - image, pageIndex++, document, document.AddPage(), ocrEngine, ocrParams, IncrementProgress, - progress.CancelToken, exportParams.Compat); - // TODO: To improve our ability to passthrough, we could consider using Pdfium to apply the transform to - // the underlying PDF file. For example, doing color shifting on individual text + image objects, or - // applying matrix changes. - // TODO: We also can consider doing this even for scanned image transforms - e.g. for deskew, maybe - // rather than rasterize that, rely on the pdf to do the skew transform, which should render better at - // different scaling. - if (IsPdfStorage(image.Storage) && image.TransformState == TransformState.Empty) - { - pdfPages.Add(pageState); - } - else - { - imagePages.Add(pageState); - } - } - - var imagePagesPipeline = ocrEngine != null - ? Pipeline.For(imagePages) - .Step(RenderStep) - .Step(InitOcrStep) - .Step(WaitForOcrStep) - .Step(WriteToPdfSharpStep) - .Run() - : Pipeline.For(imagePages) - .Step(RenderStep) - .Step(WriteToPdfSharpStep) - .Run(); - - var pdfPagesPrePipeline = ocrEngine != null - ? Pipeline.For(pdfPages).Step(CheckIfOcrNeededStep).Run() - : Task.FromResult(pdfPages); - - await pdfPagesPrePipeline; - - var pdfPagesOcrPipeline = Pipeline.For(pdfPages.Where(x => x.NeedsOcr)) - .Step(RenderStep) - .Step(InitOcrStep) - .Step(WaitForOcrStep) - .Step(WriteToPdfSharpStep) - .Run(); - - await imagePagesPipeline; - await pdfPagesOcrPipeline; - if (progress.IsCancellationRequested) return false; - - // TODO: Doing in memory as that's presumably faster than IO, but of course that's quite a bit of memory use potentially... - var stream = FinalizeAndSaveDocument(document, exportParams); - if (progress.IsCancellationRequested) return false; - - var passthroughPages = pdfPages.Where(x => !x.NeedsOcr).ToList(); - return MergePassthroughPages(stream, path, passthroughPages, exportParams, progress); - } - finally - { - // We can't use a DisposableList as the objects we need to dispose are generated on the fly - foreach (var state in imagePages.Concat(pdfPages)) - { - state.RenderedImage?.Dispose(); - } - } - }); - } - - private bool MergePassthroughPages(MemoryStream stream, string path, List passthroughPages, - PdfExportParams exportParams, ProgressHandler progress) - { - if (!passthroughPages.Any()) - { - using var fileStream = new FileStream(path, FileMode.Create); - stream.CopyTo(fileStream); - return true; - } - // TODO: Should we do this (or maybe the whole pdf export/import) in a worker to avoid contention? - // TODO: Although we would need to be careful to handle OcrRequestQueue state correctly across processes. - lock (PdfiumNativeLibrary.Instance) - { - var destBuffer = stream.GetBuffer(); - var destHandle = GCHandle.Alloc(destBuffer, GCHandleType.Pinned); - try - { - var password = exportParams.Encryption.EncryptPdf ? exportParams.Encryption.OwnerPassword : null; - using var destDoc = - Pdfium.PdfDocument.Load(destHandle.AddrOfPinnedObject(), (int) stream.Length, password); - foreach (var state in passthroughPages) - { - destDoc.DeletePage(state.PageIndex); - if (state.Image.Storage is ImageFileStorage fileStorage) - { - using var sourceDoc = Pdfium.PdfDocument.Load(fileStorage.FullPath); - CopyPage(destDoc, sourceDoc, state); - } - else if (state.Image.Storage is ImageMemoryStorage memoryStorage) - { - var sourceBuffer = memoryStorage.Stream.GetBuffer(); - var sourceHandle = GCHandle.Alloc(sourceBuffer, GCHandleType.Pinned); - try - { - using var sourceDoc = Pdfium.PdfDocument.Load(sourceHandle.AddrOfPinnedObject(), - (int) memoryStorage.Stream.Length); - CopyPage(destDoc, sourceDoc, state); - } - finally - { - sourceHandle.Free(); - } - } - if (progress.IsCancellationRequested) return false; - } - destDoc.Save(path); - return true; - } - finally - { - destHandle.Free(); - } - } - } - - private void CopyPage(Pdfium.PdfDocument destDoc, Pdfium.PdfDocument sourceDoc, PageExportState state) - { - destDoc.ImportPages(sourceDoc, "1", state.PageIndex); - } - - private static PdfDocument InitializeDocument(PdfExportParams exportParams) - { - var document = new PdfDocument(); - var creator = exportParams.Metadata.Creator; - document.Info.Creator = string.IsNullOrEmpty(creator) ? "NAPS2" : creator; - document.Info.Author = exportParams.Metadata.Author; - document.Info.Keywords = exportParams.Metadata.Keywords; - document.Info.Subject = exportParams.Metadata.Subject; - document.Info.Title = exportParams.Metadata.Title; - - if (exportParams.Encryption?.EncryptPdf == true - && (!string.IsNullOrEmpty(exportParams.Encryption.OwnerPassword) || - !string.IsNullOrEmpty(exportParams.Encryption.UserPassword))) - { - document.SecuritySettings.DocumentSecurityLevel = PdfDocumentSecurityLevel.Encrypted128Bit; - if (!string.IsNullOrEmpty(exportParams.Encryption.OwnerPassword)) - { - document.SecuritySettings.OwnerPassword = exportParams.Encryption.OwnerPassword; - } - - if (!string.IsNullOrEmpty(exportParams.Encryption.UserPassword)) - { - document.SecuritySettings.UserPassword = exportParams.Encryption.UserPassword; - } - - document.SecuritySettings.PermitAccessibilityExtractContent = - exportParams.Encryption.AllowContentCopyingForAccessibility; - document.SecuritySettings.PermitAnnotations = exportParams.Encryption.AllowAnnotations; - document.SecuritySettings.PermitAssembleDocument = - exportParams.Encryption.AllowDocumentAssembly; - document.SecuritySettings.PermitExtractContent = exportParams.Encryption.AllowContentCopying; - document.SecuritySettings.PermitFormsFill = exportParams.Encryption.AllowFormFilling; - document.SecuritySettings.PermitFullQualityPrint = - exportParams.Encryption.AllowFullQualityPrinting; - document.SecuritySettings.PermitModifyDocument = - exportParams.Encryption.AllowDocumentModification; - document.SecuritySettings.PermitPrint = exportParams.Encryption.AllowPrinting; - } - return document; - } - - private PageExportState RenderStep(PageExportState state) - { - if (state.CancelToken.IsCancellationRequested) return state; - state.RenderedImage = state.Image.Render(); - return state; - } - - private PageExportState WriteToPdfSharpStep(PageExportState state) - { - if (state.CancelToken.IsCancellationRequested) return state; - lock (state.Document) - { - var exportFormat = PrepareForExport(state); - DrawImageOnPage(state.Page, state.RenderedImage!, exportFormat, state.Compat); - if (state.OcrTask?.Result != null) - { - DrawOcrTextOnPage(state.Page, state.OcrTask.Result); - } - } - state.IncrementProgress(); - return state; - } - - private static ImageExportFormat PrepareForExport(PageExportState state) - { - var exportFormat = new ImageExportHelper() - .GetExportFormat(state.RenderedImage!, state.Image.Metadata.BitDepth, state.Image.Metadata.Lossless); - if (exportFormat.FileFormat == ImageFileFormat.Unspecified) - { - exportFormat = exportFormat with { FileFormat = ImageFileFormat.Jpeg }; - } - if (exportFormat.PixelFormat == ImagePixelFormat.BW1 && - state.RenderedImage!.LogicalPixelFormat != ImagePixelFormat.BW1) - { - state.RenderedImage = state.RenderedImage.PerformTransform(new BlackWhiteTransform()); - } - return exportFormat; - } - - private static MemoryStream FinalizeAndSaveDocument(PdfDocument document, PdfExportParams exportParams) - { - var compat = exportParams.Compat; - var now = DateTime.Now; - document.Info.CreationDate = now; - document.Info.ModificationDate = now; - if (compat == PdfCompat.PdfA1B) - { - PdfAHelper.SetCidStream(document); - PdfAHelper.DisableTransparency(document); - } - - if (compat != PdfCompat.Default) - { - PdfAHelper.SetColorProfile(document); - PdfAHelper.SetCidMap(document); - PdfAHelper.CreateXmpMetadata(document, compat); - } - - var stream = new MemoryStream(); - document.Save(stream); - return stream; - } - - private IOcrEngine? GetOcrEngine(OcrParams? ocrParams) - { - if (ocrParams?.LanguageCode != null) - { - var activeEngine = _scanningContext.OcrEngine; - if (activeEngine == null) - { - Log.Error("Supported OCR engine not installed.", ocrParams.LanguageCode); - } - else - { - return activeEngine; - } - } - return null; - } - - private PageExportState InitOcrStep(PageExportState state) - { - if (state.CancelToken.IsCancellationRequested) return state; - var ext = state.RenderedImage!.OriginalFileFormat == ImageFileFormat.Png ? ".png" : ".jpg"; - string ocrTempFilePath = Path.Combine(_scanningContext.TempFolderPath, Path.GetRandomFileName() + ext); - if (!_scanningContext.OcrRequestQueue.HasCachedResult(state.OcrEngine!, state.Image, state.OcrParams!)) - { - // Save the image to a file for use in OCR. - // We don't need to delete this file as long as we pass it to OcrRequestQueue.Enqueue, which takes - // ownership and guarantees its eventual deletion. - state.RenderedImage!.Save(ocrTempFilePath); - } - - // Start OCR - state.OcrTask = _scanningContext.OcrRequestQueue.Enqueue( - state.OcrEngine!, state.Image, ocrTempFilePath, state.OcrParams!, OcrPriority.Foreground, - state.CancelToken); - return state; - } - - private async Task WaitForOcrStep(PageExportState state) - { - if (state.CancelToken.IsCancellationRequested) return state; - await state.OcrTask!; - return state; - } - - private PageExportState CheckIfOcrNeededStep(PageExportState state) - { - if (state.CancelToken.IsCancellationRequested) return state; - try - { - if (state.Image.Storage is ImageFileStorage fileStorage) - { - state.PageDocument = PdfReader.Open(fileStorage.FullPath, PdfDocumentOpenMode.Import); - state.NeedsOcr = !new PdfiumPdfReader() - .ReadTextByPage(fileStorage.FullPath) - .Any(x => x.Trim().Length > 0); - } - else if (state.Image.Storage is ImageMemoryStorage memoryStorage) - { - state.PageDocument = PdfReader.Open(memoryStorage.Stream, PdfDocumentOpenMode.Import); - state.NeedsOcr = !new PdfiumPdfReader() - .ReadTextByPage(memoryStorage.Stream.GetBuffer(), (int) memoryStorage.Stream.Length) - .Any(x => x.Trim().Length > 0); - } - } - catch (Exception ex) - { - Log.Error("Could not import PDF page for possible OCR, falling back to non-OCR path", ex); - } - if (!state.NeedsOcr) - { - // TODO: Could also switch around the checks, not sure which order is better - state.PageDocument?.Close(); - } - return state; - } - - private static void DrawOcrTextOnPage(PdfPage page, OcrResult ocrResult) - { -#if DEBUG && DEBUGOCR - using XGraphics gfx = XGraphics.FromPdfPage(page, XGraphicsPdfPageOptions.Append); -#else - using XGraphics gfx = XGraphics.FromPdfPage(page, XGraphicsPdfPageOptions.Prepend); -#endif - var tf = new XTextFormatter(gfx); - foreach (var element in ocrResult.Elements) - { - if (string.IsNullOrEmpty(element.Text)) continue; - - var adjustedBounds = AdjustBounds(element.Bounds, (float) page.Width / ocrResult.PageBounds.w, - (float) page.Height / ocrResult.PageBounds.h); -#if DEBUG && DEBUGOCR - gfx.DrawRectangle(new XPen(XColor.FromArgb(255, 0, 0)), adjustedBounds); -#endif - var adjustedFontSize = CalculateFontSize(element.Text, adjustedBounds, gfx); - // Special case to avoid accidentally recognizing big lines as dashes/underscores - if (adjustedFontSize > 100 && (element.Text == "-" || element.Text == "_")) continue; - var font = new XFont("Times New Roman", adjustedFontSize, XFontStyle.Regular, - new XPdfFontOptions(PdfFontEncoding.Unicode)); - var adjustedTextSize = gfx.MeasureString(element.Text, font); - var verticalOffset = (adjustedBounds.Height - adjustedTextSize.Height) / 2; - var horizontalOffset = (adjustedBounds.Width - adjustedTextSize.Width) / 2; - adjustedBounds.Offset((float) horizontalOffset, (float) verticalOffset); - tf.DrawString(element.RightToLeft ? ReverseText(element.Text) : element.Text, font, XBrushes.Transparent, - adjustedBounds); - } - } - - private static string ReverseText(string text) - { - TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(text); - List elements = new List(); - while (enumerator.MoveNext()) - { - elements.Add(enumerator.GetTextElement()); - } - elements.Reverse(); - return string.Concat(elements); - } - - private void DrawImageOnPage(PdfPage page, IMemoryImage image, ImageExportFormat exportFormat, PdfCompat compat) - { - using var xImage = XImage.FromImageSource(new ImageSource(image, exportFormat)); - if (compat != PdfCompat.Default) - { - xImage.Interpolate = false; - } - var (realWidth, realHeight) = GetRealSize(image); - page.Width = realWidth; - page.Height = realHeight; - using XGraphics gfx = XGraphics.FromPdfPage(page); - gfx.DrawImage(xImage, 0, 0, realWidth, realHeight); - } - - private static (int width, int height) GetRealSize(IMemoryImage img) - { - double hAdjust = 72 / img.HorizontalResolution; - double vAdjust = 72 / img.VerticalResolution; - if (double.IsInfinity(hAdjust) || double.IsInfinity(vAdjust)) - { - hAdjust = vAdjust = 0.75; - } - double realWidth = img.Width * hAdjust; - double realHeight = img.Height * vAdjust; - return ((int) realWidth, (int) realHeight); - } - - private static XRect AdjustBounds((int x, int y, int w, int h) bounds, float hAdjust, float vAdjust) => - new XRect(bounds.x * hAdjust, bounds.y * vAdjust, bounds.w * hAdjust, bounds.h * vAdjust); - - private static int CalculateFontSize(string text, XRect adjustedBounds, XGraphics gfx) - { - int fontSizeGuess = Math.Max(1, (int) (adjustedBounds.Height)); - var measuredBoundsForGuess = - gfx.MeasureString(text, new XFont("Times New Roman", fontSizeGuess, XFontStyle.Regular)); - double adjustmentFactor = adjustedBounds.Width / measuredBoundsForGuess.Width; - int adjustedFontSize = Math.Max(1, (int) Math.Floor(fontSizeGuess * adjustmentFactor)); - return adjustedFontSize; - } - - private static bool IsPdfStorage(IImageStorage storage) => storage switch - { - ImageFileStorage fileStorage => Path.GetExtension(fileStorage.FullPath).ToLowerInvariant() == ".pdf", - ImageMemoryStorage memoryStorage => memoryStorage.TypeHint == ".pdf", - _ => false - }; - - private class PageExportState - { - public PageExportState(ProcessedImage image, int pageIndex, PdfDocument document, PdfPage page, - IOcrEngine? ocrEngine, OcrParams? ocrParams, Action incrementProgress, CancellationToken cancelToken, - PdfCompat compat) - { - Image = image; - PageIndex = pageIndex; - Document = document; - Page = page; - OcrEngine = ocrEngine; - OcrParams = ocrParams; - IncrementProgress = incrementProgress; - CancelToken = cancelToken; - Compat = compat; - } - - public ProcessedImage Image { get; } - public int PageIndex { get; } - - public PdfDocument Document { get; } - public PdfPage Page { get; set; } - public IOcrEngine? OcrEngine { get; } - public OcrParams? OcrParams { get; } - public Action IncrementProgress { get; } - public CancellationToken CancelToken { get; } - public PdfCompat Compat { get; } - - public bool NeedsOcr { get; set; } - public IMemoryImage? RenderedImage { get; set; } - public Task? OcrTask { get; set; } - public PdfDocument? PageDocument { get; set; } - } - - private class ImageSource : IImageSource - { - private readonly IMemoryImage _image; - private readonly ImageExportFormat _exportFormat; - - public ImageSource(IMemoryImage image, ImageExportFormat exportFormat) - { - _image = image; - _exportFormat = exportFormat; - } - - public void SaveAsJpeg(MemoryStream ms) - { - _image.Save(ms, ImageFileFormat.Jpeg); - } - - public void SaveAsPdfBitmap(MemoryStream ms) - { - var subPixelType = _exportFormat.PixelFormat switch - { - ImagePixelFormat.ARGB32 => SubPixelType.Bgra, - ImagePixelFormat.RGB24 or ImagePixelFormat.Gray8 => SubPixelType.Bgr, - _ => throw new InvalidOperationException("Expected 8/24/32 bit bitmap") - }; - var dstPixelInfo = new PixelInfo(_image.Width, _image.Height, subPixelType) { InvertY = true }; - ms.SetLength(dstPixelInfo.Length); - new CopyBitwiseImageOp().Perform(_image, ms.GetBuffer(), dstPixelInfo); - } - - public void SaveAsPdfIndexedBitmap(MemoryStream ms) - { - if (_image.LogicalPixelFormat != ImagePixelFormat.BW1) - throw new InvalidOperationException("Expected 1 bit bitmap"); - var dstPixelInfo = new PixelInfo(_image.Width, _image.Height, SubPixelType.Bit) { InvertY = true }; - ms.SetLength(dstPixelInfo.Length); - new CopyBitwiseImageOp().Perform(_image, ms.GetBuffer(), dstPixelInfo); - } - - public int Width => _image.Width; - public int Height => _image.Height; - public string? Name => null; - - public XImageFormat ImageFormat - { - get - { - if (_exportFormat.FileFormat == ImageFileFormat.Jpeg) - { - return XImageFormat.Jpeg; - } - if (_exportFormat.PixelFormat == ImagePixelFormat.BW1) - { - return XImageFormat.Indexed; - } - if (_exportFormat.PixelFormat == ImagePixelFormat.ARGB32) - { - return XImageFormat.Argb32; - } - // TODO: Ideally we should have Gray8 support in PdfSharp - if (_exportFormat.PixelFormat is ImagePixelFormat.RGB24 or ImagePixelFormat.Gray8) - { - return XImageFormat.Rgb24; - } - throw new Exception($"Unsupported pixel format: {_exportFormat.PixelFormat}"); - } - } - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfImportException.cs b/NAPS2.Sdk/ImportExport/Pdf/PdfImportException.cs deleted file mode 100644 index 5165bfba6f..0000000000 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfImportException.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace NAPS2.ImportExport.Pdf; - -public class PdfImportException : Exception -{ - public PdfImportException(string message) - : base(message) - { - } - - public PdfImportException(string message, Exception innerException) - : base(message, innerException) - { - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfMatrix.cs b/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfMatrix.cs deleted file mode 100644 index 77a1270032..0000000000 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfMatrix.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NAPS2.ImportExport.Pdf.Pdfium; - -public record struct PdfMatrix(float a, float b, float c, float d, float e, float f) -{ - public static PdfMatrix FillPage(float width, float height) - { - return new PdfMatrix(width, 0, 0, height, 0, 0); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfiumPdfRenderer.cs b/NAPS2.Sdk/ImportExport/Pdf/PdfiumPdfRenderer.cs deleted file mode 100644 index 112f1f63ff..0000000000 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfiumPdfRenderer.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Runtime.InteropServices; -using NAPS2.Images.Bitwise; -using NAPS2.ImportExport.Pdf.Pdfium; - -namespace NAPS2.ImportExport.Pdf; - -public class PdfiumPdfRenderer : IPdfRenderer -{ - public IEnumerable Render(ImageContext imageContext, string path, PdfRenderSize renderSize, - string? password = null) - { - // Pdfium is not thread-safe - lock (PdfiumNativeLibrary.Instance) - { - using var doc = PdfDocument.Load(path, password); - foreach (var memoryImage in RenderDocument(imageContext, renderSize, doc)) - { - yield return memoryImage; - } - } - } - - public IEnumerable Render(ImageContext imageContext, byte[] buffer, int length, - PdfRenderSize renderSize, string? password = null) - { - // Pdfium is not thread-safe - lock (PdfiumNativeLibrary.Instance) - { - var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); - try - { - using var doc = PdfDocument.Load(handle.AddrOfPinnedObject(), length, password); - foreach (var memoryImage in RenderDocument(imageContext, renderSize, doc)) - { - yield return memoryImage; - } - } - finally - { - handle.Free(); - } - } - } - - private IEnumerable RenderDocument(ImageContext imageContext, PdfRenderSize renderSize, - PdfDocument doc) - { - var pageCount = doc.PageCount; - for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) - { - using var page = doc.GetPage(pageIndex); - - var image = PdfiumImageExtractor.GetSingleImage(imageContext, page); - if (image != null) - { - yield return image; - continue; - } - yield return RenderPageToNewImage(imageContext, page, pageIndex, renderSize); - } - } - - public unsafe IMemoryImage RenderPageToNewImage(ImageContext imageContext, PdfPage page, int pageIndex, - PdfRenderSize renderSize) - { - var widthInInches = page.Width / 72; - var heightInInches = page.Height / 72; - - var (widthInPx, heightInPx, xDpi, yDpi) = renderSize.GetDimensions(widthInInches, heightInInches, pageIndex); - - var bitmap = imageContext.Create(widthInPx, heightInPx, ImagePixelFormat.RGB24); - bitmap.SetResolution(xDpi, yDpi); - - // As Pdfium only supports BGR, to be general we need to store it in an intermediate buffer, - // then use a copy operation to get the data to our output image (which might be BGR or RGB). - // TODO: Consider bypassing this by supporting BGR on mac etc. - var pixelInfo = new PixelInfo(widthInPx, heightInPx, SubPixelType.Bgr); - var buffer = new byte[pixelInfo.Length]; - fixed (byte* ptr = buffer) - { - using var pdfiumBitmap = - PdfBitmap.CreateFromPointerBgr(widthInPx, heightInPx, (IntPtr) ptr, pixelInfo.Stride); - pdfiumBitmap.FillRect(0, 0, widthInPx, heightInPx, PdfBitmap.WHITE); - pdfiumBitmap.RenderPage(page, 0, 0, widthInPx, heightInPx); - - new CopyBitwiseImageOp().Perform(buffer, pixelInfo, bitmap); - - return bitmap; - } - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfiumWorkerCoordinator.cs b/NAPS2.Sdk/ImportExport/Pdf/PdfiumWorkerCoordinator.cs deleted file mode 100644 index 7fb9fa089d..0000000000 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfiumWorkerCoordinator.cs +++ /dev/null @@ -1,35 +0,0 @@ -using NAPS2.Remoting.Worker; - -namespace NAPS2.ImportExport.Pdf; - -public class PdfiumWorkerCoordinator : IPdfRenderer -{ - private readonly WorkerPool _workerPool; - - public PdfiumWorkerCoordinator(WorkerPool workerPool) - { - _workerPool = workerPool; - } - - public IEnumerable Render(ImageContext imageContext, string path, PdfRenderSize renderSize, string? password = null) - { - if (password != null) - { - // TODO: Do we want to implement this? - throw new InvalidOperationException(); - } - // TODO: Only use worker on windows? Or what... - var image = _workerPool.Use(worker => - { - // TODO: Transmit render size - var imageStream = new MemoryStream(worker.Service.RenderPdf(path, renderSize.Dpi ?? 300)); - return imageContext.Load(imageStream); - }); - return new[] { image }; - } - - public IEnumerable Render(ImageContext imageContext, byte[] buffer, int length, PdfRenderSize renderSize, string? password = null) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Placeholders.cs b/NAPS2.Sdk/ImportExport/Placeholders.cs index e9b8177d9b..ca04f4694a 100644 --- a/NAPS2.Sdk/ImportExport/Placeholders.cs +++ b/NAPS2.Sdk/ImportExport/Placeholders.cs @@ -7,7 +7,7 @@ namespace NAPS2.ImportExport; /// Class for handling substitution of special values in file paths. For example, "$(YYYY)" can be substituted with the current year. /// Use Placeholders.All for recommended substitutions. Alternatively, you can use Placeholders.Env or Placeholders.None if you prefer. /// -public abstract class Placeholders +internal abstract class Placeholders { public const string YEAR_4_DIGITS = "$(YYYY)"; public const string YEAR_2_DIGITS = "$(YY)"; @@ -27,17 +27,22 @@ public abstract class Placeholders /// Substitutes all the standard placeholders. For example, "$(YYYY)-$(MM)-$(DD) $(hh):$(mm):$(ss)" is substituted with the current date and time. Substitutes environment variables. Handles auto-numbering for multiple files, /// using the numeric placeholders ("$(n)", "$(nn)", "$(nnn)", or "$(nnnn)") if specified; otherwise, the number is appended to the file name. /// - public static DefaultPlaceholders All => new DefaultPlaceholders(); + public static DefaultPlaceholders All => new(); + + /// + /// Substitutes all the standard placeholders. For example, "$(YYYY)-$(MM)-$(DD) $(hh):$(mm):$(ss)" is substituted with the current date and time. Substitutes environment variables. + /// + public static DefaultPlaceholders NonNumeric => new(includeNumeric: false); /// /// Substitutes environment variables in file names. Not recommended if you may be saving multiple files. /// - public static EnvironmentPlaceholders Env => new EnvironmentPlaceholders(); + public static EnvironmentPlaceholders Env => new(); /// /// Does not make any changes to the file name. Not recommended if you may be saving multiple files. /// - public static StubPlaceholders None => new StubPlaceholders(); + public static StubPlaceholders None => new(); /// /// Performs substitutions on the given file path. @@ -70,7 +75,7 @@ public class EnvironmentPlaceholders : Placeholders public class DefaultPlaceholders : Placeholders { private static readonly Dictionary> Replacements = - new Dictionary> + new() { { YEAR_4_DIGITS, dateTime => dateTime.ToString("yyyy") }, { YEAR_2_DIGITS, dateTime => dateTime.ToString("yy") }, @@ -81,13 +86,15 @@ public class DefaultPlaceholders : Placeholders { SECOND_2_DIGITS, dateTime => dateTime.ToString("ss") }, }; - private static readonly Regex NumberPlaceholderPattern = new Regex(@"\$\(n+\)"); + private static readonly Regex NumberPlaceholderPattern = new(@"\$\(n+\)"); private readonly DateTime? _dateTimeOverride; + private readonly bool _includeNumeric; - public DefaultPlaceholders(DateTime? dateTimeOverride = null) + public DefaultPlaceholders(DateTime? dateTimeOverride = null, bool includeNumeric = true) { _dateTimeOverride = dateTimeOverride; + _includeNumeric = includeNumeric; } /// @@ -95,7 +102,7 @@ public DefaultPlaceholders(DateTime? dateTimeOverride = null) /// /// The date and time to use. /// The new DefaultPlaceholders object. - public DefaultPlaceholders WithDate(DateTime dateTime) => new DefaultPlaceholders(dateTime); + public DefaultPlaceholders WithDate(DateTime dateTime) => new(dateTime, _includeNumeric); [return: NotNullIfNotNull("filePath")] public override string? Substitute(string? filePath, bool incrementIfExists = true, int numberSkip = 0, @@ -112,17 +119,21 @@ public DefaultPlaceholders(DateTime? dateTimeOverride = null) // Most placeholders don't need a special case result = Replacements.Aggregate(result, (current, ph) => current.Replace(ph.Key, ph.Value(dateTime))); // One does, however - var match = NumberPlaceholderPattern.Match(result); - if (match.Success) - { - result = NumberPlaceholderPattern.Replace(result, ""); - result = SubstituteNumber(result, match.Index, match.Length - 3, numberSkip, true); - } - else if (autoNumberDigits > 0) + if (_includeNumeric) { - result = result.Insert(result.Length - Path.GetExtension(result).Length, "."); - result = SubstituteNumber(result, result.Length - Path.GetExtension(result).Length, autoNumberDigits, - numberSkip, incrementIfExists); + var match = NumberPlaceholderPattern.Match(result); + if (match.Success) + { + result = NumberPlaceholderPattern.Replace(result, ""); + result = SubstituteNumber(result, match.Index, match.Length - 3, numberSkip, true); + } + else if (autoNumberDigits > 0) + { + result = result.Insert(result.Length - Path.GetExtension(result).Length, "."); + result = SubstituteNumber(result, result.Length - Path.GetExtension(result).Length, + autoNumberDigits, + numberSkip, incrementIfExists); + } } return result; diff --git a/NAPS2.Sdk/ImportExport/ScannedImageImporter.cs b/NAPS2.Sdk/ImportExport/ScannedImageImporter.cs deleted file mode 100644 index bad3a9b24e..0000000000 --- a/NAPS2.Sdk/ImportExport/ScannedImageImporter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using NAPS2.ImportExport.Images; -using NAPS2.ImportExport.Pdf; - -namespace NAPS2.ImportExport; - -public class ScannedImageImporter : IScannedImageImporter -{ - private readonly IScannedImageImporter _pdfImporter; - private readonly IScannedImageImporter _imageImporter; - - public ScannedImageImporter(IPdfImporter pdfImporter, IImageImporter imageImporter) - { - _pdfImporter = pdfImporter; - _imageImporter = imageImporter; - } - - public IAsyncEnumerable Import(string filePath, ImportParams importParams, ProgressHandler progress = default) - { - if (filePath == null) - { - throw new ArgumentNullException(nameof(filePath)); - } - switch (Path.GetExtension(filePath).ToLowerInvariant()) - { - case ".pdf": - return _pdfImporter.Import(filePath, importParams, progress); - default: - return _imageImporter.Import(filePath, importParams, progress); - } - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Util/Slice.cs b/NAPS2.Sdk/ImportExport/Slice.cs similarity index 99% rename from NAPS2.Sdk/Util/Slice.cs rename to NAPS2.Sdk/ImportExport/Slice.cs index d682912902..0208f3a93e 100644 --- a/NAPS2.Sdk/Util/Slice.cs +++ b/NAPS2.Sdk/ImportExport/Slice.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace NAPS2.Util; +namespace NAPS2.ImportExport; /// /// A class that represents a Python-style slice of a collection. diff --git a/NAPS2.Sdk/LICENSE b/NAPS2.Sdk/LICENSE index ccb604fc11..b6b1b2899a 100644 --- a/NAPS2.Sdk/LICENSE +++ b/NAPS2.Sdk/LICENSE @@ -1,7 +1,7 @@ NAPS2.Sdk https://www.github.com/cyanfish/naps2/ -Copyright 2009-2022 NAPS2 Contributors +Copyright 2009-2025 NAPS2 Contributors This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.Designer.cs b/NAPS2.Sdk/Lang/Resources/SdkResources.Designer.cs index fa3c731482..c7ba5a067f 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.Designer.cs +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -22,7 +21,7 @@ namespace NAPS2.Lang.Resources { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class SdkResources { + internal class SdkResources { private static global::System.Resources.ResourceManager resourceMan; @@ -36,7 +35,7 @@ internal SdkResources() { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { + internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NAPS2.Lang.Resources.SdkResources", typeof(SdkResources).Assembly); @@ -51,7 +50,7 @@ internal SdkResources() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -61,774 +60,99 @@ internal SdkResources() { } /// - /// Looks up a localized string similar to Acquiring data.... - /// - public static string AcquiringData { - get { - return ResourceManager.GetString("AcquiringData", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Operation in Progress. - /// - public static string ActiveOperations { - get { - return ResourceManager.GetString("ActiveOperations", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to All ({0}). - /// - public static string AllCount { - get { - return ResourceManager.GetString("AllCount", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An error occurred when trying to authorize.. - /// - public static string AuthError { - get { - return ResourceManager.GetString("AuthError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Deskewing.... - /// - public static string AutoDeskewing { - get { - return ResourceManager.GetString("AutoDeskewing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Deskew Progress. - /// - public static string AutoDeskewProgress { - get { - return ResourceManager.GetString("AutoDeskewProgress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An error occurred when trying to auto save.. - /// - public static string AutoSaveError { - get { - return ResourceManager.GetString("AutoSaveError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An unknown error ocurred during the batch scan.. - /// - public static string BatchError { - get { - return ResourceManager.GetString("BatchError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Batch cancelled.. - /// - public static string BatchStatusCancelled { - get { - return ResourceManager.GetString("BatchStatusCancelled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cancelling..... - /// - public static string BatchStatusCancelling { - get { - return ResourceManager.GetString("BatchStatusCancelling", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Batch completed successfully.. - /// - public static string BatchStatusComplete { - get { - return ResourceManager.GetString("BatchStatusComplete", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Batch scan stopped due to error.. - /// - public static string BatchStatusError { - get { - return ResourceManager.GetString("BatchStatusError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Scanning page {0}.... - /// - public static string BatchStatusPage { - get { - return ResourceManager.GetString("BatchStatusPage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Saving batch results.... - /// - public static string BatchStatusSaving { - get { - return ResourceManager.GetString("BatchStatusSaving", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Scanning page {0} (scan {1}).... - /// - public static string BatchStatusScanPage { - get { - return ResourceManager.GetString("BatchStatusScanPage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Waiting for scan {0}.... - /// - public static string BatchStatusWaitingForScan { - get { - return ResourceManager.GetString("BatchStatusWaitingForScan", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cancel. - /// - public static string Cancel { - get { - return ResourceManager.GetString("Cancel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cancel Batch. - /// - public static string CancelBatch { - get { - return ResourceManager.GetString("CancelBatch", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checking.... - /// - public static string CheckingForUpdates { - get { - return ResourceManager.GetString("CheckingForUpdates", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Choose Profile. - /// - public static string ChooseProfile { - get { - return ResourceManager.GetString("ChooseProfile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Clear. - /// - public static string Clear { - get { - return ResourceManager.GetString("Clear", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Close. - /// - public static string Close { - get { - return ResourceManager.GetString("Close", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Are you sure you want to cancel the batch scan?. - /// - public static string ConfirmCancelBatch { - get { - return ResourceManager.GetString("ConfirmCancelBatch", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Are you sure you want to clear {0} item(s)?. - /// - public static string ConfirmClearItems { - get { - return ResourceManager.GetString("ConfirmClearItems", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Are you sure you want to delete "{0}"?. - /// - public static string ConfirmDelete { - get { - return ResourceManager.GetString("ConfirmDelete", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Are you sure you want to delete {0} item(s)?. - /// - public static string ConfirmDeleteItems { - get { - return ResourceManager.GetString("ConfirmDeleteItems", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Are you sure you want to delete {0} profiles?. - /// - public static string ConfirmDeleteMultipleProfiles { - get { - return ResourceManager.GetString("ConfirmDeleteMultipleProfiles", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Are you sure you want to delete the profile {0}?. - /// - public static string ConfirmDeleteSingleProfile { - get { - return ResourceManager.GetString("ConfirmDeleteSingleProfile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The file {0} already exists. Do you want to overwrite it?. - /// - public static string ConfirmOverwriteFile { - get { - return ResourceManager.GetString("ConfirmOverwriteFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Are you sure you want undo your changes to {0} image(s)?. - /// - public static string ConfirmResetImages { - get { - return ResourceManager.GetString("ConfirmResetImages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Copying.... - /// - public static string Copying { - get { - return ResourceManager.GetString("Copying", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Copy Progress. - /// - public static string CopyProgress { - get { - return ResourceManager.GetString("CopyProgress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Custom ({0}x{1} {2}). - /// - public static string CustomPageSizeFormat { - get { - return ResourceManager.GetString("CustomPageSizeFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Delete. - /// - public static string Delete { - get { - return ResourceManager.GetString("Delete", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The selected scanner is busy.. - /// - public static string DeviceBusy { - get { - return ResourceManager.GetString("DeviceBusy", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The scanner's cover is open.. - /// - public static string DeviceCoverOpen { - get { - return ResourceManager.GetString("DeviceCoverOpen", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The selected scanner could not be found.. - /// - public static string DeviceNotFound { - get { - return ResourceManager.GetString("DeviceNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The selected scanner is offline.. - /// - public static string DeviceOffline { - get { - return ResourceManager.GetString("DeviceOffline", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The scanner has a paper jam.. - /// - public static string DevicePaperJam { - get { - return ResourceManager.GetString("DevicePaperJam", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The scanner is warming up.. - /// - public static string DeviceWarmingUp { - get { - return ResourceManager.GetString("DeviceWarmingUp", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Donate. - /// - public static string Donate { - get { - return ResourceManager.GetString("Donate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to NAPS2 is completely free. Consider making a donation.. - /// - public static string DonatePrompt { - get { - return ResourceManager.GetString("DonatePrompt", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You don't have permission to save files at this location.. - /// - public static string DontHavePermission { - get { - return ResourceManager.GetString("DontHavePermission", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Download Error. - /// - public static string DownloadError { - get { - return ResourceManager.GetString("DownloadError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Download Needed. - /// - public static string DownloadNeeded { - get { - return ResourceManager.GetString("DownloadNeeded", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The selected driver is not supported on this system.. - /// - public static string DriverNotSupported { - get { - return ResourceManager.GetString("DriverNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An error occurred while trying to send an email.. - /// - public static string EmailError { - get { - return ResourceManager.GetString("EmailError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Email PDF. - /// - public static string EmailPdf { - get { - return ResourceManager.GetString("EmailPdf", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Email PDF Progress. - /// - public static string EmailPdfProgress { - get { - return ResourceManager.GetString("EmailPdfProgress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error. - /// - public static string Error { - get { - return ResourceManager.GetString("Error", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An error occurred when trying to send the email.. - /// - public static string ErrorEmailing { - get { - return ResourceManager.GetString("ErrorEmailing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An error occurred when trying to save the file.. - /// - public static string ErrorSaving { - get { - return ResourceManager.GetString("ErrorSaving", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Estimated download size: {0} MB. - /// - public static string EstimatedDownloadSize { - get { - return ResourceManager.GetString("EstimatedDownloadSize", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An operation is in progress. Are you sure you want to exit and cancel the operation?. - /// - public static string ExitWithActiveOperations { - get { - return ResourceManager.GetString("ExitWithActiveOperations", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You have unsaved changes. Are you sure you want to exit and discard those changes?. - /// - public static string ExitWithUnsavedChanges { - get { - return ResourceManager.GetString("ExitWithUnsavedChanges", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The file could not be overwritten because it is currently in use.. - /// - public static string FileInUse { - get { - return ResourceManager.GetString("FileInUse", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to One or more files could not be downloaded.. - /// - public static string FilesCouldNotBeDownloaded { - get { - return ResourceManager.GetString("FilesCouldNotBeDownloaded", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} / {1} files. - /// - public static string FilesProgressFormat { - get { - return ResourceManager.GetString("FilesProgressFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to All Files. - /// - public static string FileTypeAllFiles { - get { - return ResourceManager.GetString("FileTypeAllFiles", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bitmap Files (*.bmp). - /// - public static string FileTypeBmp { - get { - return ResourceManager.GetString("FileTypeBmp", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Enhanced Windows MetaFile (*.emf). - /// - public static string FileTypeEmf { - get { - return ResourceManager.GetString("FileTypeEmf", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Exchangeable Image File (*.exif). - /// - public static string FileTypeExif { - get { - return ResourceManager.GetString("FileTypeExif", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to GIF File (*.gif). - /// - public static string FileTypeGif { - get { - return ResourceManager.GetString("FileTypeGif", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Image Files. - /// - public static string FileTypeImageFiles { - get { - return ResourceManager.GetString("FileTypeImageFiles", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to JPEG File (*.jpg, *.jpeg). - /// - public static string FileTypeJpeg { - get { - return ResourceManager.GetString("FileTypeJpeg", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PDF Document (*.pdf). - /// - public static string FileTypePdf { - get { - return ResourceManager.GetString("FileTypePdf", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PNG File (*.png). - /// - public static string FileTypePng { - get { - return ResourceManager.GetString("FileTypePng", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to TIFF File (*.tiff, *.tif). - /// - public static string FileTypeTiff { - get { - return ResourceManager.GetString("FileTypeTiff", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Image saved.. - /// - public static string ImageSaved { - get { - return ResourceManager.GetString("ImageSaved", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} images saved.. - /// - public static string ImagesSaved { - get { - return ResourceManager.GetString("ImagesSaved", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The file '{0}' could not be imported.. - /// - public static string ImportErrorCouldNot { - get { - return ResourceManager.GetString("ImportErrorCouldNot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The file '{0}' could not be imported. Only PDF files generated by NAPS2 can be imported.. - /// - public static string ImportErrorNAPS2Pdf { - get { - return ResourceManager.GetString("ImportErrorNAPS2Pdf", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Importing.... - /// - public static string Importing { - get { - return ResourceManager.GetString("Importing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Importing {0}.... - /// - public static string ImportingFormat { - get { - return ResourceManager.GetString("ImportingFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Import Progress. + /// Looks up a localized string similar to The selected scanner is busy.. /// - public static string ImportProgress { + internal static string DeviceBusy { get { - return ResourceManager.GetString("ImportProgress", resourceCulture); + return ResourceManager.GetString("DeviceBusy", resourceCulture); } } /// - /// Looks up a localized string similar to Install {0}. + /// Looks up a localized string similar to Communication with the scanning device was interrupted.. /// - public static string Install { + internal static string DeviceCommunicationFailure { get { - return ResourceManager.GetString("Install", resourceCulture); + return ResourceManager.GetString("DeviceCommunicationFailure", resourceCulture); } } /// - /// Looks up a localized string similar to Installation Complete. + /// Looks up a localized string similar to The scanner's cover is open.. /// - public static string InstallComplete { + internal static string DeviceCoverOpen { get { - return ResourceManager.GetString("InstallComplete", resourceCulture); + return ResourceManager.GetString("DeviceCoverOpen", resourceCulture); } } /// - /// Looks up a localized string similar to Installation complete. Do you want to restart NAPS2 now?. + /// Looks up a localized string similar to The selected scanner could not be found.. /// - public static string InstallCompletePromptRestart { + internal static string DeviceNotFound { get { - return ResourceManager.GetString("InstallCompletePromptRestart", resourceCulture); + return ResourceManager.GetString("DeviceNotFound", resourceCulture); } } /// - /// Looks up a localized string similar to Installation failed.. + /// Looks up a localized string similar to The selected scanner is offline.. /// - public static string InstallFailed { + internal static string DeviceOffline { get { - return ResourceManager.GetString("InstallFailed", resourceCulture); + return ResourceManager.GetString("DeviceOffline", resourceCulture); } } /// - /// Looks up a localized string similar to Installation Failed. + /// Looks up a localized string similar to The scanner has a paper jam.. /// - public static string InstallFailedTitle { + internal static string DevicePaperJam { get { - return ResourceManager.GetString("InstallFailedTitle", resourceCulture); + return ResourceManager.GetString("DevicePaperJam", resourceCulture); } } /// - /// Looks up a localized string similar to Listening on port {0}. + /// Looks up a localized string similar to The scanner is warming up.. /// - public static string ListeningOnPort { + internal static string DeviceWarmingUp { get { - return ResourceManager.GetString("ListeningOnPort", resourceCulture); + return ResourceManager.GetString("DeviceWarmingUp", resourceCulture); } } /// - /// Looks up a localized string similar to {0} ({1}x{2} {3}). + /// Looks up a localized string similar to The selected driver is not supported on this system.. /// - public static string NamedPageSizeFormat { + internal static string DriverNotSupported { get { - return ResourceManager.GetString("NamedPageSizeFormat", resourceCulture); + return ResourceManager.GetString("DriverNotSupported", resourceCulture); } } /// - /// Looks up a localized string similar to Name missing.. + /// Looks up a localized string similar to The file '{0}' could not be imported.. /// - public static string NameMissing { + internal static string ImportErrorCouldNot { get { - return ResourceManager.GetString("NameMissing", resourceCulture); + return ResourceManager.GetString("ImportErrorCouldNot", resourceCulture); } } /// /// Looks up a localized string similar to NAPS2. /// - public static string NAPS2 { + internal static string NAPS2 { get { return ResourceManager.GetString("NAPS2", resourceCulture); } } - /// - /// Looks up a localized string similar to NAPS2 Server. - /// - public static string Naps2Server { - get { - return ResourceManager.GetString("Naps2Server", resourceCulture); - } - } - /// /// Looks up a localized string similar to No device selected.. /// - public static string NoDeviceSelected { + internal static string NoDeviceSelected { get { return ResourceManager.GetString("NoDeviceSelected", resourceCulture); } @@ -837,7 +161,7 @@ public static string NoDeviceSelected { /// /// Looks up a localized string similar to No scanning device was found.. /// - public static string NoDevicesFound { + internal static string NoDevicesFound { get { return ResourceManager.GetString("NoDevicesFound", resourceCulture); } @@ -846,7 +170,7 @@ public static string NoDevicesFound { /// /// Looks up a localized string similar to The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver.. /// - public static string NoDuplexSupport { + internal static string NoDuplexSupport { get { return ResourceManager.GetString("NoDuplexSupport", resourceCulture); } @@ -855,7 +179,7 @@ public static string NoDuplexSupport { /// /// Looks up a localized string similar to The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver.. /// - public static string NoFeederSupport { + internal static string NoFeederSupport { get { return ResourceManager.GetString("NoFeederSupport", resourceCulture); } @@ -864,277 +188,52 @@ public static string NoFeederSupport { /// /// Looks up a localized string similar to No pages are in the feeder.. /// - public static string NoPagesInFeeder { + internal static string NoPagesInFeeder { get { return ResourceManager.GetString("NoPagesInFeeder", resourceCulture); } } /// - /// Looks up a localized string similar to No updates available.. - /// - public static string NoUpdates { - get { - return ResourceManager.GetString("NoUpdates", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to OCR Progress. - /// - public static string OcrProgress { - get { - return ResourceManager.GetString("OcrProgress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An update to OCR is available.. - /// - public static string OcrUpdateAvailable { - get { - return ResourceManager.GetString("OcrUpdateAvailable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to of {0}. + /// Looks up a localized string similar to An error occurred running OCR.. /// - public static string OfN { + internal static string OcrError { get { - return ResourceManager.GetString("OfN", resourceCulture); + return ResourceManager.GetString("OcrError", resourceCulture); } } /// - /// Looks up a localized string similar to Overwrite File. + /// Looks up a localized string similar to The OCR operation timed out.. /// - public static string OverwriteFile { + internal static string OcrTimeout { get { - return ResourceManager.GetString("OverwriteFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An additional component is needed to import this PDF file. Would you like to download it now?. - /// - public static string PdfImportComponentNeeded { - get { - return ResourceManager.GetString("PdfImportComponentNeeded", resourceCulture); + return ResourceManager.GetString("OcrTimeout", resourceCulture); } } /// /// Looks up a localized string similar to You do not have permission to copy content from the file '{0}'.. /// - public static string PdfNoPermissionToExtractContent { + internal static string PdfNoPermissionToExtractContent { get { return ResourceManager.GetString("PdfNoPermissionToExtractContent", resourceCulture); } } - /// - /// Looks up a localized string similar to PDF saved.. - /// - public static string PdfSaved { - get { - return ResourceManager.GetString("PdfSaved", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} of {1}. - /// - public static string PdfStatus { - get { - return ResourceManager.GetString("PdfStatus", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Print. - /// - public static string Print { - get { - return ResourceManager.GetString("Print", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} / {1}. - /// - public static string ProgressFormat { - get { - return ResourceManager.GetString("ProgressFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Recovering.... - /// - public static string Recovering { - get { - return ResourceManager.GetString("Recovering", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Recovery Progress. - /// - public static string RecoveryProgress { - get { - return ResourceManager.GetString("RecoveryProgress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Reset Image. - /// - public static string ResetImage { - get { - return ResourceManager.GetString("ResetImage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Running OCR.... - /// - public static string RunningOcr { - get { - return ResourceManager.GetString("RunningOcr", resourceCulture); - } - } - /// /// Looks up a localized string similar to The SANE driver is not available. Make sure to install the required packages:. /// - public static string SaneNotAvailable { + internal static string SaneNotAvailable { get { return ResourceManager.GetString("SaneNotAvailable", resourceCulture); } } - /// - /// Looks up a localized string similar to Save Images. - /// - public static string SaveImages { - get { - return ResourceManager.GetString("SaveImages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Save Images Progress. - /// - public static string SaveImagesProgress { - get { - return ResourceManager.GetString("SaveImagesProgress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Save PDF. - /// - public static string SavePdf { - get { - return ResourceManager.GetString("SavePdf", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Save PDF Progress. - /// - public static string SavePdfProgress { - get { - return ResourceManager.GetString("SavePdfProgress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Saving {0}.... - /// - public static string SavingFormat { - get { - return ResourceManager.GetString("SavingFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Scan. - /// - public static string Scan { - get { - return ResourceManager.GetString("Scan", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Scanned Image. - /// - public static string ScannedImage { - get { - return ResourceManager.GetString("ScannedImage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Scanning page {0}. - /// - public static string ScanPageProgress { - get { - return ResourceManager.GetString("ScanPageProgress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Scanning page {0}.... - /// - public static string ScanProgressPage { - get { - return ResourceManager.GetString("ScanProgressPage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Selected ({0}). - /// - public static string SelectedCount { - get { - return ResourceManager.GetString("SelectedCount", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Select a profile before clicking Scan.. - /// - public static string SelectProfileBeforeScan { - get { - return ResourceManager.GetString("SelectProfileBeforeScan", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Server started. - /// - public static string ServerStarted { - get { - return ResourceManager.GetString("ServerStarted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} / {1} MB. - /// - public static string SizeProgress { - get { - return ResourceManager.GetString("SizeProgress", resourceCulture); - } - } - /// /// Looks up a localized string similar to The OCR engine is not available. Make sure to install the required package:. /// - public static string TesseractNotAvailable { + internal static string TesseractNotAvailable { get { return ResourceManager.GetString("TesseractNotAvailable", resourceCulture); } @@ -1143,81 +242,45 @@ public static string TesseractNotAvailable { /// /// Looks up a localized string similar to An error occurred with the scanning driver.. /// - public static string UnknownDriverError { + internal static string UnknownDriverError { get { return ResourceManager.GetString("UnknownDriverError", resourceCulture); } } /// - /// Looks up a localized string similar to Unsaved Changes. - /// - public static string UnsavedChanges { - get { - return ResourceManager.GetString("UnsavedChanges", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An update is available.. - /// - public static string UpdateAvailable { - get { - return ResourceManager.GetString("UpdateAvailable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Update checking is disabled.. + /// Looks up a localized string similar to Unknown Scanner. /// - public static string UpdateCheckDisabled { + internal static string UnknownScanner { get { - return ResourceManager.GetString("UpdateCheckDisabled", resourceCulture); + return ResourceManager.GetString("UnknownScanner", resourceCulture); } } /// - /// Looks up a localized string similar to An error occured when trying to install the update.. - /// - public static string UpdateError { - get { - return ResourceManager.GetString("UpdateError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Update Progress. - /// - public static string UpdateProgress { - get { - return ResourceManager.GetString("UpdateProgress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Updating.... + /// Looks up a localized string similar to Version {0}. /// - public static string Updating { + internal static string Version { get { - return ResourceManager.GetString("Updating", resourceCulture); + return ResourceManager.GetString("Version", resourceCulture); } } /// - /// Looks up a localized string similar to Uploading email.... + /// Looks up a localized string similar to The worker process crashed.. /// - public static string UploadingEmail { + internal static string WorkerCrash { get { - return ResourceManager.GetString("UploadingEmail", resourceCulture); + return ResourceManager.GetString("WorkerCrash", resourceCulture); } } /// - /// Looks up a localized string similar to Version {0}. + /// Looks up a localized string similar to The worker process crashed. Check the Windows event viewer.. /// - public static string Version { + internal static string WorkerCrashWindows { get { - return ResourceManager.GetString("Version", resourceCulture); + return ResourceManager.GetString("WorkerCrashWindows", resourceCulture); } } } diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.af.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.af.resx index db345a0b46..f01b556a25 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.af.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.af.resx @@ -18,6 +18,9 @@ The selected scanner is offline. + + Kommunikasie met die skandeertoestel is onderbreek. + NAPS2 @@ -28,10 +31,10 @@ Geen skandering toestel gevind. - Có lỗi xảy ra với trình điều khiển máy quét. + Daar was n fout met die skandeer drywer. - Version {0} + Weergawe {0} The file '{0}' could not be imported. @@ -43,7 +46,7 @@ Geen bladsye is in die voerder. - You do not have permission to copy content from the file '{0}'. + Jy het nie toestemming om inhoud vanaf die lêer '{0}' te kopieer nie. The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. @@ -69,4 +72,19 @@ The selected driver is not supported on this system. + + OCR fout het voorgekom. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.ar.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.ar.resx index 60e52c7de0..2a30e9cfe9 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.ar.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.ar.resx @@ -18,6 +18,9 @@ الماسح الضوئي المحدد غير متصل. + + Communication with the scanning device was interrupted. + NAPS2 @@ -28,7 +31,7 @@ لم يتم العثور على أي جهاز مسح الضوئي. - حدث خطأ مع تعريف المسح الضوئي.. + حدث خطأ مع تعريف المسح الضوئي. الإصدارة {0} @@ -69,4 +72,19 @@ The selected driver is not supported on this system. + + An error occurred running OCR. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.bg.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.bg.resx index 7d025ce5d4..8c32304ec8 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.bg.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.bg.resx @@ -18,6 +18,9 @@ Избраният скенер е недостъпен. + + Комуникацията със сканиращото устройство беше прекъсната. + NAPS2 @@ -61,12 +64,27 @@ Подготовка на скенера. - The SANE driver is not available. Make sure to install the required packages: + Драйверът SANE не е наличен. Уверете се, че сте инсталирали необходимите пакети: - The OCR engine is not available. Make sure to install the required package: + OCR двигател не е наличен. Уверете се, че сте инсталирали необходимия пакет: - The selected driver is not supported on this system. + Избраният драйвер не се поддържа от тази система. + + + Възникна грешка при OCR. + + + Операцията за OCR изтече. + + + Работният процес се срина. + + + Работният процес се срина. Проверете програмата за преглед на събитията на Windows. + + + Unknown Scanner \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.bs.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.bs.resx new file mode 100644 index 0000000000..8766deac8c --- /dev/null +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.bs.resx @@ -0,0 +1,90 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Izabrani skener se ne može pronaći. + + + Izabrani skener je isključen. + + + Prekinuta je komunikacija sa uređajem za skeniranje. + + + NAPS2 + + + Nije izabran uređaj. + + + Nije pronađen uređaj za skeniranje. + + + Greška se pojavila kod drivera za skeniranje. + + + Verzija {0} + + + Datoteka '{0}' se ne može uvesti. + + + Izabrani skener nema podršku za korištenje ulagača papira. Ako skener ima ulagač papira, pokušajte s korištenjem drugog drajvera. + + + Nema papira u ulagaču. + + + Nemate dozvolu za kopiranje sadržaja iz datoteke '{0}'. + + + Izabrani skener nema podršku za korištenje dupleksa - dvostranog skeniranja. Ako skener treba da podržava dupleks, pokušajte s korištenjem drugog drajvera. + + + Izabrani skener je zauzet. + + + Poklopac skenera je otvoren. + + + Papir je zaglavio u skeneru. + + + Skener se zagrijava. + + + SANE drajver nije dostupan. Pobrinite se da instalirate potrebne pakete: + + + Mehanizam OPZ-a nije dostupan. Pobrinite se da instalirate potreban paket: + + + Izabrani drajver nije podržan na ovom sistemu. + + + Pojavila se greška pri pokretanju OPZ. + + + Operacija OPZ-a je istekla. + + + Radni proces je prekinut. + + + Radni proces je prekinut. Provjerite preglednik događaja u Windowsu. + + + Nepoznat skener + + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.ca.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.ca.resx index 48a4fa7561..e9c0625b6e 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.ca.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.ca.resx @@ -13,10 +13,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - El scanner seleccionat no s'ha pogut trobar . + No s'ha pogut trobar l'escàner seleccionat. - El scaner seleccionat esta apagat. + L'escàner seleccionat està apagat. + + + S'ha interromput la comunicació amb el dispositiu d'escaneig. NAPS2 @@ -28,7 +31,7 @@ No s'ha trobat cap dispositiu d'escaneig. - S'ha produït un error amb el driver del escaner. + S'ha produït un error amb el controlador de l'escàner. Versió {0} @@ -43,13 +46,13 @@ No hi ha cap pàgina a l'alimentador. - No teniu prou permisos per copiar el contingut del fitxer '{0}'. + No teniu prou permisos per a copiar el contingut del fitxer '{0}'. L'escàner seleccionat no suporta dúplex (escaneig per dues cares). Si el vostre escàner suporta aquesta tecnologia, seleccioneu un controlador diferent. - L'escàner seleccionat esta ocupat. + L'escàner seleccionat està ocupat. La tapa de l'escàner està oberta. @@ -58,7 +61,7 @@ L'escàner té un embús de paper. - S'està posant a punt l'escàner.... + L'escàner s'està escalfant. No s'ha trobat el controlador SANE. Assegureu-vos d'instal·lar les dependències necessàries: @@ -67,6 +70,21 @@ No s'ha trobat el motor OCR. Assegureu-vos d'instal·lar el paquet necessari: - El controlador seleccionat no està suportat en aquest sistema. + El controlador seleccionat no és compatible amb aquest sistema. + + + S'ha produït un error en iniciar l'OCR. + + + S'ha excedit el temps d'espera d'OCR. + + + S'ha produït un error. + + + Ha fallat la tasca. Reviseu el Visor d'esdeveniments de Windows. + + + Escàner desconegut \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.cs.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.cs.resx index bbcda741dc..5846539e16 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.cs.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.cs.resx @@ -18,6 +18,9 @@ Vybraný skener je vypnutý. + + Komunikace se skenovacím zařízením byla přerušena. + NAPS2 @@ -69,4 +72,19 @@ Vybraný ovladač není na tomto systému podporován. + + Došlo k chybě při běhu OCR. + + + Vypršel časový limit operace OCR. + + + Pracovní proces spadl. + + + Pracovní proces spadl. Zkontrolujte prohlížeč událostí systému Windows. + + + Neznámý skener + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.da.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.da.resx index f2f9356ecf..3bda5d8681 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.da.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.da.resx @@ -13,10 +13,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Den valgte skanner kan ikke findes. + Den valgte scanner blev ikke fundet. - Den valgte skanner er offline. + Den valgte scanner er offline. + + + Kommunikation med scanningsenheden blev afbrudt. NAPS2 @@ -25,7 +28,7 @@ Ingen enheder valgt. - Ingen skannerenheder blev fundet. + Ingen scannincsenheder blev fundet. Der opstod en fejl med scanner driveren. @@ -61,12 +64,27 @@ Scanneren varmer op. - The SANE driver is not available. Make sure to install the required packages: + SANE-driveren er ikke tilgængelig. Kontroller at du har installeret den påkrævede pakke: - The OCR engine is not available. Make sure to install the required package: + OCR-motoren er ikke tilgængelig. Kontroller at du har installeret den påkrævede pakke: - The selected driver is not supported on this system. + Den valgte driver er ikke understøttet på dette system. + + + Der opstod en fejl under kørsel af OCR. + + + OCR-handlingen fik timeout. + + + Arbejdsprocessen fejlede. + + + Arbejdsprocessen fejlede. Kontroller Windows event-fremviseren. + + + Unknown Scanner \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.de.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.de.resx index 0dd6867c9d..cbbc49b06b 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.de.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.de.resx @@ -18,6 +18,9 @@ Der gewählte Scanner ist ausgeschaltet (offline). + + Die Kommunikation mit dem Scan-Gerät wurde unterbrochen. + NAPS2 @@ -69,4 +72,19 @@ Der ausgewählte Treiber wird auf diesem System nicht unterstützt. + + Beim Ausführen von OCR ist ein Fehler aufgetreten. + + + Zeitüberschreitung bei der Texterkennung. + + + Der Arbeitsprozess ist abgestürzt. + + + Der Arbeitsprozess ist abgestürzt. Überprüfen Sie die Windows-Ereignisanzeige. + + + Unbekannter Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.el.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.el.resx index fcfa6334db..d29c8b9067 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.el.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.el.resx @@ -18,6 +18,9 @@ Ο επιλεγμένος σαρωτής βρίσκεται εκτός σύνδεσης (offline). + + Communication with the scanning device was interrupted. + NAPS2 @@ -61,12 +64,27 @@ Προθέρμανση σαρωτή. - The SANE driver is not available. Make sure to install the required packages: + Ο οδηγός SANE δεν είναι διαθέσιμος. Βεβαιωθείτε ότι έχετε εγκαταστήσει τα απαιτούμενα πακέτα: - The OCR engine is not available. Make sure to install the required package: + Η εφαρμογή OCR δεν είναι διαθέσιμη. Βεβαιωθείτε ότι έχετε εγκαταστήσει το απαιτούμενο πακέτο: - The selected driver is not supported on this system. + Ο επιλεγμένος οδηγός δεν υποστηρίζεται σε αυτό το σύστημα. + + + Σφάλμα κατά την εκτέλεση της OCR. + + + Εξέπνευσε ο χρόνος για την λειτουργία OCR. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.es.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.es.resx index 863bd546b3..d6b6dce079 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.es.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.es.resx @@ -18,6 +18,9 @@ El escáner seleccionado está inactivo. + + Se interrumpió la comunicación con el dispositivo de escaneo. + NAPS2 @@ -69,4 +72,19 @@ El controlador seleccionado no está admitido en este sistema. + + Se ha producido un error ejecutando OCR. + + + La operación OCR demoró + + + El proceso de trabajo falló. + + + El proceso de trabajo falló. Verifique el visor de eventos de Windows. + + + Escáner desconocido + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.et.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.et.resx index 03b1b9506c..f3930cda59 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.et.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.et.resx @@ -18,6 +18,9 @@ The selected scanner is offline. + + Communication with the scanning device was interrupted. + NAPS2 @@ -43,7 +46,7 @@ Sööturis ei ole ühtegi lehte. - Teil pole luba sisu kopeerimiseks failist \"{0}\". + Teil pole luba sisu kopeerimiseks failist "{0}". Valitud skanner ei toeta dupleksi kasutamist. Kui teie skanner peaks dupleksit toetama, proovige kasutada teist draiverit. @@ -69,4 +72,19 @@ Valitud draiverit ei toetata selles süsteemis. + + An error occurred running OCR. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.fa.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.fa.resx index 672eca7951..c7202024e7 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.fa.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.fa.resx @@ -18,6 +18,9 @@ اسکنر انتخابی خاموش است. + + Communication with the scanning device was interrupted. + NAPS2 @@ -69,4 +72,19 @@ درایور انتخابیدر این سیستم قابل استفاده نیست.. + + خطایی در OCR رخ داد. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.fi.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.fi.resx index 8e6c07a6dd..00d5d944b9 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.fi.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.fi.resx @@ -18,6 +18,9 @@ Valittu skanneri on offline-tilassa. + + Yhteys skannauslaitteen kanssa keskeytyi. + NAPS2 @@ -69,4 +72,19 @@ Valittua ajuria ei tueta tässä järjestelmässä. + + OCR:n käytössä tapahtui virhe. + + + OCR-operaatio aikakatkaistiin. + + + Työprosessi kaatui. + + + Työprosessi kaatui. Tarkista Windows-tapahtuman katselija. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.fr.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.fr.resx index 7ab211119b..eadb7f8dde 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.fr.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.fr.resx @@ -18,6 +18,9 @@ Le scanner sélectionné est éteint. + + La communication avec le périphérique de numérisation a été interrompue. + NAPS2 @@ -37,16 +40,16 @@ Le fichier « {0} » n'a pas pu être importé. - Le scanner sélectionné ne gère pas de dispositif d'alimentation. Si ce scanner en possède un, essayer d'utiliser un autre pilote. + Le scanner sélectionné ne semble pas disposer de dispositif d'alimentation. S'il en possède un, essayer d'utiliser un autre pilote. Il n'y a aucune page dans le dispositif d'alimentation. - Vous n'avez pas la permission de copier le contenu du fichier « {0} ».. + Vous n'avez pas la permission de copier le contenu du fichier « {0} ». - Le scanner sélectionné ne gère pas le recto-verso. Si ce scanner est supposé le gérer, essayer d'utiliser un autre pilote. + Le scanner sélectionné ne semble pas gérer pas le recto-verso (duplex). S'il est supposé le prendre en charge, essayer d'utiliser un autre pilote. Le scanner sélectionné est occupé. @@ -61,12 +64,27 @@ Le scanner est en train de préchauffer. - Le pilote SANE n'est pas disponible. Merci d'installer les paquets requis: + Le pilote SANE n'est pas disponible. Merci d'installer les paquets nécessaires : - Le moteur OCR n'est pas disponible. Merci d'installer le paquet requis: + Le moteur OCR n'est pas disponible. Merci d'installer le paquet requis : Le pilote sélectionné n'est pas pris en charge sur ce système. + + Une erreur est survenue lors de l'exécution de l'OCR. + + + L'opération OCR a expiré. + + + Le processus de travail a planté. + + + Le processus de travail a planté. Vérifier dans l'Observateur d'évènements Windows. + + + Scanner inconnu + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.he.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.he.resx index ef64607de4..4c44b0b39a 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.he.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.he.resx @@ -16,10 +16,13 @@ הסורק שנבחר לא נמצא. - הסורק שנבחר איננו מקוון (OFFLINE). + הסורק שנבחר איננו מקוון. + + + התקשורת מול מכשיר הסריקה נקטעה. - NAPS2 + לא עוד סתם סורק 2 לא נבחר התקן. @@ -34,10 +37,10 @@ גרסה {0} - לא ניתן לייבא את הקובץ '{0}'. + לא ניתן לייבא את הקובץ ‚{0}’. - הסורק שנבחר איננו תומך במזין מסמכים. אם לסורק יש מזין מסמכים, נסה להשתמש במנהל התקן אחר. + הסורק שנבחר איננו תומך במזין מסמכים. אם לסורק יש מזין מסמכים, כדאי לנסות להשתמש במנהל התקן אחר. אין דפים במזין המסמכים. @@ -46,27 +49,42 @@ אין לך הרשאה להעתקת תוכן מהקובץ '{0}'. - The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + הסורק שנבחר לא תומך במצב דופלקס (סריקה דו־צדדית). אם הסורק שלך אמור לתמוך בדופלקס, כדאי לנסות להשתמש במנהל התקן אחר. - The selected scanner is busy. + הסורק הנבחר עסוק. - The scanner's cover is open. + המכסה של הסורק פתוח. - The scanner has a paper jam. + נתקע דף בסורק. - The scanner is warming up. + הסורק מתחמם. - The SANE driver is not available. Make sure to install the required packages: + מנהל התקן ה־SANE לא זמין. נא לוודא שהתקנת את החבילות הנדרשות: - The OCR engine is not available. Make sure to install the required package: + מנוע זיהוי התווים האופטי לא זמין. נא לוודא שהתקנת את החבילות הנדרשות: - The selected driver is not supported on this system. + מנהל ההתקן הנבחר לא נתמך במערכת הזאת. + + + אירעה שגיאה בהרצת זיהוי תווים אופטי. + + + הזמן שהוקצב לפעילות זיהוי התווים האופטי נגמר. + + + תהליך הרקע קרס. + + + תהליך הרקע קרס. נא לבדוק את הסיבה במציג האירועים של Windows. + + + סורק לא ידוע \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.hi.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.hi.resx new file mode 100644 index 0000000000..9c24336efe --- /dev/null +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.hi.resx @@ -0,0 +1,90 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The selected scanner could not be found. + + + The selected scanner is offline. + + + स्कैनिंग डिवाइस के साथ संचार बाधित हो गया। + + + NAPS2 + + + कोई उपकरण चयनित नहीं। + + + कोई स्कैनिंग उपकरण नहीं मिला। + + + स्कैनिंग ड्राइवर के साथ कोई त्रुटि उत्पन्न हुई. + + + Version {0} + + + The file '{0}' could not be imported. + + + The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. + + + फीडर में कोई पेज नहीं है। + + + You do not have permission to copy content from the file '{0}'. + + + The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + + + The selected scanner is busy. + + + The scanner's cover is open. + + + The scanner has a paper jam. + + + The scanner is warming up. + + + The SANE driver is not available. Make sure to install the required packages: + + + The OCR engine is not available. Make sure to install the required package: + + + The selected driver is not supported on this system. + + + OCR चलाने में त्रुटि उत्पन्न हुई. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.hr.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.hr.resx index 9b359e506d..4633f7d551 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.hr.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.hr.resx @@ -13,22 +13,25 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Odabrani skener nije pronađen. + Odabrani skener nije moguće pronaći. - Odabrani skener nije uključen. + Odabrani skener je izvan mreže. + + + Prekinuta je komunikacija sa uređajem za skeniranje. NAPS2 - Uređaj nije odabran. + Nije odabran uređaj. - Uređaj za skeniranje nije pronađen. + Nije pronađen uređaj za skeniranje. - Pogreška s upravljačkim programom za skener. + Dogodila se pogreška s upravljačkim programom za skeniranje. Verzija {0} @@ -37,36 +40,51 @@ Datoteka '{0}' nije mogla biti uvezena. - The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. + Odabrani skener ne podržava korištenje ulagača. Ako vaš skener ima ulagač, pokušajte upotrijebiti drugi upravljački program. - No pages are in the feeder. + U ulagaču nema stranica. - Nemate dozvolu za kopiranje sadržaja iz datoteke '{0}'. + Nemate dopuštenje za kopiranje sadržaja iz datoteke '{0}'. - The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + Odabrani skener ne podržava obostrano skeniranje. Ako bi vaš skener trebao podržavati obostrano skeniranje, pokušajte upotrijebiti drugi upravljački program. - The selected scanner is busy. + Odabrani skener je zauzet. - The scanner's cover is open. + Poklopac skenera je otvoren. - The scanner has a paper jam. + U skeneru se zaglavio papir. The scanner is warming up. - The SANE driver is not available. Make sure to install the required packages: + SANE upravljački program nije dostupan. Provjerite jeste li instalirali potrebne pakete: - The OCR engine is not available. Make sure to install the required package: + OCR program nije dostupan. Provjerite jeste li instalirali potrebni paket: - The selected driver is not supported on this system. + Odabrani upravljački program nije podržan na ovom sustavu. + + + Došlo je do pogreške prilikom pokretanja OCR-a. + + + Isteklo je vrijeme za OCR operaciju. + + + Radni proces se srušio. + + + Radni proces se srušio. Provjerite preglednik događaja u sustavu Windows. + + + Nepoznat skener \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.hu.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.hu.resx index 3c86d2daa2..5a36d739a9 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.hu.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.hu.resx @@ -13,10 +13,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - A kiválasztott képolvasó nem található. + A kiválasztott lapolvasó nem található. - A kiválasztott képolvasó nem elérhető (leválasztva). + A kiválasztott lapolvasó inaktív. + + + A kommunikáció a lapolvasó eszközzel megszakadt. NAPS2 @@ -25,48 +28,63 @@ Nincs kiválasztott eszköz. - Nincs elérhető képolvasó. + Nem található lapolvasó eszköz. - Hiba történt a képolvasó meghajtójával.. + Hiba történt a lapolvasó illesztőprogrammal. Verzió: {0} - A '{0}' fájl nem importálható.. + A következő fájlt nem lehetett importálni: '{0}'. - The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. + A kiválasztott lapolvasó nem támogatja az lapadagoló használatát. Ha a lapolvasója rendelkezik lapadagolóval, próbáljon meg másik illesztőprogramot használni. - Nincs lap a beolvasóban. + Nincs lap a lapadagolóban. - You do not have permission to copy content from the file '{0}'. + Nincs jogosultsága a következő fájl tartalmát másolni: '{0}'. - The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + A kiválasztott lapolvasó nem támogatja a kétoldalas használatot. Ha a lapolvasó feltehetőleg támogatja a kétoldalas használatot, próbáljon meg másik illesztőprogramot használni. - A kiválasztott képolvasó nem elérhető (foglalt). + A kiválasztott lapolvasó foglalt. - The scanner's cover is open. + A lapolvasó fedele nyitva van. - The scanner has a paper jam. + A lapolvasóban papírelakadás van. - The scanner is warming up. + A lapolvasó felmelegedik. - The SANE driver is not available. Make sure to install the required packages: + A SANE illesztőprogram nem áll rendelkezésre. Győződjön meg róla, hogy telepítette a szükséges csomagokat: - The OCR engine is not available. Make sure to install the required package: + Az karakterfelismerő motor nem áll rendelkezésre. Győződjön meg róla, hogy telepítette a szükséges csomagot: - The selected driver is not supported on this system. + A kiválasztott illesztőprogram nem támogatott ezen a rendszeren. + + + Hiba történt a karakterfelismerő futtatásakor. + + + Időtúllépés a karakterfelismerés során. + + + A munkavégző folyamat összeomlott. + + + A munkavégző folyamat összeomlott. Ellenőrizze a Windows eseménynaplóját. + + + Ismeretlen lapolvasó \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.id.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.id.resx new file mode 100644 index 0000000000..e907243c9d --- /dev/null +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.id.resx @@ -0,0 +1,90 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The selected scanner could not be found. + + + The selected scanner is offline. + + + Komunikasi dengan alat pindai terganggu. + + + NAPS2 + + + No device selected. + + + No scanning device was found. + + + Galat ditemukan didalam driver piranti. + + + Version {0} + + + The file '{0}' could not be imported. + + + The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. + + + No pages are in the feeder. + + + You do not have permission to copy content from the file '{0}'. + + + The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + + + The selected scanner is busy. + + + The scanner's cover is open. + + + The scanner has a paper jam. + + + The scanner is warming up. + + + The SANE driver is not available. Make sure to install the required packages: + + + The OCR engine is not available. Make sure to install the required package: + + + The selected driver is not supported on this system. + + + Galat ditemukan dalam proses OCR. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.it.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.it.resx index 67b55ca85d..d8a9ff7648 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.it.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.it.resx @@ -16,7 +16,10 @@ Lo scanner selezionato non è stato trovato. - Lo scanner selezionato è disconnesso. + Lo scanner selezionato è offline. + + + La comunicazione con il dispositivo di scansione è stata interrotta. NAPS2 @@ -25,10 +28,10 @@ Nessun dispositivo selezionato. - Nessuno scanner non trovato. + Nessuno scanner trovato. - Errore del driver di scansione. + Si è verificato un errore con il driver di scansione. Versione {0} @@ -37,28 +40,28 @@ Il file '{0}' non può essere importato. - Lo scanner selezionato non supporta l'uso di un alimentatore. Se il tuo scanner ha un alimentatore, prova ad usare un driver differente. + Lo scanner selezionato non supporta l'uso di un alimentatore automatico. Se lo scanner ha un alimentatore automatico, prova a usare un driver differente. - Non ci sono fogli nel caricatore. + Non ci sono fogli nell'alimentatore automatico. Non hai i permessi per copiare il contenuto dal file '{0}'. - Lo scanner selezionato non supporta l'uso del duplex. Se lo scanner ha un supporto duplex, prova ad usare un driver differente. + Lo scanner selezionato non supporta la scansione fronte retro. Se lo scanner dovrebbe supportare la scansione fronte retro, prova a usare un driver differente. Lo scanner selezionato è occupato. - Coperchio dello scanner aperto. + Il coperchio dello scanner è aperto. Carta inceppata nello scanner. - Surriscaldamento scanner. + Lo scanner si sta riscaldando. Il driver SANE non è disponibile. Assicurati di aver installato il pacchetto richiesto: @@ -67,6 +70,21 @@ Il motore OCR non è disponibile. Assicurati di aver installato il pacchetto richiesto: - Il driver selezionato non è supportato in questo sistema. + Il driver selezionato non è supportato da questo sistema. + + + Si è verificato un errore durante l'esecuzione dell'OCR. + + + Tempo scaduto per l'operazione OCR. + + + Il processo di elaborazione si è bloccato. + + + Il processo di elaborazione si è bloccato. \nControlla il visualizzatore eventi di Windows. + + + Scanner sconosciuto \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.ja.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.ja.resx index 85c0731bbd..91ab2926f7 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.ja.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.ja.resx @@ -18,6 +18,9 @@ 選択されたスキャナは接続されていません。. + + Communication with the scanning device was interrupted. + NAPS2 @@ -69,4 +72,19 @@ 選択されたドライバはOSでサポートされていません. + + An error occurred running OCR. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.ko.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.ko.resx index b44348a99d..0c6216faf3 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.ko.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.ko.resx @@ -13,10 +13,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 선택된 스캐너를 찾을 수 없습니다.. + 선택된 스캐너를 찾을 수 없습니다. - 선택된 스캐너의 연결이 끊겼습니다. . + 선택된 스캐너의 연결이 끊겼습니다. + + + 스캔 장비와 연결이 끊어졌습니다. NAPS2 @@ -25,31 +28,31 @@ 선택된 장치가 없습니다.. - 스캔 장치를 찾을 수 없습니다.. + 스캔 장치를 찾을 수 없습니다. 스캔 중 오류가 발생했습니다.. - {0} 버전 + 버전 {0} - '{0}' 파일을 불러올 수 없습니다.. + '{0}' 파일을 불러올 수 없습니다. - 선택된 스캐너에서는 지급기를 사용할 수 없습니다. 만약 지급기가 있는 스캐너라면, 다른 드라이버를 선택 해 주세요.. + 선택된 스캐너에서는 지급기를 사용할 수 없습니다. 만약 지급기가 있는 스캐너라면, 다른 드라이버를 선택 해 주세요. - 공급장치에 용지가 없습니다.. + 공급장치에 용지가 없습니다. '{0}' 파일의 내용을 복사하기 위한 권한을 가지고 있지 않습니다.. - 선택된 스캐너에서는 양면 기능을 사용할 수 없습니다. 만약 스캐너가 양면 기능을 지원한다면, 다른 드라이버를 선택 해 주세요.. + 선택된 스캐너에서는 양면 기능을 사용할 수 없습니다. 만약 스캐너가 양면 기능을 지원한다면, 다른 드라이버를 선택 해 주세요. - 선택된 스캐너가 사용 중 입니다.. + 선택된 스캐너가 사용 중 입니다. 스캐너 커버 열림. @@ -61,12 +64,27 @@ 스캐너 예열 중. - The SANE driver is not available. Make sure to install the required packages: + SANE 드라이버를 사용할 수 없습니다. 다음 패키지가 설치 되어 있는지 확인해 주십시오: - The OCR engine is not available. Make sure to install the required package: + OCR 엔진을 사용할 수 없습니다. 다음 패키지가 설치 되어 있는지 확인해 주십시오: - The selected driver is not supported on this system. + 선택한 드라이버는 이 시스템에서 사용할 수 없습니다. + + + OCR 작업 중 오류가 발생했습니다. + + + OCR 작업 시간이 초과되었습니다. + + + 작업 프로세스가 튕겼습니다. + + + 작업 프로세스가 튕겼습니다. Windows 이벤트 뷰어를 확인하십시오. + + + Unknown Scanner \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.lt.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.lt.resx index 44c5072b34..a3a6f88dfd 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.lt.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.lt.resx @@ -18,6 +18,9 @@ Pasirinktas skeneris neprisijungęs. + + Communication with the scanning device was interrupted. + NAPS2 @@ -69,4 +72,19 @@ The selected driver is not supported on this system. + + An error occurred running OCR. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.lv.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.lv.resx index fb158a56d9..10cc37774e 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.lv.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.lv.resx @@ -18,6 +18,9 @@ Izvēlētais skeneris ir bezsaistē. + + Pārtrūka sakari ar skenēšanas ierīci. + NAPS2 @@ -69,4 +72,19 @@ Izvēlētais draiveris šajā sistēmā nav atbalstīts. + + Pie teksta atpazīšanas notika kļūda. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.nb.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.nb.resx index 7ee46797ed..05bed4307c 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.nb.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.nb.resx @@ -18,6 +18,9 @@ The selected scanner is offline. + + Communication with the scanning device was interrupted. + NAPS2 @@ -69,4 +72,19 @@ The selected driver is not supported on this system. + + An error occurred running OCR. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.nl.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.nl.resx index b2413aeab0..b9dc8ee88d 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.nl.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.nl.resx @@ -18,6 +18,9 @@ De geselecteerde scanner is offline. + + Communicatie met het scanapparaat is onderbroken. + NAPS2 @@ -64,9 +67,24 @@ De SANE driver is niet beschikbaar. Installeer de vereiste pakketten: - De OCR engine is niet beschikbaar. Installeer het vereiste pakket: + De OCR-engine is niet beschikbaar. Installeer het vereiste pakket: De gekozen driver wordt niet ondersteund op dit systeem. + + Er is een fout opgetreden bij het uitvoeren van OCR. + + + De OCR-bewerking is verlopen. + + + Het werkproces is gecrasht. + + + Het werkproces is gecrasht. Controleer het Windows Gebeurtenis-logboek. + + + Onbekende scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.nn.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.nn.resx index 2527ebf282..9d1991d6d6 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.nn.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.nn.resx @@ -18,6 +18,9 @@ Den valde skannaren er fråkopla. + + Kommunikasjon med skannaren vart avbroten. + NAPS2 @@ -69,4 +72,19 @@ Den valde drivaren er ikkje støtta på dette systemet. + + An error occurred running OCR. + + + The OCR operation timed out. + + + Arbeidsprosessen krasja. + + + Arbeidsprosessen krasja Sjekk Windows-hendingsvisaren. + + + Ukjent skanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.pl.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.pl.resx index a351fbbe1d..51b8135308 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.pl.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.pl.resx @@ -18,6 +18,9 @@ Wybrany skaner jest w trybie offline. + + Komunikacja z urządzeniem skanującym została przerwana. + NAPS2 @@ -69,4 +72,19 @@ Wybrany sterownik nie jest obsługiwany w tym systemie. + + Wystąpił błąd podczas uruchamiania OCR. + + + Upłynął limit czasu operacji OCR. + + + Proces roboczy uległ awarii. + + + Proces roboczy uległ awarii. Sprawdź podgląd zdarzeń systemu Windows. + + + Skaner nieznany + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.pt-BR.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.pt-BR.resx index da61a103d7..b62683bd96 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.pt-BR.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.pt-BR.resx @@ -18,6 +18,9 @@ O scanner escolhido está offline. + + A comunicação com scanner foi interrompida. + NAPS2 @@ -69,4 +72,19 @@ O driver selecionado não é suportado neste sistema. + + Ocorreu um erro ao executar o OCR. + + + O tempo limite da operação OCR expirou. + + + O processo de trabalho travou. + + + O processo de trabalho travou. Verifique o Visualizador de Eventos do Windows. + + + Scanner Desconhecido + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.pt-PT.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.pt-PT.resx index 5653e5312c..c3f2b582f5 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.pt-PT.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.pt-PT.resx @@ -13,60 +13,78 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - O scanner escolhido não foi encontrado. + O digitalizador escolhido não foi encontrado. - O scanner escolhido está offline. + O digitalizador escolhido está desligado. + + + A comunicação com o digitalizador foi interrompida. NAPS2 - Nenhum dispositivo escolhido. + Nenhum dispositivo selecionado. - Nenhum scanner foi encontrado. + Não foi encontrado um digitalizador. - Ocorreu um erro com o driver do scanner. + Ocorreu um erro com o controlador de digitalização. Versão {0} - The file '{0}' could not be imported. + Não foi possível importar o ficheiro "{0}". - The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. + Aparentemente, o digitalizador escolhido não possui um alimentador. Se o seu digitalizador possuir um alimentador, tente usar um controlador diferente. Não existem folhas no alimentador. - You do not have permission to copy content from the file '{0}'. + Não tem permissão para copiar conteúdo do ficheiro "{0}". - The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + Aparentemente, o digitalizador escolhido não permite digitalização em frente e verso. Se é suposto que o digitalizador o permita, tente usar um controlador diferente. - The selected scanner is busy. + O digitalizador escolhido está ocupado. - The scanner's cover is open. + A tampa do digitalizador está aberta. - The scanner has a paper jam. + Existe papel encravado no digitalizador. - The scanner is warming up. + O digitalizador está a aquecer. - The SANE driver is not available. Make sure to install the required packages: + O controlador SANE não está disponível. Certifique-se que instala os pacotes necessários: - The OCR engine is not available. Make sure to install the required package: + O motor OCR não está disponível. Certifique-se que instalou o pacote necessário: - The selected driver is not supported on this system. + O controlador escolhido não é suportado neste sistema. + + + Ocorreu um erro ao executar OCR. + + + A operação OCR caducou. + + + O processo de trabalho falhou. + + + O processo de trabalho falhou. Analise o visualizador de eventos Windows, + + + Digitalizador desconhecido \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.resx index 7608d4ac9c..5e9bbd6b1f 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.resx @@ -123,6 +123,9 @@ The selected scanner is offline. + + Communication with the scanning device was interrupted. + NAPS2 @@ -174,4 +177,19 @@ The selected driver is not supported on this system. + + An error occurred running OCR. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.ro.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.ro.resx index aec35e41dd..bcedfa12d0 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.ro.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.ro.resx @@ -18,6 +18,9 @@ Scanerul selectat este deconectat. + + A fost întreruptă comunicarea cu dispozitivul de scanare. + NAPS2 @@ -61,7 +64,7 @@ Scanerul se inițializează. - The SANE driver is not available. Make sure to install the required packages: + Driverul SANE nu este disponibil. Asigurați-vă că instalați pachetele necesare: Motorul OCR nu este disponibil. Asigură-te că ai instalat pachetul necesar.: @@ -69,4 +72,19 @@ Driver-ul selectat nu este suportat de acest sistem.. + + A apărut o eroare la rularea OCR. + + + Operațiunea OCR a expirat. + + + Procesul worker s-a oprit neașteptat. + + + Procesul worker s-a oprit neașteptat. Verificați Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.ru.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.ru.resx index 6ba39c3df6..6d2853181d 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.ru.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.ru.resx @@ -18,6 +18,9 @@ Выбранный сканер отключён. + + Связь с устройством сканирования была прервана. + NAPS2 @@ -37,13 +40,13 @@ Файл '{0}' не может быть импортирован. - Выбранный сканер не поддерживает использование автоподатчика. Если ваш сканер имеет АПД, попробуйте другой драйвер.. + Выбранный сканер не поддерживает использование автоподатчика. Если ваш сканер имеет АПД, попробуйте другой драйвер. В податчике нет листов. - У вас нет разрешения копировать содержимое из файла '{0}'.. + У вас нет разрешения копировать содержимое из файла '{0}'. Выбранный сканер не поддерживает дуплекс. Если Ваш сканер поддерживает двустраничное сканирование, попробуйте использовать другой драйвер. @@ -69,4 +72,19 @@ Выбранный драйвер не поддерживается в этой системе. + + При распознавании текста произошла ошибка. + + + Время выполнения операции распознавания текста истекло. + + + Рабочий процесс завершился сбоем. + + + Рабочий процесс завершился сбоем. Проверьте средство просмотра событий Windows. + + + Не найден сканер + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.si.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.si.resx index b76c8ec607..b487ce7a68 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.si.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.si.resx @@ -18,6 +18,9 @@ The selected scanner is offline. + + Communication with the scanning device was interrupted. + NAPS2 @@ -69,4 +72,19 @@ The selected driver is not supported on this system. + + OCR ධාවනය කිරීමේදී දෝෂයක් ඇති විය. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.sk.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.sk.resx index 0a96c975cf..20ae445a1a 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.sk.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.sk.resx @@ -18,6 +18,9 @@ Vybraný skener je offline. + + Komunikácia so skenovacím zariadením bola prerušená. + NAPS2 @@ -34,7 +37,7 @@ Verzia {0} - Súbor '{0}' sa nemožno importovať. + Súbor '{0}' nemožno importovať. Vybraný skener nepodporuje použitie podávača. Ak skener obsahuje podávač, skúste použiť iný ovládač. @@ -69,4 +72,19 @@ Vybraný ovládač nie je v tomto systéme podporovaný. + + Pri spustení OCR došlo k chybe. + + + Čas OCR operácie vypršal. + + + Pracovný proces spadol. + + + Pracovný proces spadol. Skontrolujte prehliadač udalostí systému Windows. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.sl.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.sl.resx index e6bc884e3d..21beba7bec 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.sl.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.sl.resx @@ -18,6 +18,9 @@ Izbran skener je izklopljen. + + Komunikacija s skenerjem je bila prekinjena. + NAPS2 @@ -69,4 +72,19 @@ Izbran gonilnik ni na voljo v tem sistemu. + + Pri zagonu OCR je prišlo do napake. + + + Časovna omejitev operacije OCR je potekla. + + + Delovni proces se je zrušil. + + + Delovni proces se je zrušil. Preveri pregledovalnik dogodkov v sistemu Windows. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.sq.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.sq.resx index e5139fa7c4..8a288af464 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.sq.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.sq.resx @@ -18,6 +18,9 @@ Skaneri i zgjedhur është jashtë linje. + + Komunikimi me pajisjen e skanimit u ndërpre. + NAPS2 @@ -69,4 +72,19 @@ Drejtuesi i zgjedhur nuk mbështetet në këtë sistem. + + Ndodhi një gabim gjatë ekzekutimit të OCR. + + + Afati i veprimit të OCR skadoi. + + + Procesi i punës u ndërpre. + + + Procesi i punës u ndërpre. Kontrolloni shikuesin e ngjarjeve të Windows. + + + Skcaner i panjohur + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.sr-CS.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.sr-CS.resx index 4a1df6661e..14694835b2 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.sr-CS.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.sr-CS.resx @@ -18,6 +18,9 @@ Odabrani skener nije uključen. + + Communication with the scanning device was interrupted. + NAPS2 @@ -69,4 +72,19 @@ The selected driver is not supported on this system. + + An error occurred running OCR. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.sr.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.sr.resx index 10bb573862..250be6cf00 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.sr.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.sr.resx @@ -18,6 +18,9 @@ Одабрани скенер није укључен. + + Communication with the scanning device was interrupted. + NAPS2 @@ -69,4 +72,19 @@ The selected driver is not supported on this system. + + An error occurred running OCR. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.sv.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.sv.resx index 10af82e348..41bf0d1396 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.sv.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.sv.resx @@ -18,6 +18,9 @@ Den valda skannern är offline. + + Kommunikationen med skanningsenheten avbröts. + NAPS2 @@ -69,4 +72,19 @@ Valda drivrutinen är inte supporterad för detta system. + + Ett fel uppstod vid körning av OCR. + + + OCR-åtgärden överskred tidsgränsen. + + + Arbetsprocessen kraschade. + + + Arbetsprocessen kraschade. Se Loggboken i Windows. + + + Okänd skanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.th.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.th.resx new file mode 100644 index 0000000000..3bab60a4a7 --- /dev/null +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.th.resx @@ -0,0 +1,90 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The selected scanner could not be found. + + + The selected scanner is offline. + + + Communication with the scanning device was interrupted. + + + NAPS2 + + + No device selected. + + + No scanning device was found. + + + An error occurred with the scanning driver. + + + เวอร์ชั่น {0} + + + ไม่สามารถนำเข้าไฟล์ '{0}' ได้ + + + The selected scanner does not support using a feeder. If your scanner does have a feeder, try using a different driver. + + + No pages are in the feeder. + + + You do not have permission to copy content from the file '{0}'. + + + The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + + + The selected scanner is busy. + + + The scanner's cover is open. + + + The scanner has a paper jam. + + + The scanner is warming up. + + + The SANE driver is not available. Make sure to install the required packages: + + + The OCR engine is not available. Make sure to install the required package: + + + The selected driver is not supported on this system. + + + An error occurred running OCR. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.tr.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.tr.resx index 6a7cf37beb..36c53bdb55 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.tr.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.tr.resx @@ -18,6 +18,9 @@ Seçilen tarayıcı çevrim dışı. + + Tarayıcıyla iletişim kesildi. + NAPS2 @@ -64,9 +67,24 @@ SANE sürücüsü kullanılamıyor. Gerekli paketlerin kuruluduğundan emin olun: - OCR motoru kullanılamıyor. Gerekli paketin kurulduğundan emin olun: + OKT motoru kullanılamıyor. Gerekli paketin kurulduğundan emin olun: Seçilen sürücü bu sistemde desteklenmiyor. + + OKT çalıştırılırken hata oluştu. + + + OKT işlemi zaman aşımına uğradı. + + + Çalışma işlemi çöktü. + + + Çalışma işlemi çöktü. Windows olay görüntüleyicisini denetleyin. + + + Bilinmeyen Tarayıcı + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.uk.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.uk.resx index 45c30263f0..ec8d54be7b 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.uk.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.uk.resx @@ -16,13 +16,16 @@ Не знайдено вибраний сканер. - Вибраний сканер відключений. + Вибраний сканер вимкнено. + + + Обмін інформацією зі сканером було перервано. NAPS2 - Не вибраний пристрій. + Не вибрано пристрій. Не знайдено жодного сканера. @@ -34,39 +37,54 @@ Версія {0} - Файл '{0}' не може бути імпортований. + Неможливо імпортувати файл '{0}'. - Цей сканер не підтримує автоматичну подачу паперу. Якщо Ваш сканер має автоматичну подачу паперу, спробуйте використати інший драйвер.. + Вибраний сканер не підтримує автоматичну подачу паперу. Якщо ваш сканер все ж має автоматичну подачу паперу, спробуйте вибрати інший драйвер.. - В пристрої відсутній папір. + У пристрої відсутній папір. - Ви не можете копіювати вміст файлу '{0}'. + Відсутні права на копіювання вмісту файлу '{0}'. - The selected scanner does not support using duplex. If your scanner is supposed to support duplex, try using a different driver. + Вибраний сканер не підтримує двостороннє сканування. Якщо ваш сканер все ж підтримує двостороннє сканування, спробуйте вибрати інший драйвер. - The selected scanner is busy. + Вибраний сканер зайнятий. - The scanner's cover is open. + Відкрито кришку сканеру. - The scanner has a paper jam. + Зминання паперу у сканері. - The scanner is warming up. + Прогрівання сканеру. - The SANE driver is not available. Make sure to install the required packages: + Драйвер SANE не доступний. Перевірте, чи встановлено такі пакети: - The OCR engine is not available. Make sure to install the required package: + Рушій розпізнавання (OCR) не доступний. Перевірте, чи встановлено такі пакети:: - The selected driver is not supported on this system. + Вибраний драйвер не підтримується системою. + + + Під час цифрового розпізнавання трапилася помилка. + + + Час операції з розпізнавання вичерпано. + + + Збій робочого процесу. + + + Збій робочого процесу. Перегляньте журнал подій Windows. + + + Невідомий сканер \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.vi.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.vi.resx index 78d8f544a6..b5ad97f770 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.vi.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.vi.resx @@ -18,6 +18,9 @@ Máy quét chọn đang ẩn. + + Communication with the scanning device was interrupted. + NAPS2 @@ -69,4 +72,19 @@ Driver bạn đang chọn không hỗ trợ hệ điều hành này.. + + An error occurred running OCR. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.zh-CN.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.zh-CN.resx index 92c51935c5..21c0b26232 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.zh-CN.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.zh-CN.resx @@ -18,6 +18,9 @@ 选定的扫描仪处于脱机状态. + + 与扫描设备的通信中断 + NAPS2 @@ -69,4 +72,19 @@ 所选驱动程序不受本系统支持. + + 运行OCR时出错。 + + + OCR操作超时 + + + 工作进程崩溃 + + + 工作进程崩溃。检查 Windows 事件查看器 + + + 未知扫描仪 + \ No newline at end of file diff --git a/NAPS2.Sdk/Lang/Resources/SdkResources.zh-TW.resx b/NAPS2.Sdk/Lang/Resources/SdkResources.zh-TW.resx index b7bee1dd9a..000d39da3d 100644 --- a/NAPS2.Sdk/Lang/Resources/SdkResources.zh-TW.resx +++ b/NAPS2.Sdk/Lang/Resources/SdkResources.zh-TW.resx @@ -18,6 +18,9 @@ 所選的掃瞄器離線. + + Communication with the scanning device was interrupted. + NAPS2 @@ -69,4 +72,19 @@ The selected driver is not supported on this system. + + An error occurred running OCR. + + + The OCR operation timed out. + + + The worker process crashed. + + + The worker process crashed. Check the Windows event viewer. + + + Unknown Scanner + \ No newline at end of file diff --git a/NAPS2.Sdk/Logging/DebugLogger.cs b/NAPS2.Sdk/Logging/DebugLogger.cs deleted file mode 100644 index a57405d32a..0000000000 --- a/NAPS2.Sdk/Logging/DebugLogger.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace NAPS2.Logging; - -public class DebugLogger : ILogger -{ - public void Error(string message) - { - Debug.WriteLine(message); - } - - public void ErrorException(string message, Exception exception) - { - Debug.WriteLine(message); - Debug.WriteLine(exception.ToString()); - } - - public void FatalException(string message, Exception exception) - { - Debug.WriteLine(message); - Debug.WriteLine(exception.ToString()); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Logging/ILogger.cs b/NAPS2.Sdk/Logging/ILogger.cs deleted file mode 100644 index eb1571223a..0000000000 --- a/NAPS2.Sdk/Logging/ILogger.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace NAPS2.Logging; - -/// -/// A base interface for logging APIs. Used by the Log class. -/// -public interface ILogger -{ - void Error(string message); - void ErrorException(string message, Exception exception); - void FatalException(string message, Exception exception); -} \ No newline at end of file diff --git a/NAPS2.Sdk/Logging/NullLogger.cs b/NAPS2.Sdk/Logging/NullLogger.cs deleted file mode 100644 index 144e67b489..0000000000 --- a/NAPS2.Sdk/Logging/NullLogger.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace NAPS2.Logging; - -/// -/// A default logging implementation that does nothing. -/// -public class NullLogger : ILogger -{ - public void Error(string message) - { - } - - public void ErrorException(string message, Exception exception) - { - } - - public void FatalException(string message, Exception exception) - { - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/NAPS2.Sdk.csproj b/NAPS2.Sdk/NAPS2.Sdk.csproj index be0711a157..02c6cefbb2 100644 --- a/NAPS2.Sdk/NAPS2.Sdk.csproj +++ b/NAPS2.Sdk/NAPS2.Sdk.csproj @@ -1,53 +1,47 @@  - net6;net462 - net6;net6-macos10.15;net462 + net6;net8;net462 + + $(TargetFrameworks);net8-macos + enable true NAPS2 - NAPS2 - Not Another PDF Scanner - NAPS2 - Not Another PDF Scanner - Copyright 2009, 2012-2020 NAPS2 Contributors; Icons from http://www.fatcow.com/free-icons - Debug;Release;DebugLang;Release-Msi;Release-Zip + Debug;Release;DebugLang;DebugNoMac + + NAPS2.Sdk + NAPS2.Sdk + NAPS2.Sdk is a fully-featured scanning library, supporting WIA, TWAIN, SANE, and ESCL scanners on Windows, Mac, and Linux. + naps2 sdk scanner wia twain sane escl + README.md + - - NONWINDOWS - - - MSI - - - ZIP - - + MAC + + $(DefineConstants);DEBUG + - - - - ..\NAPS2.Setup\lib\PdfSharpCore.dll - - - - - - - - - - - - - - + + + + + + + + + + + + @@ -63,30 +57,50 @@ <_Parameter1>NAPS2.Lib.WinForms + + <_Parameter1>NAPS2.Lib.Mac + + + <_Parameter1>NAPS2.Lib.Gtk + + + <_Parameter1>NAPS2.App.Tests + + + <_Parameter1>NAPS2.Worker + <_Parameter1>DynamicProxyGenAssembly2 - + - - + + + - + - + - + + + + SdkResources.resx + + ResXFileCodeGenerator + + \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/IOcrEngine.cs b/NAPS2.Sdk/Ocr/IOcrEngine.cs index 36c45d13a0..7e7dbe4367 100644 --- a/NAPS2.Sdk/Ocr/IOcrEngine.cs +++ b/NAPS2.Sdk/Ocr/IOcrEngine.cs @@ -1,8 +1,13 @@ using System.Threading; +using NAPS2.Scan; namespace NAPS2.Ocr; +/// +/// Interface for OCR (optical character recognition). See TesseractOcrEngine. +/// public interface IOcrEngine { - Task ProcessImage(string imagePath, OcrParams ocrParams, CancellationToken cancelToken); + Task ProcessImage(ScanningContext scanningContext, string imagePath, OcrParams ocrParams, + CancellationToken cancelToken); } \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/OcrController.cs b/NAPS2.Sdk/Ocr/OcrController.cs index 5a3451a073..0650c3b517 100644 --- a/NAPS2.Sdk/Ocr/OcrController.cs +++ b/NAPS2.Sdk/Ocr/OcrController.cs @@ -12,7 +12,9 @@ namespace NAPS2.Ocr; /// OCR results are not accessed directly - OCR is only done to populate the OcrRequestQueue cache for future Save PDF /// operations. /// -public class OcrController +// TODO: This model seems overly complicated - can we do something simpler like having a singleton OcrController on +// the ScanningContext? +internal class OcrController { private readonly ScanningContext _scanningContext; private readonly Dictionary @@ -52,6 +54,7 @@ public OcrController(ScanningContext scanningContext) image = image.WithPostProcessingData(image.PostProcessingData with { OcrCts = cts }, true); var task = _scanningContext.OcrRequestQueue.Enqueue( + _scanningContext, engine, image, tempImageFilePath, diff --git a/NAPS2.Sdk/Ocr/OcrErrorEventArgs.cs b/NAPS2.Sdk/Ocr/OcrErrorEventArgs.cs new file mode 100644 index 0000000000..2af17c10c7 --- /dev/null +++ b/NAPS2.Sdk/Ocr/OcrErrorEventArgs.cs @@ -0,0 +1,11 @@ +namespace NAPS2.Ocr; + +public class OcrErrorEventArgs +{ + public OcrErrorEventArgs(Exception exception) + { + Exception = exception; + } + + public Exception Exception { get; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/OcrMode.cs b/NAPS2.Sdk/Ocr/OcrMode.cs index ddef74ca3f..20f25df71a 100644 --- a/NAPS2.Sdk/Ocr/OcrMode.cs +++ b/NAPS2.Sdk/Ocr/OcrMode.cs @@ -1,8 +1,15 @@ namespace NAPS2.Ocr; +/// +/// The mode of an OCR request (fast/best), if supported by the engine. +/// +[Flags] public enum OcrMode { - Default, - Fast, - Best + Default = 0, + Fast = 1, + Best = 2, + WithPreProcess = 4, + FastWithPreProcess = Fast | WithPreProcess, + BestWithPreProcess = Best | WithPreProcess } \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/OcrParams.cs b/NAPS2.Sdk/Ocr/OcrParams.cs index 163c4001e8..32201a935c 100644 --- a/NAPS2.Sdk/Ocr/OcrParams.cs +++ b/NAPS2.Sdk/Ocr/OcrParams.cs @@ -7,7 +7,7 @@ /// For language codes, see /// https://tesseract-ocr.github.io/tessdoc/Data-Files#data-files-for-version-400-november-29-2016 /// -public record OcrParams(string? LanguageCode, OcrMode Mode, double TimeoutInSeconds) +public record OcrParams(string? LanguageCode, OcrMode Mode = OcrMode.Default, double TimeoutInSeconds = 0) { private OcrParams() : this(null, OcrMode.Default, 0) diff --git a/NAPS2.Sdk/Ocr/OcrRequest.cs b/NAPS2.Sdk/Ocr/OcrRequest.cs index 2246631326..e2652182b5 100644 --- a/NAPS2.Sdk/Ocr/OcrRequest.cs +++ b/NAPS2.Sdk/Ocr/OcrRequest.cs @@ -1,4 +1,6 @@ using System.Threading; +using Microsoft.Extensions.Logging; +using NAPS2.Scan; namespace NAPS2.Ocr; @@ -7,14 +9,18 @@ namespace NAPS2.Ocr; /// internal class OcrRequest { + private readonly ScanningContext _scanningContext; + private readonly ILogger _logger; private readonly OcrRequestQueue _ocrRequestQueue; private readonly TaskCompletionSource _tcs = new(); private readonly CancellationTokenSource _requestCts = new(); private string? _tempImageFilePath; private int _activeReferences = 0; - public OcrRequest(OcrRequestParams reqParams, OcrRequestQueue ocrRequestQueue) + public OcrRequest(ScanningContext scanningContext, OcrRequestQueue ocrRequestQueue, OcrRequestParams reqParams) { + _scanningContext = scanningContext; + _logger = scanningContext.Logger; _ocrRequestQueue = ocrRequestQueue; Params = reqParams; } @@ -100,11 +106,11 @@ public async Task Process() OcrResult? result = null; try { - result = await Params.Engine.ProcessImage(_tempImageFilePath!, Params.OcrParams, _requestCts.Token); + result = await Params.Engine.ProcessImage(_scanningContext, _tempImageFilePath!, Params.OcrParams, _requestCts.Token); } catch (Exception e) { - Log.ErrorException("Error in OcrEngine.ProcessImage", e); + _logger.LogError(e, "Error in OcrEngine.ProcessImage"); } SafeDelete(_tempImageFilePath!); lock (_ocrRequestQueue) @@ -114,7 +120,7 @@ public async Task Process() _tcs.SetResult(result); } - private static void SafeDelete(string path) + private void SafeDelete(string path) { try { @@ -130,7 +136,7 @@ private static void SafeDelete(string path) } catch (Exception e) { - Log.ErrorException("Error deleting temp OCR file", e); + _logger.LogError(e, "Error deleting temp OCR file"); } } } \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/OcrRequestQueue.cs b/NAPS2.Sdk/Ocr/OcrRequestQueue.cs index 66efae81f3..6669196cda 100644 --- a/NAPS2.Sdk/Ocr/OcrRequestQueue.cs +++ b/NAPS2.Sdk/Ocr/OcrRequestQueue.cs @@ -1,4 +1,6 @@ using System.Threading; +using Microsoft.Extensions.Logging; +using NAPS2.Scan; namespace NAPS2.Ocr; @@ -7,19 +9,28 @@ namespace NAPS2.Ocr; /// Allows OCR requests to be queued and prioritized. Results are cached so that requests with the same set of /// parameters (image, engine, language code, etc.) don't do duplicate work. /// -public class OcrRequestQueue +internal class OcrRequestQueue { private readonly Dictionary _requestCache = new(); private Semaphore _queueWaitHandle = new(0, int.MaxValue); - private List _workerTasks = new(); + private List _workerTasks = []; private CancellationTokenSource _workerCts = new(); /// /// Gets or sets the number of queue workers, which determines the maximum number of OCR requests that can process /// in parallel. /// - public int WorkerCount { get; init; } = Environment.ProcessorCount; - + public int WorkerCount { get; init; } = Environment.ProcessorCount switch + { + // We want OCR to use as many of the available cores (or threads in the case of SMT) as possible, but too many + // Tesseract processes can use a lot of memory, so for high core counts we use less, and hard cap at 16. + // TODO: Maybe consider checking total system memory, e.g. using https://www.nuget.org/packages/Hardware.Info + <= 8 => Environment.ProcessorCount, + <= 16 => 8, + <= 32 => Environment.ProcessorCount / 2, + > 32 => 16 + }; + /// /// For testing. Adds a delay to the worker tasks to process requests. /// @@ -43,6 +54,7 @@ public bool HasCachedResult(IOcrEngine ocrEngine, ProcessedImage image, OcrParam /// path specified as "tempImageFilePath". The file will automatically be deleted once it is no longer needed for /// the OCR request. /// + /// The scanning context object. /// The engine to run. /// The image to OCR. /// The on-disk image file path. @@ -50,33 +62,33 @@ public bool HasCachedResult(IOcrEngine ocrEngine, ProcessedImage image, OcrParam /// The priority of the request. /// A cancellation token. /// The result of the OCR operation, or null if an error occurred (e.g. engine misconfigured). - public async Task Enqueue(IOcrEngine ocrEngine, ProcessedImage image, string tempImageFilePath, - OcrParams ocrParams, OcrPriority priority, CancellationToken cancelToken) + public async Task Enqueue(ScanningContext scanningContext, IOcrEngine ocrEngine, ProcessedImage image, + string tempImageFilePath, OcrParams ocrParams, OcrPriority priority, CancellationToken cancelToken) { OcrRequest req; lock (this) { var reqParams = new OcrRequestParams(image.GetWeakReference(), ocrEngine, ocrParams); - req = _requestCache.GetOrSet(reqParams, () => new OcrRequest(reqParams, this)); + req = _requestCache.GetOrSet(reqParams, () => new OcrRequest(scanningContext, this, reqParams)); if (req.State is OcrRequestState.Canceled or OcrRequestState.Error) { // Retry with a new request - req = _requestCache[reqParams] = new OcrRequest(reqParams, this); + req = _requestCache[reqParams] = new OcrRequest(scanningContext, this, reqParams); } req.AddReference(tempImageFilePath, priority, cancelToken); } // Signal the worker tasks that a request may be ready _queueWaitHandle.Release(); // If no worker threads are running, start them - EnsureWorkerThreads(); + EnsureWorkerThreads(scanningContext); await Task.WhenAny(req.CompletedTask, cancelToken.WaitHandle.WaitOneAsync()); // If no requests are pending, stop the worker threads - EnsureWorkerThreads(); + EnsureWorkerThreads(scanningContext); // Return null if canceled return req.CompletedTask.IsCompleted ? req.CompletedTask.Result : null; } - private void EnsureWorkerThreads() + private void EnsureWorkerThreads(ScanningContext scanningContext) { lock (this) { @@ -85,21 +97,22 @@ private void EnsureWorkerThreads() { for (int i = 0; i < WorkerCount; i++) { - _workerTasks.Add(Task.Run(() => RunWorkerTask(_workerCts, _queueWaitHandle))); + _workerTasks.Add(Task.Run(() => RunWorkerTask(scanningContext, _workerCts, _queueWaitHandle))); } } if (_workerTasks.Count > 0 && !hasPending) { _workerCts.Cancel(); - _workerTasks = new List(); + _workerTasks = []; _workerCts = new CancellationTokenSource(); _queueWaitHandle = new Semaphore(0, int.MaxValue); } } } - private async Task RunWorkerTask(CancellationTokenSource cts, WaitHandle queueWaitHandle) + private async Task RunWorkerTask(ScanningContext scanningContext, CancellationTokenSource cts, WaitHandle queueWaitHandle) { + var logger = scanningContext.Logger; try { if (WorkerAddedLatency > 0) @@ -133,7 +146,7 @@ private async Task RunWorkerTask(CancellationTokenSource cts, WaitHandle queueWa } catch (Exception e) { - Log.ErrorException("Error in OcrRequestQueue.RunWorkerTask", e); + logger.LogError(e, "Error in OcrRequestQueue.RunWorkerTask"); } } -} \ No newline at end of file +} diff --git a/NAPS2.Sdk/Ocr/OcrResult.cs b/NAPS2.Sdk/Ocr/OcrResult.cs index add3eae2cc..0eda6dee2e 100644 --- a/NAPS2.Sdk/Ocr/OcrResult.cs +++ b/NAPS2.Sdk/Ocr/OcrResult.cs @@ -5,15 +5,14 @@ namespace NAPS2.Ocr; /// /// The result of an OCR request. Contains a set of elements that represent text segments. /// -public class OcrResult +public class OcrResult( + (int x, int y, int w, int h) pageBounds, + ImmutableList words, + ImmutableList lines) { - public OcrResult((int x, int y, int w, int h) pageBounds, ImmutableList elements) - { - PageBounds = pageBounds; - Elements = elements; - } + public (int x, int y, int w, int h) PageBounds { get; } = pageBounds; - public (int x, int y, int w, int h) PageBounds { get; } + public ImmutableList Words { get; } = words; - public ImmutableList Elements { get; } + public ImmutableList Lines { get; } = lines; } \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/OcrResultElement.cs b/NAPS2.Sdk/Ocr/OcrResultElement.cs index ddbedee3a3..ad312367fe 100644 --- a/NAPS2.Sdk/Ocr/OcrResultElement.cs +++ b/NAPS2.Sdk/Ocr/OcrResultElement.cs @@ -1,6 +1,15 @@ -namespace NAPS2.Ocr; +using System.Collections.Immutable; + +namespace NAPS2.Ocr; /// /// A element in the result of an OCR request that represents a text segment. /// -public record OcrResultElement(string Text, string LanguageCode, bool RightToLeft, (int x, int y, int w, int h) Bounds); \ No newline at end of file +public record OcrResultElement( + string Text, + string LanguageCode, + bool RightToLeft, + (int x, int y, int w, int h) Bounds, + int Baseline, + int FontSize, + ImmutableList Children); \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/StubOcrEngine.cs b/NAPS2.Sdk/Ocr/StubOcrEngine.cs index af02d0872c..29f40b5068 100644 --- a/NAPS2.Sdk/Ocr/StubOcrEngine.cs +++ b/NAPS2.Sdk/Ocr/StubOcrEngine.cs @@ -1,10 +1,12 @@ using System.Threading; +using NAPS2.Scan; namespace NAPS2.Ocr; -public class StubOcrEngine : IOcrEngine +internal class StubOcrEngine : IOcrEngine { - public Task ProcessImage(string imagePath, OcrParams ocrParams, CancellationToken cancelToken) + public Task ProcessImage(ScanningContext scanningContext, string imagePath, OcrParams ocrParams, + CancellationToken cancelToken) { return Task.FromResult(null); } diff --git a/NAPS2.Sdk/Ocr/TesseractLanguageData.cs b/NAPS2.Sdk/Ocr/TesseractLanguageData.cs deleted file mode 100644 index 8c8c05d88b..0000000000 --- a/NAPS2.Sdk/Ocr/TesseractLanguageData.cs +++ /dev/null @@ -1,177 +0,0 @@ -namespace NAPS2.Ocr; - -public class TesseractLanguageData -{ - public class TesseractLanguage - { - public TesseractLanguage(string filename, string code, string langName, double size, string sha1, bool rtl = false) - { - Filename = filename; - Code = code; - LangName = langName; - Size = size; - Sha1 = sha1; - RTL = rtl; - } - - public string Filename { get; } - - public string Code { get; } - - public string LangName { get; } - - public double Size { get; } - - public string Sha1 { get; } - - public bool RTL { get; } - } - - protected TesseractLanguageData(TesseractLanguage[] data) - { - Data = data; - LanguageMap = data.ToDictionary(x => $"ocr-{x.Code}", x => new Language(x.Code, x.LangName, x.RTL)); - } - - public Dictionary LanguageMap { get; set; } - - public TesseractLanguage[] Data { get; set; } - - #region Tesseract Language Data (auto-generated) - - public static readonly TesseractLanguageData Latest = new(new[] - { - new TesseractLanguage("afr.traineddata.zip", "afr", "Afrikaans", 5.44, "4278120a18e3464194df302f55417afc35415af7"), - new TesseractLanguage("amh.traineddata.zip", "amh", "Amharic", 5.55, "166219c79a3c92775ac8cc987fba91899dc63f7d"), - new TesseractLanguage("ara.traineddata.zip", "ara", "Arabic", 2.29, "6a09f2f96ee04d2bf1c887ea10bcaace429908a6", true), - new TesseractLanguage("asm.traineddata.zip", "asm", "Assamese", 2.82, "fe4b1d832af281a7947ccf86300c9574827d3b50"), - new TesseractLanguage("aze.traineddata.zip", "aze", "Azerbaijani", 5.69, "aa1092d2931dc0d500bda58987a49d3a9bb6d98d", true), - new TesseractLanguage("aze_cyrl.traineddata.zip", "aze_cyrl", "Azerbaijani (Cyrillic)", 2.88, "72fd2b3e1d6f3c88b09f238306e89742b2ae6f0f"), - new TesseractLanguage("bel.traineddata.zip", "bel", "Belarusian", 5.86, "80da0e84413031213eb30c8b4064fb25b2913cad"), - new TesseractLanguage("ben.traineddata.zip", "ben", "Bengali", 1.84, "b89191580d742a688bf435fa9ff94f9d468c4858"), - new TesseractLanguage("bod.traineddata.zip", "bod", "Tibetan", 2.44, "45c144a9d5bf1cdbec50fc24bd3d49bb8f9eba95"), - new TesseractLanguage("bos.traineddata.zip", "bos", "Bosnian", 4.08, "412d5fff06e9faee873e19cfac0d9e6c72a3c7c8"), - new TesseractLanguage("bre.traineddata.zip", "bre", "Breton", 6.46, "124f6cdd9b44fb49783fb9908777216d5308102e"), - new TesseractLanguage("bul.traineddata.zip", "bul", "Bulgarian", 4.32, "05be97ef3169fd953175e37e0b91390a39e5c198"), - new TesseractLanguage("cat.traineddata.zip", "cat", "Catalan", 3.20, "b8cb54105535c07dd4fd7b9aec441cc27f6692f8"), - new TesseractLanguage("ceb.traineddata.zip", "ceb", "Cebuano", 1.52, "484d250a6863e8e1fed00368f6a62c049b5b972c"), - new TesseractLanguage("ces.traineddata.zip", "ces", "Czech", 8.43, "69a7b67e1175ccecd39882e7d521872a93ee85c7"), - new TesseractLanguage("chi_sim.traineddata.zip", "chi_sim", "Chinese (Simplified)", 20.73, "e26f943534443c274c43a81b24ac10f4c277b9e3"), - new TesseractLanguage("chi_sim_vert.traineddata.zip", "chi_sim_vert", "Chinese (Simplified, Vertical)", 2.81, "898de9e4322bf818ae7e94cbd89834a9ac0fc7e9"), - new TesseractLanguage("chi_tra.traineddata.zip", "chi_tra", "Chinese (Traditional)", 27.26, "0eb485e9961bad5f4fe1237b13f61165c356532f"), - new TesseractLanguage("chi_tra_vert.traineddata.zip", "chi_tra_vert", "Chinese (Traditional, Vertical)", 2.70, "dbefb7180af04cf64b630473dd0ed70569369c1b"), - new TesseractLanguage("chr.traineddata.zip", "chr", "Cherokee", 0.92, "387d14e948dafe053c644e60a2429f4523e743a3"), - new TesseractLanguage("cos.traineddata.zip", "cos", "Corsican", 2.64, "892ec8f2156de1d1dd1812b730cbaea59f645097"), - new TesseractLanguage("cym.traineddata.zip", "cym", "Welsh", 3.97, "42db973712f4949012405295af0cd5eac09b73df"), - new TesseractLanguage("dan.traineddata.zip", "dan", "Danish", 5.72, "62b39c7b7eb560f2b1910b7e2abbcf62c5b9c882"), - new TesseractLanguage("dan_frak.traineddata.zip", "dan_frak", "Danish (Fraktur)", 0.65, "becc87d384ddc8f410d5d68ef8c2644bd79fa2ee"), - new TesseractLanguage("deu.traineddata.zip", "deu", "German", 7.58, "22566b9236a55c3f93324ac78ce26a09c0c4aecc"), - new TesseractLanguage("deu_frak.traineddata.zip", "deu_frak", "German (Fraktur)", 0.78, "5cd0fbf328e0c6c99f3e7bdd0b8b79ac78166f58"), - new TesseractLanguage("div.traineddata.zip", "div", "Maldivian", 1.70, "ac91a1e0c11529e958c033394d39ca727fd72091", true), - new TesseractLanguage("dzo.traineddata.zip", "dzo", "Dzongkha", 0.75, "2d5493a2157d1cf910b4fc8b3e0d861e25dd918a"), - new TesseractLanguage("ell.traineddata.zip", "ell", "Greek", 3.93, "0e3f029af86d83bbf1291bbc58d574829b4df053"), - new TesseractLanguage("eng.traineddata.zip", "eng", "English", 12.29, "64d9aa9654d5ee9e82d9b693c3445a91ffbd7b93"), - new TesseractLanguage("enm.traineddata.zip", "enm", "English (Middle)", 4.58, "06c7e1b3f4135290eae297aece61b11a413de4ba"), - new TesseractLanguage("epo.traineddata.zip", "epo", "Esperanto", 6.50, "7e4b1cc89c5fcbd9f2b2ae8caad09aa025452f1f"), - new TesseractLanguage("equ.traineddata.zip", "equ", "Math / equation detection", 0.79, "b15b9a1c006cebac5ffc35569fe01b3e7ee53e72"), - new TesseractLanguage("est.traineddata.zip", "est", "Estonian", 8.49, "6aefab9f0bdc0c080ee681d3fd359ecef3cd9269"), - new TesseractLanguage("eus.traineddata.zip", "eus", "Basque", 6.07, "c12b5e15c5bd89e11029c071050d093b2ba188c5"), - new TesseractLanguage("fao.traineddata.zip", "fao", "Faroese", 3.69, "a7da4c8c4c299557a655e0e5c1045e5beace9c9b"), - new TesseractLanguage("fas.traineddata.zip", "fas", "Persian", 0.71, "a3aadf776fc9248444d68c971066cf3f86d3c4ce", true), - new TesseractLanguage("fil.traineddata.zip", "fil", "Filipino", 2.31, "513edccb087eed2771f67c355d4c4938c8811e75"), - new TesseractLanguage("fin.traineddata.zip", "fin", "Finnish", 12.19, "45db275878b1e73777b5525dc2f01c8f1d3115fa"), - new TesseractLanguage("fra.traineddata.zip", "fra", "French", 6.55, "0ac9eeb04b334ef29c6a63e86c82aa91d0fe6fce"), - new TesseractLanguage("frk.traineddata.zip", "frk", "Frankish", 13.08, "2a22d40a403a4e03017de8ae97d4800dbcf21e06"), - new TesseractLanguage("frm.traineddata.zip", "frm", "French (Middle)", 8.30, "9ad7ab932c2b140e2dc665121ae4ab54e6df0026"), - new TesseractLanguage("fry.traineddata.zip", "fry", "Frisian (Western)", 2.42, "a455b00abd59502f6f6be2cddd11ddded3c6302e"), - new TesseractLanguage("gla.traineddata.zip", "gla", "Gaelic", 3.33, "f1f002acf9bb3d17b97e044ddfd654211364129c"), - new TesseractLanguage("gle.traineddata.zip", "gle", "Irish", 2.55, "fd95035f971a61472be035b977f91865275d2b4f"), - new TesseractLanguage("glg.traineddata.zip", "glg", "Galician", 5.38, "ab121f2c06eae328335eff6086f3da32549ac9f1"), - new TesseractLanguage("grc.traineddata.zip", "grc", "Greek (Ancient)", 3.98, "2aec27b8494b63be72e8b493184ee83e75399ffd"), - new TesseractLanguage("guj.traineddata.zip", "guj", "Gujarati", 1.88, "8365aa9722bb76774bb0648ac23233ed889464a1"), - new TesseractLanguage("hat.traineddata.zip", "hat", "Haitian", 3.40, "6b2eb7d203d7fd12ee4d283f3a096d2efa9eb1f1"), - new TesseractLanguage("heb.traineddata.zip", "heb", "Hebrew", 2.53, "8a083a920a85148966472b23f0fef57aa25d49d8", true), - new TesseractLanguage("hin.traineddata.zip", "hin", "Hindi", 2.22, "469b764d3af97d39fb175ae1ace182033a986706"), - new TesseractLanguage("hrv.traineddata.zip", "hrv", "Croatian", 7.23, "9d53c8d5c97ff8f40c2bd953a468a170d163ac77"), - new TesseractLanguage("hun.traineddata.zip", "hun", "Hungarian", 9.58, "adfc68325b8fb215b069e0f093ef96e68d0d068f"), - new TesseractLanguage("hye.traineddata.zip", "hye", "Armenian", 2.97, "a058b8ec58653ac3cad34823b4f8aec04fb970d9"), - new TesseractLanguage("iku.traineddata.zip", "iku", "Inuktitut", 3.10, "2a8927e92e3af0d45550ff8a2215310ea9b4bc35"), - new TesseractLanguage("ind.traineddata.zip", "ind", "Indonesian", 4.24, "d3c3b32e71d2fac63661cbcc842927d9a2825be8"), - new TesseractLanguage("isl.traineddata.zip", "isl", "Icelandic", 4.96, "9360af20b740d2313863dbe527fd831704bf2121"), - new TesseractLanguage("ita.traineddata.zip", "ita", "Italian", 7.80, "916998186f658546c3407201127c588539ab447c"), - new TesseractLanguage("ita_old.traineddata.zip", "ita_old", "Italian (Old)", 8.80, "b6a7efe00f7ce34f75b4a3a91c2c1d3ea83133ca"), - new TesseractLanguage("jav.traineddata.zip", "jav", "Javanese", 4.74, "5dd426e68a1a2ca4d6a6a771226e54a1a943a61e"), - new TesseractLanguage("jpn.traineddata.zip", "jpn", "Japanese", 16.77, "73b54f8cd99edffa20627583a82add77462f593a"), - new TesseractLanguage("jpn_vert.traineddata.zip", "jpn_vert", "Japanese (Vertical)", 3.88, "ff0e822c64c0ba88f9cd4caf6bc90187446da6f2"), - new TesseractLanguage("kan.traineddata.zip", "kan", "Kannada", 3.66, "7b6e48a0674c2adb39b1b8819751e7fdf1b54722"), - new TesseractLanguage("kat.traineddata.zip", "kat", "Georgian", 4.29, "c55fb40f2375c91d35409af690d8f139b92a3903"), - new TesseractLanguage("kat_old.traineddata.zip", "kat_old", "Georgian (Old)", 0.92, "4c94b5f3c90e8034536a7dc1f1e74ec66cfa1bb5"), - new TesseractLanguage("kaz.traineddata.zip", "kaz", "Kazakh", 5.70, "e07e7ffb3c656c15637b23e56cd95dc1c584a059"), - new TesseractLanguage("khm.traineddata.zip", "khm", "Khmer (Central)", 2.05, "94b300a9051018506026bfb58ce95da9bc0bd00a"), - new TesseractLanguage("kir.traineddata.zip", "kir", "Kirghiz", 10.46, "d3d8cc2168427f6dfe584349109bf18be05f6461"), - new TesseractLanguage("kor.traineddata.zip", "kor", "Korean", 7.66, "8e7dfdf16af0abd98ba87dbb5db59f140fd0429e"), - new TesseractLanguage("kor_vert.traineddata.zip", "kor_vert", "Korean (Vertical)", 1.18, "76349e042e19e5ed4bffcd4ed6f56159b2620536"), - new TesseractLanguage("kur.traineddata.zip", "kur", "Kurdish", 0.73, "3dd03488c9e05b6dcca8767c3b3d0d375a214723", true), - new TesseractLanguage("kur_ara.traineddata.zip", "kur_ara", "Kurdish (Arabic)", 1.83, "a3e0c096cda284b963dce271c358174449fea4dc"), - new TesseractLanguage("lao.traineddata.zip", "lao", "Lao", 6.52, "0c577c9b9b57a5312dc5cfe1ee3bbf5d728e5b50"), - new TesseractLanguage("lat.traineddata.zip", "lat", "Latin", 5.38, "5296894c777b799199ecbab99e5880e814fbab5e"), - new TesseractLanguage("lav.traineddata.zip", "lav", "Latvian", 5.32, "83b04ff7616468868bba6f7c9a7e071d79ddc90f"), - new TesseractLanguage("lit.traineddata.zip", "lit", "Lithuanian", 6.42, "9d244f95eceee451b54274159fddf84739ec7294"), - new TesseractLanguage("ltz.traineddata.zip", "ltz", "Luxembourgish", 3.49, "2f1ed3052e57dc7dbf548d1cdd96e14a696f882a"), - new TesseractLanguage("mal.traineddata.zip", "mal", "Malayalam", 4.73, "11616e5cf327229775b99eb48c71a9733cb18eac"), - new TesseractLanguage("mar.traineddata.zip", "mar", "Marathi", 2.84, "daa3124cd616bbbab1d1540d65fea8c943f824d5"), - new TesseractLanguage("mkd.traineddata.zip", "mkd", "Macedonian", 2.83, "acaf7cef9c12557db3a5ece01e2f84c67948bf9b"), - new TesseractLanguage("mlt.traineddata.zip", "mlt", "Maltese", 4.17, "5629bbe2c8bd96ef0a8fb0cc1fbd969c2bb59496"), - new TesseractLanguage("mon.traineddata.zip", "mon", "Mongolian", 2.53, "ad42b4564c70088b59802bfc562d04bc6a84ef71"), - new TesseractLanguage("mri.traineddata.zip", "mri", "Maori", 1.05, "b049cf217b38183855630823e3353259d5d1dd2c"), - new TesseractLanguage("msa.traineddata.zip", "msa", "Malay", 4.73, "c3f2017c05cc0d6b96f525430625a4b08db63c6e"), - new TesseractLanguage("mya.traineddata.zip", "mya", "Burmese", 5.15, "16fef298116a5c90e08e8d360322a75d0a394272"), - new TesseractLanguage("nep.traineddata.zip", "nep", "Nepali", 2.04, "54c5f5db4207ce9254a317cc250bf6eecbd9447d"), - new TesseractLanguage("nld.traineddata.zip", "nld", "Dutch", 12.59, "56d37209c62e9e6afa51d1e001886564fdd6c45e"), - new TesseractLanguage("nor.traineddata.zip", "nor", "Norwegian", 7.45, "f0466e0973265352dd37ac7d8a25ad6b76d0a0ee"), - new TesseractLanguage("oci.traineddata.zip", "oci", "Occitan", 6.09, "915f3df3502995e2867627c5bde58c83644ba796"), - new TesseractLanguage("ori.traineddata.zip", "ori", "Oriya", 2.04, "2da32dc862e1fc074185fd97f09c0a55edefaf93"), -// new TesseractLanguage { Filename = "osd.traineddata.zip", Code = "osd", LangName = "", Size = 8.22, Sha1 = "8162903ddc718157e6feeabbfdafe0e375a38001" }, - new TesseractLanguage("pan.traineddata.zip", "pan", "Panjabi", 1.66, "c29528e151531a9891904331f8e320d329a3dd92"), - new TesseractLanguage("pol.traineddata.zip", "pol", "Polish", 9.89, "8c8e6a3521e17c671defc04607808af97556d07b"), - new TesseractLanguage("por.traineddata.zip", "por", "Portuguese", 7.38, "58a8b3cddd0c0bf516bf82b7464378662d7e80f5"), - new TesseractLanguage("pus.traineddata.zip", "pus", "Pushto", 2.73, "83c093d6d2c821d6d9a3f734f753001b2590614a"), - new TesseractLanguage("que.traineddata.zip", "que", "Quechua", 4.93, "46ab85ef746d6cc0130a9ba5756fb56a250758e4"), - new TesseractLanguage("ron.traineddata.zip", "ron", "Romanian", 5.65, "d9e931572522802046750d5110ac7aa9d78c816c"), - new TesseractLanguage("rus.traineddata.zip", "rus", "Russian", 9.74, "949a12e51f29aa02dbcd7e1f41d547780876c335"), - new TesseractLanguage("san.traineddata.zip", "san", "Sanskrit", 10.86, "13129ccc5fd154f69e1632ed2bdfad3785d0f944"), - new TesseractLanguage("sin.traineddata.zip", "sin", "Sinhala", 2.19, "7d3a2c6208a4562db3e97a7e0313bd8b9cbf52a2"), - new TesseractLanguage("slk.traineddata.zip", "slk", "Slovakian", 7.50, "7cdbd545c966a281d0d8954187e1ee612b6f6d65"), - new TesseractLanguage("slk_frak.traineddata.zip", "slk_frak", "Slovakian (Fraktur)", 0.28, "050b6b8515e7e252b86a121207c205a574e9cd5b"), - new TesseractLanguage("slv.traineddata.zip", "slv", "Slovenian", 4.93, "0b20f99d0a755db2faffe1508940b166b06835af"), - new TesseractLanguage("snd.traineddata.zip", "snd", "Sindhi", 2.70, "afc0abcb26a75d833452f0d313f8d428dcfe2613"), - new TesseractLanguage("spa.traineddata.zip", "spa", "Spanish", 9.03, "21a32e0e3981bb0d62836327567361327173e0cc"), - new TesseractLanguage("spa_old.traineddata.zip", "spa_old", "Spanish (Old)", 9.79, "5d9e6c07d573f47e90443034e2b2505527315abb"), - new TesseractLanguage("sqi.traineddata.zip", "sqi", "Albanian", 4.13, "806024671905452d5c4844e15fcccb86863c5563"), - new TesseractLanguage("srp.traineddata.zip", "srp", "Serbian", 3.92, "30a4f7cc1ddff1154fe49d2a6e7a0edb747a9ec8"), - new TesseractLanguage("srp_latn.traineddata.zip", "srp_latn", "Serbian (Latin)", 5.65, "8d7f141429265ac927f1f05f4b7ae4b397a8c4ca"), - new TesseractLanguage("sun.traineddata.zip", "sun", "Sundanese", 1.46, "92643e9e815574d99a125406923bc96c1581bc41"), - new TesseractLanguage("swa.traineddata.zip", "swa", "Swahili", 3.52, "fc19a0dc5a7047d134e519cf5b0a7a5f2bbcb34e"), - new TesseractLanguage("swe.traineddata.zip", "swe", "Swedish", 8.42, "d30dbe87e640bd7e95265bd4372ccb0f83722baa"), - new TesseractLanguage("syr.traineddata.zip", "syr", "Syriac", 3.09, "2047b388123d3511e76a21441458725ec8922658"), - new TesseractLanguage("tam.traineddata.zip", "tam", "Tamil", 2.65, "8febcf0011ad2642d428cc02915190622e0d9381"), - new TesseractLanguage("tat.traineddata.zip", "tat", "Tatar", 1.74, "0aa474fdb1dcb8c6b634b366e85dcc4620c4c7fe"), - new TesseractLanguage("tel.traineddata.zip", "tel", "Telugu", 2.85, "3b0ee160a7af431a3eefabdba48b600db41b8148"), - new TesseractLanguage("tgk.traineddata.zip", "tgk", "Tajik", 2.62, "83c832eadbb937ef6bd707c07bc43bccc246accd"), - new TesseractLanguage("tgl.traineddata.zip", "tgl", "Tagalog", 3.13, "a0fdf7c7b935e33260aee265c20b96d0b90d5b08"), - new TesseractLanguage("tha.traineddata.zip", "tha", "Thai", 1.73, "1289ca3585658dbba7429621d8ab8833c872cafc"), - new TesseractLanguage("tir.traineddata.zip", "tir", "Tigrinya", 1.18, "5aacd48843a01270729954fac165e215345d1439"), - new TesseractLanguage("ton.traineddata.zip", "ton", "Tonga (Tonga Islands)", 1.13, "00a679fb18715dc2cb3bda6fa6ce682519f35f9d"), - new TesseractLanguage("tur.traineddata.zip", "tur", "Turkish", 9.58, "42993630cc2ca6e77743decce6db17c937d4d565"), - new TesseractLanguage("uig.traineddata.zip", "uig", "Uighur", 3.55, "b4d47e24f7f1f35db23450efa59d5aad20aab8a4"), - new TesseractLanguage("ukr.traineddata.zip", "ukr", "Ukrainian", 6.48, "fa30fb31bd68d252974fa0902b13a233c3860e49"), - new TesseractLanguage("urd.traineddata.zip", "urd", "Urdu", 1.97, "e3288ad91bef0987b97c2f465b1d5ad918bd8a01", true), - new TesseractLanguage("uzb.traineddata.zip", "uzb", "Uzbek", 7.48, "425c50636d22815508ec4d9c78b7218294151bcf"), - new TesseractLanguage("uzb_cyrl.traineddata.zip", "uzb_cyrl", "Uzbek (Cyrillic)", 2.78, "6a8ac1df9932528848c07b13be15526cea22d458"), - new TesseractLanguage("vie.traineddata.zip", "vie", "Vietnamese", 4.06, "f3d67cc479ae535393d6a544aae8752a718878c4"), - new TesseractLanguage("yid.traineddata.zip", "yid", "Yiddish", 2.38, "fbaf27e063c45fb366dc5cf38a472b616fc2553a"), - new TesseractLanguage("yor.traineddata.zip", "yor", "Yoruba", 1.14, "b7bcc0416531f0432af9ed523887d0aa0dfb272b"), - }); - - #endregion -} \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/TesseractOcrEngine.cs b/NAPS2.Sdk/Ocr/TesseractOcrEngine.cs index 5c1c8dc164..0849f44d38 100644 --- a/NAPS2.Sdk/Ocr/TesseractOcrEngine.cs +++ b/NAPS2.Sdk/Ocr/TesseractOcrEngine.cs @@ -1,31 +1,86 @@ using System.Collections.Immutable; +using System.Globalization; using System.Threading; +using System.Xml; +using Microsoft.Extensions.Logging; +using NAPS2.Scan; +using NAPS2.Unmanaged; +using Bounds = (int x, int y, int w, int h); namespace NAPS2.Ocr; +/// +/// OCR engine using Tesseract (https://github.com/tesseract-ocr/tesseract). +/// public class TesseractOcrEngine : IOcrEngine { private readonly string _tesseractPath; private readonly string? _languageDataBasePath; - private readonly string _tempFolder; + private readonly bool _withModes; - public TesseractOcrEngine(string tesseractPath, string? languageDataBasePath, string tempFolder) + /// + /// Gets a TesseractOcrEngine instance configured to use the Tesseract executable on the system PATH with the + /// system-installed language data. + /// + public static TesseractOcrEngine System() => + new("tesseract"); + + /// + /// Gets a TesseractOcrEngine instance configured to use the Tesseract executable from the NAPS2.Tesseract.Binaries + /// nuget package using language data .traineddata files in the specified folder. + /// + public static TesseractOcrEngine Bundled(string languageDataPath) => + new(BundlePath, languageDataPath, false); + + /// + /// Gets a TesseractOcrEngine instance configured to use the Tesseract executable from the NAPS2.Tesseract.Binaries + /// nuget package using language data .traineddata files in the specified folder. The folder is expected to have + /// subfolders named "best" and "fast" with the actual .trainneddata files that will be used based on the OcrMode. + /// + public static TesseractOcrEngine BundledWithModes(string languageDataBasePath) => + new(BundlePath, languageDataBasePath, true); + + /// + /// Gets a TesseractOcrEngine instance configured to use the specified Tesseract executable, optionally looking for + /// .traineddata files in the specified folder. + /// + public static TesseractOcrEngine Custom(string tesseractExePath, string? languageDataPath = null) => + new(tesseractExePath, languageDataPath, false); + + /// + /// Gets a TesseractOcrEngine instance configured to use the specified Tesseract executable using language data + /// .traineddata files in the specified folder. The folder is expected to have subfolders named "best" and "fast" + /// with the actual .trainneddata files that will be used based on the OcrMode. + /// + public static TesseractOcrEngine CustomWithModes(string tesseractExePath, string languageDataBasePath) => + new(tesseractExePath, languageDataBasePath, true); + + private static string BundlePath => NativeLibrary.FindExePath(PlatformCompat.System.TesseractExecutableName); + + private TesseractOcrEngine(string tesseractPath, string? languageDataBasePath = null, bool withModes = true) { _tesseractPath = tesseractPath; _languageDataBasePath = languageDataBasePath; - _tempFolder = tempFolder; + _withModes = withModes; } - - public async Task ProcessImage(string imagePath, OcrParams ocrParams, CancellationToken cancelToken) + + public async Task ProcessImage(ScanningContext scanningContext, string imagePath, OcrParams ocrParams, + CancellationToken cancelToken) { - string tempHocrFilePath = Path.Combine(_tempFolder, Path.GetRandomFileName()); + var logger = scanningContext.Logger; + string tempHocrFilePath = Path.Combine(scanningContext.TempFolderPath, Path.GetRandomFileName()); string tempHocrFilePathWithExt = tempHocrFilePath + ".hocr"; try { + if (ocrParams.Mode.HasFlag(OcrMode.WithPreProcess)) + { + PreProcessImage(scanningContext, imagePath); + } + var configVals = "-c tessedit_create_hocr=1 -c hocr_font_info=1"; var startInfo = new ProcessStartInfo { FileName = _tesseractPath, - Arguments = $"\"{imagePath}\" \"{tempHocrFilePath}\" -l {ocrParams.LanguageCode} hocr", + Arguments = $"\"{imagePath}\" \"{tempHocrFilePath}\" -l {ocrParams.LanguageCode} {configVals}", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, @@ -33,19 +88,23 @@ public TesseractOcrEngine(string tesseractPath, string? languageDataBasePath, st }; if (_languageDataBasePath != null) { - string subfolder = ocrParams.Mode == OcrMode.Best ? "best" : "fast"; - string languageDataPath = Path.Combine(_languageDataBasePath, subfolder); + string languageDataPath = _languageDataBasePath; + if (_withModes) + { + string subfolder = ocrParams.Mode.HasFlag(OcrMode.Best) ? "best" : "fast"; + languageDataPath = Path.Combine(languageDataPath, subfolder); + } startInfo.EnvironmentVariables["TESSDATA_PREFIX"] = languageDataPath; - var tessdata = new DirectoryInfo(languageDataPath); - EnsureHocrConfigExists(tessdata); } var tesseractProcess = Process.Start(startInfo); if (tesseractProcess == null) { // Couldn't start tesseract for some reason - Log.Error("Couldn't start OCR process."); + logger.LogError("Couldn't start OCR process."); return null; } + // Improve main window responsiveness + tesseractProcess.PriorityClass = ProcessPriorityClass.BelowNormal; var waitTasks = new List { @@ -63,7 +122,8 @@ public TesseractOcrEngine(string tesseractPath, string? languageDataBasePath, st { if (!cancelToken.IsCancellationRequested) { - Log.Error("OCR process timed out."); + logger.LogError("OCR process timed out."); + OcrTimeout?.Invoke(this, EventArgs.Empty); } try { @@ -73,7 +133,7 @@ public TesseractOcrEngine(string tesseractPath, string? languageDataBasePath, st } catch (Exception e) { - Log.ErrorException("Error killing OCR process", e); + logger.LogError(e, "Error killing OCR process"); } return null; } @@ -91,26 +151,18 @@ public TesseractOcrEngine(string tesseractPath, string? languageDataBasePath, st } #endif XDocument hocrDocument = XDocument.Load(tempHocrFilePathWithExt); - var pageBounds = hocrDocument.Descendants() - .Where(x => x.Attributes("class").Any(y => y.Value == "ocr_page")) - .Select(x => GetBounds(x.Attribute("title"))) - .First(); - var elements = hocrDocument.Descendants() - .Where(x => x.Attributes("class").Any(y => y.Value == "ocrx_word")) - .Where(x => !string.IsNullOrWhiteSpace(x.Value)) - .Select(x => - { - var text = x.Value; - var lang = GetNearestAncestorAttribute(x, "lang") ?? ""; - var rtl = GetNearestAncestorAttribute(x, "dir") == "rtl"; - var bounds = GetBounds(x.Attribute("title")); - return new OcrResultElement(text, lang, rtl, bounds); - }).ToImmutableList(); - return new OcrResult(pageBounds, elements); + return CreateOcrResult(hocrDocument); + } + catch (XmlException e) + { + logger.LogError(e, "Error running OCR"); + // Don't display to the error output as an xml exception may just indicate a normal OCR failure + return null; } catch (Exception e) { - Log.ErrorException("Error running OCR", e); + logger.LogError(e, "Error running OCR"); + OcrError?.Invoke(this, new OcrErrorEventArgs(e)); return null; } finally @@ -121,51 +173,162 @@ public TesseractOcrEngine(string tesseractPath, string? languageDataBasePath, st } catch (Exception e) { - Log.ErrorException("Error cleaning up OCR temp files", e); + logger.LogError(e, "Error cleaning up OCR temp files"); } } } + public event EventHandler? OcrError; + + public event EventHandler? OcrTimeout; + + private static void PreProcessImage(ScanningContext scanningContext, string imagePath) + { + IMemoryImage? image = null; + try + { + image = scanningContext.ImageContext.Load(imagePath); + image = image.PerformTransform(new CorrectionTransform(CorrectionMode.Document)); + image.Save(imagePath); + } + finally + { + image?.Dispose(); + } + } + + private OcrResult CreateOcrResult(XDocument hocrDocument) + { + var pageBounds = hocrDocument.Descendants() + .Where(element => GetClass(element) == "ocr_page") + .Select(GetBounds) + .First(); + var words = new List(); + var lines = new List(); + foreach (var lineElement in hocrDocument.Descendants() + .Where(element => GetClass(element) is "ocr_line" or "ocr_header" or "ocr_textfloat")) + { + var lineBounds = GetBounds(lineElement); + var lineAngle = GetTextAngle(lineElement); + bool isRotated = lineAngle is >= 45 or <= -45; + var baselineParams = GetBaselineParams(lineElement); + var lineWords = lineElement.Descendants() + .Where(element => GetClass(element) == "ocrx_word") + .Where(element => !string.IsNullOrWhiteSpace(element.Value)) + .Select(wordElement => + { + var wordBounds = GetBounds(wordElement); + return new OcrResultElement( + wordElement.Value, + GetNearestAncestorAttribute(wordElement, "lang") ?? "", + GetNearestAncestorAttribute(wordElement, "dir") == "rtl", + wordBounds, + // TODO: Maybe we can properly handle rotated text? + isRotated + ? wordBounds.y + wordBounds.h + : CalculateBaseline(baselineParams, lineBounds, wordBounds), + GetFontSize(wordElement), + ImmutableList.Empty); + }).ToImmutableList(); + if (lineWords.Count == 0) continue; + words.AddRange(lineWords); + lines.Add(lineWords[0] with + { + Text = string.Join(" ", lineWords.Select(x => x.Text)), + Bounds = lineBounds, + Baseline = CalculateBaseline(baselineParams, lineBounds, lineBounds), + Children = lineWords + }); + } + return new OcrResult(pageBounds, words.ToImmutableList(), lines.ToImmutableList()); + } + private static string? GetNearestAncestorAttribute(XElement x, string attributeName) { var ancestor = x.AncestorsAndSelf().FirstOrDefault(x => x.Attribute(attributeName) != null); return ancestor?.Attribute(attributeName)?.Value; } - private void EnsureHocrConfigExists(DirectoryInfo tessdata) + private string? GetClass(XElement? element) { - var configDir = new DirectoryInfo(Path.Combine(tessdata.FullName, "configs")); - if (!configDir.Exists) - { - configDir.Create(); - } - var hocrConfigFile = new FileInfo(Path.Combine(configDir.FullName, "hocr")); - if (!hocrConfigFile.Exists) - { - using var writer = hocrConfigFile.CreateText(); - writer.Write("tessedit_create_hocr 1"); - } + return element?.Attribute("class")?.Value; } - private (int x, int y, int w, int h) GetBounds(XAttribute? titleAttr) + private bool ParseData(XElement? element, string dataKey, int dataCount, out string[] parts) { - var bounds = (0, 0, 0, 0); + parts = Array.Empty(); + var titleAttr = element?.Attribute("title"); if (titleAttr != null) { foreach (var param in titleAttr.Value.Split(';')) { - string[] parts = param.Trim().Split(' '); - if (parts.Length == 5 && parts[0] == "bbox") + parts = param.Trim().Split(' '); + if (parts[0] == dataKey && parts.Length == dataCount + 1) { - int x1 = int.Parse(parts[1]), y1 = int.Parse(parts[2]); - int x2 = int.Parse(parts[3]), y2 = int.Parse(parts[4]); - bounds = (x1, y1, x2 - x1, y2 - y1); + return true; } } } + return false; + } + + private Bounds GetBounds(XElement? element) + { + var bounds = (0, 0, 0, 0); + if (ParseData(element, "bbox", 4, out string[] parts)) + { + int x1 = int.Parse(parts[1], CultureInfo.InvariantCulture), + y1 = int.Parse(parts[2], CultureInfo.InvariantCulture); + int x2 = int.Parse(parts[3], CultureInfo.InvariantCulture), + y2 = int.Parse(parts[4], CultureInfo.InvariantCulture); + bounds = (x1, y1, x2 - x1, y2 - y1); + } return bounds; } - + + private int GetFontSize(XElement? element) + { + int fontSize = 0; + if (ParseData(element, "x_fsize", 1, out string[] parts)) + { + fontSize = int.Parse(parts[1], CultureInfo.InvariantCulture); + } + return fontSize; + } + + private (float m, float b) GetBaselineParams(XElement? element) + { + float m = 0; + float b = 0; + if (ParseData(element, "baseline", 2, out string[] parts)) + { + m = float.Parse(parts[1], CultureInfo.InvariantCulture); + b = float.Parse(parts[2], CultureInfo.InvariantCulture); + } + return (m, b); + } + + private float GetTextAngle(XElement? element) + { + float angle = 0; + if (ParseData(element, "textangle", 1, out string[] parts)) + { + angle = float.Parse(parts[1], CultureInfo.InvariantCulture); + } + return angle; + } + + private int CalculateBaseline((float m, float b) baselineParams, Bounds lineBounds, Bounds elementBounds) + { + // The line baseline is a linear equation (y=mx + b), so we calculate the word baseline from the + // word offset to the left side of the line. + float midpoint = elementBounds.x + elementBounds.w / 2f; + int relativeBaseline = (int) Math.Round(baselineParams.b + + baselineParams.m * (midpoint - lineBounds.x)); + int absoluteBaseline = relativeBaseline + lineBounds.y + lineBounds.h; + return absoluteBaseline; + } + // TODO: Consider adding back CanProcess, or otherwise using this code to get the languages from a system engine // private void CheckIfInstalled() // { diff --git a/NAPS2.Sdk/Pdf/IPdfPasswordProvider.cs b/NAPS2.Sdk/Pdf/IPdfPasswordProvider.cs new file mode 100644 index 0000000000..afe89527b5 --- /dev/null +++ b/NAPS2.Sdk/Pdf/IPdfPasswordProvider.cs @@ -0,0 +1,10 @@ +namespace NAPS2.Pdf; + +/// +/// Provides a callback to prompt the user for a password to import an encrypted PDF. Alternatively, if you already have +/// the password, you can just specify it in ImportParams. +/// +public interface IPdfPasswordProvider +{ + bool ProvidePassword(string fileName, int attemptCount, out string password); +} \ No newline at end of file diff --git a/NAPS2.Sdk/Pdf/JpegFormatHelper.cs b/NAPS2.Sdk/Pdf/JpegFormatHelper.cs new file mode 100644 index 0000000000..423ec6e64f --- /dev/null +++ b/NAPS2.Sdk/Pdf/JpegFormatHelper.cs @@ -0,0 +1,136 @@ +namespace NAPS2.Pdf; + +internal static class JpegFormatHelper +{ + // JPEG format doc: https://github.com/corkami/formats/blob/master/image/jpeg.md + + public static JpegHeader? ReadHeader(Stream stream) + { + var sig1 = stream.ReadByte(); + var sig2 = stream.ReadByte(); + if (sig1 != 0xFF || sig2 != 0xD8) return null; + var header = new JpegHeader(0, 0, 0, 0, 0, false, false); + while (true) + { + var marker = stream.ReadByte(); + if (marker != 0xFF) return null; + var type = stream.ReadByte(); + if (type == -1) return null; + var len = stream.ReadByte() * 256 + stream.ReadByte() - 2; + if (len <= 0) return null; + var buf = new byte[len]; + if (stream.Read(buf, 0, len) < len) return null; + if (type == 0xE0 && len >= 12 && buf is [0x4A, 0x46, 0x49, 0x46, 0x00, ..]) // Application data (JFIF) + { + header = header with { HasJfifHeader = true }; + var units = buf[7]; + var hRes = buf[8] * 256 + buf[9]; + var vRes = buf[10] * 256 + buf[11]; + if (units == 0 && hRes > 10 && vRes > 10) // Unspecified units but assume pixels per inch + { + header = header with + { + HorizontalDpi = hRes, + VerticalDpi = vRes + }; + } + else if (units == 1) // Pixels per inch + { + header = header with + { + HorizontalDpi = hRes, + VerticalDpi = vRes + }; + } + else if (units == 2) // Pixels per cm + { + header = header with + { + HorizontalDpi = hRes * 2.54, + VerticalDpi = vRes * 2.54 + }; + } + } + if (type == 0xE1 && len >= 12 && buf is [0x45, 0x78, 0x69, 0x66, 0x00, 0x00, ..]) // Application data (EXIF) + { + // https://docs.fileformat.com/image/exif/ + + // 0x4949 = little-endian, 0x4d4d = big-endian + var flipEnd = (buf[6] == 0x49 && buf[7] == 0x49) != BitConverter.IsLittleEndian; + var reader = new EndianReader(flipEnd); + + var number = reader.ReadInt16(buf, 8); + + if (number == 42) + { + header = header with { HasExifHeader = true }; + + var ifdOffset = reader.ReadInt32(buf, 10); + var dirCount = reader.ReadInt16(buf, ifdOffset + 6); + + double xRes = 0; + double yRes = 0; + int resUnit = 0; + + for (int i = 0; i < dirCount; i++) + { + var offset = ifdOffset + 8 + i * 12; + + var tag = reader.ReadInt16(buf, offset); + + if (tag == 282) + { + var valueOffset = reader.ReadInt32(buf, offset + 8); + var num = reader.ReadInt32(buf, valueOffset + 6); + var den = reader.ReadInt32(buf, valueOffset + 10); + xRes = num / (double) den; + } + if (tag == 283) + { + var valueOffset = reader.ReadInt32(buf, offset + 8); + var num = reader.ReadInt32(buf, valueOffset + 6); + var den = reader.ReadInt32(buf, valueOffset + 10); + yRes = num / (double) den; + } + if (tag == 296) + { + resUnit = reader.ReadInt16(buf, offset + 8); + } + } + + if (xRes > 0 && yRes > 0) + { + if (resUnit == 3) // cm + { + header = header with + { + HorizontalDpi = xRes * 2.54, + VerticalDpi = yRes * 2.54 + }; + } + else // inch + { + header = header with + { + HorizontalDpi = xRes, + VerticalDpi = yRes + }; + } + } + } + } + if (type == 0xC0 && len >= 6) // Start of frame + { + return header with + { + Height = buf[1] * 256 + buf[2], + Width = buf[3] * 256 + buf[4], + NumComponents = buf[5] + }; + } + } + } + + public record JpegHeader(double HorizontalDpi, double VerticalDpi, int Width, int Height, int NumComponents, + bool HasJfifHeader, bool HasExifHeader); +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfAHelper.cs b/NAPS2.Sdk/Pdf/PdfAHelper.cs similarity index 98% rename from NAPS2.Sdk/ImportExport/Pdf/PdfAHelper.cs rename to NAPS2.Sdk/Pdf/PdfAHelper.cs index 477def60c5..520fde64a8 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfAHelper.cs +++ b/NAPS2.Sdk/Pdf/PdfAHelper.cs @@ -2,16 +2,16 @@ using PdfSharpCore.Pdf; using PdfSharpCore.Pdf.Advanced; -namespace NAPS2.ImportExport.Pdf; +namespace NAPS2.Pdf; -public static class PdfAHelper +internal static class PdfAHelper { - public static void CreateXmpMetadata(PdfDocument document, PdfCompat compat) + public static void CreateXmpMetadata(PdfDocument document, PdfCompat compat, string producer) { var metadataDict = new PdfDictionary(document); metadataDict.Elements["/Type"] = new PdfName("/Metadata"); metadataDict.Elements["/Subtype"] = new PdfName("/XML"); - metadataDict.CreateStream(CreateRawXmpMetadata(document.Info, GetConformance(compat))); + metadataDict.CreateStream(CreateRawXmpMetadata(document.Info, GetConformance(compat), producer)); document.Internals.AddObject(metadataDict); document.Internals.Catalog.Elements["/Metadata"] = metadataDict.Reference; } @@ -33,7 +33,8 @@ private static (string, string) GetConformance(PdfCompat compat) } } - private static byte[] CreateRawXmpMetadata(PdfDocumentInformation info, (string, string) conformance) + private static byte[] CreateRawXmpMetadata(PdfDocumentInformation info, (string, string) conformance, + string producer) { string xml = $@" @@ -45,7 +46,7 @@ private static byte[] CreateRawXmpMetadata(PdfDocumentInformation info, (string, xmlns:pdfaid=""http://www.aiim.org/pdfa/ns/id/"" dc:format=""application/pdf"" pdf:Keywords=""{info.Keywords}"" - pdf:Producer=""{PdfSharpCore.ProductVersionInfo.Producer}"" + pdf:Producer=""{producer}"" xmp:CreateDate=""{info.CreationDate:yyyy'-'MM'-'dd'T'HH':'mm':'ssK}"" xmp:ModifyDate=""{info.ModificationDate:yyyy'-'MM'-'dd'T'HH':'mm':'ssK}"" xmp:CreatorTool=""{info.Creator}"" diff --git a/NAPS2.Sdk/Pdf/PdfCompat.cs b/NAPS2.Sdk/Pdf/PdfCompat.cs new file mode 100644 index 0000000000..bb59357ad1 --- /dev/null +++ b/NAPS2.Sdk/Pdf/PdfCompat.cs @@ -0,0 +1,13 @@ +namespace NAPS2.Pdf; + +/// +/// Compatibility format for generating PDFs, e.g. PDF/A (https://en.wikipedia.org/wiki/PDF/A). +/// +public enum PdfCompat +{ + Default, + PdfA1B, + PdfA2B, + PdfA3B, + PdfA3U +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfEncryption.cs b/NAPS2.Sdk/Pdf/PdfEncryption.cs similarity index 84% rename from NAPS2.Sdk/ImportExport/Pdf/PdfEncryption.cs rename to NAPS2.Sdk/Pdf/PdfEncryption.cs index fb6f9d50ff..0b0ebce476 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfEncryption.cs +++ b/NAPS2.Sdk/Pdf/PdfEncryption.cs @@ -1,5 +1,8 @@ -namespace NAPS2.ImportExport.Pdf; +namespace NAPS2.Pdf; +/// +/// Configuration for PDF encryption (e.g. passwords, permissions). +/// public record PdfEncryption { public bool EncryptPdf { get; init; } diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfExportParams.cs b/NAPS2.Sdk/Pdf/PdfExportParams.cs similarity index 77% rename from NAPS2.Sdk/ImportExport/Pdf/PdfExportParams.cs rename to NAPS2.Sdk/Pdf/PdfExportParams.cs index 1dbeecad40..679fb4b233 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfExportParams.cs +++ b/NAPS2.Sdk/Pdf/PdfExportParams.cs @@ -1,5 +1,8 @@ -namespace NAPS2.ImportExport.Pdf; +namespace NAPS2.Pdf; +/// +/// Additional parameters for exporting PDFs (metadata, encryption, compatibility). +/// public record PdfExportParams { public PdfExportParams() diff --git a/NAPS2.Sdk/Pdf/PdfExporter.cs b/NAPS2.Sdk/Pdf/PdfExporter.cs new file mode 100644 index 0000000000..be6c01fdfc --- /dev/null +++ b/NAPS2.Sdk/Pdf/PdfExporter.cs @@ -0,0 +1,787 @@ +using System.Globalization; +using System.Threading; +using Microsoft.Extensions.Logging; +using NAPS2.Images.Bitwise; +using NAPS2.ImportExport; +using NAPS2.Ocr; +using NAPS2.Pdf.Pdfium; +using NAPS2.Scan; +using PdfSharpCore.Drawing; +using PdfSharpCore.Pdf; +using PdfSharpCore.Pdf.IO; +using PdfSharpCore.Pdf.Security; +using PdfDocument = PdfSharpCore.Pdf.PdfDocument; +using PdfPage = PdfSharpCore.Pdf.PdfPage; +using Alphabet = NAPS2.Pdf.PdfFontPicker.Alphabet; + +namespace NAPS2.Pdf; + +/// +/// Exports images to a PDF file. +/// +public class PdfExporter +{ + private const int PDF_VERSION_14 = 14; + private const int PDF_VERSION_17 = 17; + private const string PDFIUM_PRODUCER = "PDFium"; + + private readonly ScanningContext _scanningContext; + private readonly ILogger _logger; + + public PdfExporter(ScanningContext scanningContext) + { + _scanningContext = scanningContext; + _logger = scanningContext.Logger; + } + + public Task Export(string path, ICollection images, + PdfExportParams? exportParams = null, OcrParams? ocrParams = null, ProgressHandler progress = default) + => Export(new OutputPathOrStream(path, null), images, exportParams, ocrParams, progress); + + public Task Export(Stream stream, ICollection images, + PdfExportParams? exportParams = null, OcrParams? ocrParams = null, ProgressHandler progress = default) + => Export(new OutputPathOrStream(null, stream), images, exportParams, ocrParams, progress); + + private async Task Export(OutputPathOrStream output, ICollection images, + PdfExportParams? exportParams = null, OcrParams? ocrParams = null, ProgressHandler progress = default) + { + return await Task.Run(async () => + { + // The current iteration of PDF export is fairly complicated. We do a hybrid export using both PdfSharp + // and Pdfium. "Simple" exports just use PdfSharp. If we have imported PDF pages that are stored as PDFs + // (i.e. they weren't generated by NAPS2 and can't be extracted to a single image), we use Pdfium to: + // 1. Check if the pages have text (and therefore are ineligible for OCR). + // 2. Add those "passthrough" pages to the final PDF file (under certain conditions). + // For context PdfSharp has a number of bugs with handling arbitrary PDFs, so using Pdfium for exporting + // those PDF pages lets us avoid those bugs (given we also use Pdfium for importing). + // + // Export is also complicated by the fact that we may or may not have OCR enabled, and when it is enabled, + // we want to parallelize and pipeline the different operations (image rendering, OCR, PDF saving) to + // maximize performance. + // + // It would be simpler if we could use Pdfium for everything, but it doesn't support a lot of features we + // need, e.g. configuring interpolation, encryption, PDF/A, etc. + + exportParams ??= new PdfExportParams(); + var document = InitializeDocument(exportParams); + + // TODO: Consider storing text from imported image-based pages in PostProcessingData so it can be saved even + // when not exporting with OCR (assuming no transforms). + var ocrEngine = GetOcrEngine(ocrParams); + + var imagePages = new List(); + var pdfPages = new List(); + + int currentProgress = 0; + + void IncrementProgress() + { + Interlocked.Increment(ref currentProgress); + progress.Report(currentProgress, images.Count); + } + + progress.Report(0, images.Count); + + try + { + int pageIndex = 0; + foreach (var image in images) + { + var pageState = new PageExportState( + image, pageIndex++, document, document.AddPage(), ocrEngine, ocrParams, IncrementProgress, + progress.CancelToken, exportParams.Compat); + // TODO: To improve our ability to passthrough, we could consider using Pdfium to apply the transform to + // the underlying PDF file. For example, doing color shifting on individual text + image objects, or + // applying matrix changes. + // TODO: We also can consider doing this even for scanned image transforms - e.g. for deskew, maybe + // rather than rasterize that, rely on the pdf to do the skew transform, which should render better at + // different scaling. + if (IsPdfStorage(image.Storage) && image.TransformState == TransformState.Empty) + { + pdfPages.Add(pageState); + } + else + { + imagePages.Add(pageState); + } + } + + var imagePagesPipeline = ocrEngine != null + ? Pipeline.For(imagePages) + .Step(RenderStep) + .Step(InitOcrStep) + .Step(WaitForOcrStep) + .Step(WriteToPdfSharpStep) + .Run() + : Pipeline.For(imagePages) + .Step(RenderStep) + .Step(WriteToPdfSharpStep) + .Run(); + + var pdfPagesPrePipeline = ocrEngine != null + ? Pipeline.For(pdfPages).Step(CheckIfOcrNeededStep).Run() + : Task.FromResult(pdfPages); + + await pdfPagesPrePipeline; + + var pdfPagesOcrPipeline = Pipeline.For(pdfPages.Where(x => x.NeedsOcr)) + .Step(RenderStep) + .Step(InitOcrStep) + .Step(WaitForOcrStep) + .Run(); + + await imagePagesPipeline; + await pdfPagesOcrPipeline; + if (progress.IsCancellationRequested) return false; + + var producer = pdfPages.Any() ? PDFIUM_PRODUCER : PdfSharpCore.ProductVersionInfo.Producer; + // TODO: Doing in memory as that's presumably faster than IO, but of course that's quite a bit of memory use potentially... + var stream = FinalizeAndSaveDocument(document, exportParams, producer); + if (progress.IsCancellationRequested) return false; + + return MergePassthroughPages(stream, output, pdfPages, exportParams, progress); + } + finally + { + // We can't use a DisposableList as the objects we need to dispose are generated on the fly + foreach (var state in imagePages.Concat(pdfPages)) + { + state.Embedder?.Dispose(); + } + } + }); + } + + private bool MergePassthroughPages(MemoryStream stream, OutputPathOrStream output, + List passthroughPages, PdfExportParams exportParams, ProgressHandler progress) + { + if (!passthroughPages.Any()) + { + output.CopyFromStream(stream); + return true; + } + // TODO: Should we do this (or maybe the whole pdf export/import) in a worker to avoid contention? + // TODO: Although we would need to be careful to handle OcrRequestQueue state correctly across processes. + lock (PdfiumNativeLibrary.Instance) + { + var password = exportParams.Encryption.EncryptPdf ? exportParams.Encryption.OwnerPassword : null; + using var destDoc = Pdfium.PdfDocument.Load(stream, password); + using var fontSubsets = + new PdfiumFontSubsets(destDoc, passthroughPages.Select(state => state.OcrTask?.Result)); + foreach (var state in passthroughPages) + { + destDoc.DeletePage(state.PageIndex); + if (state.Image.Storage is ImageFileStorage fileStorage) + { + using var sourceDoc = Pdfium.PdfDocument.Load(fileStorage.FullPath); + CopyPage(destDoc, sourceDoc, state); + } + else if (state.Image.Storage is ImageMemoryStorage memoryStorage) + { + using var sourceDoc = Pdfium.PdfDocument.Load(memoryStorage.Stream); + CopyPage(destDoc, sourceDoc, state); + } + if (state.OcrTask?.Result != null) + { + using var page = destDoc.GetPage(state.PageIndex); + DrawOcrTextOnPdfiumPage(state.Page, destDoc, page, fontSubsets, state.OcrTask.Result); + } + if (progress.IsCancellationRequested) return false; + } + output.SavePdfDoc(destDoc); + return true; + } + } + + private void CopyPage(Pdfium.PdfDocument destDoc, Pdfium.PdfDocument sourceDoc, PageExportState state) + { + destDoc.ImportPages(sourceDoc, "1", state.PageIndex); + } + + private static PdfDocument InitializeDocument(PdfExportParams exportParams) + { + var document = new PdfDocument(); + var creator = exportParams.Metadata.Creator; + document.Info.Creator = string.IsNullOrEmpty(creator) ? "NAPS2" : creator; + document.Info.Author = exportParams.Metadata.Author; + document.Info.Keywords = exportParams.Metadata.Keywords; + document.Info.Subject = exportParams.Metadata.Subject; + document.Info.Title = exportParams.Metadata.Title; + + if (exportParams.Encryption?.EncryptPdf == true + && (!string.IsNullOrEmpty(exportParams.Encryption.OwnerPassword) || + !string.IsNullOrEmpty(exportParams.Encryption.UserPassword))) + { + document.SecuritySettings.DocumentSecurityLevel = PdfDocumentSecurityLevel.Encrypted128Bit; + if (!string.IsNullOrEmpty(exportParams.Encryption.OwnerPassword)) + { + document.SecuritySettings.OwnerPassword = exportParams.Encryption.OwnerPassword; + } + + if (!string.IsNullOrEmpty(exportParams.Encryption.UserPassword)) + { + document.SecuritySettings.UserPassword = exportParams.Encryption.UserPassword; + } + + document.SecuritySettings.PermitAccessibilityExtractContent = + exportParams.Encryption.AllowContentCopyingForAccessibility; + document.SecuritySettings.PermitAnnotations = exportParams.Encryption.AllowAnnotations; + document.SecuritySettings.PermitAssembleDocument = + exportParams.Encryption.AllowDocumentAssembly; + document.SecuritySettings.PermitExtractContent = exportParams.Encryption.AllowContentCopying; + document.SecuritySettings.PermitFormsFill = exportParams.Encryption.AllowFormFilling; + document.SecuritySettings.PermitFullQualityPrint = + exportParams.Encryption.AllowFullQualityPrinting; + document.SecuritySettings.PermitModifyDocument = + exportParams.Encryption.AllowDocumentModification; + document.SecuritySettings.PermitPrint = exportParams.Encryption.AllowPrinting; + } + return document; + } + + private PageExportState RenderStep(PageExportState state) + { + if (state.CancelToken.IsCancellationRequested) return state; + state.Embedder = GetRenderedImageOrDirectJpegEmbedder(state); + return state; + } + + private IEmbedder GetRenderedImageOrDirectJpegEmbedder(PageExportState state) + { + if (state.Image.IsUntransformedJpegFile(out var jpegPath)) + { + // Special case if we have an un-transformed JPEG - just use the original file instead of re-encoding + using var fileStream = new FileStream(jpegPath, FileMode.Open, FileAccess.Read); + var jpegHeader = JpegFormatHelper.ReadHeader(fileStream); + // Ensure it's not a grayscale image as those are known to not be embeddable + if (jpegHeader is { NumComponents: > 1 }) + { + return new DirectJpegEmbedder(jpegHeader, jpegPath); + } + } + return new RenderedImageEmbedder(state.Image.Render()); + } + + private PageExportState WriteToPdfSharpStep(PageExportState state) + { + if (state.CancelToken.IsCancellationRequested) return state; + lock (state.Document) + { + var exportFormat = state.Embedder!.PrepareForExport(state.Image.Metadata); + DrawImageOnPage(state.Page, state.Embedder, state.Image.Metadata.PageSize, exportFormat, state.Compat); + if (state.OcrTask?.Result != null) + { + DrawOcrTextOnPage(state.Page, state.OcrTask.Result); + } + } + state.IncrementProgress(); + return state; + } + + private static MemoryStream FinalizeAndSaveDocument(PdfDocument document, PdfExportParams exportParams, + string producer) + { + var compat = exportParams.Compat; + var now = DateTime.Now; + document.Info.CreationDate = now; + document.Info.ModificationDate = now; + if (compat == PdfCompat.PdfA1B) + { + PdfAHelper.SetCidStream(document); + PdfAHelper.DisableTransparency(document); + } + + if (compat != PdfCompat.Default) + { + PdfAHelper.SetColorProfile(document); + PdfAHelper.SetCidMap(document); + PdfAHelper.CreateXmpMetadata(document, compat, producer); + } + + document.Version = compat switch + { + PdfCompat.PdfA2B or PdfCompat.PdfA3B or PdfCompat.PdfA3U => PDF_VERSION_17, + _ => PDF_VERSION_14 + }; + + var stream = new MemoryStream(); + document.Save(stream); + return stream; + } + + private IOcrEngine? GetOcrEngine(OcrParams? ocrParams) + { + if (ocrParams?.LanguageCode != null) + { + var activeEngine = _scanningContext.OcrEngine; + if (activeEngine == null) + { + _logger.LogError("Supported OCR engine not installed."); + } + else + { + return activeEngine; + } + } + return null; + } + + private PageExportState InitOcrStep(PageExportState state) + { + if (state.CancelToken.IsCancellationRequested) return state; + var ext = state.Embedder!.OriginalFileFormat == ImageFileFormat.Png ? ".png" : ".jpg"; + string ocrTempFilePath = Path.Combine(_scanningContext.TempFolderPath, Path.GetRandomFileName() + ext); + if (!_scanningContext.OcrRequestQueue.HasCachedResult(state.OcrEngine!, state.Image, state.OcrParams!)) + { + // Save the image to a file for use in OCR. + // We don't need to delete this file as long as we pass it to OcrRequestQueue.Enqueue, which takes + // ownership and guarantees its eventual deletion. + // TODO: If the image has transforms and we're saving as JPEG, we should cache to avoid double-rendering + using var fileStream = new FileStream(ocrTempFilePath, FileMode.Create, FileAccess.Write); + state.Embedder.CopyToStream(fileStream); + } + + // Start OCR + state.OcrTask = _scanningContext.OcrRequestQueue.Enqueue( + _scanningContext, state.OcrEngine!, state.Image, ocrTempFilePath, state.OcrParams!, OcrPriority.Foreground, + state.CancelToken); + return state; + } + + private async Task WaitForOcrStep(PageExportState state) + { + if (state.CancelToken.IsCancellationRequested) return state; + await state.OcrTask!; + return state; + } + + private PageExportState CheckIfOcrNeededStep(PageExportState state) + { + if (state.CancelToken.IsCancellationRequested) return state; + try + { + if (state.Image.Storage is ImageFileStorage fileStorage) + { + state.NeedsOcr = !new PdfiumPdfReader() + .ReadTextByPage(fileStorage.FullPath) + .Any(x => x.Trim().Length > 0); + } + else if (state.Image.Storage is ImageMemoryStorage memoryStorage) + { + state.NeedsOcr = !new PdfiumPdfReader() + .ReadTextByPage(memoryStorage.Stream.GetBuffer(), (int) memoryStorage.Stream.Length) + .Any(x => x.Trim().Length > 0); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not import PDF page for possible OCR, falling back to non-OCR path"); + } + return state; + } + + private static void DrawOcrTextOnPage(PdfPage page, OcrResult ocrResult) + { +#if DEBUG && DEBUGOCR + using XGraphics gfx = XGraphics.FromPdfPage(page, XGraphicsPdfPageOptions.Append); +#else + using XGraphics gfx = XGraphics.FromPdfPage(page, XGraphicsPdfPageOptions.Prepend); +#endif + foreach (var info in GetOcrTextToDraw(page, ocrResult, gfx)) + { + var font = new XFont(info.FontFamily, info.FontSize, XFontStyle.Regular, + new XPdfFontOptions(PdfFontEncoding.Unicode)); +#if DEBUG && DEBUGOCR + gfx.DrawRectangle(new XPen(XColor.FromArgb(255, 0, 0)), info.Bounds); + gfx.DrawString(info.Text, font, XBrushes.Blue, info.X, info.Y, XStringFormats.BaseLineLeft); +#else + gfx.DrawString(info.Text, font, XBrushes.Transparent, info.X, info.Y, XStringFormats.BaseLineLeft); +#endif + } + } + + private static void DrawOcrTextOnPdfiumPage(PdfPage page, Pdfium.PdfDocument pdfiumDocument, + Pdfium.PdfPage pdfiumPage, PdfiumFontSubsets fontSubsets, OcrResult ocrResult) + { + // Set the page measurements so the coordinates are scaled correctly in GetOcrTextToDraw + page.Width = pdfiumPage.Width; + page.Height = pdfiumPage.Height; + using XGraphics gfx = XGraphics.FromPdfPage(page, XGraphicsPdfPageOptions.Prepend); + foreach (var info in GetOcrTextToDraw(page, ocrResult, gfx)) + { + var textObj = pdfiumDocument.NewText(fontSubsets[info.FontFamily], info.FontSize); +#if DEBUG && DEBUGOCR + textObj.FillColor = (0, 0, 255, 255); +#else + textObj.TextRenderMode = TextRenderMode.Invisible; +#endif + textObj.SetText(info.Text); + // The matrix alignment here is equivalent to the PDFSharp BaseLineLeft alignment + textObj.Matrix = new PdfMatrix(1, 0, 0, 1, info.X, (float) page.Height - info.Y); + pdfiumPage.InsertObject(textObj); + } + pdfiumPage.GenerateContent(); + } + + private static IEnumerable GetOcrTextToDraw(PdfPage page, OcrResult ocrResult, XGraphics gfx) + { + double hAdjust = page.Width / ocrResult.PageBounds.w; + double vAdjust = page.Height / ocrResult.PageBounds.h; + foreach (var line in ocrResult.Lines) + { + var lineFontFamily = PdfFontPicker.GetBestFont(line.LanguageCode); + var lineFontSize = line.FontSize; + // Chinese/Japanese/Korean languages don't need font size alignment as words are generally just 1 char + if (!IsCjk(line.LanguageCode)) + { + // Only measure words with at least 3 characters to avoid noise + var eligibleWords = line.Children.Where(word => word.Text.Length >= 3).ToList(); + if (eligibleWords.Count > 1) + { + // In case Tesseract underestimated the font size, keep increasing it as long as all words are still + // within their bounds. + while (true) + { + var font = new XFont(lineFontFamily, lineFontSize + 1, XFontStyle.Regular); + if (eligibleWords.All( + word => gfx.MeasureString(word.Text, font).Width < word.Bounds.w * hAdjust)) + { + lineFontSize++; + } + else + { + break; + } + } + } + } + for (int i = 0; i < line.Children.Count; i++) + { + var word = line.Children[i]; + if (string.IsNullOrEmpty(word.Text)) continue; + + bool hasNextWord = i + 1 < line.Children.Count; + var rightBound = hasNextWord ? line.Children[i + 1].Bounds.x : ocrResult.PageBounds.w; + var adjustedRightBound = rightBound * hAdjust; + var adjustedX = word.Bounds.x * hAdjust; + var adjustedY = word.Baseline * vAdjust; + + // We make sure there's enough distance between this word and the next to fit a space (" "), so that + // when you Ctrl+A and Ctrl+C in a PDF file, the words don't blend together + var wordFontSize = ClampFontSizeByRightBound(word, lineFontSize, adjustedX, adjustedRightBound, + hasNextWord, gfx); + + // Special case to avoid accidentally recognizing big lines as dashes/underscores + if (wordFontSize > 100 && (word.Text == "-" || word.Text == "_")) continue; + + yield return new TextDrawInfo( + word.RightToLeft ? ReverseText(word.Text) : word.Text, + lineFontFamily, + wordFontSize, + (int) Math.Round(adjustedX), + (int) Math.Round(adjustedY)); + } + } + } + + private static bool IsCjk(string langCode) + { + var alphabet = PdfFontPicker.MapLanguageCodeToAlphabet(langCode); + return alphabet is + Alphabet.ChineseSimplified or + Alphabet.ChineseTraditional or + Alphabet.Japanese or + Alphabet.Korean; + } + + private static string ReverseText(string text) + { + TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(text); + var elements = new List(); + while (enumerator.MoveNext()) + { + elements.Add(enumerator.GetTextElement()); + } + elements.Reverse(); + return string.Concat(elements); + } + + private void DrawImageOnPage(PdfPage page, IEmbedder embedder, PageSize? pageSize, ImageExportFormat exportFormat, + PdfCompat compat) + { + using var xImage = XImage.FromImageSource(new ImageSource(embedder, exportFormat)); + if (compat != PdfCompat.Default) + { + xImage.Interpolate = false; + } + var (realWidth, realHeight) = GetRealSize(embedder, pageSize); + page.Width = realWidth; + page.Height = realHeight; + using XGraphics gfx = XGraphics.FromPdfPage(page); + gfx.DrawImage(xImage, 0, 0, realWidth, realHeight); + } + + private static (double width, double height) GetRealSize(IEmbedder embedder, PageSize? pageSize) + { + double hAdjust = 72 / embedder.HorizontalDpi; + double vAdjust = 72 / embedder.VerticalDpi; + if (double.IsInfinity(hAdjust) || double.IsInfinity(vAdjust)) + { + hAdjust = vAdjust = 0.75; + } + double realWidth = embedder.Width * hAdjust; + double realHeight = embedder.Height * vAdjust; + + // Use the scanned page size if it's close enough + // It might not be close enough if we've cropped the image or if the scanner didn't produce the requested size + if (pageSize != null) + { + var pageHorDpi = embedder.Width / (double) pageSize.WidthInInches; + var pageVerDpi = embedder.Height / (double) pageSize.HeightInInches; + // We expect a margin of error of <1 since most of the inaccuracy comes from file formats like JPEG only + // storing integral DPIs + if (Math.Abs(embedder.HorizontalDpi - pageHorDpi) <= 1 && + Math.Abs(embedder.VerticalDpi - pageVerDpi) <= 1) + { + realWidth = (double) pageSize.WidthInInches * 72; + realHeight = (double) pageSize.HeightInInches * 72; + } + } + + // PDF page size precision is 3 decimal places and image matrix precision is 4 decimal places. + // We round to 3 decimal places to ensure they match exactly. + realWidth = Math.Round(realWidth, 3); + realHeight = Math.Round(realHeight, 3); + + return (realWidth, realHeight); + } + + private static int ClampFontSizeByRightBound(OcrResultElement element, int initialFontSize, double x, + double rightBound, bool includeSpace, + XGraphics gfx) + { + var fontSize = initialFontSize; + if (IsCjk(element.LanguageCode)) + { + // No word separators so no need to ensure space between words + return fontSize; + } + if (rightBound < 0) + { + // No word to the right + return fontSize; + } + var fontFamily = PdfFontPicker.GetBestFont(element.LanguageCode); + while (fontSize > 2) + { + var spaceWidth = includeSpace + ? gfx.MeasureString(" ", new XFont(fontFamily, fontSize, XFontStyle.Regular)).Width + : 0; + var measuredBounds = + gfx.MeasureString(element.Text, new XFont(fontFamily, fontSize, XFontStyle.Regular)); + if (measuredBounds.Width + x <= rightBound - spaceWidth) + { + break; + } + fontSize--; + } + return fontSize; + } + + private static bool IsPdfStorage(IImageStorage storage) => storage switch + { + ImageFileStorage fileStorage => Path.GetExtension(fileStorage.FullPath).ToLowerInvariant() == ".pdf", + ImageMemoryStorage memoryStorage => memoryStorage.TypeHint == ".pdf", + _ => false + }; + + private record TextDrawInfo(string Text, string FontFamily, int FontSize, int X, int Y); + + private class PageExportState + { + public PageExportState(ProcessedImage image, int pageIndex, PdfDocument document, PdfPage page, + IOcrEngine? ocrEngine, OcrParams? ocrParams, Action incrementProgress, CancellationToken cancelToken, + PdfCompat compat) + { + Image = image; + PageIndex = pageIndex; + Document = document; + Page = page; + OcrEngine = ocrEngine; + OcrParams = ocrParams; + IncrementProgress = incrementProgress; + CancelToken = cancelToken; + Compat = compat; + } + + public ProcessedImage Image { get; } + public int PageIndex { get; } + + public PdfDocument Document { get; } + public PdfPage Page { get; set; } + public IOcrEngine? OcrEngine { get; } + public OcrParams? OcrParams { get; } + public Action IncrementProgress { get; } + public CancellationToken CancelToken { get; } + public PdfCompat Compat { get; } + + public bool NeedsOcr { get; set; } + public IEmbedder? Embedder { get; set; } + public Task? OcrTask { get; set; } + } + + private class ImageSource : IImageSource + { + private readonly IEmbedder _embedder; + private readonly ImageExportFormat _exportFormat; + + public ImageSource(IEmbedder embedder, ImageExportFormat exportFormat) + { + _embedder = embedder; + _exportFormat = exportFormat; + } + + public void SaveAsJpeg(MemoryStream ms) + { + _embedder.CopyToStream(ms); + } + + public void SaveAsPdfBitmap(MemoryStream ms) + { + var image = _embedder.Image; + var subPixelType = _exportFormat.PixelFormat switch + { + ImagePixelFormat.ARGB32 => SubPixelType.Bgra, + ImagePixelFormat.RGB24 or ImagePixelFormat.Gray8 => SubPixelType.Bgr, + _ => throw new InvalidOperationException("Expected 8/24/32 bit bitmap") + }; + var dstPixelInfo = + new PixelInfo(image.Width, image.Height, subPixelType, strideAlign: 4) { InvertY = true }; + ms.SetLength(dstPixelInfo.Length); + new CopyBitwiseImageOp().Perform(image, ms.GetBuffer(), dstPixelInfo); + } + + public void SaveAsPdfIndexedBitmap(MemoryStream ms) + { + var image = _embedder.Image; + image.UpdateLogicalPixelFormat(); + if (image.LogicalPixelFormat != ImagePixelFormat.BW1) + throw new InvalidOperationException("Expected 1 bit bitmap"); + var dstPixelInfo = + new PixelInfo(image.Width, image.Height, SubPixelType.Bit) { InvertY = true }; + ms.SetLength(dstPixelInfo.Length); + new CopyBitwiseImageOp().Perform(image, ms.GetBuffer(), dstPixelInfo); + } + + public int Width => _embedder.Width; + public int Height => _embedder.Height; + public string? Name => null; + + public XImageFormat ImageFormat + { + get + { + if (_exportFormat.FileFormat == ImageFileFormat.Jpeg) + { + return XImageFormat.Jpeg; + } + if (_exportFormat.PixelFormat == ImagePixelFormat.BW1) + { + return XImageFormat.Indexed; + } + if (_exportFormat.PixelFormat == ImagePixelFormat.ARGB32) + { + return XImageFormat.Argb32; + } + // TODO: Ideally we should have Gray8 support in PdfSharp + if (_exportFormat.PixelFormat is ImagePixelFormat.RGB24 or ImagePixelFormat.Gray8) + { + return XImageFormat.Rgb24; + } + throw new Exception($"Unsupported pixel format: {_exportFormat.PixelFormat}"); + } + } + } + + private interface IEmbedder : IDisposable + { + void CopyToStream(Stream stream); + ImageExportFormat PrepareForExport(ImageMetadata metadata); + IMemoryImage Image { get; } + int Width { get; } + int Height { get; } + double HorizontalDpi { get; } + double VerticalDpi { get; } + ImageFileFormat OriginalFileFormat { get; } + } + + private class RenderedImageEmbedder : IEmbedder + { + public RenderedImageEmbedder(IMemoryImage image) + { + Image = image; + } + + public IMemoryImage Image { get; private set; } + public int Width => Image.Width; + public int Height => Image.Height; + public double HorizontalDpi => Image.HorizontalResolution; + public double VerticalDpi => Image.VerticalResolution; + public ImageFileFormat OriginalFileFormat => Image.OriginalFileFormat; + + public void CopyToStream(Stream stream) + { + // PDFs require RGB channels so we need to make sure we're exporting that. + Image.Save(stream, ImageFileFormat.Jpeg, new ImageSaveOptions { PixelFormatHint = ImagePixelFormat.RGB24 }); + } + + public ImageExportFormat PrepareForExport(ImageMetadata metadata) + { + var exportFormat = ImageExportHelper.GetExportFormat(Image, metadata.Lossless); + if (exportFormat.FileFormat == ImageFileFormat.Unknown) + { + exportFormat = exportFormat with { FileFormat = ImageFileFormat.Jpeg }; + } + return exportFormat; + } + + public void Dispose() + { + Image.Dispose(); + } + } + + private class DirectJpegEmbedder : IEmbedder + { + private readonly JpegFormatHelper.JpegHeader _header; + private readonly string _path; + + public DirectJpegEmbedder(JpegFormatHelper.JpegHeader header, string path) + { + _header = header; + _path = path; + } + + public IMemoryImage Image => throw new InvalidOperationException(); + public int Width => _header.Width; + public int Height => _header.Height; + public double HorizontalDpi => _header.HorizontalDpi; + public double VerticalDpi => _header.VerticalDpi; + public ImageFileFormat OriginalFileFormat => ImageFileFormat.Jpeg; + + public void CopyToStream(Stream stream) + { + using var fileStream = new FileStream(_path, FileMode.Open, FileAccess.Read); + fileStream.CopyTo(stream); + } + + public ImageExportFormat PrepareForExport(ImageMetadata metadata) + { + return new ImageExportFormat(ImageFileFormat.Jpeg, ImagePixelFormat.RGB24); + } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Pdf/PdfFontPicker.cs b/NAPS2.Sdk/Pdf/PdfFontPicker.cs new file mode 100644 index 0000000000..dfcdb7ebb9 --- /dev/null +++ b/NAPS2.Sdk/Pdf/PdfFontPicker.cs @@ -0,0 +1,222 @@ +namespace NAPS2.Pdf; + +/// +/// Determines the best font to use for generating OCR text in exported PDFs. As this text is invisible, the quality +/// and style of the font aren't so important - what matters is that the font is installed on the system by default and +/// supports the characters in the current language's alphabet. +/// +internal static class PdfFontPicker +{ + public static string GetBestFont(string languageCode) + { + // This logic is incomplete, but the goal is to get PdfFontTests passing, which works as + // the default font supports multiple scripts. e.g. Times New Roman supports most everything + // except CJK (Chinese-Japanese-Korean) + var alphabet = MapLanguageCodeToAlphabet(languageCode); + +#if NET6_0_OR_GREATER + if (OperatingSystem.IsMacOS()) + { + return GetMacFont(alphabet); + } + if (OperatingSystem.IsLinux()) + { + return GetLinuxFont(alphabet); + } +#endif + return GetWindowsFont(alphabet); + } + + private static string GetWindowsFont(Alphabet alphabet) + { + return alphabet switch + { + // See https://learn.microsoft.com/en-us/typography/fonts/windows_10_font_list + Alphabet.Bengali => "Nirmala UI", + Alphabet.CanadianAboriginal => "Gadugi", + Alphabet.Cherokee => "Gadugi", + Alphabet.Devanagari => "Nirmala UI", + Alphabet.Ethiopic => "Ebrima", + Alphabet.Georgian => "Calibri", + Alphabet.Gujarati => "Nirmala UI", + Alphabet.Gurmukhi => "Nirmala UI", + Alphabet.Kannada => "Nirmala UI", + Alphabet.Khmer => "Leelawadee UI", + Alphabet.Lao => "Leelawadee UI", + Alphabet.Malayalam => "Nirmala UI", + Alphabet.Myanmar => "Myanmar Text", + Alphabet.Oriya => "Kalinga", // Supplemental font, needs installation via Control Panel + Alphabet.Sinhala => "Nirmala UI", + Alphabet.Syriac => "Estrangelo Edessa", // Supplemental font, needs installation via Control Panel + Alphabet.Tamil => "Nirmala UI", + Alphabet.Telugu => "Nirmala UI", + Alphabet.Thaana => "MV Boli", + Alphabet.Thai => "Leelawadee UI", + Alphabet.Tibetan => "Microsoft Himalaya", + Alphabet.ChineseSimplified => "Microsoft YaHei", + Alphabet.ChineseTraditional => "Microsoft JhengHei", + Alphabet.Japanese => "MS Gothic", + Alphabet.Korean => "Malgun Gothic", + _ => "Times New Roman" + }; + } + + private static string GetMacFont(Alphabet alphabet) + { + return alphabet switch + { + Alphabet.Arabic => ".SF Arabic", + Alphabet.Armenian => ".SF Armenian", + Alphabet.Bengali => "Bangla MN", + Alphabet.CanadianAboriginal => "Euphemia UCAS", + Alphabet.Cherokee => "Plantagenet Cherokee", + Alphabet.Devanagari => "Devanagari MT", + Alphabet.Ethiopic => "Kefa", + Alphabet.Georgian => ".SF Georgian", + Alphabet.Gujarati => "Gujarati Sangam MN", + Alphabet.Gurmukhi => "Gurmukhi MN", + Alphabet.Kannada => "Kannada MN", + Alphabet.Khmer => "Khmer Sangam MN", + Alphabet.Lao => "Lao MN", + Alphabet.Malayalam => "Malayalam MN", + Alphabet.Myanmar => "Myanmar MN", + Alphabet.Oriya => "Oriya MN", + Alphabet.Sinhala => "Sinhala MN", + Alphabet.Syriac => "Noto Sans Syriac", + Alphabet.Tamil => "Tamil MN", + Alphabet.Telugu => "Telugu MN", + Alphabet.Thaana => "Noto Sans Thaana", + Alphabet.Thai => "Sathu", + Alphabet.Tibetan => "Kailasa", + Alphabet.ChineseSimplified => "Heiti SC", + Alphabet.ChineseTraditional => "Heiti TC", + Alphabet.Japanese => ".Aqua Kana", + Alphabet.Korean => "AppleMyungjo", + _ => "Times New Roman" + }; + } + + private static string GetLinuxFont(Alphabet alphabet) + { + return alphabet switch + { + // Noto fonts aren't always going to be installed, but they're among the most common + Alphabet.Arabic => "Noto Sans Arabic", + Alphabet.Armenian => "Noto Sans Armenian", + Alphabet.Bengali => "Noto Sans Bengali", + Alphabet.CanadianAboriginal => "Noto Sans CanAborig", + Alphabet.Cherokee => "Noto Sans Cherokee", + Alphabet.Devanagari => "Noto Sans Devanagari", + Alphabet.Ethiopic => "Noto Sans Ethiopic", + Alphabet.Georgian => "Noto Sans Georgian", + Alphabet.Gujarati => "Noto Sans Gujarati", + Alphabet.Gurmukhi => "Noto Sans Gurmukhi", + Alphabet.Kannada => "Noto Sans Kannada", + Alphabet.Khmer => "Noto Sans Khmer", + Alphabet.Lao => "Noto Sans Lao", + Alphabet.Malayalam => "Noto Sans Malayalam", + Alphabet.Myanmar => "Noto Sans Myanmar", + Alphabet.Oriya => "Noto Sans Oriya", + Alphabet.Sinhala => "Noto Sans Sinhala", + Alphabet.Syriac => "Noto Sans Syriac", + Alphabet.Tamil => "Noto Sans Tamil", + Alphabet.Telugu => "Noto Sans Telugu", + Alphabet.Thaana => "Noto Sans Thaana", + Alphabet.Thai => "Noto Sans Thai", + Alphabet.Tibetan => "Noto Serif Tibetan", + Alphabet.ChineseSimplified => "Noto Sans CJK SC", + Alphabet.ChineseTraditional => "Noto Sans CJK TC", + Alphabet.Japanese => "Noto Sans CJK JP", + Alphabet.Korean => "Noto Sans CJK KR", + // Liberation Serif is broadly included in Linux distros and is designed to have the same measurements + // as Times New Roman. + _ => "Liberation Serif" + }; + } + + public enum Alphabet + { + Unknown, + // Common (supported by Times New Roman / Liberation Serif) + Latin, + Cyrillic, + Greek, + Hebrew, + // Uncommon (needs special handling) + Arabic, + Armenian, + Bengali, + CanadianAboriginal, + Cherokee, + Devanagari, + Ethiopic, + Georgian, + Gujarati, + Gurmukhi, + Kannada, + Khmer, + Lao, + Malayalam, + Myanmar, + Oriya, + Sinhala, + Syriac, + Tamil, + Telugu, + Thaana, + Thai, + Tibetan, + // CJK (needs special handling) + ChineseSimplified, + ChineseTraditional, + Japanese, + Korean + } + + // Reference: https://github.com/tesseract-ocr/langdata_lstm/tree/main/script + public static Alphabet MapLanguageCodeToAlphabet(string languageCode) + { + return languageCode.ToLowerInvariant() switch + { + "ara" or "fas" or "kur_ara" or "pus" or "snd" or "uig" or "urd" => Alphabet.Arabic, + "hye" => Alphabet.Armenian, + "asm" or "ben" => Alphabet.Bengali, + "iku" => Alphabet.CanadianAboriginal, + "chr" => Alphabet.Cherokee, + "aze_cyrl" or "bel" or "bul" or "kaz" or "kir" or "mkd" or "mon" or "rus" or "srp" or "tgk" or "ukr" or "uzb_cyrl" => Alphabet.Cyrillic, + "hin" or "mar" or "nep" or "san" => Alphabet.Devanagari, + "amh" or "tir" => Alphabet.Ethiopic, + "kat" or "kat_old" => Alphabet.Georgian, + "ell" or "grc" => Alphabet.Greek, + "guj" => Alphabet.Gujarati, + "pan" => Alphabet.Gurmukhi, + "heb" or "yid" => Alphabet.Hebrew, + "kan" => Alphabet.Kannada, + "khm" => Alphabet.Khmer, + "lao" => Alphabet.Lao, + "afr" or "aze" or "bos" or "bre" or "cat" or "ceb" or "ces" or "cos" or "cym" or "dan" or "deu" or "eng" or + "epo" or "est" or "eus" or "fao" or "fil" or "fin" or "fra" or "fry" or "gla" or "gle" or "glg" or + "hat" or "hrv" or "hun" or "ind" or "isl" or "ita" or "jav" or "lat" or "lav" or "lit" or "ltz" or + "mlt" or "mri" or "msa" or "nld" or "nor" or "oci" or "pol" or "por" or "que" or "ron" or "slk" or + "slv" or "spa" or "sqi" or "srp_latn" or "sun" or "swa" or "swe" or "tat" or "ton" or "tur" or "uzb" or + "yor" => Alphabet.Latin, + // Tesseract has separate "scripts" for Fraktur and Vietnamese but they both just use Latin characters + "enm" or "frm" or "deu_latf" or "ita_old" or "spa_old" or "vie" => Alphabet.Latin, + "mal" => Alphabet.Malayalam, + "mya" => Alphabet.Myanmar, + "ori" => Alphabet.Oriya, + "sin" => Alphabet.Sinhala, + "syr" => Alphabet.Syriac, + "tam" => Alphabet.Tamil, + "tel" => Alphabet.Telugu, + "div" => Alphabet.Thaana, + "tha" => Alphabet.Thai, + "bod" or "dzo" => Alphabet.Tibetan, + "chi_sim" or "chi_sim_vert" => Alphabet.ChineseSimplified, + "chi_tra" or "chi_tra_vert" => Alphabet.ChineseTraditional, + "jpn" or "jpn_vert" => Alphabet.Japanese, + "kor" or "kor_vert" => Alphabet.Korean, + _ => Alphabet.Unknown + }; + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfImporter.cs b/NAPS2.Sdk/Pdf/PdfImporter.cs similarity index 67% rename from NAPS2.Sdk/ImportExport/Pdf/PdfImporter.cs rename to NAPS2.Sdk/Pdf/PdfImporter.cs index 1ed2355d7c..85a353703d 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfImporter.cs +++ b/NAPS2.Sdk/Pdf/PdfImporter.cs @@ -1,36 +1,32 @@ -using NAPS2.ImportExport.Images; -using NAPS2.ImportExport.Pdf.Pdfium; +using NAPS2.ImportExport; +using NAPS2.Pdf.Pdfium; using NAPS2.Scan; -namespace NAPS2.ImportExport.Pdf; +namespace NAPS2.Pdf; -public class PdfImporter : IPdfImporter +/// +/// Imports PDF files. +/// +public class PdfImporter { private const int MAX_PASSWORD_ATTEMPTS = 5; private readonly ScanningContext _scanningContext; private readonly IPdfPasswordProvider? _pdfPasswordProvider; - private readonly ImportPostProcessor _importPostProcessor; - public PdfImporter(ScanningContext scanningContext) - : this(scanningContext, null) - { - } - - public PdfImporter(ScanningContext scanningContext, IPdfPasswordProvider? pdfPasswordProvider) - : this(scanningContext, pdfPasswordProvider, new ImportPostProcessor()) - { - } - - internal PdfImporter(ScanningContext scanningContext, IPdfPasswordProvider? pdfPasswordProvider, - ImportPostProcessor importPostProcessor) + public PdfImporter(ScanningContext scanningContext, IPdfPasswordProvider? pdfPasswordProvider = null) { _scanningContext = scanningContext; _pdfPasswordProvider = pdfPasswordProvider; - _importPostProcessor = importPostProcessor; } public IAsyncEnumerable Import(string filePath, ImportParams? importParams = null, + ProgressHandler progress = default) => Import(new InputPathOrStream(filePath, null, null), importParams, progress); + + public IAsyncEnumerable Import(Stream stream, ImportParams? importParams = null, + ProgressHandler progress = default) => Import(new InputPathOrStream(null, stream, null), importParams, progress); + + internal IAsyncEnumerable Import(InputPathOrStream input, ImportParams? importParams = null, ProgressHandler progress = default) { importParams ??= new ImportParams(); @@ -40,7 +36,7 @@ public IAsyncEnumerable Import(string filePath, ImportParams? im lock (PdfiumNativeLibrary.Instance) { - using var document = LoadDocument(filePath, importParams); + using var document = LoadDocument(input, importParams); if (document == null) return; progress.Report(0, document.PageCount); @@ -57,6 +53,11 @@ public IAsyncEnumerable Import(string filePath, ImportParams? im { if (progress.IsCancellationRequested) return; var image = GetImageFromPage(page, importParams); + if (input.FilePath != null) + { + image = image.WithPostProcessingData( + image.PostProcessingData with { OriginalFilePath = input.FilePath }, true); + } progress.Report(++i, document.PageCount); produceImage(image); } @@ -64,7 +65,7 @@ public IAsyncEnumerable Import(string filePath, ImportParams? im }); } - private PdfDocument? LoadDocument(string filePath, ImportParams importParams) + private PdfDocument? LoadDocument(InputPathOrStream input, ImportParams importParams) { PdfDocument? doc = null; try @@ -75,25 +76,28 @@ public IAsyncEnumerable Import(string filePath, ImportParams? im { try { - doc = PdfDocument.Load(filePath, password); + doc = input.LoadPdfDoc(password); break; } catch (PdfiumException ex) when (ex.ErrorCode == PdfiumErrorCode.PasswordNeeded && _pdfPasswordProvider != null) { - if (!_pdfPasswordProvider.ProvidePassword(Path.GetFileName(filePath), passwordAttempts++, - out password)) + if (!_pdfPasswordProvider.ProvidePassword(input.FileName, passwordAttempts++, out password)) { return null; } } catch (PdfiumException ex) when (ex.ErrorCode == PdfiumErrorCode.FileNotFoundOrUnavailable) { - if (!File.Exists(filePath)) + if (input.FilePath != null && !File.Exists(input.FilePath)) + { + throw new FileNotFoundException($"Could not find pdf file: '{input.FilePath}'"); + } + if (input.FilePath != null) { - throw new FileNotFoundException($"Could not find pdf file '{filePath}'."); + throw new IOException($"Could not open pdf file for reading: '{input.FilePath}'"); } - throw new IOException($"Error reading pdf file '{filePath}'."); + throw new IOException("Could not open pdf file for reading"); } } return doc; @@ -107,11 +111,12 @@ public IAsyncEnumerable Import(string filePath, ImportParams? im private ProcessedImage GetImageFromPage(PdfPage page, ImportParams importParams) { - using var storage = PdfiumImageExtractor.GetSingleImage(_scanningContext.ImageContext, page); + using var storage = PdfiumImageExtractor.GetSingleImage(_scanningContext.ImageContext, page, false); if (storage != null) { - var image = _scanningContext.CreateProcessedImage(storage, BitDepth.Color, false, -1); - return _importPostProcessor.AddPostProcessingData( + var pageSize = new PageSize((decimal) page.Width * 72, (decimal) page.Height * 72, PageSizeUnit.Inch); + var image = _scanningContext.CreateProcessedImage(storage, false, -1, pageSize); + return ImportPostProcessor.AddPostProcessingData( image, storage, importParams.ThumbnailSize, @@ -141,7 +146,7 @@ private ProcessedImage ExportRawPdfPage(PdfPage page, ImportParams importParams) } var image = _scanningContext.CreateProcessedImage(storage); - return _importPostProcessor.AddPostProcessingData( + return ImportPostProcessor.AddPostProcessingData( image, null, importParams.ThumbnailSize, diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfMetadata.cs b/NAPS2.Sdk/Pdf/PdfMetadata.cs similarity index 69% rename from NAPS2.Sdk/ImportExport/Pdf/PdfMetadata.cs rename to NAPS2.Sdk/Pdf/PdfMetadata.cs index 7ae003da06..22e116b93a 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfMetadata.cs +++ b/NAPS2.Sdk/Pdf/PdfMetadata.cs @@ -1,5 +1,8 @@ -namespace NAPS2.ImportExport.Pdf; +namespace NAPS2.Pdf; +/// +/// Represents standard PDF metadata (e.g. author, subject, title). +/// public record PdfMetadata { public string Author { get; init; } = ""; diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/Colorspace.cs b/NAPS2.Sdk/Pdf/Pdfium/Colorspace.cs similarity index 77% rename from NAPS2.Sdk/ImportExport/Pdf/Pdfium/Colorspace.cs rename to NAPS2.Sdk/Pdf/Pdfium/Colorspace.cs index 88adf201a0..cea2d6467e 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/Colorspace.cs +++ b/NAPS2.Sdk/Pdf/Pdfium/Colorspace.cs @@ -1,6 +1,6 @@ -namespace NAPS2.ImportExport.Pdf.Pdfium; +namespace NAPS2.Pdf.Pdfium; -public enum Colorspace +internal enum Colorspace { Unknown = 0, DeviceGray = 1, diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/NativePdfiumObject.cs b/NAPS2.Sdk/Pdf/Pdfium/NativePdfiumObject.cs similarity index 83% rename from NAPS2.Sdk/ImportExport/Pdf/Pdfium/NativePdfiumObject.cs rename to NAPS2.Sdk/Pdf/Pdfium/NativePdfiumObject.cs index e22452a074..fe0698752f 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/NativePdfiumObject.cs +++ b/NAPS2.Sdk/Pdf/Pdfium/NativePdfiumObject.cs @@ -1,9 +1,9 @@ using System.Runtime.InteropServices; using System.Threading; -namespace NAPS2.ImportExport.Pdf.Pdfium; +namespace NAPS2.Pdf.Pdfium; -public abstract class NativePdfiumObject : IDisposable +internal abstract class NativePdfiumObject : IDisposable { private bool _disposed; private IntPtr _handle; @@ -57,7 +57,10 @@ protected virtual void Dispose(bool disposing) { if (Handle != IntPtr.Zero) { - DisposeHandle(); + lock (PdfiumNativeLibrary.Instance) + { + DisposeHandle(); + } } _disposed = true; } @@ -78,8 +81,6 @@ public void Dispose() ~NativePdfiumObject() { - // TODO: This isn't necessarily going to work as we don't have a lock, not sure the best way to handle it - // Though this does provide a way to give some kind of error when running tests Dispose(false); } } \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfBitmap.cs b/NAPS2.Sdk/Pdf/Pdfium/PdfBitmap.cs similarity index 57% rename from NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfBitmap.cs rename to NAPS2.Sdk/Pdf/Pdfium/PdfBitmap.cs index 2ff5e6cc57..032a2caf2b 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfBitmap.cs +++ b/NAPS2.Sdk/Pdf/Pdfium/PdfBitmap.cs @@ -1,14 +1,23 @@ -namespace NAPS2.ImportExport.Pdf.Pdfium; +namespace NAPS2.Pdf.Pdfium; -public class PdfBitmap : NativePdfiumObject +internal class PdfBitmap : NativePdfiumObject { public const uint BLACK = 0; public const uint WHITE = uint.MaxValue; - public static PdfBitmap CreateFromPointerBgr(int width, int height, IntPtr scan0, int stride) + public static PdfBitmap CreateFromPointerBgr(int width, int height, IntPtr scan0, int stride) => + CreateFromPointer(width, height, scan0, stride, PdfiumNativeLibrary.FPDFBitmap_BGR); + + public static PdfBitmap CreateFromPointer(int width, int height, IntPtr scan0, int stride, int format) + { + return new PdfBitmap( + Native.FPDFBitmap_CreateEx(width, height, format, scan0, stride)); + } + + public static PdfBitmap Create(int width, int height, int format) { return new PdfBitmap( - Native.FPDFBitmap_CreateEx(width, height, PdfiumNativeLibrary.FPDFBitmap_BGR, scan0, stride)); + Native.FPDFBitmap_CreateEx(width, height, format, IntPtr.Zero, 0)); } internal PdfBitmap(IntPtr handle) : base(handle) @@ -21,12 +30,7 @@ internal PdfBitmap(IntPtr handle) : base(handle) public int Stride => Native.FPDFBitmap_GetStride(Handle); - public ImagePixelFormat Format => Native.FPDFBitmap_GetFormat(Handle) switch - { - PdfiumNativeLibrary.FPDFBitmap_BGR => ImagePixelFormat.RGB24, - PdfiumNativeLibrary.FPDFBitmap_BGRA => ImagePixelFormat.ARGB32, - _ => ImagePixelFormat.Unsupported - }; + public int Format => Native.FPDFBitmap_GetFormat(Handle); public IntPtr Buffer => Native.FPDFBitmap_GetBuffer(Handle); @@ -43,7 +47,7 @@ public void FillRect(int x, int y, int width, int height, uint color) public void RenderPage(PdfPage page, int x, int y, int width, int height) { int rotate = 0; - int flags = PdfiumNativeLibrary.FPDF_PRINTING; + int flags = PdfiumNativeLibrary.FPDF_PRINTING | PdfiumNativeLibrary.FPDF_ANNOT; Native.FPDF_RenderPageBitmap(Handle, page.Handle, x, y, width, height, rotate, flags); } } \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfDocument.cs b/NAPS2.Sdk/Pdf/Pdfium/PdfDocument.cs similarity index 51% rename from NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfDocument.cs rename to NAPS2.Sdk/Pdf/Pdfium/PdfDocument.cs index 175e8b8cc9..e2d9f0f1e1 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfDocument.cs +++ b/NAPS2.Sdk/Pdf/Pdfium/PdfDocument.cs @@ -1,19 +1,63 @@ using System.Runtime.InteropServices; using System.Text; -namespace NAPS2.ImportExport.Pdf.Pdfium; +namespace NAPS2.Pdf.Pdfium; // TODO: Use PdfiumException (with a message, defaulting to unknown error code for the property) instead of other exception types -public class PdfDocument : NativePdfiumObject +internal class PdfDocument : NativePdfiumObject { public static PdfDocument Load(string path, string? password = null) { - return new PdfDocument(Native.FPDF_LoadDocument(path, password), PlatformCompat.System.FileReadLock(path)); + return new PdfDocument( + Native.FPDF_LoadDocument(ToUtf(path)!, ToUtf(password)), + PlatformCompat.System.FileReadLock(path)); + } + + public static PdfDocument Load(Stream stream, string? password = null) + { + byte[]? buffer = null; + if (stream is MemoryStream memoryStream) + { + try + { + buffer = memoryStream.GetBuffer(); + } + catch (Exception) + { + // The buffer might not be exposable + } + } + if (buffer == null) + { + memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + buffer = memoryStream.GetBuffer(); + } + return Load(buffer, (int) stream.Length, password); + } + + public static PdfDocument Load(byte[] buffer, int length, string? password = null) + { + var gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + return new PdfDocument( + Native.FPDF_LoadMemDocument(gcHandle.AddrOfPinnedObject(), length, ToUtf(password)), + gcHandle: gcHandle); } public static PdfDocument Load(IntPtr buffer, int length, string? password = null) { - return new PdfDocument(Native.FPDF_LoadMemDocument(buffer, length, password)); + return new PdfDocument( + Native.FPDF_LoadMemDocument(buffer, length, ToUtf(password))); + } + + // TODO: If we upgrade to .NET Framework 4.7 we can use [MarshalAs(UnmanagedType.LPUTF8Str)] + private static byte[]? ToUtf(string? str) + { + if (str == null) + { + return null; + } + return Encoding.UTF8.GetBytes(str); } public static PdfDocument CreateNew() @@ -22,14 +66,18 @@ public static PdfDocument CreateNew() } private readonly IDisposable? _readLock; + private GCHandle _gcHandle; - private PdfDocument(IntPtr handle, IDisposable? readLock = null) : base(handle) + private PdfDocument(IntPtr handle, IDisposable? readLock = null, GCHandle gcHandle = default) : base(handle) { _readLock = readLock; + _gcHandle = gcHandle; } public int PageCount => Native.FPDF_GetPageCount(Handle); + public int? Version => Native.FPDF_GetFileVersion(Handle, out int version) ? version : null; + public PdfPage GetPage(int pageIndex) { return new PdfPage(Native.FPDF_LoadPage(Handle, pageIndex), this, pageIndex); @@ -45,6 +93,16 @@ public PdfPageObject NewImage() return new PdfPageObject(Native.FPDFPageObj_NewImageObj(Handle), this, null, true); } + public PdfPageObject NewText(string font, int fontSize) + { + return new PdfPageObject(Native.FPDFPageObj_NewTextObj(Handle, font, fontSize), this, null, true); + } + + public PdfPageObject NewText(PdfFont font, int fontSize) + { + return new PdfPageObject(Native.FPDFPageObj_CreateTextObj(Handle, font.Handle, fontSize), this, null, true); + } + public PdfPage NewPage(double width, double height) { return new PdfPage(Native.FPDFPage_New(Handle, int.MaxValue, width, height), this, -1); @@ -71,6 +129,20 @@ public string GetMetaText(string tag) return Encoding.Unicode.GetString(buffer, 0, buffer.Length - 2); } + public PdfFormEnv CreateFormEnv() + { + var formInfo = new PdfiumNativeLibrary.FPDF_FormFillInfo { version = 2 }; + var formInfoHandle = GCHandle.Alloc(formInfo, GCHandleType.Pinned); + var ptr = formInfoHandle.AddrOfPinnedObject(); + return new PdfFormEnv(Native.FPDFDOC_InitFormFillEnvironment(Handle, ptr), formInfoHandle); + } + + public PdfFont LoadFont(byte[] data) + { + return new PdfFont(Native.FPDFText_LoadFont(Handle, data, data.Length, PdfiumNativeLibrary.FPDF_FONT_TRUETYPE, + true)); + } + public void Save(string path) { using var stream = new FileStream(path, FileMode.Create); @@ -104,6 +176,10 @@ protected override void Dispose(bool disposing) if (disposing) { _readLock?.Dispose(); + if (_gcHandle.IsAllocated) + { + _gcHandle.Free(); + } } } diff --git a/NAPS2.Sdk/Pdf/Pdfium/PdfFont.cs b/NAPS2.Sdk/Pdf/Pdfium/PdfFont.cs new file mode 100644 index 0000000000..5478654b88 --- /dev/null +++ b/NAPS2.Sdk/Pdf/Pdfium/PdfFont.cs @@ -0,0 +1,13 @@ +namespace NAPS2.Pdf.Pdfium; + +internal class PdfFont : NativePdfiumObject +{ + public PdfFont(IntPtr handle) : base(handle) + { + } + + protected override void DisposeHandle() + { + Native.FPDFFont_Close(Handle); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Pdf/Pdfium/PdfFormEnv.cs b/NAPS2.Sdk/Pdf/Pdfium/PdfFormEnv.cs new file mode 100644 index 0000000000..066c9902c0 --- /dev/null +++ b/NAPS2.Sdk/Pdf/Pdfium/PdfFormEnv.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; + +namespace NAPS2.Pdf.Pdfium; + +internal class PdfFormEnv : NativePdfiumObject +{ + private readonly GCHandle _formInfoHandle; + + public PdfFormEnv(IntPtr handle, GCHandle formInfoHandle) : base(handle) + { + _formInfoHandle = formInfoHandle; + } + + public void DrawForms(PdfBitmap bitmap, PdfPage page) + { + Native.FPDF_FFLDraw(Handle, bitmap.Handle, page.Handle, 0, 0, bitmap.Width, bitmap.Height, 0, + PdfiumNativeLibrary.FPDF_PRINTING); + } + + protected override void DisposeHandle() + { + Native.FPDFDOC_ExitFormFillEnvironment(Handle); + _formInfoHandle.Free(); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfImageMetadata.cs b/NAPS2.Sdk/Pdf/Pdfium/PdfImageMetadata.cs similarity index 79% rename from NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfImageMetadata.cs rename to NAPS2.Sdk/Pdf/Pdfium/PdfImageMetadata.cs index efa3fa571f..71653ef2aa 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfImageMetadata.cs +++ b/NAPS2.Sdk/Pdf/Pdfium/PdfImageMetadata.cs @@ -1,8 +1,8 @@ using System.Runtime.InteropServices; -namespace NAPS2.ImportExport.Pdf.Pdfium; +namespace NAPS2.Pdf.Pdfium; -public struct PdfImageMetadata +internal struct PdfImageMetadata { public int Width; public int Height; diff --git a/NAPS2.Sdk/Pdf/Pdfium/PdfMatrix.cs b/NAPS2.Sdk/Pdf/Pdfium/PdfMatrix.cs new file mode 100644 index 0000000000..1a888c9319 --- /dev/null +++ b/NAPS2.Sdk/Pdf/Pdfium/PdfMatrix.cs @@ -0,0 +1,22 @@ +namespace NAPS2.Pdf.Pdfium; + +// See PDF standard 1.7 section 4.2.2 and 4.2.3 +internal record struct PdfMatrix(float a, float b, float c, float d, float e, float f) +{ + private const float TOLERANCE = 0.00001f; + + public static bool EqualsWithinTolerance(PdfMatrix first, PdfMatrix second) + { + return Math.Abs(first.a - second.a) < TOLERANCE && + Math.Abs(first.b - second.b) < TOLERANCE && + Math.Abs(first.c - second.c) < TOLERANCE && + Math.Abs(first.d - second.d) < TOLERANCE && + Math.Abs(first.e - second.e) < TOLERANCE && + Math.Abs(first.f - second.f) < TOLERANCE; + } + + public static PdfMatrix FillPage(float width, float height) + { + return new PdfMatrix(width, 0, 0, height, 0, 0); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfPage.cs b/NAPS2.Sdk/Pdf/Pdfium/PdfPage.cs similarity index 91% rename from NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfPage.cs rename to NAPS2.Sdk/Pdf/Pdfium/PdfPage.cs index 8c739e92d4..cc7468d473 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfPage.cs +++ b/NAPS2.Sdk/Pdf/Pdfium/PdfPage.cs @@ -1,6 +1,6 @@ -namespace NAPS2.ImportExport.Pdf.Pdfium; +namespace NAPS2.Pdf.Pdfium; -public class PdfPage : NativePdfiumObject +internal class PdfPage : NativePdfiumObject { internal PdfPage(IntPtr handle, PdfDocument document, int pageIndex) : base(handle) @@ -24,6 +24,8 @@ public PdfText GetText() return new PdfText(Native.FPDFText_LoadPage(Handle)); } + public int AnnotCount => Native.FPDFPage_GetAnnotCount(Handle); + public int ObjectCount => Native.FPDFPage_CountObjects(Handle); public void InsertObject(PdfPageObject pageObject) diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfPageObject.cs b/NAPS2.Sdk/Pdf/Pdfium/PdfPageObject.cs similarity index 75% rename from NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfPageObject.cs rename to NAPS2.Sdk/Pdf/Pdfium/PdfPageObject.cs index 7289224d47..e2ed8ff1ca 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfPageObject.cs +++ b/NAPS2.Sdk/Pdf/Pdfium/PdfPageObject.cs @@ -1,9 +1,9 @@ using System.Runtime.InteropServices; using System.Text; -namespace NAPS2.ImportExport.Pdf.Pdfium; +namespace NAPS2.Pdf.Pdfium; -public class PdfPageObject : NativePdfiumObject +internal class PdfPageObject : NativePdfiumObject { private readonly bool _owned; @@ -71,23 +71,44 @@ public PdfMatrix Matrix } } - public (uint r, uint g, uint b, uint a) GetStrokeColor() + public (uint r, uint g, uint b, uint a) StrokeColor { - // TODO: Maybe fill color? Or something else to get the text color - if (!Native.FPDFPageObj_GetStrokeColor(Handle, out var r, out var g, out var b, out var a)) + get + { + if (!Native.FPDFPageObj_GetStrokeColor(Handle, out var r, out var g, out var b, out var a)) + { + throw new Exception("Could not get stroke color"); + } + return (r, g, b, a); + } + set { - throw new Exception("Could not get stroke color"); + var (r, g, b, a) = value; + if (!Native.FPDFPageObj_SetStrokeColor(Handle, r, g, b, a)) + { + throw new Exception("Could not set stroke color"); + } } - return (r, g, b, a); } - public (uint r, uint g, uint b, uint a) GetFillColor() + public (uint r, uint g, uint b, uint a) FillColor { - if (!Native.FPDFPageObj_GetFillColor(Handle, out var r, out var g, out var b, out var a)) + get { - throw new Exception("Could not get fill color"); + if (!Native.FPDFPageObj_GetFillColor(Handle, out var r, out var g, out var b, out var a)) + { + throw new Exception("Could not get fill color"); + } + return (r, g, b, a); + } + set + { + var (r, g, b, a) = value; + if (!Native.FPDFPageObj_SetFillColor(Handle, r, g, b, a)) + { + throw new Exception("Could not set fill color"); + } } - return (r, g, b, a); } public string GetText(PdfText pageText) @@ -98,7 +119,19 @@ public string GetText(PdfText pageText) return Encoding.Unicode.GetString(buffer); } - public TextRenderMode TextRenderMode => (TextRenderMode) Native.FPDFTextObj_GetTextRenderMode(Handle); + public void SetText(string text) + { + if (!Native.FPDFText_SetText(Handle, text)) + { + throw new Exception("Could not set text"); + } + } + + public TextRenderMode TextRenderMode + { + get => (TextRenderMode) Native.FPDFTextObj_GetTextRenderMode(Handle); + set => Native.FPDFTextObj_SetTextRenderMode(Handle, (int) value); + } public PdfBitmap GetBitmap() { diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfText.cs b/NAPS2.Sdk/Pdf/Pdfium/PdfText.cs similarity index 90% rename from NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfText.cs rename to NAPS2.Sdk/Pdf/Pdfium/PdfText.cs index 1609a2263f..06eeb6c8a5 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfText.cs +++ b/NAPS2.Sdk/Pdf/Pdfium/PdfText.cs @@ -1,8 +1,8 @@ using System.Text; -namespace NAPS2.ImportExport.Pdf.Pdfium; +namespace NAPS2.Pdf.Pdfium; -public class PdfText : NativePdfiumObject +internal class PdfText : NativePdfiumObject { internal PdfText(IntPtr handle) : base(handle) { diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfiumErrorCode.cs b/NAPS2.Sdk/Pdf/Pdfium/PdfiumErrorCode.cs similarity index 76% rename from NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfiumErrorCode.cs rename to NAPS2.Sdk/Pdf/Pdfium/PdfiumErrorCode.cs index df25a78af9..1af17dd6ac 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfiumErrorCode.cs +++ b/NAPS2.Sdk/Pdf/Pdfium/PdfiumErrorCode.cs @@ -1,6 +1,6 @@ -namespace NAPS2.ImportExport.Pdf.Pdfium; +namespace NAPS2.Pdf.Pdfium; -public enum PdfiumErrorCode +internal enum PdfiumErrorCode { Success = 0, Unknown = 1, diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfiumException.cs b/NAPS2.Sdk/Pdf/Pdfium/PdfiumException.cs similarity index 80% rename from NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfiumException.cs rename to NAPS2.Sdk/Pdf/Pdfium/PdfiumException.cs index cda05e62f7..1c1116b80a 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfiumException.cs +++ b/NAPS2.Sdk/Pdf/Pdfium/PdfiumException.cs @@ -1,6 +1,6 @@ -namespace NAPS2.ImportExport.Pdf.Pdfium; +namespace NAPS2.Pdf.Pdfium; -public class PdfiumException : Exception +internal class PdfiumException : Exception { public PdfiumException(PdfiumErrorCode errorCode) : base($"Pdf error: {errorCode}") diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfiumNativeLibrary.cs b/NAPS2.Sdk/Pdf/Pdfium/PdfiumNativeLibrary.cs similarity index 72% rename from NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfiumNativeLibrary.cs rename to NAPS2.Sdk/Pdf/Pdfium/PdfiumNativeLibrary.cs index 3ec95f3056..b89a7ce7f3 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/PdfiumNativeLibrary.cs +++ b/NAPS2.Sdk/Pdf/Pdfium/PdfiumNativeLibrary.cs @@ -2,13 +2,13 @@ // ReSharper disable InconsistentNaming -namespace NAPS2.ImportExport.Pdf.Pdfium; +namespace NAPS2.Pdf.Pdfium; -public class PdfiumNativeLibrary : Unmanaged.NativeLibrary +internal class PdfiumNativeLibrary : Unmanaged.NativeLibrary { private static readonly Lazy LazyInstance = new(() => { - var testRoot = Environment.GetEnvironmentVariable("NAPS2_TEST_ROOT"); + var testRoot = Environment.GetEnvironmentVariable("NAPS2_TEST_DEPS"); var libraryPath = FindLibraryPath(PlatformCompat.System.PdfiumLibraryName, testRoot); var nativeLib = new PdfiumNativeLibrary(libraryPath); nativeLib.FPDF_InitLibrary(); @@ -17,9 +17,11 @@ public class PdfiumNativeLibrary : Unmanaged.NativeLibrary public static PdfiumNativeLibrary Instance => LazyInstance.Value; + public const int FPDFBitmap_Gray = 1; public const int FPDFBitmap_BGR = 2; public const int FPDFBitmap_BGRA = 4; + public const int FPDF_ANNOT = 0x01; public const int FPDF_PRINTING = 0x800; public const int FPDF_REVERSE_BYTE_ORDER = 0x10; @@ -30,6 +32,8 @@ public class PdfiumNativeLibrary : Unmanaged.NativeLibrary public const int FPDF_PAGEOBJ_TEXT = 1; public const int FPDF_PAGEOBJ_IMAGE = 3; + public const int FPDF_FONT_TRUETYPE = 2; + public PdfiumNativeLibrary(string libraryPath) : base(libraryPath) { @@ -61,18 +65,16 @@ public delegate void FPDFBitmap_FillRect_delegate(IntPtr bitmap, int left, int t public delegate IntPtr FPDF_CreateNewDocument_delegate(); - public delegate IntPtr FPDF_LoadDocument_delegate([MarshalAs(UnmanagedType.LPStr)] string filePath, - [MarshalAs(UnmanagedType.LPStr)] - string? password); + public delegate IntPtr FPDF_LoadDocument_delegate(byte[] filePath, byte[]? password); - public delegate IntPtr FPDF_LoadMemDocument_delegate(IntPtr buffer, int size, - [MarshalAs(UnmanagedType.LPStr)] - string? password); + public delegate IntPtr FPDF_LoadMemDocument_delegate(IntPtr buffer, int size, byte[]? password); public delegate void FPDF_CloseDocument_delegate(IntPtr document); public delegate bool FPDF_SaveAsCopy_delegate(IntPtr document, ref FPDF_FileWrite fileWrite, int flags); + public delegate bool FPDF_GetFileVersion_delegate(IntPtr document, out int fileVersion); + public delegate IntPtr FPDF_GetMetaText_delegate(IntPtr document, [MarshalAs(UnmanagedType.LPStr)] string tag, byte[]? buffer, IntPtr buflen); @@ -96,7 +98,8 @@ public delegate void FPDF_RenderPageBitmap_delegate(IntPtr bitmap, IntPtr page, public delegate IntPtr FPDFText_ClosePage_delegate(IntPtr text_page); public delegate bool FPDF_ImportPages_delegate(IntPtr dest_doc, IntPtr src_doc, - [MarshalAs(UnmanagedType.LPStr)] string? pagerange, int index); + [MarshalAs(UnmanagedType.LPStr)] + string? pagerange, int index); public delegate int FPDFText_CountChars_delegate(IntPtr text_page); @@ -107,6 +110,20 @@ public delegate bool FPDFText_GetCharBox_delegate(IntPtr text_page, int index, o public delegate IntPtr FPDFPageObj_NewImageObj_delegate(IntPtr document); + public delegate IntPtr FPDFPageObj_NewTextObj_delegate(IntPtr document, + [MarshalAs(UnmanagedType.LPStr)] + string font, float font_size); + + public delegate IntPtr FPDFPageObj_CreateTextObj_delegate(IntPtr document, IntPtr font, float font_size); + + public delegate IntPtr FPDFText_LoadFont_delegate(IntPtr document, byte[] data, int size, int font_type, bool cid); + + public delegate void FPDFFont_Close_delegate(IntPtr font); + + public delegate bool FPDFText_SetText_delegate(IntPtr text_object, + [MarshalAs(UnmanagedType.LPWStr)] + string text); + public delegate void FPDFPageObj_Destroy_delegate(IntPtr page_obj); public delegate void FPDFPage_InsertObject_delegate(IntPtr page, IntPtr page_obj); @@ -133,13 +150,16 @@ public delegate bool FPDFImageObj_LoadJpegFileInline_delegate(IntPtr pages, int public delegate bool FPDFPage_RemoveObject_delegate(IntPtr page, IntPtr page_obj); + public delegate int FPDFPage_GetAnnotCount_delegate(IntPtr page); + public delegate bool FPDFPage_HasTransparency_delegate(IntPtr page); public delegate IntPtr FPDFImageObj_GetBitmap_delegate(IntPtr image_object); public delegate IntPtr FPDFImageObj_GetImageDataRaw_delegate(IntPtr image_object, byte[]? buffer, IntPtr buflen); - public delegate IntPtr FPDFImageObj_GetImageDataDecoded_delegate(IntPtr image_object, byte[]? buffer, IntPtr buflen); + public delegate IntPtr + FPDFImageObj_GetImageDataDecoded_delegate(IntPtr image_object, byte[]? buffer, IntPtr buflen); public delegate IntPtr FPDFImageObj_GetRenderedBitmap_delegate(IntPtr document, IntPtr page, IntPtr image_object); @@ -160,6 +180,10 @@ public delegate bool FPDFPageObj_GetStrokeColor_delegate(IntPtr page_object, out public delegate bool FPDFPageObj_GetFillColor_delegate(IntPtr page_object, out uint r, out uint g, out uint b, out uint a); + public delegate bool FPDFPageObj_SetStrokeColor_delegate(IntPtr page_object, uint r, uint g, uint b, uint a); + + public delegate bool FPDFPageObj_SetFillColor_delegate(IntPtr page_object, uint r, uint g, uint b, uint a); + public delegate int FPDFPageObj_GetType_delegate(IntPtr page_object); public delegate IntPtr @@ -167,6 +191,15 @@ public delegate IntPtr public delegate int FPDFTextObj_GetTextRenderMode_delegate(IntPtr text); + public delegate bool FPDFTextObj_SetTextRenderMode_delegate(IntPtr text, int render_mode); + + public delegate IntPtr FPDFDOC_InitFormFillEnvironment_delegate(IntPtr document, IntPtr formInfo); + + public delegate void FPDFDOC_ExitFormFillEnvironment_delegate(IntPtr handle); + + public delegate void FPDF_FFLDraw_delegate(IntPtr handle, IntPtr bitmap, IntPtr page, int start_x, int start_y, int size_x, + int size_y, int rotate, int flags); + public FPDF_InitLibrary_delegate FPDF_InitLibrary => Load(); public FPDF_GetLastError_delegate FPDF_GetLastError => Load(); public FPDFBitmap_Create_delegate FPDFBitmap_Create => Load(); @@ -183,6 +216,7 @@ public delegate IntPtr public FPDF_LoadMemDocument_delegate FPDF_LoadMemDocument => Load(); public FPDF_CloseDocument_delegate FPDF_CloseDocument => Load(); public FPDF_SaveAsCopy_delegate FPDF_SaveAsCopy => Load(); + public FPDF_GetFileVersion_delegate FPDF_GetFileVersion => Load(); public FPDF_GetMetaText_delegate FPDF_GetMetaText => Load(); public FPDF_GetPageCount_delegate FPDF_GetPageCount => Load(); public FPDF_LoadPage_delegate FPDF_LoadPage => Load(); @@ -198,13 +232,21 @@ public delegate IntPtr public FPDFText_GetUnicode_delegate FPDFText_GetUnicode => Load(); public FPDFText_GetCharBox_delegate FPDFText_GetCharBox => Load(); public FPDFPageObj_NewImageObj_delegate FPDFPageObj_NewImageObj => Load(); + public FPDFPageObj_NewTextObj_delegate FPDFPageObj_NewTextObj => Load(); + public FPDFPageObj_CreateTextObj_delegate FPDFPageObj_CreateTextObj => Load(); + public FPDFText_LoadFont_delegate FPDFText_LoadFont => Load(); + public FPDFFont_Close_delegate FPDFFont_Close => Load(); + public FPDFText_SetText_delegate FPDFText_SetText => Load(); public FPDFPageObj_Destroy_delegate FPDFPageObj_Destroy => Load(); public FPDFPage_InsertObject_delegate FPDFPage_InsertObject => Load(); public FPDFImageObj_SetBitmap_delegate FPDFImageObj_SetBitmap => Load(); public FPDFPage_New_delegate FPDFPage_New => Load(); public FPDFPage_GenerateContent_delegate FPDFPage_GenerateContent => Load(); public FPDFPageObj_SetMatrix_delegate FPDFPageObj_SetMatrix => Load(); - public FPDFPageObj_HasTransparency_delegate FPDFPageObj_HasTransparency => Load(); + + public FPDFPageObj_HasTransparency_delegate FPDFPageObj_HasTransparency => + Load(); + public FPDFImageObj_LoadJpegFile_delegate FPDFImageObj_LoadJpegFile => Load(); public FPDFImageObj_LoadJpegFileInline_delegate FPDFImageObj_LoadJpegFileInline => @@ -213,10 +255,15 @@ public delegate IntPtr public FPDFPage_CountObjects_delegate FPDFPage_CountObjects => Load(); public FPDFPage_GetObject_delegate FPDFPage_GetObject => Load(); public FPDFPage_RemoveObject_delegate FPDFPage_RemoveObject => Load(); + public FPDFPage_GetAnnotCount_delegate FPDFPage_GetAnnotCount => Load(); public FPDFPage_HasTransparency_delegate FPDFPage_HasTransparency => Load(); public FPDFImageObj_GetBitmap_delegate FPDFImageObj_GetBitmap => Load(); - public FPDFImageObj_GetImageDataRaw_delegate FPDFImageObj_GetImageDataRaw => Load(); - public FPDFImageObj_GetImageDataDecoded_delegate FPDFImageObj_GetImageDataDecoded => Load(); + + public FPDFImageObj_GetImageDataRaw_delegate FPDFImageObj_GetImageDataRaw => + Load(); + + public FPDFImageObj_GetImageDataDecoded_delegate FPDFImageObj_GetImageDataDecoded => + Load(); public FPDFImageObj_GetRenderedBitmap_delegate FPDFImageObj_GetRenderedBitmap => Load(); @@ -238,12 +285,29 @@ public delegate IntPtr public FPDFPageObj_GetFillColor_delegate FPDFPageObj_GetFillColor => Load(); + public FPDFPageObj_SetStrokeColor_delegate FPDFPageObj_SetStrokeColor => + Load(); + + public FPDFPageObj_SetFillColor_delegate FPDFPageObj_SetFillColor => + Load(); + public FPDFPageObj_GetType_delegate FPDFPageObj_GetType => Load(); public FPDFTextObj_GetText_delegate FPDFTextObj_GetText => Load(); public FPDFTextObj_GetTextRenderMode_delegate FPDFTextObj_GetTextRenderMode => Load(); + public FPDFTextObj_SetTextRenderMode_delegate FPDFTextObj_SetTextRenderMode => + Load(); + + public FPDFDOC_InitFormFillEnvironment_delegate FPDFDOC_InitFormFillEnvironment => + Load(); + + public FPDFDOC_ExitFormFillEnvironment_delegate FPDFDOC_ExitFormFillEnvironment => + Load(); + + public FPDF_FFLDraw_delegate FPDF_FFLDraw => Load(); + public struct FPDF_FileWrite { public int version; @@ -264,4 +328,44 @@ public struct FPDF_FileAccess [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate int GetBlock_delegate(IntPtr param, IntPtr position, IntPtr buffer, IntPtr size); + +#pragma warning disable CS0169 + public struct FPDF_FormFillInfo + { + public int version; + + private IntPtr Release; + private IntPtr FFI_Invalidate; + private IntPtr FFI_OutputSelectedRect; + private IntPtr FFI_SetCursor; + private IntPtr FFI_SetTimer; + private IntPtr FFI_KillTimer; + private IntPtr FFI_GetLocalTime; + private IntPtr FFI_OnChange; + private IntPtr FFI_GetPage; + private IntPtr FFI_GetCurrentPage; + private IntPtr FFI_GetRotation; + private IntPtr FFI_ExecuteNamedAction; + private IntPtr FFI_SetTextFieldFocus; + private IntPtr FFI_DoURIAction; + private IntPtr FFI_DoGoToAction; + + private IntPtr m_pJsPlatform; + + private IntPtr FFI_DisplayCaret; + private IntPtr FFI_GetCurrentPageIndex; + private IntPtr FFI_SetCurrentPage; + private IntPtr FFI_GotoURL; + private IntPtr FFI_GetPageViewRect; + private IntPtr FFI_PageEvent; + private IntPtr FFI_PopupMenu; + private IntPtr FFI_OpenFile; + private IntPtr FFI_EmailTo; + private IntPtr FFI_UploadTo; + private IntPtr FFI_GetPlatform; + private IntPtr FFI_GetLanguage; + private IntPtr FFI_DownloadFromURL; + private IntPtr FFI_PostRequestURL; + private IntPtr FFI_PutRequestURL; + } } \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/TextRenderMode.cs b/NAPS2.Sdk/Pdf/Pdfium/TextRenderMode.cs similarity index 70% rename from NAPS2.Sdk/ImportExport/Pdf/Pdfium/TextRenderMode.cs rename to NAPS2.Sdk/Pdf/Pdfium/TextRenderMode.cs index c793799935..56bc3cbfbf 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/Pdfium/TextRenderMode.cs +++ b/NAPS2.Sdk/Pdf/Pdfium/TextRenderMode.cs @@ -1,6 +1,6 @@ -namespace NAPS2.ImportExport.Pdf.Pdfium; +namespace NAPS2.Pdf.Pdfium; -public enum TextRenderMode +internal enum TextRenderMode { Unknown = -1, Fill = 0, diff --git a/NAPS2.Sdk/Pdf/PdfiumFontSubsets.cs b/NAPS2.Sdk/Pdf/PdfiumFontSubsets.cs new file mode 100644 index 0000000000..178d88231e --- /dev/null +++ b/NAPS2.Sdk/Pdf/PdfiumFontSubsets.cs @@ -0,0 +1,39 @@ +using NAPS2.Ocr; +using NAPS2.Pdf.Pdfium; +using PdfSharpCore.Utils; + +namespace NAPS2.Pdf; + +/// +/// Creates and manages the lifetime of font subsets for Pdfium exporting. +/// +internal class PdfiumFontSubsets : IDisposable +{ + private readonly Dictionary _fonts; + + public PdfiumFontSubsets(PdfDocument pdfiumDocument, IEnumerable ocrResults) + { + var fontSubsetBuilders = new Dictionary(); + foreach (var element in ocrResults.WhereNotNull().SelectMany(result => result.Words)) + { + // Map the OCR language to a font that supports its glyphs + var fontName = PdfFontPicker.GetBestFont(element.LanguageCode); + // TODO: What happens if the font name isn't found? + var builder = fontSubsetBuilders.GetOrSet(fontName, () => new FontSubsetBuilder(fontName)); + // Include the glyphs from the current text in the font subset + builder.AddGlyphs(element.Text); + } + // Load each font subset into Pdfium + _fonts = fontSubsetBuilders.ToDictionary(kvp => kvp.Key, kvp => pdfiumDocument.LoadFont(kvp.Value.Build())); + } + + public PdfFont this[string fontName] => _fonts[fontName]; + + public void Dispose() + { + foreach (var font in _fonts.Values) + { + font.Dispose(); + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfiumImageExtractor.cs b/NAPS2.Sdk/Pdf/PdfiumImageExtractor.cs similarity index 58% rename from NAPS2.Sdk/ImportExport/Pdf/PdfiumImageExtractor.cs rename to NAPS2.Sdk/Pdf/PdfiumImageExtractor.cs index 0c9b80dc41..1f69e47797 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfiumImageExtractor.cs +++ b/NAPS2.Sdk/Pdf/PdfiumImageExtractor.cs @@ -1,17 +1,17 @@ using NAPS2.Images.Bitwise; -using NAPS2.ImportExport.Pdf.Pdfium; +using NAPS2.Pdf.Pdfium; -namespace NAPS2.ImportExport.Pdf; +namespace NAPS2.Pdf; -public static class PdfiumImageExtractor +internal static class PdfiumImageExtractor { - public static IMemoryImage? GetSingleImage(ImageContext imageContext, PdfPage page) + public static IMemoryImage? GetSingleImage(ImageContext imageContext, PdfPage page, bool ignoreHiddenText) { - using var imageObj = GetSingleImageObject(page); + using var imageObj = GetSingleImageObject(page, ignoreHiddenText); if (imageObj != null) { var metadata = imageObj.ImageMetadata; - var image = GetImageFromObject(imageContext, imageObj, metadata); + var image = GetImageFromObject(imageContext, page, imageObj, metadata); if (image != null) { image.SetResolution((int) Math.Round(metadata.HorizontalDpi), (int) Math.Round(metadata.VerticalDpi)); @@ -23,20 +23,27 @@ public static class PdfiumImageExtractor // TODO: This could be wrong if the image object has a mask, but GetRenderedBitmap does a re-encode which we don't really want // Ideally we would be do this conditionally based on the presence of a mask, if pdfium could provide us that info - private static IMemoryImage? GetImageFromObject(ImageContext imageContext, PdfPageObject imageObj, + private static IMemoryImage? GetImageFromObject(ImageContext imageContext, PdfPage page, PdfPageObject imageObj, PdfImageMetadata metadata) { + // Otherwise we render the entire page with the known image dimensions + return ExtractRawImageData(imageContext, imageObj, metadata) ?? + RenderPdfPageToNewImage(imageContext, page, metadata); + } + + private static IMemoryImage? ExtractRawImageData(ImageContext imageContext, PdfPageObject imageObj, + PdfImageMetadata metadata) + { + if (metadata.Colorspace is not (Colorspace.DeviceRgb or Colorspace.DeviceGray or Colorspace.Indexed)) + { + return null; + } // TODO: This condition is never actually true for some reason, we need to use this code path if there is either a monochrome mask or softmask // TODO: Might need a pdfium fix. if (imageObj.HasTransparency) { // If the image has transparency, that implies the bitmap has a mask, so we need to use GetRenderedBitmap // to apply the mask and get the correct image. - using var pdfBitmap = imageObj.GetRenderedBitmap(); - if (pdfBitmap.Format is ImagePixelFormat.RGB24 or ImagePixelFormat.ARGB32) - { - return CopyPdfBitmapToNewImage(imageContext, pdfBitmap, metadata); - } return null; } // First try and read the raw image data, this is most efficient if we can handle it @@ -46,6 +53,7 @@ public static class PdfiumImageExtractor } if (imageObj.HasImageFilters("FlateDecode")) { + // TODO: Add tests for these cases to PdfiumPdfRendererTests if (metadata.BitsPerPixel == 24 && metadata.Colorspace == Colorspace.DeviceRgb) { return LoadRaw(imageContext, imageObj.GetImageDataDecoded(), metadata, ImagePixelFormat.RGB24, @@ -57,35 +65,49 @@ public static class PdfiumImageExtractor SubPixelType.Bit); } } - if (imageObj.HasImageFilters("CCITTFaxDecode")) - { - return CcittReader.LoadRawCcitt(imageContext, imageObj.GetImageDataDecoded(), metadata); - } - // If we can't read the raw data ourselves, we can try and rely on Pdfium to materialize a bitmap, which is a - // bit less efficient - // TODO: Maybe add support for black & white here too, with tests - // TODO: Also this won't have test coverage if everything is covered by the "raw" tests, maybe either find a - // test case or just have a switch to test this specifically - // TODO: Is 32 bit even possible here? As alpha is implemented with masks - if (metadata.BitsPerPixel == 24 || metadata.BitsPerPixel == 32) - { - using var pdfBitmap = imageObj.GetBitmap(); - if (pdfBitmap.Format is ImagePixelFormat.RGB24 or ImagePixelFormat.ARGB32) - { - return CopyPdfBitmapToNewImage(imageContext, pdfBitmap, metadata); - } - } - // Otherwise we fall back to relying on Pdfium to render the whole page which is least efficient and won't have - // the correct DPI + // Previously we also had a way to load the raw CCITTFaxDecode filter with a custom CcittReader class, but that + // failed in some cases (https://github.com/cyanfish/naps2/issues/117) return null; } + private static IMemoryImage RenderPdfPageToNewImage(ImageContext imageContext, PdfPage page, + PdfImageMetadata metadata) + { + // This maintains the correct image dimensions/resolution as we have that info from the metadata. + using var pdfBitmap = RenderPdfPageToBitmap(page, metadata); + return CopyPdfBitmapToNewImage(imageContext, pdfBitmap, metadata); + } + + private static PdfBitmap RenderPdfPageToBitmap(PdfPage page, PdfImageMetadata imageMetadata) + { + var w = imageMetadata.Width; + var h = imageMetadata.Height; + var format = imageMetadata.BitsPerPixel switch + { + 1 or 8 => PdfiumNativeLibrary.FPDFBitmap_Gray, + 24 => PdfiumNativeLibrary.FPDFBitmap_BGR, + 32 => PdfiumNativeLibrary.FPDFBitmap_BGRA, + _ => throw new ArgumentException() + }; + var pdfiumBitmap = PdfBitmap.Create(w, h, format); + pdfiumBitmap.FillRect(0, 0, w, h, PdfBitmap.WHITE); + pdfiumBitmap.RenderPage(page, 0, 0, w, h); + return pdfiumBitmap; + } + private static IMemoryImage CopyPdfBitmapToNewImage(ImageContext imageContext, PdfBitmap pdfBitmap, PdfImageMetadata imageMetadata) { - var dstImage = imageContext.Create(pdfBitmap.Width, pdfBitmap.Height, pdfBitmap.Format); + var (targetPixelFormat, subPixelType) = imageMetadata.BitsPerPixel switch + { + 1 => (ImagePixelFormat.BW1, SubPixelType.Gray), + 8 => (ImagePixelFormat.Gray8, SubPixelType.Gray), + 24 => (ImagePixelFormat.RGB24, SubPixelType.Bgr), + 32 => (ImagePixelFormat.ARGB32, SubPixelType.Bgra), + _ => throw new ArgumentException() + }; + var dstImage = imageContext.Create(pdfBitmap.Width, pdfBitmap.Height, targetPixelFormat); dstImage.SetResolution(imageMetadata.HorizontalDpi, imageMetadata.VerticalDpi); - var subPixelType = pdfBitmap.Format == ImagePixelFormat.ARGB32 ? SubPixelType.Bgra : SubPixelType.Bgr; var srcPixelInfo = new PixelInfo(pdfBitmap.Width, pdfBitmap.Height, subPixelType, pdfBitmap.Stride); new CopyBitwiseImageOp().Perform(pdfBitmap.Buffer, srcPixelInfo, dstImage); return dstImage; @@ -101,8 +123,12 @@ private static IMemoryImage LoadRaw(ImageContext imageContext, byte[] buffer, Pd return image; } - public static PdfPageObject? GetSingleImageObject(PdfPage page) + public static PdfPageObject? GetSingleImageObject(PdfPage page, bool ignoreHiddenText) { + if (page.AnnotCount > 0) + { + return null; + } using var pageText = page.GetText(); PdfPageObject? imageObject = null; var objectCount = page.ObjectCount; @@ -112,11 +138,13 @@ private static IMemoryImage LoadRaw(ImageContext imageContext, byte[] buffer, Pd var pageObj = page.GetObject(i); // TODO: We could consider, even in cases where we don't have an exact matrix match etc., getting a smarter dpi estimate. // TODO: But it's not clear how well that will render if there's a subpixel offset. - if (pageObj.IsImage && pageObj.Matrix == PdfMatrix.FillPage(page.Width, page.Height) && imageObject == null) + if (pageObj.IsImage && + PdfMatrix.EqualsWithinTolerance(pageObj.Matrix, PdfMatrix.FillPage(page.Width, page.Height)) && + imageObject == null) { imageObject = pageObj; } - else if (pageObj.IsText && (imageObject == null || IsInvisibleText(pageObj))) + else if (ignoreHiddenText && pageObj.IsText && (imageObject == null || IsInvisibleText(pageObj))) { // Skip invisible text or text that's underneath the image // TODO: This could be wrong if the image object has transparency @@ -133,7 +161,7 @@ private static IMemoryImage LoadRaw(ImageContext imageContext, byte[] buffer, Pd private static bool IsInvisibleText(PdfPageObject pageObj) { return pageObj.TextRenderMode == TextRenderMode.Invisible - || pageObj.TextRenderMode == TextRenderMode.Fill && pageObj.GetFillColor().a == 0 - || pageObj.TextRenderMode == TextRenderMode.Stroke && pageObj.GetStrokeColor().a == 0; + || pageObj is { TextRenderMode: TextRenderMode.Fill, FillColor.a: 0 } + || pageObj is { TextRenderMode: TextRenderMode.Stroke, StrokeColor.a: 0 }; } } \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfiumPdfExporter.cs b/NAPS2.Sdk/Pdf/PdfiumPdfExporter.cs similarity index 96% rename from NAPS2.Sdk/ImportExport/Pdf/PdfiumPdfExporter.cs rename to NAPS2.Sdk/Pdf/PdfiumPdfExporter.cs index bf4ff91c45..8f7a683853 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfiumPdfExporter.cs +++ b/NAPS2.Sdk/Pdf/PdfiumPdfExporter.cs @@ -1,26 +1,36 @@ -using NAPS2.ImportExport.Pdf.Pdfium; +using Microsoft.Extensions.Logging; using NAPS2.Ocr; +using NAPS2.Pdf.Pdfium; using NAPS2.Scan; -namespace NAPS2.ImportExport.Pdf; +namespace NAPS2.Pdf; // TODO: Experimental. Also remember that this is failing with access violations on 32-bit (see tests). -public class PdfiumPdfExporter : IPdfExporter +internal class PdfiumPdfExporter { private readonly ScanningContext _scanningContext; + private readonly ILogger _logger; public PdfiumPdfExporter(ScanningContext scanningContext) { _scanningContext = scanningContext; + _logger = scanningContext.Logger; + } + + public Task Export(Stream stream, ICollection images, PdfExportParams? exportParams = null, OcrParams? ocrParams = null, + ProgressHandler progress = default) + { + throw new NotSupportedException(); } public async Task Export(string path, ICollection images, - PdfExportParams exportParams, OcrParams? ocrParams = null, ProgressHandler progress = default) + PdfExportParams? exportParams = null, OcrParams? ocrParams = null, ProgressHandler progress = default) { return await Task.Run(() => { lock (PdfiumNativeLibrary.Instance) { + exportParams ??= new PdfExportParams(); var compat = exportParams.Compat; using var document = PdfDocument.CreateNew(); @@ -65,7 +75,7 @@ public async Task Export(string path, ICollection images, var activeEngine = _scanningContext.OcrEngine; if (activeEngine == null) { - Log.Error("Supported OCR engine not installed.", ocrParams.LanguageCode); + _logger.LogError("Supported OCR engine not installed."); } else { diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfiumPdfReader.cs b/NAPS2.Sdk/Pdf/PdfiumPdfReader.cs similarity index 74% rename from NAPS2.Sdk/ImportExport/Pdf/PdfiumPdfReader.cs rename to NAPS2.Sdk/Pdf/PdfiumPdfReader.cs index 122bf0e129..e98d628660 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfiumPdfReader.cs +++ b/NAPS2.Sdk/Pdf/PdfiumPdfReader.cs @@ -1,9 +1,9 @@ using System.Runtime.InteropServices; -using NAPS2.ImportExport.Pdf.Pdfium; +using NAPS2.Pdf.Pdfium; -namespace NAPS2.ImportExport.Pdf; +namespace NAPS2.Pdf; -public class PdfiumPdfReader +internal class PdfiumPdfReader { public PdfMetadata ReadMetadata(string path, string? password = null) { @@ -37,18 +37,10 @@ public IEnumerable ReadTextByPage(byte[] buffer, int length, string? pas { lock (PdfiumNativeLibrary.Instance) { - var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); - try - { - using var doc = PdfDocument.Load(handle.AddrOfPinnedObject(), length, password); - foreach (var text in DoReadTextByPage(doc)) - { - yield return text; - } - } - finally + using var doc = PdfDocument.Load(buffer, length, password); + foreach (var text in DoReadTextByPage(doc)) { - handle.Free(); + yield return text; } } } diff --git a/NAPS2.Sdk/Pdf/PdfiumPdfRenderer.cs b/NAPS2.Sdk/Pdf/PdfiumPdfRenderer.cs new file mode 100644 index 0000000000..a3de423ac5 --- /dev/null +++ b/NAPS2.Sdk/Pdf/PdfiumPdfRenderer.cs @@ -0,0 +1,113 @@ +using NAPS2.Images.Bitwise; +using NAPS2.Pdf.Pdfium; + +namespace NAPS2.Pdf; + +internal class PdfiumPdfRenderer : IPdfRenderer +{ + public IEnumerable Render(ImageContext imageContext, string path, PdfRenderSize renderSize, + string? password = null) + { + // Pdfium is not thread-safe + lock (PdfiumNativeLibrary.Instance) + { + using var doc = PdfDocument.Load(path, password); + foreach (var memoryImage in RenderDocument(imageContext, renderSize, doc)) + { + yield return memoryImage; + } + } + } + + public IEnumerable Render(ImageContext imageContext, byte[] buffer, int length, + PdfRenderSize renderSize, string? password = null) + { + // Pdfium is not thread-safe + lock (PdfiumNativeLibrary.Instance) + { + using var doc = PdfDocument.Load(buffer, length, password); + foreach (var memoryImage in RenderDocument(imageContext, renderSize, doc)) + { + yield return memoryImage; + } + } + } + + public IMemoryImage RenderPage(ImageContext imageContext, string path, PdfRenderSize renderSize, + int pageIndex = 0, string? password = null) + { + // Pdfium is not thread-safe + lock (PdfiumNativeLibrary.Instance) + { + using var doc = PdfDocument.Load(path, password); + return RenderDocument(imageContext, renderSize, doc, pageIndex).Single(); + } + } + + public IMemoryImage RenderPage(ImageContext imageContext, byte[] buffer, int length, PdfRenderSize renderSize, + int pageIndex, string? password = null) + { + // Pdfium is not thread-safe + lock (PdfiumNativeLibrary.Instance) + { + using var doc = PdfDocument.Load(buffer, length, password); + return RenderDocument(imageContext, renderSize, doc, pageIndex).Single(); + } + } + + private IEnumerable RenderDocument(ImageContext imageContext, PdfRenderSize renderSize, + PdfDocument doc, int? pageIndex = null) + { + var pageCount = doc.PageCount; + int start = pageIndex ?? 0; + int end = pageIndex ?? pageCount - 1; + for (int i = start; i <= end; i++) + { + using var page = doc.GetPage(i); + + if (!NoExtraction) + { + var image = PdfiumImageExtractor.GetSingleImage(imageContext, page, true); + if (image != null) + { + yield return image; + continue; + } + } + yield return RenderPageToNewImage(imageContext, page, i, renderSize); + } + } + + public IMemoryImage RenderPageToNewImage(ImageContext imageContext, PdfPage page, int pageIndex, + PdfRenderSize renderSize) + { + var widthInInches = page.Width / 72; + var heightInInches = page.Height / 72; + + var (widthInPx, heightInPx, xDpi, yDpi) = renderSize.GetDimensions(widthInInches, heightInInches, pageIndex); + + var bitmap = imageContext.Create(widthInPx, heightInPx, ImagePixelFormat.RGB24); + bitmap.SetResolution(xDpi, yDpi); + + using var pdfiumBitmap = + PdfBitmap.Create(widthInPx, heightInPx, PdfiumNativeLibrary.FPDFBitmap_BGR); + pdfiumBitmap.FillRect(0, 0, widthInPx, heightInPx, PdfBitmap.WHITE); + pdfiumBitmap.RenderPage(page, 0, 0, widthInPx, heightInPx); + + // We need to draw forms so that filled forms and signatures are visible + using var formEnv = page.Document.CreateFormEnv(); + formEnv.DrawForms(pdfiumBitmap, page); + + var pixelInfo = new PixelInfo(pdfiumBitmap.Width, pdfiumBitmap.Height, SubPixelType.Bgr, pdfiumBitmap.Stride); + new CopyBitwiseImageOp().Perform(pdfiumBitmap.Buffer, pixelInfo, bitmap); + + return bitmap; + } + + /// + /// If true, full Pdfium rendering will always be used instead of the more efficient (and resolution-preserving) + /// direct image extraction. This can be set for tests to ensure that any incompatibilities with the encoded image + /// are identified. + /// + public bool NoExtraction { get; set; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Pdf/PdfiumWorkerCoordinator.cs b/NAPS2.Sdk/Pdf/PdfiumWorkerCoordinator.cs new file mode 100644 index 0000000000..7fea7e7dba --- /dev/null +++ b/NAPS2.Sdk/Pdf/PdfiumWorkerCoordinator.cs @@ -0,0 +1,50 @@ +using NAPS2.Remoting.Worker; + +namespace NAPS2.Pdf; + +internal class PdfiumWorkerCoordinator : IPdfRenderer +{ + private readonly WorkerPool _workerPool; + + public PdfiumWorkerCoordinator(WorkerPool workerPool) + { + _workerPool = workerPool; + } + + public IEnumerable Render(ImageContext imageContext, string path, PdfRenderSize renderSize, + string? password = null) + { + if (password != null) + { + // TODO: Do we want to implement this? + throw new InvalidOperationException(); + } + var image = _workerPool.Use( + WorkerType.Native, + worker => + { + // TODO: Transmit render size + var imageStream = new MemoryStream(worker.Service.RenderPdf(path, renderSize.Dpi ?? 300)); + return imageContext.Load(imageStream); + }); + return new[] { image }; + } + + public IEnumerable Render(ImageContext imageContext, byte[] buffer, int length, + PdfRenderSize renderSize, string? password = null) + { + throw new NotImplementedException(); + } + + public IMemoryImage RenderPage(ImageContext imageContext, string path, PdfRenderSize renderSize, int pageIndex, + string? password = null) + { + throw new NotImplementedException(); + } + + public IMemoryImage RenderPage(ImageContext imageContext, byte[] buffer, int length, PdfRenderSize renderSize, int pageIndex, + string? password = null) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Platform/IRuntimeCompat.cs b/NAPS2.Sdk/Platform/IRuntimeCompat.cs deleted file mode 100644 index e8da94dd6e..0000000000 --- a/NAPS2.Sdk/Platform/IRuntimeCompat.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace NAPS2.Platform; - -// TODO: Remove this class as we no longer support mono -public interface IRuntimeCompat -{ - bool UseToolStripRenderHack { get; } - - bool SetToolbarFont { get; } - - bool IsImagePaddingSupported { get; } - - bool IsToolbarTextboxSupported { get; } - - bool SetImageListSizeOnImageCollection { get; } - - bool UseSpaceInListViewItem { get; } - - bool RefreshListViewAfterChange { get; } - - string? ExeRunner { get; } - - bool UseWorker { get; } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Platform/ISystemCompat.cs b/NAPS2.Sdk/Platform/ISystemCompat.cs index e2284d97a4..9546da1bc3 100644 --- a/NAPS2.Sdk/Platform/ISystemCompat.cs +++ b/NAPS2.Sdk/Platform/ISystemCompat.cs @@ -1,6 +1,6 @@ namespace NAPS2.Platform; -public interface ISystemCompat +internal interface ISystemCompat { bool IsWiaDriverSupported { get; } @@ -10,18 +10,39 @@ public interface ISystemCompat bool IsSaneDriverSupported { get; } + bool IsEsclDriverSupported { get; } + + bool SupportsTheme { get; } + + bool SupportsShowPageNumbers { get; } + + bool SupportsProfilesToolbar { get; } + + bool SupportsButtonActions { get; } + + bool SupportsKeyboardShortcuts { get; } + + bool SupportsSingleInstance { get; } + bool CanUseWin32 { get; } - // TODO: Implement Print/Email on Mac/Linux bool CanEmail { get; } bool CanPrint { get; } - bool UseSystemTesseract { get; } + bool CombinedPdfAndImageSaving { get; } + + bool ShouldRememberBackgroundOperations { get; } bool RenderInWorker { get; } - bool UseSeparateWorkerExe { get; } + bool SupportsWinX86Worker { get; } + + string? NativeWorkerAlias { get; } + + string? WinX86WorkerAlias { get; } + + string WorkerCrashMessage { get; } string[] ExeSearchPaths { get; } @@ -35,6 +56,8 @@ public interface ISystemCompat string SaneLibraryName { get; } + bool IsLibUsbReliable { get; } + IntPtr LoadLibrary(string path); IntPtr LoadSymbol(IntPtr libraryHandle, string symbol); diff --git a/NAPS2.Sdk/Platform/Linux/LinuxInterop.cs b/NAPS2.Sdk/Platform/Linux/LinuxInterop.cs index c7c6a9b37e..f129d806df 100644 --- a/NAPS2.Sdk/Platform/Linux/LinuxInterop.cs +++ b/NAPS2.Sdk/Platform/Linux/LinuxInterop.cs @@ -2,14 +2,23 @@ namespace NAPS2.Platform.Linux; -public static class LinuxInterop +internal static class LinuxInterop { - [DllImport("libc.so.6")] + [DllImport("libdl.so.2")] public static extern IntPtr dlopen(string filename, int flags); - [DllImport("libc.so.6")] + [DllImport("libdl.so.2")] public static extern string dlerror(); - [DllImport("libc.so.6")] + [DllImport("libdl.so.2")] public static extern IntPtr dlsym(IntPtr handle, string symbol); + + [DllImport("libc.so.6")] + public static extern int setenv(string name, string value, int overwrite); + + [DllImport("libc.so.6")] + public static extern int readlink(string path, byte[] buffer, int bufferSize); + + [DllImport("libc.so.6")] + public static extern int symlink(string targetPath, string linkPath); } \ No newline at end of file diff --git a/NAPS2.Sdk/Platform/LinuxSystemCompat.cs b/NAPS2.Sdk/Platform/LinuxSystemCompat.cs index 51be41b921..6d48a3eecb 100644 --- a/NAPS2.Sdk/Platform/LinuxSystemCompat.cs +++ b/NAPS2.Sdk/Platform/LinuxSystemCompat.cs @@ -1,12 +1,13 @@ -using NAPS2.Platform.Linux; +using System.Runtime.InteropServices; +using NAPS2.Platform.Linux; namespace NAPS2.Platform; -public class LinuxSystemCompat : ISystemCompat +internal class LinuxSystemCompat : ISystemCompat { private const int RTLD_LAZY = 1; private const int RTLD_GLOBAL = 8; - + public bool IsWiaDriverSupported => false; public bool IsTwainDriverSupported => false; @@ -15,21 +16,46 @@ public class LinuxSystemCompat : ISystemCompat public bool IsSaneDriverSupported => true; + public bool IsEsclDriverSupported => true; + + public bool SupportsTheme => false; + + public bool SupportsShowPageNumbers => false; + + public bool SupportsProfilesToolbar => true; + + public bool SupportsButtonActions => true; + + public bool SupportsKeyboardShortcuts => true; + + public bool SupportsSingleInstance => true; + public bool CanUseWin32 => false; - public bool CanEmail => false; + public bool CanEmail => true; + + public bool CanPrint => true; - public bool CanPrint => false; + public bool CombinedPdfAndImageSaving => false; - public bool UseSystemTesseract => false; + public bool ShouldRememberBackgroundOperations => true; public bool RenderInWorker => false; - public bool UseSeparateWorkerExe => false; + public bool SupportsWinX86Worker => false; + + public string? NativeWorkerAlias => null; + + public string? WinX86WorkerAlias => null; + + public string WorkerCrashMessage => SdkResources.WorkerCrash; public string[] ExeSearchPaths => LibrarySearchPaths; - public string[] LibrarySearchPaths => new[] { "_linux" }; + public string[] LibrarySearchPaths => new[] + { + RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "_linuxarm" : "_linux" + }; public string TesseractExecutableName => "tesseract"; @@ -38,14 +64,16 @@ public class LinuxSystemCompat : ISystemCompat public string[]? SaneLibraryDeps => null; public string SaneLibraryName => "libsane.so.1"; - + + public bool IsLibUsbReliable => true; + public IntPtr LoadLibrary(string path) => LinuxInterop.dlopen(path, RTLD_LAZY | RTLD_GLOBAL); public IntPtr LoadSymbol(IntPtr libraryHandle, string symbol) => LinuxInterop.dlsym(libraryHandle, symbol); public string GetLoadError() => LinuxInterop.dlerror(); - public void SetEnv(string name, string value) => throw new NotSupportedException(); + public void SetEnv(string name, string value) => LinuxInterop.setenv(name, value, 1); public IDisposable FileReadLock(string path) => new FileStream(path, FileMode.Open, FileAccess.Read); diff --git a/NAPS2.Sdk/Platform/Mac/MacInterop.cs b/NAPS2.Sdk/Platform/Mac/MacInterop.cs index 48f2df6314..62b11555d9 100644 --- a/NAPS2.Sdk/Platform/Mac/MacInterop.cs +++ b/NAPS2.Sdk/Platform/Mac/MacInterop.cs @@ -2,7 +2,7 @@ namespace NAPS2.Platform.Mac; -public static class MacInterop +internal static class MacInterop { [DllImport("libSystem.dylib")] public static extern IntPtr dlopen(string filename, int flags); diff --git a/NAPS2.Sdk/Platform/MacSystemCompat.cs b/NAPS2.Sdk/Platform/MacSystemCompat.cs index 72eacff288..cbf880acff 100644 --- a/NAPS2.Sdk/Platform/MacSystemCompat.cs +++ b/NAPS2.Sdk/Platform/MacSystemCompat.cs @@ -3,7 +3,7 @@ namespace NAPS2.Platform; -public class MacSystemCompat : ISystemCompat +internal class MacSystemCompat : ISystemCompat { private const int RTLD_LAZY = 1; private const int RTLD_GLOBAL = 8; @@ -16,17 +16,39 @@ public class MacSystemCompat : ISystemCompat public bool IsSaneDriverSupported => true; + public bool IsEsclDriverSupported => true; + + public bool SupportsTheme => false; + + public bool SupportsShowPageNumbers => false; + + public bool SupportsProfilesToolbar => false; + + public bool SupportsButtonActions => false; + + public bool SupportsKeyboardShortcuts => true; + + public bool SupportsSingleInstance => false; + public bool CanUseWin32 => false; - public bool CanEmail => false; + public bool CanEmail => true; + + public bool CanPrint => true; - public bool CanPrint => false; + public bool CombinedPdfAndImageSaving => true; - public bool UseSystemTesseract => false; + public bool ShouldRememberBackgroundOperations => true; public bool RenderInWorker => false; - public bool UseSeparateWorkerExe => false; + public bool SupportsWinX86Worker => false; + + public string? NativeWorkerAlias => null; + + public string? WinX86WorkerAlias => null; + + public string WorkerCrashMessage => SdkResources.WorkerCrash; public string[] ExeSearchPaths => LibrarySearchPaths; @@ -39,8 +61,10 @@ public string[] LibrarySearchPaths : "_mac"; return new[] { + "", prefix, - $"../Resources/{prefix}" // Path in .app bundle + $"../Resources/{prefix}", // Path in .app bundle, + $"../NAPS2.App/Contents/Resources/{prefix}" // Path to universal .app bundle for tests }; } } @@ -53,6 +77,8 @@ public string[] LibrarySearchPaths public string SaneLibraryName => "libsane.1.dylib"; + public bool IsLibUsbReliable => false; + public IntPtr LoadLibrary(string path) => MacInterop.dlopen(path, RTLD_LAZY | RTLD_GLOBAL); public IntPtr LoadSymbol(IntPtr libraryHandle, string symbol) => MacInterop.dlsym(libraryHandle, symbol); diff --git a/NAPS2.Sdk/Platform/PlatformCompat.cs b/NAPS2.Sdk/Platform/PlatformCompat.cs index c9e88bc84d..55cd788372 100644 --- a/NAPS2.Sdk/Platform/PlatformCompat.cs +++ b/NAPS2.Sdk/Platform/PlatformCompat.cs @@ -1,6 +1,8 @@ -namespace NAPS2.Platform; +using System.Runtime.InteropServices; -public class PlatformCompat +namespace NAPS2.Platform; + +internal class PlatformCompat { private static ISystemCompat _systemCompat; @@ -30,11 +32,12 @@ static PlatformCompat() } private static ISystemCompat GetWindowsSystemCompat() => - Environment.Is64BitProcess - ? new Windows64SystemCompat() - : Environment.Is64BitOperatingSystem - ? new Windows32On64SystemCompat() - : new Windows32SystemCompat(); + RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm64 => new WindowsArm64SystemCompat(), + Architecture.X86 => new Windows32SystemCompat(), + _ => new Windows64SystemCompat() + }; public static ISystemCompat System { diff --git a/NAPS2.Sdk/Platform/Windows/Win32.cs b/NAPS2.Sdk/Platform/Windows/Win32.cs index bc4119346a..2b9d236710 100644 --- a/NAPS2.Sdk/Platform/Windows/Win32.cs +++ b/NAPS2.Sdk/Platform/Windows/Win32.cs @@ -1,11 +1,13 @@ using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Text; namespace NAPS2.Platform.Windows; /// /// Helper class for common Win32 methods called via P/Invoke. /// -public static class Win32 +internal static class Win32 { [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam); @@ -18,13 +20,25 @@ public static class Win32 [return: MarshalAs(UnmanagedType.Bool)] public static extern bool EnableWindow(IntPtr hWnd, bool bEnable); + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetParent(IntPtr hWndChild, IntPtr hWndNewParent); + [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommands nCmdShow); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr AddDllDirectory(string directory); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr SetDllDirectory(string directory); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool SetDllDirectory(string lpPathName); + public static extern bool SetDefaultDllDirectories(int directoryFlags); + + public const int LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000; [DllImport("kernel32.dll")] public static extern IntPtr LoadLibrary(string path); @@ -35,6 +49,37 @@ public static class Win32 [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")] public static extern void CopyMemory(IntPtr dst, IntPtr src, uint len); + [DllImport("user32.dll")] + public static extern int GetMessage(out Message lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); + + [DllImport("user32.dll")] + public static extern bool TranslateMessage(in Message lpmsg); + + [DllImport("user32.dll")] + public static extern bool DispatchMessage(ref Message lpmsg); + + [DllImport("user32.dll")] + public static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr DefWindowProcW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll")] + public static extern IntPtr CreateWindowEx(int dwExStyle, string lpClassName, string? lpWindowName, int dwStyle, + int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); + + [DllImport("user32.dll")] + public static extern bool DestroyWindow(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + public static extern UInt16 RegisterClassW(in WndClass lpWndClass); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern uint RegisterWindowMessage(string lpString); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder packageFullName); + public enum ShowWindowCommands { Hide = 0, @@ -51,4 +96,138 @@ public enum ShowWindowCommands ShowDefault = 10, ForceMinimize = 11 } + + public struct Message + { + public IntPtr hWnd; + public int msg; + public IntPtr wParam; + public IntPtr lParam; + public uint time; + public Point pt; + } + + public struct Point + { + public int x; + public int y; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct WndClass + { + public uint style; + public IntPtr lpfnWndProc; + public int cbClsExtra; + public int cbWndExtra; + public IntPtr hInstance; + public IntPtr hIcon; + public IntPtr hCursor; + public IntPtr hbrBackground; + [MarshalAs(UnmanagedType.LPWStr)] public string lpszMenuName; + [MarshalAs(UnmanagedType.LPWStr)] public string lpszClassName; + } + + public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [ComImport] + [Guid("00021401-0000-0000-C000-000000000046")] + public class ShellLink; + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("000214F9-0000-0000-C000-000000000046")] + public interface IShellLink + { + void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out IntPtr pfd, + int fFlags); + + void GetIDList(out IntPtr ppidl); + void SetIDList(IntPtr pidl); + void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName); + void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); + void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath); + void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); + void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath); + void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); + void GetHotkey(out short pwHotkey); + void SetHotkey(short wHotkey); + void GetShowCmd(out int piShowCmd); + void SetShowCmd(int iShowCmd); + + void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, + out int piIcon); + + void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); + void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved); + void Resolve(IntPtr hwnd, int fFlags); + void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); + } + + [Flags] + public enum ASSOC_FILTER + { + ASSOC_FILTER_NONE = 0x00000000, + ASSOC_FILTER_RECOMMENDED = 0x00000001 + } + + [Guid("973810ae-9599-4b88-9e4d-6ee98c9552da"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IEnumAssocHandlers + { + [PreserveSig] + int Next(int celt, out IAssocHandler rgelt, out int pceltFetched); + } + + [Guid("f04061ac-1659-4a3f-a954-775aa57fc083"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IAssocHandler + { + void GetName([MarshalAs(UnmanagedType.LPWStr)] out string ppsz); + void GetUIName([MarshalAs(UnmanagedType.LPWStr)] out string ppsz); + void GetIconLocation([MarshalAs(UnmanagedType.LPWStr)] out string ppszPath, out int pIndex); + + [PreserveSig] + int IsRecommended(); + + void MakeDefault([MarshalAs(UnmanagedType.LPWStr)] string pszDescription); + void Invoke(IDataObject pdo); + void CreateInvoker(IDataObject pdo, out /*IAssocHandlerInvoker*/ object invoker); + } + + [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IShellItem + { + IDataObject BindToHandler(IBindCtx? pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid bhid, + [MarshalAs(UnmanagedType.LPStruct)] Guid riid); + } + + [Guid("B63EA76D-1F85-456F-A19C-48159EFA858B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IShellItemArray + { + IDataObject BindToHandler(IBindCtx? pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid bhid, + [MarshalAs(UnmanagedType.LPStruct)] Guid riid); + } + + [DllImport("shell32.dll")] + public static extern int SHCreateShellItemArrayFromIDLists(int cidl, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] rgpidl, + out IShellItemArray ppsiItemArray); + + [DllImport("shlwapi.dll", BestFitMapping = false, CharSet = CharSet.Unicode, ExactSpelling = true, + SetLastError = false, ThrowOnUnmappableChar = true)] + public static extern int SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, int cchOutBuf, + IntPtr ppvReserved); + + [DllImport("shell32.dll", CharSet = CharSet.Unicode)] + public static extern int SHCreateItemFromParsingName(string pszPath, IBindCtx? pbc, + [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IShellItem ppv); + + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern int SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string name, IntPtr bindingContext, + out IntPtr pidl, uint sfgaoIn, out uint psfgaoOut); + + [DllImport("shell32.dll", CharSet = CharSet.Unicode)] + public static extern int SHAssocEnumHandlers(string pszExtra, ASSOC_FILTER afFilter, + out IEnumAssocHandlers ppEnumHandler); + + [DllImport("shell32.dll")] + public static extern void ILFree(IntPtr pidl); } \ No newline at end of file diff --git a/NAPS2.Sdk/Platform/Windows/Win32MessagePump.cs b/NAPS2.Sdk/Platform/Windows/Win32MessagePump.cs new file mode 100644 index 0000000000..8781821a69 --- /dev/null +++ b/NAPS2.Sdk/Platform/Windows/Win32MessagePump.cs @@ -0,0 +1,164 @@ +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace NAPS2.Platform.Windows; + +/// +/// Allows creation of a Win32 event loop without any references to WinForms or WPF. +/// +[System.Runtime.Versioning.SupportedOSPlatform("windows")] +internal class Win32MessagePump : IInvoker, IDisposable +{ + private const string WND_CLASS_NAME = "MPWndClass"; + private const string RUN_QUEUED_ACTIONS_MESSAGE_NAME = "MPRunQueuedActions"; + + public static Win32MessagePump Create() + { + return new Win32MessagePump(); + } + + // We store the delegate as an instance variable so it doesn't get garbage collected + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable + private readonly Win32.WndProc _wndProcDelegate; + private readonly uint _runQueuedActionsMessage; + + private readonly Queue _queue = new(); + private readonly Thread? _messageLoopThread; + private bool _stopped; + + private Win32MessagePump() + { + _messageLoopThread = Thread.CurrentThread; + _wndProcDelegate = CustomWndProc; + + Win32.RegisterClassW(new Win32.WndClass + { + lpszClassName = WND_CLASS_NAME, + lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate), + hInstance = Process.GetCurrentProcess().Handle + }); + + _runQueuedActionsMessage = Win32.RegisterWindowMessage(RUN_QUEUED_ACTIONS_MESSAGE_NAME); + + Handle = Win32.CreateWindowEx(0, WND_CLASS_NAME, "", 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, + Process.GetCurrentProcess().Handle, IntPtr.Zero); + } + + public Func? Filter { get; set; } + + public ILogger Logger { get; set; } = NullLogger.Instance; + + public IntPtr Handle { get; } + + public void Invoke(Action action) + { + if (Thread.CurrentThread == _messageLoopThread) + { + action(); + return; + } + var toggle = new ManualResetEvent(false); + lock (_queue) + { + _queue.Enqueue(() => + { + action(); + toggle.Set(); + }); + Win32.PostMessage(Handle, _runQueuedActionsMessage, IntPtr.Zero, IntPtr.Zero); + } + toggle.WaitOne(); + } + + public void InvokeDispatch(Action action) + { + lock (_queue) + { + _queue.Enqueue(action); + Win32.PostMessage(Handle, _runQueuedActionsMessage, IntPtr.Zero, IntPtr.Zero); + } + } + + public T InvokeGet(Func func) + { + T value = default!; + Invoke(() => value = func()); + return value; + } + + public void RunMessageLoop() + { + try + { + while (!_stopped && Win32.GetMessage(out var msg, IntPtr.Zero, 0, 0) > 0) + { + if (!(Filter?.Invoke(Handle, msg.msg, msg.wParam, msg.lParam) ?? false)) + { + Win32.TranslateMessage(msg); + Win32.DispatchMessage(ref msg); + } + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error in message loop"); + } + } + + private void RunQueuedActions() + { + var actionsToCall = new List(); + lock (_queue) + { + while (_queue.Count > 0) + { + actionsToCall.Add(_queue.Dequeue()); + } + } + // Run the actions outside the lock to avoid deadlock scenarios + foreach (var action in actionsToCall) + { + try + { + action(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error in invoked action"); + } + } + } + + private IntPtr CustomWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if (msg == _runQueuedActionsMessage) + { + RunQueuedActions(); + return IntPtr.Zero; + } + return Win32.DefWindowProcW(hWnd, msg, wParam, lParam); + } + + public IntPtr CreateBackgroundWindow(IntPtr parent = default) + { + return InvokeGet(() => + Win32.CreateWindowEx(0, WND_CLASS_NAME, "", 0, 0, 0, 0, 0, parent, IntPtr.Zero, + Process.GetCurrentProcess().Handle, IntPtr.Zero)); + } + + public void CloseWindow(IntPtr window) + { + InvokeDispatch(() => Win32.DestroyWindow(window)); + } + + public void Dispose() + { + InvokeDispatch(() => + { + Win32.DestroyWindow(Handle); + _stopped = true; + }); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Platform/Windows/Win32TwainHandleManager.cs b/NAPS2.Sdk/Platform/Windows/Win32TwainHandleManager.cs new file mode 100644 index 0000000000..98dbf1a334 --- /dev/null +++ b/NAPS2.Sdk/Platform/Windows/Win32TwainHandleManager.cs @@ -0,0 +1,80 @@ +#if !MAC +using NAPS2.Scan.Internal.Twain; +using NTwain; + +namespace NAPS2.Platform.Windows; + +/// +/// TwainHandleManager implementation that uses a Win32MessagePump to get window handles to hand off to TWAIN. +/// +[System.Runtime.Versioning.SupportedOSPlatform("windows")] +internal class Win32TwainHandleManager : TwainHandleManager +{ + private readonly Win32MessagePump _messagePump; + private IntPtr _parentWindow; + private IntPtr _disabledWindow; + private bool _disposed; + private IntPtr? _handle; + + public Win32TwainHandleManager(Win32MessagePump messagePump) + { + _messagePump = messagePump; + } + + public override IntPtr GetDsmHandle(IntPtr dialogParent, bool useNativeUi) + { + // This handle is used for the TWAIN event loop. However, in some cases (e.g. an early error) it can still + // be used for UI. + return _handle ??= GetHandle(dialogParent, useNativeUi); + } + + public override IntPtr GetEnableHandle(IntPtr dialogParent, bool useNativeUi) + { + // This handle is used as the parent window for TWAIN UI + return _handle ??= GetHandle(dialogParent, useNativeUi); + } + + private IntPtr GetHandle(IntPtr dialogParent, bool useNativeUi) + { + // If we are expected to show UI, ideally we'd just return dialogParent. But I've found some issues with that + // where the window can become non-interactable (e.g. unable to cancel a native UI scan). The cause might be + // related to the window being in another process. + _parentWindow = _messagePump.CreateBackgroundWindow(dialogParent); + + if (dialogParent != IntPtr.Zero) + { + // At the Windows API level, a modal window is implemented by doing two things: + // 1. Setting the parent on the child window + // 2. Disabling the parent window + // We do this rather than calling ShowDialog to avoid blocking the thread. + if (useNativeUi) + { + // We only want to disable the parent window if we're showing the native UI. Otherwise, we expect that + // the NAPS2 UI should be interactable, and the only UI shown should be error messages. + Win32.EnableWindow(dialogParent, false); + } + _disabledWindow = dialogParent; + } + + return _parentWindow; + } + + public override MessageLoopHook CreateMessageLoopHook(IntPtr dialogParent = default, bool useNativeUi = false) + { + return new Win32MessageLoopHook(_messagePump, GetDsmHandle(dialogParent, useNativeUi)); + } + + public override IInvoker Invoker => _messagePump; + + public override void Dispose() + { + if (_disposed) return; + _disposed = true; + _messagePump.CloseWindow(_parentWindow); + if (_disabledWindow != IntPtr.Zero) + { + Win32.EnableWindow(_disabledWindow, true); + } + } +} +#endif \ No newline at end of file diff --git a/NAPS2.Sdk/Platform/Windows/WindowsEnvironment.cs b/NAPS2.Sdk/Platform/Windows/WindowsEnvironment.cs new file mode 100644 index 0000000000..d5e2858174 --- /dev/null +++ b/NAPS2.Sdk/Platform/Windows/WindowsEnvironment.cs @@ -0,0 +1,27 @@ +using System.Text; + +namespace NAPS2.Platform.Windows; + +internal static class WindowsEnvironment +{ + private const long APPMODEL_ERROR_NO_PACKAGE = 15700L; + + public static bool IsRunningAsMsix + { + get + { +#if NET6_0_OR_GREATER + if (OperatingSystem.IsWindowsVersionAtLeast(10)) + { + int length = 0; + var sb = new StringBuilder(0); + Win32.GetCurrentPackageFullName(ref length, sb); + sb = new StringBuilder(length); + int result = Win32.GetCurrentPackageFullName(ref length, sb); + return result != APPMODEL_ERROR_NO_PACKAGE; + } +#endif + return false; + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Platform/Windows32On64SystemCompat.cs b/NAPS2.Sdk/Platform/Windows32On64SystemCompat.cs deleted file mode 100644 index 351a5403cf..0000000000 --- a/NAPS2.Sdk/Platform/Windows32On64SystemCompat.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NAPS2.Platform; - -public class Windows32On64SystemCompat : Windows32SystemCompat -{ - // If we're running a 64-bit OS, we should prefer to run 64-bit exes and fall back to 32-bit if not found. - public override string[] ExeSearchPaths => new[] { "_win64", "_win32" }; -} \ No newline at end of file diff --git a/NAPS2.Sdk/Platform/Windows32SystemCompat.cs b/NAPS2.Sdk/Platform/Windows32SystemCompat.cs index 0365d67faa..85b2cadd8f 100644 --- a/NAPS2.Sdk/Platform/Windows32SystemCompat.cs +++ b/NAPS2.Sdk/Platform/Windows32SystemCompat.cs @@ -2,7 +2,7 @@ namespace NAPS2.Platform; -public class Windows32SystemCompat : WindowsSystemCompat +internal class Windows32SystemCompat : WindowsSystemCompat { public override string[] ExeSearchPaths => new[] { "_win32" }; diff --git a/NAPS2.Sdk/Platform/Windows64SystemCompat.cs b/NAPS2.Sdk/Platform/Windows64SystemCompat.cs index 78aba18cbe..fbed020828 100644 --- a/NAPS2.Sdk/Platform/Windows64SystemCompat.cs +++ b/NAPS2.Sdk/Platform/Windows64SystemCompat.cs @@ -1,15 +1,9 @@ -using NAPS2.Platform.Windows; +namespace NAPS2.Platform; -namespace NAPS2.Platform; - -public class Windows64SystemCompat : WindowsSystemCompat +internal class Windows64SystemCompat : WindowsSystemCompat { - public override string[] ExeSearchPaths => new[] { "_win64" }; + // If we're running a 64-bit OS, we should prefer to run 64-bit exes and fall back to 32-bit if not found. + public override string[] ExeSearchPaths => new[] { "_win64", "_win32" }; public override string[] LibrarySearchPaths => new[] { "_win64" }; - - public override IntPtr LoadSymbol(IntPtr libraryHandle, string symbol) - { - return Win32.GetProcAddress(libraryHandle, symbol); - } } \ No newline at end of file diff --git a/NAPS2.Sdk/Platform/WindowsArm64SystemCompat.cs b/NAPS2.Sdk/Platform/WindowsArm64SystemCompat.cs new file mode 100644 index 0000000000..bef5816e0f --- /dev/null +++ b/NAPS2.Sdk/Platform/WindowsArm64SystemCompat.cs @@ -0,0 +1,8 @@ +namespace NAPS2.Platform; + +internal class WindowsArm64SystemCompat : WindowsSystemCompat +{ + public override string[] ExeSearchPaths => new[] { "_winarm" }; + + public override string[] LibrarySearchPaths => new[] { "_winarm" }; +} \ No newline at end of file diff --git a/NAPS2.Sdk/Platform/WindowsSystemCompat.cs b/NAPS2.Sdk/Platform/WindowsSystemCompat.cs index d0e56f0a7e..f812f4fd4f 100644 --- a/NAPS2.Sdk/Platform/WindowsSystemCompat.cs +++ b/NAPS2.Sdk/Platform/WindowsSystemCompat.cs @@ -3,31 +3,55 @@ namespace NAPS2.Platform; -public abstract class WindowsSystemCompat : ISystemCompat +internal abstract class WindowsSystemCompat : ISystemCompat { public bool IsWiaDriverSupported => true; - public bool IsTwainDriverSupported => true; + public virtual bool IsTwainDriverSupported => true; public bool IsAppleDriverSupported => false; public bool IsSaneDriverSupported => false; + public bool IsEsclDriverSupported => true; + + public bool SupportsTheme => true; + + public bool SupportsShowPageNumbers => true; + + public bool SupportsProfilesToolbar => true; + + public bool SupportsButtonActions => true; + + public bool SupportsKeyboardShortcuts => true; + + public bool SupportsSingleInstance => true; + public bool CanUseWin32 => true; public bool CanEmail => true; public bool CanPrint => true; - public bool UseSystemTesseract => false; + public bool CombinedPdfAndImageSaving => false; + + public bool ShouldRememberBackgroundOperations => true; public bool RenderInWorker => true; - public bool UseSeparateWorkerExe => true; + public virtual bool SupportsWinX86Worker => true; + + // Due to weird MSIX permission issues, we need to run worker processes using their aliases so that Windows sets + // them up with "package identity" which will allow them to load their DLLs. + public string? NativeWorkerAlias => WindowsEnvironment.IsRunningAsMsix ? "NAPS2_Alias.exe" : null; - public abstract string[] ExeSearchPaths { get; } + public string? WinX86WorkerAlias => WindowsEnvironment.IsRunningAsMsix ? "NAPS2.Worker_Alias.exe" : null; - public abstract string[] LibrarySearchPaths { get; } + public string WorkerCrashMessage => SdkResources.WorkerCrashWindows; + + public abstract string[] ExeSearchPaths { get; } + + public abstract string[] LibrarySearchPaths { get; } public string TesseractExecutableName => "tesseract.exe"; @@ -37,11 +61,14 @@ public abstract class WindowsSystemCompat : ISystemCompat public string SaneLibraryName => "sane.dll"; + public bool IsLibUsbReliable => true; + public IntPtr LoadLibrary(string path) => Win32.LoadLibrary(path); public string GetLoadError() => Marshal.GetLastWin32Error().ToString(); - public abstract IntPtr LoadSymbol(IntPtr libraryHandle, string symbol); + public virtual IntPtr LoadSymbol(IntPtr libraryHandle, string symbol) => + Win32.GetProcAddress(libraryHandle, symbol); public void SetEnv(string name, string value) => throw new NotSupportedException(); diff --git a/NAPS2.Sdk/README.md b/NAPS2.Sdk/README.md index d017c60389..6f63a287e6 100644 --- a/NAPS2.Sdk/README.md +++ b/NAPS2.Sdk/README.md @@ -1,9 +1,94 @@ # NAPS2.Sdk -> NAPS2.Sdk is a **work in progress**. Nuget packages will be made available once it is ready for public consumption. +[![NuGet](https://img.shields.io/nuget/v/NAPS2.Sdk)](https://www.nuget.org/packages/NAPS2.Sdk/) NAPS2.Sdk is a fully-featured scanning library, supporting WIA, TWAIN, SANE, and ESCL scanners on Windows, Mac, and Linux. +## Packages + +NAPS2.Sdk is modular, and depending on your needs you may have to reference a different set of packages. + +### Required Packages + +- **[NAPS2.Sdk](https://www.nuget.org/packages/NAPS2.Sdk/)** + - Contains core scanning functionality for all platforms. +- Exactly one of: + - **[NAPS2.Images.Gdi](https://www.nuget.org/packages/NAPS2.Images.Gdi/)** + - For working with `System.Drawing.Bitmap` images. (Windows Forms) + - **[NAPS2.Images.Wpf](https://www.nuget.org/packages/NAPS2.Images.Wpf/)** + - For working with ` System.Windows.Media.Imaging` images. (WPF) + - **[NAPS2.Images.Gtk](https://www.nuget.org/packages/NAPS2.Images.Gtk/)** + - For working with `Gdk.Pixbuf` images. (Linux) + - **[NAPS2.Images.Mac](https://www.nuget.org/packages/NAPS2.Images.Mac/)** + - For working with `AppKit.NSImage` images. (Mac) + - **[NAPS2.Images.ImageSharp](https://www.nuget.org/packages/NAPS2.Images.ImageSharp/)** + - For working with [`ImageSharp`](https://github.com/SixLabors/ImageSharp) images. + +### Optional Packages + +- **[NAPS2.Sdk.Worker.Win32](https://www.nuget.org/packages/NAPS2.Sdk.Worker.Win32/)** + - For scanning with [TWAIN on Windows](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/TwainSample.cs). +- **[NAPS2.Pdfium.Binaries](https://www.nuget.org/packages/NAPS2.Pdfium.Binaries/)** + - For [importing PDFs](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/PdfImportSample.cs). +- **[NAPS2.Sane.Binaries](https://www.nuget.org/packages/NAPS2.Sane.Binaries/)** + - For [using SANE drivers]() on Mac. (Linux has them pre-installed, and Windows isn't supported.) +- **[NAPS2.Tesseract.Binaries](https://www.nuget.org/packages/NAPS2.Tesseract.Binaries/)** + - For [running OCR](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/OcrSample.cs). (You can also use a separate Tesseract installation if you like.) +- **[NAPS2.Escl.Server](https://www.nuget.org/packages/NAPS2.Escl.Server/)** + - For [sharing scanners](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/NetworkSharingSample.cs) across the local network. + +## Usage + +```c# +// Set up +using var scanningContext = new ScanningContext(new GdiImageContext()); +var controller = new ScanController(scanningContext); + +// Query for available scanning devices +var devices = await controller.GetDeviceList(); + +// Set scanning options +var options = new ScanOptions +{ + Device = devices.First(), + PaperSource = PaperSource.Feeder, + PageSize = PageSize.A4, + Dpi = 300 +}; + +// Scan and save images +int i = 1; +await foreach (var image in controller.Scan(options)) +{ + image.Save($"page{i++}.jpg"); +} + +// Scan and save PDF +var images = await controller.Scan(options).ToListAsync(); +var pdfExporter = new PdfExporter(scanningContext); +await pdfExporter.Export("doc.pdf", images); +``` + +More [samples](https://github.com/cyanfish/naps2/tree/master/NAPS2.Sdk.Samples): +- ["Hello World" scanning](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/HelloWorldSample.cs) +- [Scan and save to PDF/images](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/ScanAndSaveSample.cs) +- [Scan with TWAIN drivers](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/TwainSample.cs) +- [Scan to System.Drawing.Bitmap](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/ScanToBitmapSample.cs) +- [Import and export PDFs](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/PdfImportSample.cs) +- [Export PDFs with OCR](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/OcrSample.cs) +- [Store image data on the filesystem](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/FileStorageSample.cs) +- [Share scanners on the local network](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/NetworkSharingSample.cs) + +Also see: +- [SDK Homepage](https://www.naps2.com/sdk) +- [Full Api Docs](https://www.naps2.com/sdk/doc/api/) + +## Web Scanning with JS/TS + +NAPS2's [scanner-sharing](https://github.com/cyanfish/naps2/blob/master/NAPS2.Sdk.Samples/NetworkSharingSample.cs) server uses ESCL, which is a [standard](https://mopria.org/mopria-escl-specification) HTTP protocol and can be used from a web browser with JavaScript or TypeScript. + +See the [naps2-webscan](https://github.com/cyanfish/naps2-webscan) project for example code to scan from a browser. + ## Drivers | | Windows | Mac | Linux | @@ -24,11 +109,23 @@ Apple's [ImageCaptureCore](https://developer.apple.com/documentation/imagecaptur [ESCL](https://mopria.org/mopria-escl-specification), also known as Apple AirScan, is a standard protocol for scanning over a network. Many modern scanners support ESCL, and as it's a network protocol, specific drivers aren't required. ESCL can also be used over a USB connection in some cases. -## Usage +### Choosing a Driver + +Each platform has a default driver (WIA on Windows, Apple on Mac, and SANE on Linux). To use another driver, you only need to specify it when querying for devices: + +```c# +var devices = await controller.GetDeviceList(Driver.Twain); +``` + +### Worker Processes + +Using the TWAIN driver on Windows usually requires the calling process to be 32-bit. If you want to use TWAIN from a 64-bit process, NAPS2 provides a 32-bit worker process: -See the [Samples](https://github.com/cyanfish/naps2/tree/master/NAPS2.Sdk.Samples). +```c# +// Reference the NAPS2.Sdk.Worker.Win32 package and call this method +scanningContext.SetUpWin32Worker(); +``` ## Contributing - -Looking to contribute to NAPS2 or NAPS2.Sdk? Have a look at the [Developer Onboarding](https://www.naps2.com/doc/dev-onboarding) page. +Looking to contribute to NAPS2 or NAPS2.Sdk? Have a look at the [wiki](https://github.com/cyanfish/naps2/wiki/1.-Building-&-Development-Environment). diff --git a/NAPS2.Sdk/Remoting/ContentTypes.cs b/NAPS2.Sdk/Remoting/ContentTypes.cs new file mode 100644 index 0000000000..67e26f8141 --- /dev/null +++ b/NAPS2.Sdk/Remoting/ContentTypes.cs @@ -0,0 +1,8 @@ +namespace NAPS2.Remoting; + +internal static class ContentTypes +{ + public const string PDF = "application/pdf"; + public const string PNG = "image/png"; + public const string JPEG = "image/jpeg"; +} \ No newline at end of file diff --git a/NAPS2.Sdk/Remoting/Error.proto b/NAPS2.Sdk/Remoting/Error.proto index c1416c689a..ae462cf365 100644 --- a/NAPS2.Sdk/Remoting/Error.proto +++ b/NAPS2.Sdk/Remoting/Error.proto @@ -6,4 +6,5 @@ message Error { string type = 1; string message = 2; string stackTrace = 3; + Error innerException = 4; } diff --git a/NAPS2.Sdk/Remoting/Pipes.cs b/NAPS2.Sdk/Remoting/Pipes.cs deleted file mode 100644 index 159e9cfe15..0000000000 --- a/NAPS2.Sdk/Remoting/Pipes.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System.IO.Pipes; -using System.Text; -using System.Threading; - -namespace NAPS2.Remoting; - -/// -/// A class for simple inter-process communication between NAPS2 instances via named pipes. -/// -public static class Pipes -{ - public const string MSG_SCAN_WITH_DEVICE = "SCAN_WDEV_"; - public const string MSG_ACTIVATE = "ACTIVATE"; - public const string MSG_KILL_PIPE_SERVER = "KILL_PIPE_SERVER"; - public const string MSG_CLOSE_WINDOW = "CLOSE_WINDOW"; - - // An arbitrary non-secret unique name with a single format argument (for the process ID). - // This could be edtion/version-specific, but I like the idea that if the user is running a portable version and - // happens to have NAPS2 installed too, the scan button will propagate to the portable version. - private const string PIPE_NAME_FORMAT = "NAPS2_PIPE_v1_{0}"; - // The timeout is small since pipe connections should be on the local machine only. - private const int TIMEOUT = 1000; - - private static bool _serverRunning; - - private static string GetPipeName(Process process) - { - return string.Format(PIPE_NAME_FORMAT, process.Id); - } - - /// - /// Send a message to a NAPS2 instance running a pipe server. - /// - /// The process to send the message to. - /// The message to send. - public static bool SendMessage(Process recipient, string msg) - { - try - { - using var pipeClient = new NamedPipeClientStream(".", GetPipeName(recipient), PipeDirection.Out); - //MessageBox.Show("Sending msg:" + msg); - pipeClient.Connect(TIMEOUT); - var streamString = new StreamString(pipeClient); - streamString.WriteString(msg); - //MessageBox.Show("Sent"); - return true; - } - catch (Exception e) - { - Log.ErrorException("Error sending message through pipe", e); - return false; - } - } - - /// - /// Start a pipe server on a background thread, calling the callback each time a message is received. Only one pipe server can be running per process. - /// - /// The message callback. - public static void StartServer(Action msgCallback) - { - if (_serverRunning) - { - return; - } - var thread = new Thread(() => - { - try - { - using var pipeServer = new NamedPipeServerStream(GetPipeName(Process.GetCurrentProcess()), PipeDirection.In); - while (true) - { - pipeServer.WaitForConnection(); - var streamString = new StreamString(pipeServer); - var msg = streamString.ReadString(); - //MessageBox.Show("Received msg:" + msg); - if (msg == MSG_KILL_PIPE_SERVER) - { - break; - } - msgCallback(msg); - pipeServer.Disconnect(); - } - } - catch (Exception ex) - { - Log.ErrorException("Error in named pipe server", ex); - } - _serverRunning = false; - }); - _serverRunning = true; - thread.Start(); - } - - /// - /// Kills the pipe server background thread if one is running. - /// - public static void KillServer() - { - if (_serverRunning) - { - SendMessage(Process.GetCurrentProcess(), MSG_KILL_PIPE_SERVER); - } - } - - /// - /// From https://msdn.microsoft.com/en-us/library/bb546085%28v=vs.110%29.aspx - /// - private class StreamString - { - private Stream _ioStream; - private UnicodeEncoding _streamEncoding; - - public StreamString(Stream ioStream) - { - _ioStream = ioStream; - _streamEncoding = new UnicodeEncoding(); - } - - public string ReadString() - { - int len; - len = _ioStream.ReadByte() * 256; - len += _ioStream.ReadByte(); - byte[] inBuffer = new byte[len]; - _ioStream.Read(inBuffer, 0, len); - - return _streamEncoding.GetString(inBuffer); - } - - public int WriteString(string outString) - { - byte[] outBuffer = _streamEncoding.GetBytes(outString); - int len = outBuffer.Length; - if (len > UInt16.MaxValue) - { - len = (int)UInt16.MaxValue; - } - _ioStream.WriteByte((byte)(len / 256)); - _ioStream.WriteByte((byte)(len & 255)); - _ioStream.Write(outBuffer, 0, len); - _ioStream.Flush(); - - return outBuffer.Length + 2; - } - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Remoting/RemotingHelper.cs b/NAPS2.Sdk/Remoting/RemotingHelper.cs index f02280a80c..6e08b9f776 100644 --- a/NAPS2.Sdk/Remoting/RemotingHelper.cs +++ b/NAPS2.Sdk/Remoting/RemotingHelper.cs @@ -3,27 +3,52 @@ namespace NAPS2.Remoting; -public static class RemotingHelper +internal static class RemotingHelper { public static void HandleErrors(Error error) { if (error != null && !string.IsNullOrEmpty(error.Type)) { - var exceptionType = Assembly.GetAssembly(typeof(ScanDriverException))! - .GetTypes() - .FirstOrDefault(x => x.FullName == error.Type); - if (exceptionType != null) + var exception = MakeExceptionObject(error); + exception.PreserveStackTrace(); + throw exception; + } + } + + private static Exception MakeExceptionObject(Error error) + { + var exceptionType = Assembly.GetAssembly(typeof(ScanDriverException))!.GetType(error.Type, false); + var exception = CreateExceptionType(exceptionType); + var messageField = + typeof(Exception).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance); + var stackTraceField = typeof(Exception).GetField("_stackTraceString", + BindingFlags.NonPublic | BindingFlags.Instance); + var innerExceptionField = typeof(Exception).GetField("_innerException", + BindingFlags.NonPublic | BindingFlags.Instance); + var typePrefix = exceptionType == null ? $"{error.Type}: " : ""; + messageField?.SetValue(exception, typePrefix + error.Message); + stackTraceField?.SetValue(exception, error.StackTrace); + if (error.InnerException != null) + { + innerExceptionField?.SetValue(exception, MakeExceptionObject(error.InnerException)); + } + return exception; + } + + private static Exception CreateExceptionType(Type? exceptionType) + { + if (exceptionType != null && typeof(ScanDriverException).IsAssignableFrom(exceptionType)) + { + try + { + return (Exception) Activator.CreateInstance(exceptionType)!; + } + catch (Exception) { - var exception = (Exception)Activator.CreateInstance(exceptionType)!; - var messageField = typeof(Exception).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance); - var stackTraceField = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); - messageField?.SetValue(exception, error.Message); - stackTraceField?.SetValue(exception, error.StackTrace); - exception.PreserveStackTrace(); - throw exception; + // If the exception is not constructable, just use the default Exception type } - throw new Exception($"An error occurred on the gRPC server.\n{error.Type}: {error.Message}\n{error.StackTrace}"); } + return new Exception(); } public static Error ToError(Exception e) => @@ -31,6 +56,7 @@ public static Error ToError(Exception e) => { Type = e.GetType().FullName, Message = e.Message, - StackTrace = e.StackTrace + StackTrace = e.StackTrace, + InnerException = e.InnerException != null ? ToError(e.InnerException) : null }; } \ No newline at end of file diff --git a/NAPS2.Sdk/Remoting/SequencedWriter.cs b/NAPS2.Sdk/Remoting/SequencedWriter.cs index 1272d30cae..2c2e8c9c21 100644 --- a/NAPS2.Sdk/Remoting/SequencedWriter.cs +++ b/NAPS2.Sdk/Remoting/SequencedWriter.cs @@ -2,7 +2,7 @@ namespace NAPS2.Remoting; -public class SequencedWriter +internal class SequencedWriter { private readonly IServerStreamWriter _serverStreamWriter; private Task _lastWriteTask = Task.CompletedTask; diff --git a/NAPS2.Sdk/Remoting/Server/ScanJob.cs b/NAPS2.Sdk/Remoting/Server/ScanJob.cs new file mode 100644 index 0000000000..07f758d751 --- /dev/null +++ b/NAPS2.Sdk/Remoting/Server/ScanJob.cs @@ -0,0 +1,234 @@ +using System.Globalization; +using System.Threading; +using NAPS2.Escl; +using NAPS2.Escl.Server; +using NAPS2.Pdf; +using NAPS2.Scan; +using NAPS2.Serialization; + +namespace NAPS2.Remoting.Server; + +internal class ScanJob : IEsclScanJob +{ + private readonly ScanningContext _scanningContext; + private readonly ScanController _controller; + + private readonly CancellationTokenSource _cts = new(); + private readonly TaskCompletionSource _completedTcs = new(); + private readonly IAsyncEnumerator _enumerable; + private readonly List _allImages = []; + private readonly List _pdfImages = []; + private readonly Dictionary _lastProgressByPageNumber = new(); + + private int _currentPage = 1; + private Action? _statusCallback; + private Exception? _lastError; + private Task? _pausedNextDocumentTask; + + public ScanJob(ScanningContext scanningContext, ScanController controller, ScanDevice device, + EsclScanSettings settings) + { + _scanningContext = scanningContext; + _controller = controller; + _controller.PageProgress += (_, args) => _lastProgressByPageNumber[args.PageNumber] = args.Progress; + _controller.PageEnd += (_, args) => + { + _statusCallback?.Invoke(StatusTransition.PageComplete); + _allImages.Add(args.Image); + }; + _controller.ScanEnd += (_, args) => + { + if (args.HasError) + { + _lastError = args.Error; + } + _statusCallback?.Invoke(StatusTransition.ScanComplete); + _completedTcs.TrySetResult(!args.HasError); + }; + + var requestedFormat = settings.DocumentFormat; + ContentType = requestedFormat switch + { + ContentTypes.PNG or ContentTypes.PDF => requestedFormat, + _ => ContentTypes.JPEG + }; + var options = new ScanOptions + { + Device = device, + Dpi = Math.Max(settings.XResolution, settings.YResolution), + BitDepth = settings.ColorMode switch + { + EsclColorMode.BlackAndWhite1 => BitDepth.BlackAndWhite, + EsclColorMode.Grayscale8 or EsclColorMode.Grayscale16 => BitDepth.Grayscale, + _ => BitDepth.Color + }, + PaperSource = (settings.InputSource, settings.Duplex) switch + { + (EsclInputSource.Feeder, false) => PaperSource.Feeder, + (EsclInputSource.Feeder, true) => PaperSource.Duplex, + _ => PaperSource.Flatbed + }, + PageSize = settings.Width > 0 && settings.Height > 0 + ? new PageSize(settings.Width / 300m, settings.Height / 300m, PageSizeUnit.Inch) + : PageSize.Letter, + PageAlign = SnapToAlignment(settings.XOffset, settings.Width, EsclInputCaps.DEFAULT_MAX_WIDTH), + Quality = settings.CompressionFactor ?? ScanOptions.DEFAULT_QUALITY, + MaxQuality = ContentType == ContentTypes.PNG + }; + + try + { + _enumerable = controller.Scan(options, _cts.Token).GetAsyncEnumerator(); + } + catch (Exception) + { + _statusCallback?.Invoke(StatusTransition.AbortJob); + _statusCallback?.Invoke(StatusTransition.ScanComplete); + throw; + } + } + + private HorizontalAlign SnapToAlignment(int x, int width, int maxWidth) + { + if (x == 0 || width >= maxWidth) return HorizontalAlign.Right; + var fraction = x / (double) (maxWidth - width); + return fraction switch + { + <= 0.25 => HorizontalAlign.Right, + >= 0.75 => HorizontalAlign.Left, + _ => HorizontalAlign.Center, + }; + } + + public string ContentType { get; } + + public void Cancel() + { + _cts.Cancel(); + _statusCallback?.Invoke(StatusTransition.CancelJob); + } + + public void RegisterStatusTransitionCallback(Action callback) + { + _statusCallback = callback; + } + + public async Task WaitForNextDocument(CancellationToken cancelToken) + { + Task nextDocumentTask; + lock (this) + { + nextDocumentTask = _pausedNextDocumentTask ?? Task.Run(async () => + { + if (ContentType == ContentTypes.PDF) + { + // For PDFs we merge all the pages into a single PDF document, so we need to wait for the full scan here + if (!await _enumerable.MoveNextAsync()) + { + return false; + } + do + { + _currentPage++; + _pdfImages.Add(_enumerable.Current); + } while (await _enumerable.MoveNextAsync()); + return true; + } + + if (await _enumerable.MoveNextAsync()) + { + _currentPage++; + return true; + } + return false; + }); + _pausedNextDocumentTask = null; + } + await Task.WhenAny(nextDocumentTask, cancelToken.WaitHandle.WaitOneAsync()); + lock (this) + { + if (!nextDocumentTask.IsCompleted) + { + _pausedNextDocumentTask = nextDocumentTask; + throw new TaskCanceledException(); + } + } + return await nextDocumentTask; + } + + public async Task WriteDocumentTo(Stream stream) + { + if (ContentType == ContentTypes.JPEG) + { + _enumerable.Current.Save(stream, ImageFileFormat.Jpeg); + stream.Dispose(); + _enumerable.Current.Dispose(); + } + if (ContentType == ContentTypes.PNG) + { + _enumerable.Current.Save(stream, ImageFileFormat.Png); + stream.Dispose(); + _enumerable.Current.Dispose(); + } + if (ContentType == ContentTypes.PDF) + { + var pdfExporter = new PdfExporter(_scanningContext); + await pdfExporter.Export(stream, _pdfImages); + stream.Dispose(); + foreach (var image in _pdfImages) + { + image.Dispose(); + } + _pdfImages.Clear(); + } + } + + public async Task WriteProgressTo(Stream stream) + { + var pageEndTcs = new TaskCompletionSource(); + var streamWriter = new StreamWriter(stream); + var pageNumber = _currentPage; + + void WriteProgress(double progress) + { + streamWriter.WriteLine(progress.ToString(CultureInfo.InvariantCulture)); + streamWriter.Flush(); + } + void OnPageProgress(object? sender, PageProgressEventArgs e) + { + if (e.PageNumber == pageNumber) + { + WriteProgress(e.Progress); + } + } + void OnPageEnd(object? sender, PageEndEventArgs e) => pageEndTcs.TrySetResult(true); + + if (_lastProgressByPageNumber.TryGetValue(pageNumber, out var lastPageProgress)) + { + WriteProgress(lastPageProgress); + } + + _controller.PageProgress += OnPageProgress; + _controller.PageEnd += OnPageEnd; + await Task.WhenAny(pageEndTcs.Task, _completedTcs.Task); + _controller.PageProgress -= OnPageProgress; + _controller.PageEnd -= OnPageEnd; + } + + public async Task WriteErrorDetailsTo(Stream stream) + { + if (_lastError != null) + { + using var streamWriter = new StreamWriter(stream); + await streamWriter.WriteLineAsync(RemotingHelper.ToError(_lastError).ToXml()); + } + } + + public void Dispose() + { + foreach (var image in _allImages) + { + image.Dispose(); + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Remoting/Server/ScanServer.cs b/NAPS2.Sdk/Remoting/Server/ScanServer.cs new file mode 100644 index 0000000000..de4ed17ce6 --- /dev/null +++ b/NAPS2.Sdk/Remoting/Server/ScanServer.cs @@ -0,0 +1,110 @@ +using System.Security.Cryptography.X509Certificates; +using NAPS2.Escl; +using NAPS2.Escl.Server; +using NAPS2.Scan; + +namespace NAPS2.Remoting.Server; + +/// +/// Allows scanning devices to be shared across the network. Clients can connect by using Driver.Escl when scanning. +/// +public class ScanServer : IDisposable +{ + private readonly ScanningContext _scanningContext; + private readonly Dictionary _currentDevices = new(); + private readonly IEsclServer _esclServer; + private byte[]? _defaultIconPng; + + public ScanServer(ScanningContext scanningContext, IEsclServer esclServer) + { + _scanningContext = scanningContext; + _esclServer = esclServer; + _esclServer.Logger = _scanningContext.Logger; + ScanControllerFactory = () => new ScanController(scanningContext); + } + + /// + /// A unique ID that is used to help derive the UUIDs for shared scanners. If you expect to have multiple shared + /// scanners with the same name/model on the same network it may be useful to set this to a unique value. + /// + public Guid InstanceId { get; set; } + + /// + /// The security policy to use for the ESCL server. + /// + public EsclSecurityPolicy SecurityPolicy + { + get => _esclServer.SecurityPolicy; + set => _esclServer.SecurityPolicy = value; + } + + /// + /// The certificate to be used for TLS connections to the server. If not specified, a self-signed certificate will + /// be generated when the server starts (unless prevented by the security policy). + /// + public X509Certificate2? Certificate + { + get => _esclServer.Certificate; + set => _esclServer.Certificate = value; + } + + internal Func ScanControllerFactory { get; set; } + + public void SetDefaultIcon(IMemoryImage icon) => + SetDefaultIcon(icon.SaveToMemoryStream(ImageFileFormat.Png).ToArray()); + + public void SetDefaultIcon(byte[] iconPng) => _defaultIconPng = iconPng; + + public void RegisterDevice(ScanDevice device, string? displayName = null, int port = 0, int tlsPort = 0) => + RegisterDevice(new ScanServerDevice + { Device = device, Name = displayName ?? device.Name, Port = port, TlsPort = tlsPort }); + + private void RegisterDevice(ScanServerDevice sharedDevice) + { + var esclDeviceConfig = MakeEsclDeviceConfig(sharedDevice); + _currentDevices.Add(sharedDevice, esclDeviceConfig); + _esclServer.AddDevice(esclDeviceConfig); + } + + public void UnregisterDevice(ScanDevice device, string? displayName = null) => + UnregisterDevice(new ScanServerDevice { Device = device, Name = displayName ?? device.Name }); + + private void UnregisterDevice(ScanServerDevice sharedDevice) + { + var esclDeviceConfig = _currentDevices[sharedDevice]; + _currentDevices.Remove(sharedDevice); + _esclServer.RemoveDevice(esclDeviceConfig); + } + + internal (int port, int tlsPort) GetDevicePorts(ScanDevice device, string? displayName = null) => + GetDevicePorts(new ScanServerDevice { Device = device, Name = displayName ?? device.Name }); + + private (int port, int tlsPort) GetDevicePorts(ScanServerDevice sharedDevice) + { + var esclDeviceConfig = _currentDevices[sharedDevice]; + return (esclDeviceConfig.Port, esclDeviceConfig.TlsPort); + } + + private EsclDeviceConfig MakeEsclDeviceConfig(ScanServerDevice device) + { + return new EsclDeviceConfig + { + Port = device.Port, + TlsPort = device.TlsPort, + Capabilities = new EsclCapabilities + { + MakeAndModel = device.Name, + Uuid = device.GetUuid(InstanceId), + IconPng = _defaultIconPng, + // TODO: Ideally we want to get the actual device capabilities (flatbed/feeder, resolution etc.) + }, + CreateJob = settings => new ScanJob(_scanningContext, ScanControllerFactory(), device.Device, settings) + }; + } + + public Task Start() => _esclServer.Start(); + + public Task Stop() => _esclServer.Stop(); + + public void Dispose() => _esclServer.Dispose(); +} \ No newline at end of file diff --git a/NAPS2.Sdk/Remoting/Server/ScanServerDevice.cs b/NAPS2.Sdk/Remoting/Server/ScanServerDevice.cs new file mode 100644 index 0000000000..3f30abd7ea --- /dev/null +++ b/NAPS2.Sdk/Remoting/Server/ScanServerDevice.cs @@ -0,0 +1,26 @@ +using System.Security.Cryptography; +using System.Text; +using NAPS2.Scan; + +namespace NAPS2.Remoting.Server; + +internal record ScanServerDevice +{ + public required string Name { get; init; } + public required ScanDevice Device { get; init; } + public int Port { get; init; } + public int TlsPort { get; init; } + + public string GetUuid(Guid instanceId) + { + var key = $"{Device.Driver};{Device.ID};{Name};{instanceId}"; + var uniqueHash = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(key)); + return new Guid(uniqueHash.Take(16).ToArray()).ToString("D"); + } + + public virtual bool Equals(ScanServerDevice? other) => + other is not null && Name == other.Name && Device == other.Device; + + public override int GetHashCode() => + Name.GetHashCode() * 23 + Device.GetHashCode(); +} \ No newline at end of file diff --git a/NAPS2.Sdk/Remoting/Worker/IWorkerFactory.cs b/NAPS2.Sdk/Remoting/Worker/IWorkerFactory.cs index ff7a61b102..08da19a351 100644 --- a/NAPS2.Sdk/Remoting/Worker/IWorkerFactory.cs +++ b/NAPS2.Sdk/Remoting/Worker/IWorkerFactory.cs @@ -1,10 +1,14 @@ -namespace NAPS2.Remoting.Worker; +using NAPS2.Scan; + +namespace NAPS2.Remoting.Worker; /// /// A factory interface to spawn NAPS2.Worker.exe instances as needed. /// -public interface IWorkerFactory +internal interface IWorkerFactory { - void Init(); - WorkerContext Create(); + void Init(ScanningContext scanningContext, WorkerFactoryInitOptions? options = null); + WorkerContext Create(ScanningContext scanningContext, WorkerType workerType); + void RecreateSpareWorkers(); + void StopSpareWorkers(); } \ No newline at end of file diff --git a/NAPS2.Sdk/Remoting/Worker/ProcessJob.cs b/NAPS2.Sdk/Remoting/Worker/ProcessJob.cs index 531d2dc9cb..912a932ea3 100644 --- a/NAPS2.Sdk/Remoting/Worker/ProcessJob.cs +++ b/NAPS2.Sdk/Remoting/Worker/ProcessJob.cs @@ -7,7 +7,7 @@ namespace NAPS2.Remoting.Worker; /// the calling executable, so that there are no zombie processes left behind if the caller terminates. /// https://docs.microsoft.com/en-us/windows/desktop/procthread/job-objects /// -public class Job : IDisposable +internal class Job : IDisposable { [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr CreateJobObject(IntPtr a, string? lpName); diff --git a/NAPS2.Sdk/Remoting/Worker/WorkerContext.cs b/NAPS2.Sdk/Remoting/Worker/WorkerContext.cs index 804319b1a5..97b5d10b3b 100644 --- a/NAPS2.Sdk/Remoting/Worker/WorkerContext.cs +++ b/NAPS2.Sdk/Remoting/Worker/WorkerContext.cs @@ -1,48 +1,84 @@ -namespace NAPS2.Remoting.Worker; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using NAPS2.Scan; + +namespace NAPS2.Remoting.Worker; /// /// A class storing the objects the client needs to use a NAPS2.Worker.exe instance. /// -public class WorkerContext : IDisposable +internal class WorkerContext : IDisposable { /// /// Timeout after attempting to normally stop a worker before it is killed. /// private static readonly TimeSpan WorkerStopTimeout = TimeSpan.FromSeconds(60); - public WorkerContext(WorkerServiceAdapter service, Process process) + private readonly ILogger _logger; + private bool _stopped; + + internal WorkerContext(ScanningContext scanningContext, WorkerType workerType, WorkerServiceAdapter service, + Process process) { + ScanningContext = scanningContext; + _logger = scanningContext.Logger; + Type = workerType; Service = service; Process = process; } - - public WorkerServiceAdapter Service { get; } + + public ScanningContext ScanningContext { get; } + + public WorkerType Type { get; } + + internal WorkerServiceAdapter Service { get; } public Process Process { get; } - public void Dispose() + public async Task Stop() { - try + if (_stopped) return; + _stopped = true; + + // Try to cleanly stop the worker + Task.Run(() => { - Service.StopWorker(); - Task.Delay(WorkerStopTimeout).ContinueWith(t => + try { - try - { - if (!Process.HasExited) - { - Process.Kill(); - } - } - catch (Exception e) - { - Log.ErrorException("Error killing worker", e); - } - }); - } - catch (Exception e) + Service.StopWorker(); + } + catch (RpcException e) when (e.Status.StatusCode == StatusCode.Unavailable) + { + // This can happen normally if the system is shutting down (and terminated the worker processes) so we + // don't log as an error. + _logger.LogDebug("Could not stop the worker process. It may have crashed."); + } + catch (Exception e) + { + _logger.LogError(e, "Error stopping worker"); + } + }).AssertNoAwait(); + + // Wait for either the worker process to close or for our timeout + await Task.WhenAny(Process.WaitForExitAsync(), Task.Delay(WorkerStopTimeout)).ConfigureAwait(false); + + // If the worker process still hasn't closed we kill it now + if (!Process.HasExited) { - Log.ErrorException("Error stopping worker", e); + _logger.LogError("Killing unresponsive worker"); + try + { + Process.Kill(); + } + catch (Exception e) + { + _logger.LogError(e, "Error killing unresponsive worker"); + } } } + + public void Dispose() + { + Stop().AssertNoAwait(); + } } \ No newline at end of file diff --git a/NAPS2.Sdk/Remoting/Worker/WorkerFactory.cs b/NAPS2.Sdk/Remoting/Worker/WorkerFactory.cs index 0fe1fa53d4..ca75fdad97 100644 --- a/NAPS2.Sdk/Remoting/Worker/WorkerFactory.cs +++ b/NAPS2.Sdk/Remoting/Worker/WorkerFactory.cs @@ -1,82 +1,110 @@ using System.Collections.Concurrent; using GrpcDotNetNamedPipes; +using Microsoft.Extensions.Logging; +using NAPS2.Scan; +using NAPS2.Unmanaged; namespace NAPS2.Remoting.Worker; /// -/// A class to manage the lifecycle of NAPS2.Worker.exe instances and hook up the WCF channels. +/// A class to manage the lifecycle of worker processes and hook up the named pipe channels. /// -public class WorkerFactory : IWorkerFactory +internal class WorkerFactory : IWorkerFactory { - public const string WORKER_EXE_NAME = "NAPS2.Worker.exe"; public const string PIPE_NAME_FORMAT = "NAPS2.Worker.{0}"; + private const int PIPE_CONNECTION_TIMEOUT = 10_000; + private const int TAKE_WORKER_TIMEOUT = 10_000; + private readonly Dictionary _environmentVariables; - public static string?[] SearchDirs => new[] - { - AssemblyHelper.LibFolder, - AssemblyHelper.EntryFolder - }; - - private readonly FileStorageManager _fileStorageManager; - - private string? _workerExePath; - private BlockingCollection? _workerQueue; - - public WorkerFactory(FileStorageManager fileStorageManager) - { - _fileStorageManager = fileStorageManager; - } + private Dictionary>? _workerQueues; - private string WorkerExePath + public static WorkerFactory CreateDefault() { - get + var env = new Dictionary(); +#if NET6_0_OR_GREATER + if (OperatingSystem.IsMacOS()) { - if (_workerExePath == null) + // The intended way to load sane dependencies (libusb, libjpeg) is by enumerating SaneLibraryDeps and for + // each, calling dlopen. Then when we load sane it will use those loaded libraries. + // However, while that works on my arm64 macOS 13, it doesn't on my x64 macOS 10.15. I'm not sure why. + // But setting DYLD_LIBRARY_PATH on the sane worker process does work. + // TODO: This means there may be some cases where in-process sane won't work, which could affect SDK users. + var sanePath = NativeLibrary.FindLibraryPath(PlatformCompat.System.SaneLibraryName); + if (sanePath.Contains('/')) { - foreach (var dir in SearchDirs.WhereNotNull()) - { - _workerExePath = Path.Combine(dir, WORKER_EXE_NAME); - if (File.Exists(_workerExePath)) - { - break; - } - } + env["DYLD_LIBRARY_PATH"] = Path.GetFullPath(Path.GetDirectoryName(sanePath)!); } - - return _workerExePath!; } + if (!OperatingSystem.IsWindows()) + { + return new WorkerFactory(Environment.ProcessPath!, null, env); + } +#endif + var exePath = Path.Combine(AssemblyHelper.EntryFolder, "NAPS2.exe"); + string[] candidateWorkerPaths = + { +#if DEBUG + Path.Combine(AssemblyHelper.EntryFolder, + @"..\..\..\..\..\NAPS2.App.Worker\bin\Debug\net9-windows\win-x86\NAPS2.Worker.exe"), +#endif + Path.Combine(AssemblyHelper.EntryFolder, "NAPS2.Worker.exe"), + Path.Combine(AssemblyHelper.EntryFolder, "lib", "NAPS2.Worker.exe") + }; + string workerExePath = ""; + foreach (var candidateWorkerPath in candidateWorkerPaths) + { + workerExePath = candidateWorkerPath; + if (File.Exists(workerExePath)) break; + } + return new WorkerFactory(exePath, workerExePath, env); + } + + public WorkerFactory(string nativeWorkerExePath, string? winX86WorkerExePath = null, + Dictionary? environmentVariables = null) + { + NativeWorkerExePath = nativeWorkerExePath; + WinX86WorkerExePath = winX86WorkerExePath; + _environmentVariables = environmentVariables ?? new Dictionary(); } - private Process StartWorkerProcess() + public string NativeWorkerExePath { get; } + public string? WinX86WorkerExePath { get; } + + private Process StartWorkerProcess(WorkerType workerType) { var parentId = Process.GetCurrentProcess().Id; - Process? proc; - if (PlatformCompat.System.UseSeparateWorkerExe) + ProcessStartInfo startInfo; + if (workerType == WorkerType.WinX86) { - proc = Process.Start(new ProcessStartInfo + if (!PlatformCompat.System.SupportsWinX86Worker || WinX86WorkerExePath == null) + { + throw new InvalidOperationException("Unexpected worker configuration"); + } + startInfo = new ProcessStartInfo { - FileName = WorkerExePath, + FileName = PlatformCompat.System.WinX86WorkerAlias ?? WinX86WorkerExePath, Arguments = $"{parentId}", RedirectStandardInput = true, RedirectStandardOutput = true, UseShellExecute = false - }); + }; } else { -#if NET6_0_OR_GREATER - proc = Process.Start(new ProcessStartInfo + startInfo = new ProcessStartInfo { - FileName = Environment.ProcessPath, + FileName = PlatformCompat.System.NativeWorkerAlias ?? NativeWorkerExePath, Arguments = $"worker {parentId}", RedirectStandardInput = true, RedirectStandardOutput = true, UseShellExecute = false - }); -#else - throw new Exception("Unexpected worker configuration"); -#endif + }; + } + foreach (var name in _environmentVariables.Keys) + { + startInfo.EnvironmentVariables[name] = _environmentVariables[name]; } + var proc = Process.Start(startInfo); if (proc == null) { throw new Exception("Could not start worker process"); @@ -96,10 +124,13 @@ private Process StartWorkerProcess() } } + // TODO: Since we set RedirectStandardOutput, we should consume stdout to prevent the buffer from filling up and + // stalling the worker process var readyStr = proc.StandardOutput.ReadLine(); if (readyStr?.Trim() == "error") { - throw new InvalidOperationException("The worker could not start due to an error. See the worker logs."); + var error = proc.StandardOutput.ReadToEnd(); + throw new InvalidOperationException($"The worker could not start due to an error: {error}"); } if (readyStr?.Trim() != "ready") @@ -110,39 +141,112 @@ private Process StartWorkerProcess() return proc; } - private void StartWorkerService() + private void StartWorkerService(ScanningContext scanningContext, WorkerType workerType, bool spare) { Task.Run(() => { - var proc = StartWorkerProcess(); - var channel = new NamedPipeChannel(".", string.Format(PIPE_NAME_FORMAT, proc.Id)); - _workerQueue!.Add(new WorkerContext(new WorkerServiceAdapter(channel), proc)); + try + { + var proc = StartWorkerProcess(workerType); + var options = new NamedPipeChannelOptions + { + ConnectionTimeout = PIPE_CONNECTION_TIMEOUT + }; + var channel = new NamedPipeChannel(".", string.Format(PIPE_NAME_FORMAT, proc.Id), options); + _workerQueues![workerType] + .Add(new WorkerContext(scanningContext, workerType, new WorkerServiceAdapter(channel), proc)); + } + catch (Exception ex) + { + // If we're just starting a spare worker, don't log errors (e.g. if we're using the SDK and don't even + // need a worker) + if (!spare) + { + scanningContext.Logger.LogError(ex, "Could not start worker"); + } + } }); } - private WorkerContext NextWorker() + private WorkerContext NextWorker(ScanningContext scanningContext, WorkerType workerType) { - StartWorkerService(); - return _workerQueue!.Take(); + StartWorkerService(scanningContext, workerType, false); + if (!_workerQueues![workerType]!.TryTake(out var worker, TAKE_WORKER_TIMEOUT)) + { + throw new InvalidOperationException("Could not start a worker process; see logs for details"); + } + return worker; } - public void Init() + public void Init(ScanningContext scanningContext, WorkerFactoryInitOptions? options = null) { - if (_workerQueue == null) + if (!File.Exists(NativeWorkerExePath)) + { + scanningContext.Logger.LogDebug($"Native worker exe does not exist: {NativeWorkerExePath}"); + } + if (WinX86WorkerExePath != null && !File.Exists(WinX86WorkerExePath)) { - _workerQueue = new BlockingCollection(); - StartWorkerService(); + scanningContext.Logger.LogDebug($"WinX86 worker exe does not exist: {WinX86WorkerExePath}"); + } + + options ??= new WorkerFactoryInitOptions(); + if (_workerQueues == null) + { + _workerQueues = new() + { + { WorkerType.Native, new BlockingCollection() }, + { WorkerType.WinX86, new BlockingCollection() } + }; + if (options.StartSpareWorkers) + { + // We start a "spare" worker so that when we need one, it's immediately ready (and then we'll start another + // spare for the next request). + StartWorkerService(scanningContext, WorkerType.Native, true); + if (PlatformCompat.System.SupportsWinX86Worker) + { + // On windows as we need 32-bit and 64-bit workers for different things, we will have two spare workers, + // which isn't ideal but not a big deal. + StartWorkerService(scanningContext, WorkerType.WinX86, true); + } + } } } - public WorkerContext Create() + public WorkerContext Create(ScanningContext scanningContext, WorkerType workerType) { - if (_workerQueue == null) + if (_workerQueues == null) { throw new InvalidOperationException("WorkerFactory has not been initialized"); } - var worker = NextWorker(); - worker.Service.Init(_fileStorageManager.FolderPath); + var worker = NextWorker(scanningContext, workerType); + worker.Service.Init(scanningContext.FileStorageManager?.FolderPath); return worker; } + + public void RecreateSpareWorkers() + { + if (_workerQueues == null) return; + foreach (var queue in _workerQueues.Values) + { + if (queue.TryTake(out var worker)) + { + worker.Stop().AssertNoAwait(); + StartWorkerService(worker.ScanningContext, worker.Type, true); + } + } + } + + public void StopSpareWorkers() + { + if (_workerQueues == null) return; + var stopTasks = new List(); + foreach (var queue in _workerQueues.Values) + { + while (queue.TryTake(out var worker)) + { + stopTasks.Add(worker.Stop()); + } + } + Task.WhenAll(stopTasks).Wait(); + } } \ No newline at end of file diff --git a/NAPS2.Sdk/Remoting/Worker/WorkerFactoryInitOptions.cs b/NAPS2.Sdk/Remoting/Worker/WorkerFactoryInitOptions.cs new file mode 100644 index 0000000000..58205ffce9 --- /dev/null +++ b/NAPS2.Sdk/Remoting/Worker/WorkerFactoryInitOptions.cs @@ -0,0 +1,6 @@ +namespace NAPS2.Remoting.Worker; + +internal class WorkerFactoryInitOptions +{ + public bool StartSpareWorkers { get; set; } = true; +} \ No newline at end of file diff --git a/NAPS2.Sdk/Remoting/Worker/WorkerPool.cs b/NAPS2.Sdk/Remoting/Worker/WorkerPool.cs index 519fb93202..6eb4b86aa4 100644 --- a/NAPS2.Sdk/Remoting/Worker/WorkerPool.cs +++ b/NAPS2.Sdk/Remoting/Worker/WorkerPool.cs @@ -1,18 +1,19 @@ using System.Threading; +using NAPS2.Scan; namespace NAPS2.Remoting.Worker; -public class WorkerPool : IDisposable +internal class WorkerPool : IDisposable { private const int TICK_INTERVAL = 5000; - - private readonly IWorkerFactory _workerFactory; + + private readonly ScanningContext _scanningContext; private readonly Timer _timer; private List _entries = new(); - public WorkerPool(IWorkerFactory workerFactory) + public WorkerPool(ScanningContext scanningContext) { - _workerFactory = workerFactory; + _scanningContext = scanningContext; _timer = new Timer(Tick, null, 0, TICK_INTERVAL); } @@ -31,8 +32,12 @@ private void Tick(object? state) } } - public T Use(Func func) + public T Use(WorkerType workerType, Func func) { + if (workerType != WorkerType.Native) + { + throw new NotSupportedException("WorkerPool only supports native workers"); + } var worker = Take(); T result; try @@ -58,7 +63,7 @@ private WorkerContext Take() _entries.RemoveAt(_entries.Count - 1); return entry.Worker; } - return _workerFactory.Create(); + return _scanningContext.CreateWorker(WorkerType.Native)!; } } diff --git a/NAPS2.Sdk/Remoting/Worker/WorkerServer.cs b/NAPS2.Sdk/Remoting/Worker/WorkerServer.cs new file mode 100644 index 0000000000..5aeee414c1 --- /dev/null +++ b/NAPS2.Sdk/Remoting/Worker/WorkerServer.cs @@ -0,0 +1,73 @@ +using System.Threading; +using GrpcDotNetNamedPipes; +using NAPS2.ImportExport.Email.Mapi; +using NAPS2.Platform.Windows; +using NAPS2.Scan; +using NAPS2.Scan.Internal.Twain; + +namespace NAPS2.Remoting.Worker; + +/// +/// Entry point for the NAPS2.Worker.exe binary. You can use this to build your own custom binary instead of using the +/// one in the NAPS2.Sdk.Worker.Win32 nuget package. +/// +public static class WorkerServer +{ +#pragma warning disable CS1998 + public static async Task Run(ScanningContext scanningContext, CancellationToken cancellationToken = default) + { + try + { + var tcs = new TaskCompletionSource(); + var run = async () => { await tcs.Task; }; + var stop = () => tcs.SetResult(true); + +#if !MAC +#if NET6_0_OR_GREATER + if (OperatingSystem.IsWindows()) + { +#endif + var messagePump = Win32MessagePump.Create(); + messagePump.Logger = scanningContext.Logger; + Invoker.Current = messagePump; +#pragma warning disable CA1416 + TwainHandleManager.Factory = () => new Win32TwainHandleManager(messagePump); + run = async () => messagePump.RunMessageLoop(); + stop = () => messagePump.Dispose(); +#pragma warning restore CA1416 +#if NET6_0_OR_GREATER + } +#endif +#endif + + var server = + new NamedPipeServer(string.Format(WorkerFactory.PIPE_NAME_FORMAT, Process.GetCurrentProcess().Id)); + var serviceImpl = new WorkerServiceImpl(scanningContext, + new ThumbnailRenderer(scanningContext.ImageContext), new StubMapiWrapper(), +#if MAC + new StubTwainController()); +#else + new LocalTwainController(scanningContext)); +#endif + serviceImpl.OnStop += (_, _) => stop(); + WorkerService.BindService(server.ServiceBinder, serviceImpl); + cancellationToken.Register(() => serviceImpl.Stop()); + server.Start(); + try + { + Console.WriteLine(@"ready"); + await run(); + } + finally + { + server.Kill(); + } + } + catch (Exception ex) + { + Console.WriteLine(@"error"); + Console.WriteLine(ex.ToString()); + throw; + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Remoting/Worker/WorkerService.proto b/NAPS2.Sdk/Remoting/Worker/WorkerService.proto index f1bb94712a..5965cfb9da 100644 --- a/NAPS2.Sdk/Remoting/Worker/WorkerService.proto +++ b/NAPS2.Sdk/Remoting/Worker/WorkerService.proto @@ -8,13 +8,16 @@ import "Serialization/SerializedImage.proto"; service WorkerService { rpc Init (InitRequest) returns (InitResponse) {} rpc Wia10NativeUi (Wia10NativeUiRequest) returns (Wia10NativeUiResponse) {} + rpc LoadMapi (LoadMapiRequest) returns (LoadMapiResponse) {} rpc SendMapiEmail (SendMapiEmailRequest) returns (SendMapiEmailResponse) {} rpc RenderThumbnail (RenderThumbnailRequest) returns (RenderThumbnailResponse) {} rpc RenderPdf (RenderPdfRequest) returns (RenderPdfResponse) {} rpc ImportPostProcess (ImportPostProcessRequest) returns (ImportPostProcessResponse) {} - rpc GetDeviceList (GetDeviceListRequest) returns (GetDeviceListResponse) {} + rpc GetDevices (GetDevicesRequest) returns (stream GetDevicesResponse) {} + rpc GetCaps (GetCapsRequest) returns (GetCapsResponse) {} rpc Scan (ScanRequest) returns (stream ScanResponse) {} rpc TwainGetDeviceList (GetDeviceListRequest) returns (GetDeviceListResponse) {} + rpc TwainGetCaps (GetCapsRequest) returns (GetCapsResponse) {} rpc TwainScan (TwainScanRequest) returns (stream TwainScanResponse) {} rpc StopWorker (StopWorkerRequest) returns (StopWorkerResponse) {} } @@ -37,6 +40,24 @@ message Wia10NativeUiResponse { string wiaConfigurationXml = 2; } +message GetDevicesRequest { + string optionsXml = 1; +} + +message GetDevicesResponse { + NAPS2.Remoting.Error error = 1; + string deviceXml = 2; +} + +message GetCapsRequest { + string optionsXml = 1; +} + +message GetCapsResponse { + NAPS2.Remoting.Error error = 1; + string scanCapsXml = 2; +} + message GetDeviceListRequest { string optionsXml = 1; } @@ -56,6 +77,7 @@ message ScanResponse { NAPS2.Serialization.SerializedImage image = 2; ProgressEvent progress = 3; PageStartEvent pageStart = 4; + DeviceUriChangedEvent deviceUriChanged = 5; } } @@ -66,8 +88,21 @@ message ProgressEvent { message PageStartEvent { } +message DeviceUriChangedEvent { + string iconUri = 1; + string connectionUri = 2; +} + +message LoadMapiRequest { + string clientName = 1; +} + +message LoadMapiResponse { + bool loaded = 2; +} + message SendMapiEmailRequest { - string clientName = 1; + string clientName = 1; string emailMessageXml = 2; } @@ -123,6 +158,7 @@ message TwainScanResponse { TwainPageStart pageStart = 2; TwainNativeImage nativeImage = 3; TwainMemoryBuffer memoryBuffer = 4; + TwainTransferCanceled transferCanceled = 5; } } @@ -153,3 +189,6 @@ message TwainMemoryBuffer { int32 yOffset = 5; int32 bytesPerRow = 6; } + +message TwainTransferCanceled { +} diff --git a/NAPS2.Sdk/Remoting/Worker/WorkerServiceAdapter.cs b/NAPS2.Sdk/Remoting/Worker/WorkerServiceAdapter.cs index 5f0bdd5145..0fb9425e5a 100644 --- a/NAPS2.Sdk/Remoting/Worker/WorkerServiceAdapter.cs +++ b/NAPS2.Sdk/Remoting/Worker/WorkerServiceAdapter.cs @@ -3,6 +3,7 @@ using NAPS2.ImportExport.Email; using NAPS2.ImportExport.Email.Mapi; using NAPS2.Scan; +using NAPS2.Scan.Exceptions; using NAPS2.Scan.Internal; using NAPS2.Scan.Internal.Twain; using NAPS2.Scan.Internal.Wia; @@ -10,7 +11,7 @@ namespace NAPS2.Remoting.Worker; -public class WorkerServiceAdapter +internal class WorkerServiceAdapter { private readonly WorkerService.WorkerServiceClient _client; @@ -19,7 +20,7 @@ public WorkerServiceAdapter(CallInvoker callInvoker) _client = new WorkerService.WorkerServiceClient(callInvoker); } - public void Init(string recoveryFolderPath) + public void Init(string? recoveryFolderPath) { var req = new InitRequest { RecoveryFolderPath = recoveryFolderPath ?? "" }; var resp = _client.Init(req); @@ -42,12 +43,52 @@ public void Init(string recoveryFolderPath) return resp.WiaConfigurationXml.FromXml(); } - public async Task> GetDeviceList(ScanOptions options) + public async Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback) { - var req = new GetDeviceListRequest { OptionsXml = options.ToXml() }; - var resp = await _client.GetDeviceListAsync(req); - RemotingHelper.HandleErrors(resp.Error); - return resp.DeviceListXml.FromXml>(); + var req = new GetDevicesRequest + { + OptionsXml = options.ToXml() + }; + try + { + var streamingCall = _client.GetDevices(req, cancellationToken: cancelToken); + while (await streamingCall.ResponseStream.MoveNext()) + { + var resp = streamingCall.ResponseStream.Current; + RemotingHelper.HandleErrors(resp.Error); + callback(resp.DeviceXml.FromXml()); + } + } + catch (RpcException ex) + { + if (ex.StatusCode == StatusCode.Unavailable) + { + throw new ScanDriverUnknownException(PlatformCompat.System.WorkerCrashMessage, ex); + } + if (ex.Status.StatusCode != StatusCode.Cancelled) + { + throw; + } + } + } + + public async Task GetCaps(ScanOptions options, CancellationToken cancelToken) + { + try + { + var req = new GetCapsRequest { OptionsXml = options.ToXml() }; + var resp = await _client.GetCapsAsync(req, cancellationToken: cancelToken); + RemotingHelper.HandleErrors(resp.Error); + return resp.ScanCapsXml.FromXml(); + } + catch (RpcException ex) + { + if (ex.StatusCode == StatusCode.Unavailable) + { + throw new ScanDriverUnknownException(PlatformCompat.System.WorkerCrashMessage, ex); + } + throw; + } } public async Task Scan(ScanningContext scanningContext, ScanOptions options, CancellationToken cancelToken, @@ -78,17 +119,32 @@ public async Task Scan(ScanningContext scanningContext, ScanOptions options, Can new DeserializeImageOptions()); imageCallback?.Invoke(renderableImage, resp.Image.RenderedFilePath); } + if (resp.DeviceUriChanged != null) + { + scanEvents.DeviceUriChanged(resp.DeviceUriChanged.IconUri, resp.DeviceUriChanged.ConnectionUri); + } } } catch (RpcException ex) { - if (ex.Status.StatusCode != StatusCode.Cancelled) + if (ex.StatusCode == StatusCode.Unavailable) + { + throw new ScanDriverUnknownException(PlatformCompat.System.WorkerCrashMessage, ex); + } + if (ex.StatusCode != StatusCode.Cancelled) { throw; } } } + public async Task CanLoadMapi(string? clientName) + { + var req = new LoadMapiRequest { ClientName = clientName }; + var resp = await _client.LoadMapiAsync(req); + return resp.Loaded; + } + public async Task SendMapiEmail(string? clientName, EmailMessage message) { var req = new SendMapiEmailRequest { ClientName = clientName, EmailMessageXml = message.ToXml() }; @@ -126,7 +182,7 @@ public byte[] RenderPdf(string path, float dpi) public void StopWorker() { - _client.StopWorkerAsync(new StopWorkerRequest()); + _client.StopWorker(new StopWorkerRequest()); } public async Task TwainScan(ScanOptions options, CancellationToken cancelToken, ITwainEvents twainEvents) @@ -154,11 +210,19 @@ public async Task TwainScan(ScanOptions options, CancellationToken cancelToken, { twainEvents.MemoryBufferTransferred(resp.MemoryBuffer); } + if (resp.TransferCanceled != null) + { + twainEvents.TransferCanceled(resp.TransferCanceled); + } } } catch (RpcException ex) { - if (ex.Status.StatusCode != StatusCode.Cancelled) + if (ex.StatusCode == StatusCode.Unavailable) + { + throw new ScanDriverUnknownException(PlatformCompat.System.WorkerCrashMessage, ex); + } + if (ex.StatusCode != StatusCode.Cancelled) { throw; } @@ -167,10 +231,40 @@ public async Task TwainScan(ScanOptions options, CancellationToken cancelToken, public async Task> TwainGetDeviceList(ScanOptions options) { - var req = new GetDeviceListRequest { OptionsXml = options.ToXml() }; - var resp = await _client.TwainGetDeviceListAsync(req); - RemotingHelper.HandleErrors(resp.Error); - return resp.DeviceListXml.FromXml>(); + try + { + var req = new GetDeviceListRequest { OptionsXml = options.ToXml() }; + var resp = await _client.TwainGetDeviceListAsync(req); + RemotingHelper.HandleErrors(resp.Error); + return resp.DeviceListXml.FromXml>(); + } + catch (RpcException ex) + { + if (ex.StatusCode == StatusCode.Unavailable) + { + throw new ScanDriverUnknownException(PlatformCompat.System.WorkerCrashMessage, ex); + } + throw; + } + } + + public async Task TwainGetCaps(ScanOptions options) + { + try + { + var req = new GetCapsRequest { OptionsXml = options.ToXml() }; + var resp = await _client.TwainGetCapsAsync(req); + RemotingHelper.HandleErrors(resp.Error); + return resp.ScanCapsXml.FromXml(); + } + catch (RpcException ex) + { + if (ex.StatusCode == StatusCode.Unavailable) + { + throw new ScanDriverUnknownException(PlatformCompat.System.WorkerCrashMessage, ex); + } + throw; + } } public ProcessedImage ImportPostProcess(ScanningContext scanningContext, ProcessedImage img, int? thumbnailSize, diff --git a/NAPS2.Sdk/Remoting/Worker/WorkerServiceImpl.cs b/NAPS2.Sdk/Remoting/Worker/WorkerServiceImpl.cs index e6418d49f9..dbfce23f7b 100644 --- a/NAPS2.Sdk/Remoting/Worker/WorkerServiceImpl.cs +++ b/NAPS2.Sdk/Remoting/Worker/WorkerServiceImpl.cs @@ -1,10 +1,11 @@ using System.Threading; using Google.Protobuf; using Grpc.Core; +using NAPS2.ImportExport; using NAPS2.ImportExport.Email; using NAPS2.ImportExport.Email.Mapi; using NAPS2.ImportExport.Images; -using NAPS2.ImportExport.Pdf; +using NAPS2.Pdf; using NAPS2.Scan; using NAPS2.Scan.Internal; using NAPS2.Scan.Internal.Twain; @@ -16,39 +17,33 @@ namespace NAPS2.Remoting.Worker; -// TODO: Scan and GetDeviceList are obsolete (see TwainScan and TwainGetDeviceList), as Twain is the only thing that -// needs the worker. But I'm keeping them around for now as they could come in handy later. -public class WorkerServiceImpl : WorkerService.WorkerServiceBase +internal class WorkerServiceImpl : WorkerService.WorkerServiceBase { private readonly ScanningContext _scanningContext; private readonly IRemoteScanController _remoteScanController; private readonly ThumbnailRenderer _thumbnailRenderer; private readonly IMapiWrapper _mapiWrapper; - private readonly ITwainSessionController _twainSessionController; - private readonly ImportPostProcessor _importPostProcessor; + private readonly ITwainController _twainController; private readonly AutoResetEvent _ongoingCallFinished = new(false); private int _ongoingCallCount; public WorkerServiceImpl(ScanningContext scanningContext, ThumbnailRenderer thumbnailRenderer, - IMapiWrapper mapiWrapper, ITwainSessionController twainSessionController) + IMapiWrapper mapiWrapper, ITwainController twainController) : this(scanningContext, new RemoteScanController(scanningContext), - thumbnailRenderer, mapiWrapper, twainSessionController, - new ImportPostProcessor()) + thumbnailRenderer, mapiWrapper, twainController) { } internal WorkerServiceImpl(ScanningContext scanningContext, IRemoteScanController remoteScanController, ThumbnailRenderer thumbnailRenderer, - IMapiWrapper mapiWrapper, ITwainSessionController twainSessionController, - ImportPostProcessor importPostProcessor) + IMapiWrapper mapiWrapper, ITwainController twainController) { _scanningContext = scanningContext; _remoteScanController = remoteScanController; _thumbnailRenderer = thumbnailRenderer; _mapiWrapper = mapiWrapper; - _twainSessionController = twainSessionController; - _importPostProcessor = importPostProcessor; + _twainController = twainController; } public override Task Init(InitRequest request, ServerCallContext context) @@ -75,6 +70,9 @@ public override Task Wia10NativeUi(Wia10NativeUiRequest r #if MAC throw new NotSupportedException(); #else +#if NET6_0_OR_GREATER + if (!OperatingSystem.IsWindows()) throw new NotSupportedException(); +#endif using var callRef = StartCall(); try { @@ -108,22 +106,40 @@ public override Task Wia10NativeUi(Wia10NativeUiRequest r #endif } - public override async Task GetDeviceList(GetDeviceListRequest request, - ServerCallContext context) + public override async Task GetDevices(GetDevicesRequest request, + IServerStreamWriter responseStream, ServerCallContext context) { using var callRef = StartCall(); + var sequencedWriter = new SequencedWriter(responseStream); try { var scanOptions = request.OptionsXml.FromXml(); - var deviceList = await _remoteScanController.GetDeviceList(scanOptions); - return new GetDeviceListResponse - { - DeviceListXml = deviceList.ToXml() - }; + await _remoteScanController.GetDevices(scanOptions, + context.CancellationToken, + device => sequencedWriter.Write(new GetDevicesResponse + { + DeviceXml = device.ToXml() + })); } catch (Exception e) { - return new GetDeviceListResponse { Error = RemotingHelper.ToError(e) }; + sequencedWriter.Write(new GetDevicesResponse { Error = RemotingHelper.ToError(e) }); + } + await sequencedWriter.WaitForCompletion(); + } + + public override async Task GetCaps(GetCapsRequest request, ServerCallContext context) + { + using var callRef = StartCall(); + try + { + var scanOptions = request.OptionsXml.FromXml(); + var caps = await _remoteScanController.GetCaps(scanOptions, context.CancellationToken); + return new GetCapsResponse { ScanCapsXml = caps?.ToXml() ?? "" }; + } + catch (Exception e) + { + return new GetCapsResponse { Error = RemotingHelper.ToError(e) }; } } @@ -145,6 +161,14 @@ public override async Task Scan(ScanRequest request, IServerStreamWriter sequencedWriter.Write(new ScanResponse + { + DeviceUriChanged = new DeviceUriChangedEvent + { + IconUri = iconUri, + ConnectionUri = connectionUri + } }) ); await _remoteScanController.Scan(request.OptionsXml.FromXml(), @@ -168,6 +192,27 @@ await _remoteScanController.Scan(request.OptionsXml.FromXml(), await sequencedWriter.WaitForCompletion(); } + public override Task LoadMapi(LoadMapiRequest request, + ServerCallContext context) + { + using var callRef = StartCall(); + try + { + var loaded = _mapiWrapper.CanLoadClient(request.ClientName); + return Task.FromResult(new LoadMapiResponse + { + Loaded = loaded + }); + } + catch (Exception) + { + return Task.FromResult(new LoadMapiResponse + { + Loaded = false + }); + } + } + public override async Task SendMapiEmail(SendMapiEmailRequest request, ServerCallContext context) { @@ -187,7 +232,7 @@ public override async Task SendMapiEmail(SendMapiEmailReq } } - public override Task RenderThumbnail(RenderThumbnailRequest request, + public override async Task RenderThumbnail(RenderThumbnailRequest request, ServerCallContext context) { using var callRef = StartCall(); @@ -199,16 +244,16 @@ public override Task RenderThumbnail(RenderThumbnailReq }; using var image = ImageSerializer.Deserialize(_scanningContext, request.Image, deserializeOptions); - var thumbnail = _thumbnailRenderer.Render(image, request.Size); + var thumbnail = await _thumbnailRenderer.Render(image, request.Size); var stream = thumbnail.SaveToMemoryStream(ImageFileFormat.Png); - return Task.FromResult(new RenderThumbnailResponse + return new RenderThumbnailResponse { Thumbnail = ByteString.FromStream(stream) - }); + }; } catch (Exception e) { - return Task.FromResult(new RenderThumbnailResponse { Error = RemotingHelper.ToError(e) }); + return new RenderThumbnailResponse { Error = RemotingHelper.ToError(e) }; } } @@ -218,8 +263,8 @@ public override Task RenderPdf(RenderPdfRequest request, Serv try { var renderer = new PdfiumPdfRenderer(); - using var image = renderer - .Render(_scanningContext.ImageContext, request.Path, PdfRenderSize.FromDpi(request.Dpi)).Single(); + using var image = renderer.RenderPage(_scanningContext.ImageContext, request.Path, + PdfRenderSize.FromDpi(request.Dpi)); var stream = image.SaveToMemoryStream(ImageFileFormat.Png); return Task.FromResult(new RenderPdfResponse { @@ -243,7 +288,7 @@ public override Task ImportPostProcess(ImportPostProc int? thumbnailSize = request.ThumbnailSize == 0 ? null : request.ThumbnailSize; var barcodeOptions = request.BarcodeDetectionOptionsXml.FromXml(); using var newImage = - _importPostProcessor.AddPostProcessingData(image, null, thumbnailSize, barcodeOptions, true); + ImportPostProcessor.AddPostProcessingData(image, null, thumbnailSize, barcodeOptions, true); return Task.FromResult(new ImportPostProcessResponse { Image = ImageSerializer.Serialize(newImage, @@ -293,7 +338,7 @@ public override async Task TwainGetDeviceList(GetDeviceLi try { var options = request.OptionsXml.FromXml(); - var deviceList = await _twainSessionController.GetDeviceList(options); + var deviceList = await _twainController.GetDeviceList(options); return new GetDeviceListResponse { DeviceListXml = deviceList.ToXml() @@ -305,6 +350,24 @@ public override async Task TwainGetDeviceList(GetDeviceLi } } + public override async Task TwainGetCaps(GetCapsRequest request, ServerCallContext context) + { + using var callRef = StartCall(); + try + { + var options = request.OptionsXml.FromXml(); + var caps = await _twainController.GetCaps(options); + return new GetCapsResponse + { + ScanCapsXml = caps?.ToXml() ?? "" + }; + } + catch (Exception e) + { + return new GetCapsResponse { Error = RemotingHelper.ToError(e) }; + } + } + public override async Task TwainScan(TwainScanRequest request, IServerStreamWriter responseStream, ServerCallContext context) { @@ -324,10 +387,14 @@ public override async Task TwainScan(TwainScanRequest request, memoryBuffer => sequencedWriter.Write(new TwainScanResponse { MemoryBuffer = memoryBuffer + }), + canceled => sequencedWriter.Write(new TwainScanResponse + { + TransferCanceled = canceled }) ); var options = request.OptionsXml.FromXml(); - await _twainSessionController.StartScan(options, twainEvents, context.CancellationToken); + await _twainController.StartScan(options, twainEvents, context.CancellationToken); } catch (Exception e) { diff --git a/NAPS2.Sdk/Remoting/Worker/WorkerType.cs b/NAPS2.Sdk/Remoting/Worker/WorkerType.cs new file mode 100644 index 0000000000..a4ecd6746c --- /dev/null +++ b/NAPS2.Sdk/Remoting/Worker/WorkerType.cs @@ -0,0 +1,7 @@ +namespace NAPS2.Remoting.Worker; + +internal enum WorkerType +{ + Native, + WinX86 +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/BarcodeDetectionOptions.cs b/NAPS2.Sdk/Scan/BarcodeDetectionOptions.cs index 6d933f5d22..986e8aef49 100644 --- a/NAPS2.Sdk/Scan/BarcodeDetectionOptions.cs +++ b/NAPS2.Sdk/Scan/BarcodeDetectionOptions.cs @@ -2,6 +2,9 @@ namespace NAPS2.Scan; +/// +/// Options for detecting barcodes using ZXing. +/// public class BarcodeDetectionOptions { public bool DetectBarcodes { get; set; } diff --git a/NAPS2.Sdk/Scan/BarcodeDetector.cs b/NAPS2.Sdk/Scan/BarcodeDetector.cs index c95a830d20..8ee54e0c97 100644 --- a/NAPS2.Sdk/Scan/BarcodeDetector.cs +++ b/NAPS2.Sdk/Scan/BarcodeDetector.cs @@ -8,29 +8,29 @@ namespace NAPS2.Scan; /// A wrapper around the ZXing library that detects patch-t and other barcodes. /// http://www.alliancegroup.co.uk/patch-codes.htm /// -public static class BarcodeDetector +internal static class BarcodeDetector { private static readonly BarcodeFormat PATCH_T_FORMAT = BarcodeFormat.CODE_39; - public static BarcodeDetection Detect(IMemoryImage image, BarcodeDetectionOptions options) + public static Barcode Detect(IMemoryImage image, BarcodeDetectionOptions options) { // TODO: Probably shouldn't have DetectBarcodes be in the options class? The call shouldn't happen at all. if (!options.DetectBarcodes) { - return BarcodeDetection.NotAttempted; + return Barcode.NoDetection; } var zxingOptions = options.ZXingOptions ?? new DecodingOptions { TryHarder = true, - PossibleFormats = options.PatchTOnly ? new List { PATCH_T_FORMAT } : null + PossibleFormats = options.PatchTOnly ? [PATCH_T_FORMAT] : null }; var reader = new BarcodeReader(x => new MemoryImageLuminanceSource(x)) { Options = zxingOptions }; var result = reader.Decode(image); - return new BarcodeDetection(true, result != null, result?.Text); + return new Barcode(true, result != null, result?.Text); } private class MemoryImageLuminanceSource : LuminanceSource diff --git a/NAPS2.Sdk/Scan/BitDepthCaps.cs b/NAPS2.Sdk/Scan/BitDepthCaps.cs new file mode 100644 index 0000000000..bac91e853b --- /dev/null +++ b/NAPS2.Sdk/Scan/BitDepthCaps.cs @@ -0,0 +1,22 @@ +namespace NAPS2.Scan; + +/// +/// Represents valid values for ScanOptions.BitDepth as part of PerSourceCaps. +/// +public class BitDepthCaps +{ + /// + /// Whether the scanner supports BitDepth.Color. + /// + public bool SupportsColor { get; init; } + + /// + /// Whether the scanner supports BitDepth.Grayscale. + /// + public bool SupportsGrayscale { get; init; } + + /// + /// Whether the scanner supports BitDepth.BlackAndWhite. + /// + public bool SupportsBlackAndWhite { get; init; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/DeviceUriChangedEventArgs.cs b/NAPS2.Sdk/Scan/DeviceUriChangedEventArgs.cs new file mode 100644 index 0000000000..518814094d --- /dev/null +++ b/NAPS2.Sdk/Scan/DeviceUriChangedEventArgs.cs @@ -0,0 +1,20 @@ +namespace NAPS2.Scan; + +public class DeviceUriChangedEventArgs : EventArgs +{ + public DeviceUriChangedEventArgs(string? iconUri, string? connectionUri) + { + IconUri = iconUri; + ConnectionUri = connectionUri; + } + + /// + /// Gets the new icon URI. + /// + public string? IconUri { get; } + + /// + /// Gets the new connection URI. + /// + public string? ConnectionUri { get; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/DpiCaps.cs b/NAPS2.Sdk/Scan/DpiCaps.cs new file mode 100644 index 0000000000..c877d198a9 --- /dev/null +++ b/NAPS2.Sdk/Scan/DpiCaps.cs @@ -0,0 +1,70 @@ +using System.Collections.Immutable; + +namespace NAPS2.Scan; + +/// +/// Represents valid values for ScanOptions.Dpi as part of PerSourceCaps. +/// +public class DpiCaps +{ + private static readonly int[] TargetCommonValues = [0, 100, 150, 200, 300, 400, 600, 800, 1200, 2400, 4800]; + + /// + /// Creates an instance of DpiCaps that allows values in the specified range. + /// + /// The lowest valid DPI value, inclusive. + /// The highest valid DPI value, inclusive. + /// The increment between valid DPI values (must be >0). + /// + public static DpiCaps ForRange(int min, int max, int step) + { + if (step <= 0) return new DpiCaps(); + var values = new List(); + for (int i = min; i <= max; i += step) + { + values.Add(i); + } + return new DpiCaps + { + Values = values.ToImmutableList() + }; + } + + /// + /// Allowed values for ScanOptions.Dpi. + /// + public ImmutableList? Values { get; init; } + + /// + /// Recommended values for ScanOptions.Dpi to be presented to the user. + /// + public ImmutableList? CommonValues + { + get + { + if (Values == null) return null; + var commonValues = new List(); + int j = 0; + for (int i = 0; i < Values.Count;i++) + { + int value = Values[i]; + if (i == Values.Count - 1) + { + commonValues.Add(value); + continue; + } + bool include = false; + while (j < TargetCommonValues.Length && TargetCommonValues[j] <= value) + { + include = true; + j++; + } + if (include) + { + commonValues.Add(value); + } + } + return commonValues.ToImmutableList(); + } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Driver.cs b/NAPS2.Sdk/Scan/Driver.cs index 2b83473254..2367bb07a6 100644 --- a/NAPS2.Sdk/Scan/Driver.cs +++ b/NAPS2.Sdk/Scan/Driver.cs @@ -1,11 +1,40 @@ namespace NAPS2.Scan; +/// +/// Specifies the driver type to be used for performing the actual scan. Available options depend on the platform. +///
+/// See https://github.com/cyanfish/naps2/tree/master/NAPS2.Sdk#drivers for more details. +///
public enum Driver { + /// + /// Use the default driver for the platform (Windows -> Wia, Mac -> Apple, Linux -> Sane). + /// Default, + + /// + /// Use a WIA driver (Windows-only). + /// Wia, + + /// + /// Use a TWAIN driver (Windows-only). Mac can use TWAIN indirectly through the Apple driver type. + /// Twain, + + /// + /// Use an Apple ImageCaptureCore driver (Mac-only). You will also need to compile against a macOS framework target + /// (e.g net8-macos) to use this driver type. + /// Apple, + + /// + /// Use a SANE driver (Linux and Mac). To use on Mac you'll also need to reference the NAPS2.Sane.Binaries package. + /// Sane, + + /// + /// Use an ESCL network driver (all platforms). + /// Escl } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/DriverNames.cs b/NAPS2.Sdk/Scan/DriverNames.cs index b01db27fc0..447525926e 100644 --- a/NAPS2.Sdk/Scan/DriverNames.cs +++ b/NAPS2.Sdk/Scan/DriverNames.cs @@ -1,11 +1,11 @@ namespace NAPS2.Scan; -public static class DriverNames +internal static class DriverNames { // TODO: Ideally we just change everything to use the Driver enum public const string WIA = "wia"; public const string TWAIN = "twain"; public const string SANE = "sane"; - // TODO: Remove this - public const string PROXY = "proxy"; + public const string ESCL = "escl"; + public const string APPLE = "apple"; } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/EsclOptions.cs b/NAPS2.Sdk/Scan/EsclOptions.cs new file mode 100644 index 0000000000..fcb652f5af --- /dev/null +++ b/NAPS2.Sdk/Scan/EsclOptions.cs @@ -0,0 +1,19 @@ +using NAPS2.Escl; + +namespace NAPS2.Scan; + +/// +/// Scanning options specific to the ESCL driver. +/// +public class EsclOptions +{ + /// + /// The maximum time (in ms) to search for ESCL devices when calling GetDevices or at the start of a scan. + /// + public int SearchTimeout { get; set; } = 5000; + + /// + /// The security policy to use for ESCL connections. + /// + public EsclSecurityPolicy SecurityPolicy { get; set; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Exceptions/DeviceException.cs b/NAPS2.Sdk/Scan/Exceptions/DeviceException.cs index c9e0a137d0..fc4a77d935 100644 --- a/NAPS2.Sdk/Scan/Exceptions/DeviceException.cs +++ b/NAPS2.Sdk/Scan/Exceptions/DeviceException.cs @@ -1,5 +1,8 @@ namespace NAPS2.Scan.Exceptions; +/// +/// Indicates an exception related to the physical device (e.g. offline, busy, paper jam). +/// public class DeviceException : ScanDriverException { public DeviceException(string message) @@ -10,4 +13,44 @@ public DeviceException(string message) public DeviceException() { } -} \ No newline at end of file +} + +/// +/// Indicates the scanning device's automatic document feeder (ADF) is empty and doesn't have any pages to scan. +/// +public class DeviceFeederEmptyException() : DeviceException(SdkResources.NoPagesInFeeder); + +/// +/// Indicates the scanning device is offline and a connection couldn't be established. +/// +public class DeviceOfflineException() : DeviceException(SdkResources.DeviceOffline); + +/// +/// Indicates the connection with the scanning device was interrupted. +/// +public class DeviceCommunicationException() : DeviceException(SdkResources.DeviceCommunicationFailure); + +/// +/// Indicates the scanning device is busy with another operation. Maybe retry later. +/// +public class DeviceBusyException() : DeviceException(SdkResources.DeviceBusy); + +/// +/// Indicates the scanning device is currently warming up and can't be interacted with. +/// +public class DeviceWarmingUpException() : DeviceException(SdkResources.DeviceWarmingUp); + +/// +/// Indicates the scanning device's flatbed cover is open and needs to be closed before scanning. +/// +public class DeviceCoverOpenException() : DeviceException(SdkResources.DeviceCoverOpen); + +/// +/// Indicates the scanning device could not be found. It may have been uninstalled or disconnected. +/// +public class DeviceNotFoundException() : DeviceException(SdkResources.DeviceNotFound); + +/// +/// Indicates the scanning device's automatic document feeder (ADF) has a paper jam that needs to be cleared. +/// +public class DevicePaperJamException() : DeviceException(SdkResources.DevicePaperJam); \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Exceptions/DeviceNotFoundException.cs b/NAPS2.Sdk/Scan/Exceptions/DeviceNotFoundException.cs deleted file mode 100644 index ef48d53acb..0000000000 --- a/NAPS2.Sdk/Scan/Exceptions/DeviceNotFoundException.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace NAPS2.Scan.Exceptions; - -public class DeviceNotFoundException : ScanDriverException -{ - public DeviceNotFoundException() - : base(SdkResources.DeviceNotFound) - { - } - - public DeviceNotFoundException(string message) - : base(message) - { - } - - public DeviceNotFoundException(Exception innerException) - : base(SdkResources.DeviceNotFound, innerException) - { - } - - public DeviceNotFoundException(string message, Exception innerException) - : base(message, innerException) - { - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Exceptions/DriverNotSupportedException.cs b/NAPS2.Sdk/Scan/Exceptions/DriverNotSupportedException.cs index 1a68624412..85bcd9eb81 100644 --- a/NAPS2.Sdk/Scan/Exceptions/DriverNotSupportedException.cs +++ b/NAPS2.Sdk/Scan/Exceptions/DriverNotSupportedException.cs @@ -1,5 +1,8 @@ namespace NAPS2.Scan.Exceptions; +/// +/// Indicates that a driver was selected that isn't supported with the current platform or framework. +/// public class DriverNotSupportedException : ScanDriverException { public DriverNotSupportedException() diff --git a/NAPS2.Sdk/Scan/Exceptions/NoDuplexSupportException.cs b/NAPS2.Sdk/Scan/Exceptions/NoDuplexSupportException.cs index 5797c1c1dd..b03ca029da 100644 --- a/NAPS2.Sdk/Scan/Exceptions/NoDuplexSupportException.cs +++ b/NAPS2.Sdk/Scan/Exceptions/NoDuplexSupportException.cs @@ -1,5 +1,8 @@ namespace NAPS2.Scan.Exceptions; +/// +/// Indicates that PaperSource.Duplex was selected but the scanning device/driver doesn't support duplex scanning. +/// public class NoDuplexSupportException : ScanDriverException { public NoDuplexSupportException() diff --git a/NAPS2.Sdk/Scan/Exceptions/NoFeederSupportException.cs b/NAPS2.Sdk/Scan/Exceptions/NoFeederSupportException.cs index 08318bac2e..43988a2e9e 100644 --- a/NAPS2.Sdk/Scan/Exceptions/NoFeederSupportException.cs +++ b/NAPS2.Sdk/Scan/Exceptions/NoFeederSupportException.cs @@ -1,5 +1,8 @@ namespace NAPS2.Scan.Exceptions; +/// +/// Indicates that PaperSource.Feeder was selected but the scanning device/driver doesn't support feeder scanning. +/// public class NoFeederSupportException : ScanDriverException { public NoFeederSupportException() diff --git a/NAPS2.Sdk/Scan/Exceptions/NoPagesException.cs b/NAPS2.Sdk/Scan/Exceptions/NoPagesException.cs deleted file mode 100644 index f313990756..0000000000 --- a/NAPS2.Sdk/Scan/Exceptions/NoPagesException.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace NAPS2.Scan.Exceptions; - -public class NoPagesException : ScanDriverException -{ - public NoPagesException() - : base(SdkResources.NoPagesInFeeder) - { - } - - public NoPagesException(string message) - : base(message) - { - } - - public NoPagesException(Exception innerException) - : base(SdkResources.NoPagesInFeeder, innerException) - { - } - - public NoPagesException(string message, Exception innerException) - : base(message, innerException) - { - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Exceptions/SaneNotAvailableException.cs b/NAPS2.Sdk/Scan/Exceptions/SaneNotAvailableException.cs deleted file mode 100644 index 62f690458d..0000000000 --- a/NAPS2.Sdk/Scan/Exceptions/SaneNotAvailableException.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace NAPS2.Scan.Exceptions; - -public class SaneNotAvailableException : ScanDriverException -{ - private const string PACKAGES = "\nsane\nsane-utils"; - - public SaneNotAvailableException() : base(SdkResources.SaneNotAvailable + PACKAGES) - { - } - - public SaneNotAvailableException(Exception innerException) : base(SdkResources.SaneNotAvailable + PACKAGES, innerException) - { - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Exceptions/ScanDriverException.cs b/NAPS2.Sdk/Scan/Exceptions/ScanDriverException.cs index 9e33e97831..37adb3224d 100644 --- a/NAPS2.Sdk/Scan/Exceptions/ScanDriverException.cs +++ b/NAPS2.Sdk/Scan/Exceptions/ScanDriverException.cs @@ -1,5 +1,9 @@ namespace NAPS2.Scan.Exceptions; +/// +/// Indicates an exception with the scanning driver. The concrete class may be a well-known exception type (e.g. +/// DeviceOfflineException) or an unknown exception (ScanDriverUnknownException). +/// public abstract class ScanDriverException : Exception { protected ScanDriverException() diff --git a/NAPS2.Sdk/Scan/Exceptions/ScanDriverUnknownException.cs b/NAPS2.Sdk/Scan/Exceptions/ScanDriverUnknownException.cs index f9e24b9b95..213f60c25e 100644 --- a/NAPS2.Sdk/Scan/Exceptions/ScanDriverUnknownException.cs +++ b/NAPS2.Sdk/Scan/Exceptions/ScanDriverUnknownException.cs @@ -1,5 +1,8 @@ namespace NAPS2.Scan.Exceptions; +/// +/// Indicates an unknown exception with the scanning driver that should be logged with full diagnostics. +/// public class ScanDriverUnknownException : ScanDriverException { public ScanDriverUnknownException() diff --git a/NAPS2.Sdk/Scan/HorizontalAlign.cs b/NAPS2.Sdk/Scan/HorizontalAlign.cs new file mode 100644 index 0000000000..bdca97fe9a --- /dev/null +++ b/NAPS2.Sdk/Scan/HorizontalAlign.cs @@ -0,0 +1,11 @@ +namespace NAPS2.Scan; + +/// +/// Specifies the alignment of the physical page on a flatbed scanner. +/// +public enum HorizontalAlign +{ + Right, + Center, + Left +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/IDevicePrompt.cs b/NAPS2.Sdk/Scan/IDevicePrompt.cs deleted file mode 100644 index f3d0006405..0000000000 --- a/NAPS2.Sdk/Scan/IDevicePrompt.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace NAPS2.Scan; - -public interface IDevicePrompt -{ - public ScanDevice? PromptForDevice(List deviceList, IntPtr dialogParent); -} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/IScanController.cs b/NAPS2.Sdk/Scan/IScanController.cs deleted file mode 100644 index 655eabd780..0000000000 --- a/NAPS2.Sdk/Scan/IScanController.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading; - -namespace NAPS2.Scan; - -public interface IScanController -{ - Task> GetDeviceList(ScanOptions options); - - IAsyncEnumerable Scan(ScanOptions options, CancellationToken cancelToken = default); -} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Apple/AppleScanDriver.cs b/NAPS2.Sdk/Scan/Internal/Apple/AppleScanDriver.cs index f472f0c4b5..739bd009c5 100644 --- a/NAPS2.Sdk/Scan/Internal/Apple/AppleScanDriver.cs +++ b/NAPS2.Sdk/Scan/Internal/Apple/AppleScanDriver.cs @@ -14,24 +14,39 @@ public AppleScanDriver(ScanningContext scanningContext) _scanningContext = scanningContext; } - public async Task> GetDeviceList(ScanOptions options) + public async Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback) { using var reader = new DeviceReader(); + reader.DeviceFound += (_, args) => + { + var device = args.Device; + if (device.Uuid != null && device.Name != null) + { + callback(new ScanDevice(Driver.Apple, device.Uuid, device.Name)); + } + }; reader.Start(); - await Task.Delay(2000); - return reader.Devices - .Where(x => x.Uuid != null && x.Name != null) - .Select(x => new ScanDevice(x.Uuid!, x.Name!)) - .ToList(); + await Task.Delay(2000, cancelToken); + } + + public async Task GetCaps(ScanOptions options, CancellationToken cancelToken) + { + using var reader = new DeviceReader(); + // Note we don't want to dispose the device, as the ICDeviceBrowser manages its lifetime. + var device = await GetDevice(reader, options.Device!); + using var oper = + new DeviceOperator(_scanningContext, device, reader, options, cancelToken, null!, null!); + return await oper.GetCaps(); } public async Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) { using var reader = new DeviceReader(); - using var device = await GetDevice(reader, options.Device!); + // Note we don't want to dispose the device, as the ICDeviceBrowser manages its lifetime. + var device = await GetDevice(reader, options.Device!); using var oper = - new DeviceOperator(_scanningContext, device, options, cancelToken, scanEvents, callback); + new DeviceOperator(_scanningContext, device, reader, options, cancelToken, scanEvents, callback); await oper.Scan(); } @@ -53,7 +68,7 @@ private async Task GetDevice(DeviceReader reader, ScanDevice sc } catch (TaskCanceledException) { - throw new DeviceException(SdkResources.DeviceOffline); + throw new DeviceOfflineException(); } } } diff --git a/NAPS2.Sdk/Scan/Internal/Apple/DeviceOperator.cs b/NAPS2.Sdk/Scan/Internal/Apple/DeviceOperator.cs index cf6c9bdf55..13d1df11c5 100644 --- a/NAPS2.Sdk/Scan/Internal/Apple/DeviceOperator.cs +++ b/NAPS2.Sdk/Scan/Internal/Apple/DeviceOperator.cs @@ -1,9 +1,13 @@ #if MAC +using System.Collections.Immutable; using System.Threading; +using AppKit; using CoreGraphics; using Foundation; using ImageCaptureCore; +using Microsoft.Extensions.Logging; using NAPS2.Images.Bitwise; +using NAPS2.Images.Mac; using NAPS2.Scan.Exceptions; namespace NAPS2.Scan.Internal.Apple; @@ -11,22 +15,31 @@ namespace NAPS2.Scan.Internal.Apple; internal class DeviceOperator : ICScannerDeviceDelegate { private readonly ScanningContext _scanningContext; + private readonly ILogger _logger; private readonly ICScannerDevice _device; + private ICScannerFunctionalUnit? _unit; + private nuint _resolution; + private readonly DeviceReader _reader; private readonly ScanOptions _options; private readonly IScanEvents _scanEvents; private readonly Action _callback; private readonly TaskCompletionSource _openSessionTcs = new(); private readonly TaskCompletionSource _readyTcs = new(); private TaskCompletionSource _unitTcs = new(); - private readonly TaskCompletionSource _scanTcs = new(); + private readonly TaskCompletionSource _scanSuccessTcs = new(); + private readonly TaskCompletionSource _scanCompleteTcs = new(); + private TaskCompletionSource? _cancelTcs; private readonly TaskCompletionSource _closeTcs = new(); + private Task? _writeToCallback; private MemoryStream? _buffer; - public DeviceOperator(ScanningContext scanningContext, ICScannerDevice device, ScanOptions options, - CancellationToken cancelToken, IScanEvents scanEvents, Action callback) + public DeviceOperator(ScanningContext scanningContext, ICScannerDevice device, DeviceReader reader, + ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) { _scanningContext = scanningContext; + _logger = scanningContext.Logger; _device = device; + _reader = reader; _options = options; _scanEvents = scanEvents; _callback = callback; @@ -36,45 +49,53 @@ public DeviceOperator(ScanningContext scanningContext, ICScannerDevice device, S _openSessionTcs.TrySetCanceled(); _readyTcs.TrySetCanceled(); _unitTcs.TrySetCanceled(); - _scanTcs.TrySetCanceled(); + _scanSuccessTcs.TrySetCanceled(); _closeTcs.TrySetCanceled(); }); } public override void DidOpenSession(ICDevice device, NSError? error) { + _logger.LogDebug("DidOpenSession {Error}", error); SetResultOrError(_openSessionTcs, error); } public override void DidBecomeReady(ICDevice device) { + _logger.LogDebug("DidBecomeReady"); _readyTcs.TrySetResult(); } public override void DidCloseSession(ICDevice device, NSError? error) { + _logger.LogDebug("DidCloseSession {Error}", error); SetResultOrError(_closeTcs, error); } public override void DidReceiveStatusInformation(ICDevice device, NSDictionary status) { var state = status[ICStatusNotificationKeys.NotificationKey] as NSString; - Console.WriteLine($"{nameof(DidReceiveStatusInformation)}: Status: {status} State {state}"); + _logger.LogDebug("DidReceiveStatusInformation {State}", state); if (state == ICScannerStatus.WarmingUp) { _scanEvents.PageStart(); } + if (_cancelTcs != null && !_unit!.ScanInProgress) + { + _cancelTcs.SetResult(); + } } public override void DidEncounterError(ICDevice device, NSError? error) { + _logger.LogDebug("DidEncounterError {Error}", error); var ex = error != null ? new DeviceException(error.Description) : new DeviceException(); // TODO: Put these in a list or something _openSessionTcs.TrySetException(ex); _readyTcs.TrySetException(ex); _unitTcs.TrySetException(ex); - _scanTcs.TrySetException(ex); + _scanSuccessTcs.TrySetException(ex); _closeTcs.TrySetException(ex); } @@ -82,52 +103,157 @@ public override void DidEncounterError(ICDevice device, NSError? error) // TODO: to become available before sending a busy error. public override void DidBecomeAvailable(ICScannerDevice scanner) { - Console.WriteLine($"{nameof(DidBecomeAvailable)}: {scanner}"); + _logger.LogDebug("DidBecomeAvailable"); } public override void DidSelectFunctionalUnit( ICScannerDevice scanner, ICScannerFunctionalUnit functionalUnit, NSError? error) { + _logger.LogDebug("DidSelectFunctionalUnit {Unit} {Error}", functionalUnit.GetType().Name, error); SetResultOrError(_unitTcs, functionalUnit, error); } public override void DidScanToBandData(ICScannerDevice scanner, ICScannerBandData data) { - var (pixelFormat, subPixelType) = (data.PixelDataType, data.NumComponents, data.BitsPerComponent) switch - { - (ICScannerPixelDataType.BW, 1, 1) => (ImagePixelFormat.BW1, SubPixelType.Bit), - (ICScannerPixelDataType.Gray, 1, 8) => (ImagePixelFormat.Gray8, SubPixelType.Gray), - (ICScannerPixelDataType.Rgb, 3, 8) => (ImagePixelFormat.RGB24, SubPixelType.Rgb), - (ICScannerPixelDataType.Rgb, 4, 8) => (ImagePixelFormat.ARGB32, SubPixelType.Rgba), - _ => (ImagePixelFormat.Unsupported, null) - }; - if (pixelFormat == ImagePixelFormat.Unsupported) + var expectedBufferLength = (int) (data.FullImageHeight * data.BytesPerRow); + _buffer ??= new MemoryStream(expectedBufferLength); + data.DataBuffer!.AsStream().CopyTo(_buffer); + // TODO: The buffer gets written pretty much all at once, at least for escl - maybe we can/should reuse TwainProgressEstimator + _scanEvents.PageProgress(_buffer.Length / (double) expectedBufferLength); + + if (_buffer.Length >= expectedBufferLength) { - // TODO: Set errors - return; + _logger.LogDebug("DidScanToBandData buffer complete"); + var fullBuffer = _buffer; + _buffer = null; + var tcs = new TaskCompletionSource(); + // Ensure sequencing is maintained when writing to the callback even if copy tasks finish out of order + var previousCallback = _writeToCallback ?? Task.CompletedTask; + _writeToCallback = Task.Run(async () => + { + await previousCallback; + var image = await tcs.Task; + if (image != null) + { + _callback(image); + } + }); + Task.Run(() => + { + try + { + // We prefer to use the provided color profile for maximum color accuracy. If one isn't present we + // fall back to a direct bitwise copy if it's in a supported pixel format. + var (pixelFormat, subPixelType) = + (data.PixelDataType, data.NumComponents, data.BitsPerComponent) switch + { + (ICScannerPixelDataType.BW, 1, 1) => (ImagePixelFormat.BW1, SubPixelType.Bit), + (ICScannerPixelDataType.Gray, 1, 8) => (ImagePixelFormat.Gray8, SubPixelType.Gray), + (ICScannerPixelDataType.Rgb, 3, 8) => (ImagePixelFormat.RGB24, SubPixelType.Rgb), + (ICScannerPixelDataType.Rgb, 4, 8) => (ImagePixelFormat.RGB24, SubPixelType.Rgbn), + _ => (ImagePixelFormat.Unknown, null) + }; + _logger.LogDebug( + "Image data: width {Width}, height {Height}, type {Type}, comp {Comp}, " + + "bits/comp {BitsPerComp}, bits/pixel {BitsPerPixel}, bytes/row {BytesPerRow}, data len {DataLen}", + data.FullImageWidth, data.FullImageHeight, data.PixelDataType, data.NumComponents, + data.BitsPerComponent, data.BitsPerPixel, data.BytesPerRow, fullBuffer.Length); + if (data.ColorSyncProfilePath != null) + { + _logger.LogDebug($"Flushing image with color sync profile {data.ColorSyncProfilePath}"); + FlushImageWithColorSpace(tcs, fullBuffer, data, subPixelType); + } + else if (pixelFormat != ImagePixelFormat.Unknown && subPixelType != null) + { + _logger.LogDebug($"Flushing image with pixel format {pixelFormat}"); + FlushImageDirectly(tcs, fullBuffer, data, subPixelType, pixelFormat); + } + else + { + _logger.LogError( + "No color sync profile and unsupported ICC pixel format " + + "{PixelDataType} {NumComponents} {BitsPerComponent}", + data.PixelDataType, data.NumComponents, data.BitsPerComponent); + } + _scanEvents.PageStart(); + } + finally + { + tcs.TrySetResult(null); + } + }); } + } + + private void FlushImageDirectly(TaskCompletionSource tcs, MemoryStream fullBuffer, + ICScannerBandData data, SubPixelType subPixelType, + ImagePixelFormat pixelFormat) + { + var image = _scanningContext.ImageContext.Create( + (int) data.FullImageWidth, (int) data.FullImageHeight, pixelFormat); var bufferInfo = new PixelInfo( (int) data.FullImageWidth, (int) data.FullImageHeight, subPixelType!, (int) data.BytesPerRow); _buffer ??= new MemoryStream((int) bufferInfo.Length); - data.DataBuffer!.AsStream().CopyTo(_buffer); - // TODO: The buffer gets written pretty much all at once, at least for escl - maybe we can/should reuse TwainProgressEstimator - _scanEvents.PageProgress(_buffer.Length / (double) bufferInfo.Length); - if (_buffer.Length >= bufferInfo.Length) + new CopyBitwiseImageOp().Perform(fullBuffer.GetBuffer(), bufferInfo, image); + _logger.LogDebug("Setting resolution to {Dpi}", _resolution); + image.SetResolution(_resolution, _resolution); + tcs.SetResult(image); + } + + private void FlushImageWithColorSpace(TaskCompletionSource tcs, MemoryStream fullBuffer, + ICScannerBandData data, SubPixelType? subPixelType) + { + var colorSpace = CGColorSpace.CreateIccData(NSData.FromFile(data.ColorSyncProfilePath!)); + var w = (int) data.FullImageWidth; + var h = (int) data.FullImageHeight; + var bitsPerComponent = (int) data.BitsPerComponent; + var bitsPerPixel = (int) data.BitsPerPixel; + var bytesPerRow = (int) data.BytesPerRow; + var flags = (bitsPerPixel == 32 ? CGBitmapFlags.NoneSkipLast : CGBitmapFlags.None) | + CGBitmapFlags.ByteOrderDefault; + var buffer = fullBuffer.GetBuffer(); + var dataProvider = new CGDataProvider(buffer, 0, buffer.Length); + + // There is an apparent bug in ImageCaptureCore where grayscale images can report a bytesPerRow value that is + // aligned to a word boundary (and the buffer is sized to match), but the actual data is stored as if that + // wasn't the case, leaving a block of zeros at the end of the buffer. We correct for this here. + // TODO: Is there any case where this will backfire? Can we detect the problem (e.g. by checking for zeros at + // the end of the buffer)? + if (subPixelType == SubPixelType.Gray) { - var image = _scanningContext.ImageContext.Create( - (int) data.FullImageWidth, (int) data.FullImageHeight, pixelFormat); - new CopyBitwiseImageOp().Perform(_buffer.GetBuffer(), bufferInfo, image); - _callback(image); - _buffer = null; + bytesPerRow = w; + } + + var cgImage = new CGImage(w, h, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpace, flags, + dataProvider, null, true, CGColorRenderingIntent.Default); + var imageRep = new NSBitmapImageRep(cgImage); + var nsImage = new NSImage(); + nsImage.AddRepresentation(imageRep); + // TODO: Could maybe do this without the NAPS2.Images.Mac reference but that would require duplicating + // a bunch of logic to normalize image reps etc. + var macImage = new MacImage(nsImage); + _logger.LogDebug("Setting resolution to {Dpi}", _resolution); + macImage.SetResolution(_resolution, _resolution); + if (_scanningContext.ImageContext is MacImageContext) + { + tcs.SetResult(macImage); + } + else + { + var image = macImage.Copy(_scanningContext.ImageContext); + macImage.Dispose(); + tcs.SetResult(image); } } public override void DidCompleteScan(ICScannerDevice scanner, NSError? error) { - SetResultOrError(_scanTcs, error); + _logger.LogDebug("DidCompleteScan {Error}", error); + SetResultOrError(_scanSuccessTcs, error); + SetResultOrError(_scanCompleteTcs, error); } private void SetResultOrError(TaskCompletionSource tcs, NSError? error) @@ -163,53 +289,210 @@ public override void DidRemoveDevice(ICDevice device) { } + public async Task GetCaps() + { + try + { + _device.Delegate = this; + _logger.LogDebug("ICC: Opening session for caps"); + _device.RequestOpenSession(); + await _openSessionTcs.Task; + _logger.LogDebug("ICC: Waiting for ready"); + await _readyTcs.Task; + + var unitTypes = _device.AvailableFunctionalUnitTypes; + bool supportsFeeder = unitTypes.Contains((NSNumber) (int) ICScannerFunctionalUnitType.Flatbed); + bool supportsFlatbed = unitTypes.Contains((NSNumber) (int) ICScannerFunctionalUnitType.DocumentFeeder); + bool supportsDuplex = false; + PerSourceCaps? flatbedCaps = null; + PerSourceCaps? feederCaps = null; + PerSourceCaps? duplexCaps = null; + + _logger.LogDebug("ICC: Selecting flatbed unit"); + _unit = await SelectUnit(ICScannerFunctionalUnitType.Flatbed); + if (_unit is ICScannerFunctionalUnitFlatbed flatbedUnit) + { + flatbedCaps = GetCapsFromUnit(flatbedUnit); + } + + _logger.LogDebug("ICC: Selecting feeder unit"); + _unit = await SelectUnit(ICScannerFunctionalUnitType.DocumentFeeder); + if (_unit is ICScannerFunctionalUnitDocumentFeeder feederUnit) + { + supportsDuplex = feederUnit.SupportsDuplexScanning; + feederCaps = GetCapsFromUnit(feederUnit); + if (supportsDuplex) duplexCaps = feederCaps; + } + + _logger.LogDebug("ICC: Closing session"); + _device.RequestCloseSession(); + await _closeTcs.Task; + _logger.LogDebug("ICC: Caps query success"); + + return new ScanCaps + { + MetadataCaps = new() + { + SerialNumber = _device.SerialNumber + }, + PaperSourceCaps = new() + { + SupportsFlatbed = supportsFlatbed, + SupportsFeeder = supportsFeeder, + SupportsDuplex = supportsDuplex, + CanCheckIfFeederHasPaper = true + }, + FlatbedCaps = flatbedCaps, + FeederCaps = feederCaps, + DuplexCaps = duplexCaps + }; + } + finally + { + if (_device.HasOpenSession) + { + _logger.LogDebug("ICC: Closing session (in finally)"); + _device.RequestCloseSession(); + } + } + } + + private PerSourceCaps GetCapsFromUnit(ICScannerFunctionalUnit unit) + { + unit.MeasurementUnit = ICScannerMeasurementUnit.Inches; + return new PerSourceCaps + { + DpiCaps = new() + { + Values = unit.SupportedResolutions.Select(x => (int) x).Order().ToImmutableList() + }, + BitDepthCaps = new() + { + SupportsBlackAndWhite = unit.SupportedBitDepths.Contains((nuint) ICScannerBitDepth.Bits1), + SupportsGrayscale = unit.SupportedBitDepths.Contains((nuint) ICScannerBitDepth.Bits8), + SupportsColor = unit.SupportedBitDepths.Contains((nuint) ICScannerBitDepth.Bits8) + }, + PageSizeCaps = new() + { + ScanArea = new PageSize( + (decimal) unit.PhysicalSize.Width, + (decimal) unit.PhysicalSize.Height, + PageSizeUnit.Inch) + } + }; + } + public async Task Scan() { try { _device.Delegate = this; + _logger.LogDebug("ICC: Opening session"); _device.RequestOpenSession(); await _openSessionTcs.Task; + _logger.LogDebug("ICC: Waiting for ready"); await _readyTcs.Task; - var unit = await SelectUnit(_options.PaperSource == PaperSource.Flatbed + _logger.LogDebug("ICC: Selecting unit"); + _unit = await SelectUnit(_options.PaperSource is PaperSource.Flatbed or PaperSource.Auto ? ICScannerFunctionalUnitType.Flatbed : ICScannerFunctionalUnitType.DocumentFeeder); - SetScanArea(unit); - // TODO: Check supported resolutions? - unit.Resolution = (nuint) _options.Dpi; - unit.BitDepth = _options.BitDepth == BitDepth.BlackAndWhite + if (_unit is ICScannerFunctionalUnitDocumentFeeder { SupportsDuplexScanning: true } feederUnit) + { + feederUnit.DuplexScanningEnabled = _options.PaperSource == PaperSource.Duplex; + } + _logger.LogDebug("ICC: Setting scan parameters"); + SetScanArea(_unit); + _resolution = GetClosestResolution((nuint) _options.Dpi, _unit); + _unit.Resolution = _resolution; + _unit.BitDepth = _options.BitDepth == BitDepth.BlackAndWhite ? ICScannerBitDepth.Bits1 : ICScannerBitDepth.Bits8; - unit.PixelDataType = _options.BitDepth switch + _unit.PixelDataType = _options.BitDepth switch { BitDepth.BlackAndWhite => ICScannerPixelDataType.BW, BitDepth.Grayscale => ICScannerPixelDataType.Gray, _ => ICScannerPixelDataType.Rgb }; _device.TransferMode = ICScannerTransferMode.MemoryBased; - // TODO: increase? or maybe not as this could still be useful progress for twain scanners _device.MaxMemoryBandSize = 65536; + _logger.LogDebug("ICC: Requesting scan"); _device.RequestScan(); - await _scanTcs.Task; + await _scanSuccessTcs.Task; + if (_writeToCallback == null && _unit is ICScannerFunctionalUnitDocumentFeeder { DocumentLoaded: false }) + { + _logger.LogDebug("ICC: No pages in feeder"); + throw new DeviceFeederEmptyException(); + } + if (_writeToCallback != null) + { + _logger.LogDebug("ICC: Waiting for scan results"); + await _writeToCallback; + } + _logger.LogDebug("ICC: Closing session"); _device.RequestCloseSession(); await _closeTcs.Task; + _logger.LogDebug("ICC: Scan success"); } catch (TaskCanceledException) { - // TODO: Cancellation not working - _device.CancelScan(); + if (_unit != null && _unit.ScanInProgress) + { + _cancelTcs = new TaskCompletionSource(); + _logger.LogDebug("ICC: Cancelling scan"); + _device.CancelScan(); + await Task.WhenAny(_scanCompleteTcs.Task, _cancelTcs.Task); + } + _logger.LogDebug("ICC: Scan cancelled"); } finally { if (_device.HasOpenSession) { + _logger.LogDebug("ICC: Closing session (in finally)"); _device.RequestCloseSession(); } } } + private ICScannerDocumentType GetDocumentTypeFromPageSize(PageSize? pageSize) + { + // TODO: Maybe some tolerance, e.g. if translating over EsclScanServer? + if (pageSize == PageSize.A3) return ICScannerDocumentType.A3; + if (pageSize == PageSize.A4) return ICScannerDocumentType.A4; + if (pageSize == PageSize.A5) return ICScannerDocumentType.A5; + if (pageSize == PageSize.Letter) return ICScannerDocumentType.USLetter; + if (pageSize == PageSize.Legal) return ICScannerDocumentType.USLegal; + if (pageSize == PageSize.B4) return ICScannerDocumentType.IsoB4; + if (pageSize == PageSize.B5) return ICScannerDocumentType.IsoB5; + return ICScannerDocumentType.Default; + } + + private nuint GetClosestResolution(nuint dpi, ICScannerFunctionalUnit unit) + { + var targetDpi = dpi; + if (unit.SupportedResolutions.Count > 0) + { + targetDpi = unit.SupportedResolutions.MinBy(x => Math.Abs((int) (x - dpi))); + } + if (targetDpi != dpi) + { + _logger.LogDebug("ICC: Correcting resolution from {InDpi} to {OutDpi}", dpi, targetDpi); + } + return targetDpi; + } + private void SetScanArea(ICScannerFunctionalUnit unit) { + // Setting DocumentType should be redundant (setting ScanArea is more general), but it shouldn't hurt and may + // help with some issues with particular scanners. + if (_unit is ICScannerFunctionalUnitDocumentFeeder feederUnit) + { + feederUnit.DocumentType = GetDocumentTypeFromPageSize(_options.PageSize); + } + if (_unit is ICScannerFunctionalUnitFlatbed flatbedUnit) + { + flatbedUnit.DocumentType = GetDocumentTypeFromPageSize(_options.PageSize); + } unit.MeasurementUnit = ICScannerMeasurementUnit.Inches; var maxSize = unit.PhysicalSize; var width = Math.Min((double) _options.PageSize!.WidthInInches, maxSize.Width); @@ -239,5 +522,14 @@ private async Task SelectUnit(ICScannerFunctionalUnitTy } return _device.SelectedFunctionalUnit; } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _device.Delegate = null; + } + base.Dispose(disposing); + } } #endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Apple/DeviceReader.cs b/NAPS2.Sdk/Scan/Internal/Apple/DeviceReader.cs index 7fb59c34ae..02c1548f50 100644 --- a/NAPS2.Sdk/Scan/Internal/Apple/DeviceReader.cs +++ b/NAPS2.Sdk/Scan/Internal/Apple/DeviceReader.cs @@ -22,6 +22,11 @@ public void Start() _browser.Start(); } + public void Stop() + { + _browser.Stop(); + } + public event EventHandler? DeviceFound; public override void DidAddDevice(ICDeviceBrowser browser, ICDevice device, bool moreComing) @@ -43,8 +48,9 @@ protected override void Dispose(bool disposing) { if (disposing) { - // _browser.Stop(); - // _browser.Dispose(); + _browser.Delegate = null; + _browser.Stop(); + _browser.Dispose(); } base.Dispose(disposing); } diff --git a/NAPS2.Sdk/Scan/Internal/Escl/EsclScanDriver.cs b/NAPS2.Sdk/Scan/Internal/Escl/EsclScanDriver.cs index c373bdf6e8..72d4c37a90 100644 --- a/NAPS2.Sdk/Scan/Internal/Escl/EsclScanDriver.cs +++ b/NAPS2.Sdk/Scan/Internal/Escl/EsclScanDriver.cs @@ -1,53 +1,569 @@ -#if ESCL +using System.Collections.Immutable; +using System.Net.Http; +using System.Net.Sockets; using System.Threading; +using Microsoft.Extensions.Logging; using NAPS2.Escl; using NAPS2.Escl.Client; +using NAPS2.Pdf; +using NAPS2.Remoting; using NAPS2.Scan.Exceptions; +using NAPS2.Serialization; namespace NAPS2.Scan.Internal.Escl; -public class EsclScanDriver : IScanDriver +internal class EsclScanDriver : IScanDriver { + private const int MAX_DOCUMENT_TRIES = 5; + private const int DOCUMENT_RETRY_INTERVAL = 1000; + private readonly ScanningContext _scanningContext; + private readonly ILogger _logger; + + private static string GetUuid(ScanDevice device) + { + var parts = device.ID.Split('|'); + if (parts.Length == 1) + { + // Current IDs are just the UUID + return parts[0]; + } + if (parts.Length != 2) + { + throw new ArgumentException("Invalid ESCL device ID"); + } + // Old IDs have both the IP and UUID separated by "|" + return parts[1]; + } public EsclScanDriver(ScanningContext scanningContext) { _scanningContext = scanningContext; + _logger = scanningContext.Logger; } - - public async Task> GetDeviceList(ScanOptions options) + + public async Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback) { // TODO: Run location in a persistent background service - EsclServiceLocator locator = new EsclServiceLocator(); - var services = await locator.Locate(); - return services.Select(x => new ScanDevice(x.Uuid, x.Name)).ToList(); + var localIPsTask = options.ExcludeLocalIPs ? LocalIPsHelper.Get() : null; + using var locator = new EsclServiceLocator(service => + { + // TODO: Consider limiting available devices by security policy + var ip = service.IpV4 ?? service.IpV6!; + if (options.ExcludeLocalIPs && localIPsTask!.Result.Contains(ip.ToString())) + { + return; + } + var id = service.Uuid; + var name = string.IsNullOrEmpty(service.ScannerName) + ? $"{ip}" + : $"{service.ScannerName} ({ip})"; + var client = new EsclClient(service); + callback(new ScanDevice(Driver.Escl, id, name, client.IconUri, client.ConnectionUri)); + }); + locator.Logger = _logger; + locator.Start(); + try + { + await Task.Delay(options.EsclOptions.SearchTimeout, cancelToken); + } + catch (TaskCanceledException) + { + } + } + + public async Task GetCaps(ScanOptions options, CancellationToken cancelToken) + { + if (cancelToken.IsCancellationRequested) return new ScanCaps(); + + try + { + var (client, caps) = await GetEsclClientWithCaps(options, cancelToken, ScanEvents.Stub); + if (client == null || caps == null) return new ScanCaps(); + return new ScanCaps + { + MetadataCaps = new() + { + Model = caps.MakeAndModel, + Manufacturer = caps.Manufacturer, + SerialNumber = caps.SerialNumber, + IconUri = client.IconUri + }, + PaperSourceCaps = new() + { + SupportsFlatbed = caps.PlatenCaps != null, + SupportsFeeder = caps.AdfSimplexCaps != null, + SupportsDuplex = caps.AdfDuplexCaps != null, + CanCheckIfFeederHasPaper = true + }, + FlatbedCaps = MapCaps(caps.PlatenCaps), + FeederCaps = MapCaps(caps.AdfSimplexCaps), + DuplexCaps = MapCaps(caps.AdfDuplexCaps) + }; + } + catch (HttpRequestException ex) when (ex.InnerException is TaskCanceledException or SocketException) + { + // A connection timeout manifests as TaskCanceledException + _logger.LogError(ex, "Error connecting to ESCL device"); + throw new DeviceCommunicationException(); + } + catch (TaskCanceledException) + { + } + return new ScanCaps(); + } + + private PerSourceCaps? MapCaps(EsclInputCaps? caps) + { + if (caps == null) + { + return null; + } + return PerSourceCaps.UnionAll(caps.SettingProfiles.Select(profile => MapSettingProfile(caps, profile))); + } + + private PerSourceCaps MapSettingProfile(EsclInputCaps caps, EsclSettingProfile profile) + { + DpiCaps? dpiCaps = null; + if (profile.DiscreteResolutions.Count > 0) + { + dpiCaps = new DpiCaps + { + Values = profile.DiscreteResolutions + .Where(res => res.XResolution == res.YResolution) + .Select(res => res.XResolution).ToImmutableList() + }; + } + else if (profile.XResolutionRange != null && profile.YResolutionRange != null) + { + int min = Math.Max(profile.XResolutionRange.Min, profile.YResolutionRange.Min); + int max = Math.Min(profile.XResolutionRange.Max, profile.YResolutionRange.Max); + int step = Math.Max(profile.XResolutionRange.Step, profile.YResolutionRange.Step); + dpiCaps = DpiCaps.ForRange(min, max, step); + } + return new PerSourceCaps + { + DpiCaps = dpiCaps, + BitDepthCaps = new BitDepthCaps + { + SupportsColor = profile.ColorModes.Contains(EsclColorMode.RGB24), + SupportsGrayscale = profile.ColorModes.Contains(EsclColorMode.Grayscale8), + SupportsBlackAndWhite = profile.ColorModes.Contains(EsclColorMode.BlackAndWhite1) + }, + PageSizeCaps = caps.MaxWidth != null && caps.MaxHeight != null + ? new PageSizeCaps + { + ScanArea = new PageSize(caps.MaxWidth.Value / 300m, caps.MaxHeight.Value / 300m, PageSizeUnit.Inch) + } + : null + }; + } + + public async Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, + Action callback) + { + if (cancelToken.IsCancellationRequested) return; + + try + { + var (client, caps) = await GetEsclClientWithCaps(options, cancelToken, scanEvents); + if (client == null || caps == null) return; + var status = await client.GetStatus(); + bool hasProgressExtension = caps.Naps2Extensions?.Contains("Progress") ?? false; + bool hasErrorDetailsExtension = caps.Naps2Extensions?.Contains("ErrorDetails") ?? false; + bool hasShortTimeoutExtension = caps.Naps2Extensions?.Contains("ShortTimeout") ?? false; + bool hasAnyDpiExtension = caps.Naps2Extensions?.Contains("AnyDpi") ?? false; + var scanSettings = GetScanSettings(options, caps, hasAnyDpiExtension); + Action? progressCallback = hasProgressExtension ? scanEvents.PageProgress : null; + + if (cancelToken.IsCancellationRequested) return; + + VerifyStatus(status, scanSettings); + + var job = await CreateScanJobAndCorrectInvalidSettings(client, scanSettings); + + var cancelOnce = new Once(() => client.CancelJob(job).AssertNoAwait()); + using var cancelReg = cancelToken.Register(cancelOnce.Run); + + try + { + if (scanSettings.InputSource == EsclInputSource.Platen) + { + scanEvents.PageStart(); + } + while (true) + { + if (scanSettings.InputSource != EsclInputSource.Platen) + { + scanEvents.PageStart(); + } + var doc = await GetNextDocumentWithRetries(client, job, progressCallback, hasShortTimeoutExtension); + if (doc == null) break; + foreach (var image in GetImagesFromRawDocument(options, doc)) + { + callback(image); + } + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "ESCL driver error"); + cancelOnce.Run(); + // The root cause for the exception might be a server-side scanning error, so prefer to throw a more + // descriptive error rather than an HTTP-based exception. + if (hasErrorDetailsExtension) + { + await CheckErrorDetails(client, job); + } + // If not, then just throw the error we got + throw; + } + } + catch (HttpRequestException ex) when (ex.InnerException is TaskCanceledException or SocketException) + { + // A connection timeout manifests as TaskCanceledException + _logger.LogError(ex, "Error connecting to ESCL device"); + throw new DeviceCommunicationException(); + } + catch (TaskCanceledException) + { + } + } + + private async Task<(EsclClient?, EsclCapabilities?)> GetEsclClientWithCaps(ScanOptions options, + CancellationToken cancelToken, IScanEvents scanEvents) + { + EsclClient client; + string deviceId = options.Device!.ID; + + void SetUpClient() + { + client.SecurityPolicy = options.EsclOptions.SecurityPolicy; + client.Logger = _logger; + client.CancelToken = cancelToken; + } + void MaybeSendNewUris() + { + string? iconUri = client.IconUri; + string connectionUri = client.ConnectionUri; + if (iconUri != options.Device.IconUri || connectionUri != options.Device.ConnectionUri) + { + scanEvents.DeviceUriChanged(iconUri, connectionUri); + } + } + + // If we only have a URI to connect, just use it. + if (deviceId.StartsWith("http://") || deviceId.StartsWith("https://")) + { + client = new EsclClient(new Uri(deviceId)); + SetUpClient(); + EsclCapabilities caps; + try + { + caps = await client.GetCapabilities(); + } + catch (HttpRequestException) + { + throw new DeviceOfflineException(); + } + return (client, caps); + } + + // If we have both a UUID and a ConnectionUri, race an mDNS request with a GetCapabilities request. + // This is the best of both worlds: + // - If the connection info is up to date, we connect directly immediately. + // - If the IP has changed, we find out and fall back to that + // TODO: Maybe racing [GetCapabilities] with [mDNS + GetCapabilities] is slightly more optimal + var serviceTask = FindDeviceEsclService(options, cancelToken); + if (options.Device!.ConnectionUri != null) + { + client = new EsclClient(new Uri(options.Device.ConnectionUri)); + SetUpClient(); + var capsTask = client.GetCapabilities(); + await Task.WhenAny(capsTask, serviceTask); + if (capsTask.Status == TaskStatus.RanToCompletion) + { + return (client, await capsTask); + } + } + + // If we have no known connection info (or failed to connect with it), use the mDNS response for the client + var service = await serviceTask; + if (cancelToken.IsCancellationRequested) return (null, null); + if (service == null) throw new DeviceOfflineException(); + client = new EsclClient(service); + MaybeSendNewUris(); + SetUpClient(); + return (client, await client.GetCapabilities()); + } + + private async Task CreateScanJobAndCorrectInvalidSettings(EsclClient client, EsclScanSettings scanSettings) + { + _logger.LogDebug("Creating ESCL job: format {Format}, source {Source}, mode {Mode}", + scanSettings.DocumentFormat, scanSettings.InputSource, scanSettings.ColorMode); + EsclJob job; + try + { + job = await client.CreateScanJob(scanSettings); + } + catch (HttpRequestException ex) when (scanSettings.ColorMode == EsclColorMode.BlackAndWhite1 && + ex.Message.Contains("409 (Conflict)")) + { + scanSettings = scanSettings with + { + ColorMode = EsclColorMode.Grayscale8, + DocumentFormat = ContentTypes.JPEG + }; + _logger.LogDebug("Scanning in Grayscale instead of Black & White due to HTTP 409 response"); + job = await client.CreateScanJob(scanSettings); + } + return job; } - public async Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) + private async Task GetNextDocumentWithRetries(EsclClient client, EsclJob job, + Action? progress, bool shortTimeout) { - EsclServiceLocator locator = new EsclServiceLocator(); - var services = await locator.Locate(); - var service = services.FirstOrDefault(x => x.Uuid == options.Device!.ID) ?? - throw new DeviceException(SdkResources.DeviceOffline); - var client = new EsclClient(service); - var status = await client.GetStatus(); - var job = await client.CreateScanJob(new EsclScanSettings()); + int retries = 0; while (true) { - scanEvents.PageStart(); - byte[] doc; try { - // TODO: PDF or jpeg? - doc = await client.NextDocument(job); + return await client.NextDocument(job, progress, shortTimeout); } catch (Exception ex) { - // TODO: Log if not 404 or something (maybe return null from nextdoc) - break; + _logger.LogDebug(ex, "ESCL NextDocument error"); + if (++retries > MAX_DOCUMENT_TRIES) + { + _logger.LogDebug("ESCL NextDocument failed, no more retries left"); + throw; + } + EsclJobState jobState = EsclJobState.Unknown; + try + { + var status = await client.GetStatus(); + jobState = status.JobStates.Get(job.UriPath); + } + catch (Exception) + { + _logger.LogDebug("ESCL GetStatus failed, could not get job state"); + } + if (jobState is not (EsclJobState.Pending or EsclJobState.Processing or EsclJobState.Unknown)) + { + // Only retry if the job is pending or processing + _logger.LogDebug("ESCL NextDocument failed, not retrying as job state is {State}", jobState); + throw; + } + _logger.LogDebug("ESCL NextDocument failed, retrying as job state is {State}", jobState); + await Task.Delay(DOCUMENT_RETRY_INTERVAL); + } + } + } + + private async Task CheckErrorDetails(EsclClient client, EsclJob job) + { + string? errorDetails = null; + try + { + errorDetails = await client.ErrorDetails(job); + } + catch (Exception) + { + // Ignore + } + if (!string.IsNullOrEmpty(errorDetails)) + { + RemotingHelper.HandleErrors(errorDetails!.FromXml()); + } + } + + private static void VerifyStatus(EsclScannerStatus status, EsclScanSettings scanSettings) + { + if (status.State is EsclScannerState.Processing or EsclScannerState.Testing or EsclScannerState.Stopped) + { + throw new DeviceBusyException(); + } + if (status.State == EsclScannerState.Down) + { + throw new DeviceOfflineException(); + } + if (scanSettings.InputSource == EsclInputSource.Feeder) + { + if (status.AdfState == EsclAdfState.ScannerAdfEmpty) + { + throw new DeviceFeederEmptyException(); + } + if (status.AdfState == EsclAdfState.ScannerAdfJam) + { + throw new DevicePaperJamException(); + } + if (status.AdfState is not (EsclAdfState.Unknown or EsclAdfState.ScannerAdfProcessing + or EsclAdfState.ScannedAdfLoaded)) + { + throw new DeviceException(status.AdfState.ToString()); + } + } + } + + private async Task FindDeviceEsclService(ScanOptions options, CancellationToken cancelToken) + { + var foundTcs = new TaskCompletionSource(); + var deviceUuid = GetUuid(options.Device!); + using var locator = new EsclServiceLocator(service => + { + if (service.Uuid == deviceUuid) + { + foundTcs.TrySetResult(service); + } + }); + Task.Delay(options.EsclOptions.SearchTimeout, cancelToken) + .ContinueWith(_ => foundTcs.TrySetResult(null)).AssertNoAwait(); + locator.Logger = _scanningContext.Logger; + locator.Start(); + return await foundTcs.Task; + } + + private IEnumerable GetImagesFromRawDocument(ScanOptions options, RawDocument doc) + { + if (doc.ContentType == ContentTypes.PDF) + { + // TODO: For SDK some kind an error message if Pdfium isn't present + var renderer = new PdfiumPdfRenderer(); + foreach (var image in renderer.Render(_scanningContext.ImageContext, doc.Data, doc.Data.Length, + PdfRenderSize.FromDpi(options.Dpi))) + { + yield return image; + } + } + else + { + yield return _scanningContext.ImageContext.Load(doc.Data); + } + } + + private EsclScanSettings GetScanSettings(ScanOptions options, EsclCapabilities caps, bool hasAnyDpiExtension) + { + if (options.PaperSource == PaperSource.Feeder && caps.AdfSimplexCaps == null) + { + throw new NoFeederSupportException(); + } + if (options.PaperSource == PaperSource.Duplex && caps.AdfDuplexCaps == null) + { + throw new NoDuplexSupportException(); + } + if (options.PaperSource is PaperSource.Flatbed or PaperSource.Auto + && caps.PlatenCaps == null && caps.AdfSimplexCaps != null) + { + options.PaperSource = PaperSource.Feeder; + } + + var (inputCaps, inputSource, duplex) = options.PaperSource switch + { + PaperSource.Feeder => (caps.AdfSimplexCaps, EsclInputSource.Feeder, false), + PaperSource.Duplex => (caps.AdfDuplexCaps, EsclInputSource.Feeder, true), + _ => (caps.PlatenCaps, EsclInputSource.Platen, false), + }; + inputCaps ??= new EsclInputCaps(); + + var colorMode = options.BitDepth switch + { + BitDepth.Color => EsclColorMode.RGB24, + BitDepth.Grayscale => EsclColorMode.Grayscale8, + BitDepth.BlackAndWhite => EsclColorMode.BlackAndWhite1, + _ => EsclColorMode.RGB24 + }; + int dpi = options.Dpi; + + var settingProfile = inputCaps.SettingProfiles.FirstOrDefault(x => x.ColorModes.Contains(colorMode)) + ?? inputCaps.SettingProfiles.FirstOrDefault(); + + if (settingProfile != null) + { + colorMode = MaybeCorrectColorMode(settingProfile, colorMode); + + if (!hasAnyDpiExtension) + { + var discreteResolutions = + settingProfile.DiscreteResolutions.Where(res => res.XResolution == res.YResolution) + .Select(res => res.XResolution).ToList(); + if (discreteResolutions.Any()) + { + dpi = discreteResolutions.OrderBy(v => Math.Abs(v - dpi)).First(); + } + + if (settingProfile.XResolutionRange != null && settingProfile.YResolutionRange != null) + { + int min = Math.Max(settingProfile.XResolutionRange.Min, settingProfile.YResolutionRange.Min); + int max = Math.Min(settingProfile.XResolutionRange.Max, settingProfile.YResolutionRange.Max); + dpi = dpi.Clamp(min, max); + } } - callback(_scanningContext.ImageContext.Load(doc)); + + _logger.LogDebug("ESCL setting profile supports formats: {Formats}", + string.Join(",", settingProfile.DocumentFormats.Concat(settingProfile.DocumentFormatsExt).Distinct())); + } + + var width = (int) Math.Round(options.PageSize!.WidthInInches * 300); + var height = (int) Math.Round(options.PageSize!.HeightInInches * 300); + if (inputCaps.MaxWidth is > 0) + { + width = Math.Min(width, inputCaps.MaxWidth.Value); + } + if (inputCaps.MaxHeight is > 0) + { + height = Math.Min(height, inputCaps.MaxHeight.Value); + } + + var contentType = ContentTypes.JPEG; + if (options.BitDepth == BitDepth.BlackAndWhite || options.MaxQuality) + { + bool supportsPng = settingProfile != null && settingProfile.DocumentFormats + .Concat(settingProfile.DocumentFormatsExt).Contains(ContentTypes.PNG); + contentType = supportsPng ? ContentTypes.PNG : ContentTypes.PDF; + } + + return new EsclScanSettings + { + Width = width, + Height = height, + XResolution = dpi, + YResolution = dpi, + ColorMode = colorMode, + InputSource = inputSource, + Duplex = duplex, + DocumentFormat = contentType, + XOffset = options.PageAlign switch + { + HorizontalAlign.Left => inputCaps.MaxWidth is > 0 ? inputCaps.MaxWidth.Value - width : 0, + HorizontalAlign.Center => inputCaps.MaxWidth is > 0 ? (inputCaps.MaxWidth.Value - width) / 2 : 0, + _ => 0 + }, + CompressionFactor = caps.CompressionFactorSupport is { Min: 0, Max: 100, Step: 1 } ? options.Quality : null + // TODO: Brightness/contrast, etc. + }; + } + + private static EsclColorMode MaybeCorrectColorMode(EsclSettingProfile settingProfile, EsclColorMode colorMode) + { + if (settingProfile.ColorModes.Contains(colorMode)) + { + return colorMode; + } + if (colorMode == EsclColorMode.BlackAndWhite1) + { + if (settingProfile.ColorModes.Contains(EsclColorMode.Grayscale8)) + { + colorMode = EsclColorMode.Grayscale8; + } + else if (settingProfile.ColorModes.Contains(EsclColorMode.RGB24)) + { + colorMode = EsclColorMode.RGB24; + } + } + else if (colorMode == EsclColorMode.Grayscale8 && settingProfile.ColorModes.Contains(EsclColorMode.RGB24)) + { + colorMode = EsclColorMode.RGB24; } + return colorMode; } -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/ILocalPostProcessor.cs b/NAPS2.Sdk/Scan/Internal/ILocalPostProcessor.cs index 07bfa357d4..ef2ff78992 100644 --- a/NAPS2.Sdk/Scan/Internal/ILocalPostProcessor.cs +++ b/NAPS2.Sdk/Scan/Internal/ILocalPostProcessor.cs @@ -1,7 +1,7 @@ namespace NAPS2.Scan.Internal; /// -/// Performs local post-processing on an image just before it is returned from IScanController. +/// Performs local post-processing on an image just before it is returned from ScanController. /// internal interface ILocalPostProcessor { diff --git a/NAPS2.Sdk/Scan/Internal/IRemoteScanController.cs b/NAPS2.Sdk/Scan/Internal/IRemoteScanController.cs index 2ce53042d0..eaeb12702e 100644 --- a/NAPS2.Sdk/Scan/Internal/IRemoteScanController.cs +++ b/NAPS2.Sdk/Scan/Internal/IRemoteScanController.cs @@ -7,7 +7,9 @@ namespace NAPS2.Scan.Internal; /// internal interface IRemoteScanController { - Task> GetDeviceList(ScanOptions options); + Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback); + + Task GetCaps(ScanOptions options, CancellationToken cancelToken); Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback); } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/IScanBridge.cs b/NAPS2.Sdk/Scan/Internal/IScanBridge.cs index 3e1d36ac84..9bca99e0bd 100644 --- a/NAPS2.Sdk/Scan/Internal/IScanBridge.cs +++ b/NAPS2.Sdk/Scan/Internal/IScanBridge.cs @@ -7,7 +7,9 @@ namespace NAPS2.Scan.Internal; /// internal interface IScanBridge { - Task> GetDeviceList(ScanOptions options); + Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback); + + Task GetCaps(ScanOptions options, CancellationToken cancelToken); Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback); } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/IScanDriver.cs b/NAPS2.Sdk/Scan/Internal/IScanDriver.cs index c00f21f440..ce77cfa474 100644 --- a/NAPS2.Sdk/Scan/Internal/IScanDriver.cs +++ b/NAPS2.Sdk/Scan/Internal/IScanDriver.cs @@ -4,7 +4,10 @@ namespace NAPS2.Scan.Internal; internal interface IScanDriver { - Task> GetDeviceList(ScanOptions options); + Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback); - Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback); + Task GetCaps(ScanOptions options, CancellationToken cancelToken); + + Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, + Action callback); } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/IScanEvents.cs b/NAPS2.Sdk/Scan/Internal/IScanEvents.cs index f7642b049b..21010b5bf9 100644 --- a/NAPS2.Sdk/Scan/Internal/IScanEvents.cs +++ b/NAPS2.Sdk/Scan/Internal/IScanEvents.cs @@ -1,8 +1,9 @@ namespace NAPS2.Scan.Internal; -public interface IScanEvents +internal interface IScanEvents { // This only includes events that can't be otherwise inferred. void PageStart(); void PageProgress(double progress); + void DeviceUriChanged(string? iconUri, string? connectionUri); } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/InProcScanBridge.cs b/NAPS2.Sdk/Scan/Internal/InProcScanBridge.cs index 230d103005..63f128448a 100644 --- a/NAPS2.Sdk/Scan/Internal/InProcScanBridge.cs +++ b/NAPS2.Sdk/Scan/Internal/InProcScanBridge.cs @@ -19,8 +19,11 @@ public InProcScanBridge(ScanningContext scanningContext, IRemoteScanController r _remoteScanController = remoteScanController; } - public Task> GetDeviceList(ScanOptions options) => - _remoteScanController.GetDeviceList(options); + public Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback) => + _remoteScanController.GetDevices(options, cancelToken, callback); + + public Task GetCaps(ScanOptions options, CancellationToken cancelToken) => + _remoteScanController.GetCaps(options, cancelToken); public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) => _remoteScanController.Scan(options, cancelToken, scanEvents, callback); diff --git a/NAPS2.Sdk/Scan/Internal/LocalIPsHelper.cs b/NAPS2.Sdk/Scan/Internal/LocalIPsHelper.cs new file mode 100644 index 0000000000..a54bfa1722 --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/LocalIPsHelper.cs @@ -0,0 +1,15 @@ +using System.Net.NetworkInformation; + +namespace NAPS2.Scan.Internal; + +internal static class LocalIPsHelper +{ + public static Task> Get() + { + return Task.Run(() => + NetworkInterface.GetAllNetworkInterfaces() + .SelectMany(x => x.GetIPProperties().UnicastAddresses) + .Select(x => x.Address.ToString()) + .ToHashSet()); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/LocalPostProcessor.cs b/NAPS2.Sdk/Scan/Internal/LocalPostProcessor.cs index 4aec2fec73..ac1e548688 100644 --- a/NAPS2.Sdk/Scan/Internal/LocalPostProcessor.cs +++ b/NAPS2.Sdk/Scan/Internal/LocalPostProcessor.cs @@ -26,11 +26,9 @@ private void RunBackgroundOcr(ref ProcessedImage image, ScanOptions options, str { if (tempPath == null) { - if (string.IsNullOrEmpty(options.NetworkOptions.Ip)) - { - throw new InvalidOperationException("Expected OCR tempPath to be set for non-network scan"); - } - tempPath = _scanningContext.SaveToTempFile(image, options.BitDepth); + throw new InvalidOperationException("Expected OCR tempPath to be set"); + // TODO: If we ever support a network scan bridge again, we'll want to set this here in that case + // tempPath = _scanningContext.SaveToTempFile(image, options.BitDepth); } _ocrController.Start(ref image, tempPath, options.OcrParams, options.OcrPriority).AssertNoAwait(); } diff --git a/NAPS2.Sdk/Scan/Internal/NetworkScanBridge.cs b/NAPS2.Sdk/Scan/Internal/NetworkScanBridge.cs deleted file mode 100644 index e717ed7c48..0000000000 --- a/NAPS2.Sdk/Scan/Internal/NetworkScanBridge.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Threading; - -namespace NAPS2.Scan.Internal; - -/// -/// Represents scanning across a network on a different machine. -/// -internal class NetworkScanBridge : IScanBridge -{ - private readonly ScanningContext _scanningContext; - - public NetworkScanBridge(ScanningContext scanningContext) - { - _scanningContext = scanningContext; - } - - public Task> GetDeviceList(ScanOptions options) => throw new NotImplementedException(); - - // TODO: On the network server, make sure to throttle progress events - public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) => throw new NotImplementedException(); -} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/RemotePostProcessor.cs b/NAPS2.Sdk/Scan/Internal/RemotePostProcessor.cs index 16c50e745e..84ff388efa 100644 --- a/NAPS2.Sdk/Scan/Internal/RemotePostProcessor.cs +++ b/NAPS2.Sdk/Scan/Internal/RemotePostProcessor.cs @@ -1,13 +1,17 @@ -namespace NAPS2.Scan.Internal; +using Microsoft.Extensions.Logging; +using NAPS2.Images.Bitwise; + +namespace NAPS2.Scan.Internal; -// TODO: Add tests for this class internal class RemotePostProcessor : IRemotePostProcessor { private readonly ScanningContext _scanningContext; + private readonly ILogger _logger; public RemotePostProcessor(ScanningContext scanningContext) { _scanningContext = scanningContext; + _logger = scanningContext.Logger; } @@ -26,25 +30,28 @@ public RemotePostProcessor(ScanningContext scanningContext) // return image; //} - public ProcessedImage? PostProcess(IMemoryImage image, ScanOptions options, PostProcessingContext postProcessingContext) + public ProcessedImage? PostProcess(IMemoryImage image, ScanOptions options, + PostProcessingContext postProcessingContext) { image = DoInitialTransforms(image, options); try { - if (options.ExcludeBlankPages && - BlankDetector.IsBlank(image, options.BlankPageWhiteThreshold, options.BlankPageCoverageThreshold)) + if (options.ExcludeBlankPages) { - // TODO: Consider annotating the image as blank via postprocessingdata rather than excluding here - // TODO: In theory we might want to add some functionality to allow the user to correct blank detection - return null; + var op = new BlankDetectionImageOp(options.BlankPageWhiteThreshold, options.BlankPageCoverageThreshold); + op.Perform(image); + if (op.IsBlank) + { + // TODO: Consider annotating the image as blank via postprocessingdata rather than excluding here + // TODO: In theory we might want to add some functionality to allow the user to correct blank detection + return null; + } } - var bitDepth = options.UseNativeUI ? BitDepth.Color : options.BitDepth; - var scannedImage = _scanningContext.CreateProcessedImage(image, bitDepth, options.MaxQuality, - options.Quality); + var scannedImage = _scanningContext.CreateProcessedImage(image, options.MaxQuality, + options.Quality, options.PageSize); DoRevertibleTransforms(ref scannedImage, ref image, options, postProcessingContext); postProcessingContext.TempPath = SaveForBackgroundOcr(image, options); - // TODO: We need to attach the thumbnail to the scanned image return scannedImage; } finally @@ -56,9 +63,9 @@ public RemotePostProcessor(ScanningContext scanningContext) private IMemoryImage DoInitialTransforms(IMemoryImage original, ScanOptions options) { - if (!PlatformCompat.System.CanUseWin32 && options.BitDepth == BitDepth.BlackAndWhite) + if (!options.UseNativeUI && options.BitDepth == BitDepth.BlackAndWhite) { - // TODO: Don't do this here, do it where BitmapHelper is used or something + // Ensure we actually have a black & white image (this is a no-op if we already do) original = original.PerformTransform(new BlackWhiteTransform(-options.Brightness)); } @@ -71,90 +78,118 @@ private IMemoryImage DoInitialTransforms(IMemoryImage original, ScanOptions opti if (!options.UseNativeUI && (options.StretchToPageSize || options.CropToPageSize)) { - float width = original.Width / original.HorizontalResolution; - float height = original.Height / original.VerticalResolution; - if (float.IsNaN(width) || float.IsNaN(height)) + scaled = CropAndStretch(original, options, scaled); + } + + return scaled; + } + + private IMemoryImage CropAndStretch(IMemoryImage original, ScanOptions options, IMemoryImage scaled) + { + if (original.HorizontalResolution <= 0 || original.VerticalResolution <= 0) + { + _logger.LogDebug("Skipping StretchToPageSize/CropToPageSize as there is no resolution data"); + return scaled; + } + + float width = original.Width / original.HorizontalResolution; + float height = original.Height / original.VerticalResolution; + + if ((options.PageSize!.Width > options.PageSize.Height) ^ (width > height)) + { + if (options.CropToPageSize) { - width = original.Width; - height = original.Height; + scaled = scaled.PerformTransform(new CropTransform( + 0, + (int) ((width - (float) options.PageSize.HeightInInches) * original.HorizontalResolution), + 0, + (int) ((height - (float) options.PageSize.WidthInInches) * original.VerticalResolution) + )); } - - if (options.PageSize!.Width > options.PageSize.Height && width < height) + else { - if (options.CropToPageSize) - { - scaled = scaled.PerformTransform(new CropTransform( - 0, - (int) ((width - (float) options.PageSize.HeightInInches) * original.HorizontalResolution), - 0, - (int) ((height - (float) options.PageSize.WidthInInches) * original.VerticalResolution) - )); - } - else - { - scaled.SetResolution((float) (original.Width / options.PageSize.HeightInInches), - (float) (original.Height / options.PageSize.WidthInInches)); - } + scaled.SetResolution((float) (original.Width / options.PageSize.HeightInInches), + (float) (original.Height / options.PageSize.WidthInInches)); + } + } + else + { + if (options.CropToPageSize) + { + scaled = scaled.PerformTransform(new CropTransform + ( + 0, + (int) ((width - (float) options.PageSize.WidthInInches) * original.HorizontalResolution), + 0, + (int) ((height - (float) options.PageSize.HeightInInches) * original.VerticalResolution) + )); } else { - if (options.CropToPageSize) - { - scaled = scaled.PerformTransform(new CropTransform - ( - 0, - (int) ((width - (float) options.PageSize.WidthInInches) * original.HorizontalResolution), - 0, - (int) ((height - (float) options.PageSize.HeightInInches) * original.VerticalResolution) - )); - } - else - { - scaled.SetResolution((float) (original.Width / options.PageSize.WidthInInches), (float) (original.Height / options.PageSize.HeightInInches)); - } + scaled.SetResolution((float) (original.Width / options.PageSize.WidthInInches), + (float) (original.Height / options.PageSize.HeightInInches)); } } - - scaled.UpdateLogicalPixelFormat(); return scaled; } // TODO: This is more than just transforms. - private void DoRevertibleTransforms(ref ProcessedImage processedImage, ref IMemoryImage image, ScanOptions options, PostProcessingContext postProcessingContext) + private void DoRevertibleTransforms(ref ProcessedImage processedImage, ref IMemoryImage image, ScanOptions options, + PostProcessingContext postProcessingContext) { - var data = processedImage.PostProcessingData; - if (options.ThumbnailSize.HasValue) + var data = processedImage.PostProcessingData with { - data = data with - { - // TODO: Maybe there's a way we can do this without needing to clone - Thumbnail = image.Clone().PerformTransform(new ThumbnailTransform(options.ThumbnailSize.Value)) - }; + PageNumber = postProcessingContext.PageNumber + }; + + if ((!options.UseNativeUI && options.BrightnessContrastAfterScan) || + options.Driver is not (Driver.Wia or Driver.Twain)) + { + processedImage = processedImage.WithTransform(new BrightnessTransform(options.Brightness), true); + processedImage = processedImage.WithTransform(new TrueContrastTransform(options.Contrast), true); } - if (!options.UseNativeUI && options.BrightnessContrastAfterScan) + if (options.PaperSource == PaperSource.Duplex) { - processedImage = AddTransformAndUpdateThumbnail(processedImage, ref image, new BrightnessTransform(options.Brightness), options); - processedImage = AddTransformAndUpdateThumbnail(processedImage, ref image, new TrueContrastTransform(options.Contrast), options); + data = data with + { + PageSide = postProcessingContext.PageNumber % 2 == 0 ? PageSide.Back : PageSide.Front + }; + if (options.FlipDuplexedPages && data.PageSide == PageSide.Back) + { + processedImage = processedImage.WithTransform(new RotationTransform(180), true); + } } - // TODO: Do we need to restrict this to only when an actual duplex scan is happening? - if (options.FlipDuplexedPages && postProcessingContext.PageNumber % 2 == 0) + if (options.RotateDegrees != 0) { - processedImage = AddTransformAndUpdateThumbnail(processedImage, ref image, new RotationTransform(180), options); + processedImage = processedImage.WithTransform(new RotationTransform(options.RotateDegrees), true); } if (options.AutoDeskew) { - processedImage = AddTransformAndUpdateThumbnail(processedImage, ref image, Deskewer.GetDeskewTransform(image), options); + processedImage = processedImage.WithTransform(Deskewer.GetDeskewTransform(image), true); } - if (!data.BarcodeDetection.IsBarcodePresent) + if (!data.Barcode.IsDetected) { // Even if barcode detection was attempted previously and failed, image adjustments may improve detection. - data = data with { BarcodeDetection = BarcodeDetector.Detect(image, options.BarcodeDetectionOptions) }; + data = data with + { + Barcode = BarcodeDetector.Detect(image, options.BarcodeDetectionOptions) + }; + } + if (options.ThumbnailSize.HasValue) + { + data = data with + { + // TODO: Maybe there's a way we can do this without needing to clone + Thumbnail = image.Clone() + .PerformAllTransforms(processedImage.TransformState.Transforms) + .PerformTransform(new ThumbnailTransform(options.ThumbnailSize.Value)), + ThumbnailTransformState = processedImage.TransformState + }; } - data = data with { ThumbnailTransformState = processedImage.TransformState }; processedImage = processedImage.WithPostProcessingData(data, true); } @@ -164,34 +199,8 @@ private void DoRevertibleTransforms(ref ProcessedImage processedImage, ref IMemo { // TODO: If we use tesseract as a library, this is something that that could potentially improve (i.e. not having to save to disk) // But then again, that doesn't make as much sense on systems (i.e. linux) where tesseract would be provided as an external package - return _scanningContext.SaveToTempFile(bitmap, options.BitDepth); + return _scanningContext.SaveToTempFile(bitmap); } return null; } - - private ProcessedImage AddTransformAndUpdateThumbnail(ProcessedImage processedImage, ref IMemoryImage image, Transform transform, ScanOptions options) - { - if (transform.IsNull) - { - return processedImage; - } - ProcessedImage transformed = processedImage.WithTransform(transform, true); - if (options.ThumbnailSize.HasValue) - { - // TODO: We may want to do the transform on the original thumbnail, maybe situationally? - // TODO: Should probably dispose the original image & thumbnail - // TODO: This should probably be done even without thumbnails, otherwise deskew/barcode might misfire - // TODO: If we're doing a number of transforms, this is redundant... - // TODO: So basically we should probably do ONE thumbnail render, after all transforms are determined. - // TODO: BUT we should have some kind of fast path (not just used here) that moves the thumbnail transform up the transform stack - // TODO: as long as subsequent transforms are size agnostic (i.e. 90 deg rotation). - image = image.PerformTransform(transform); - transformed = transformed.WithPostProcessingData( - transformed.PostProcessingData with - { - Thumbnail = image.PerformTransform(new ThumbnailTransform(options.ThumbnailSize.Value)) - }, true); - } - return transformed; - } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/RemoteScanController.cs b/NAPS2.Sdk/Scan/Internal/RemoteScanController.cs index 5ebc24c708..03a8988928 100644 --- a/NAPS2.Sdk/Scan/Internal/RemoteScanController.cs +++ b/NAPS2.Sdk/Scan/Internal/RemoteScanController.cs @@ -18,14 +18,22 @@ public RemoteScanController(IScanDriverFactory scanDriverFactory, IRemotePostPro _remotePostProcessor = remotePostProcessor; } - public async Task> GetDeviceList(ScanOptions options) + public async Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback) { - var deviceList = await _scanDriverFactory.Create(options).GetDeviceList(options); - if (options.Driver == Driver.Twain && !options.TwainOptions.IncludeWiaDevices) + await _scanDriverFactory.Create(options).GetDevices(options, cancelToken, device => { - deviceList = deviceList.Where(x => !x.ID.StartsWith("WIA-", StringComparison.InvariantCulture)).ToList(); - } - return deviceList; + var skipWiaDevices = options.Driver == Driver.Twain && !options.TwainOptions.IncludeWiaDevices; + if (skipWiaDevices && device.ID.StartsWith("WIA-", StringComparison.InvariantCulture)) + { + return; + } + callback(device); + }); + } + + public async Task GetCaps(ScanOptions options, CancellationToken cancelToken) + { + return await _scanDriverFactory.Create(options).GetCaps(options, cancelToken); } public async Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, @@ -33,7 +41,11 @@ public async Task Scan(ScanOptions options, CancellationToken cancelToken, IScan { var driver = _scanDriverFactory.Create(options); var progressThrottle = new EventThrottle(scanEvents.PageProgress); - var driverScanEvents = new ScanEvents(scanEvents.PageStart, progressThrottle.OnlyIfChanged); + var driverScanEvents = new ScanEvents(() => + { + scanEvents.PageStart(); + progressThrottle.Reset(); + }, progressThrottle.OnlyIfChanged, scanEvents.DeviceUriChanged); int pageNumber = 0; await driver.Scan(options, cancelToken, driverScanEvents, image => { diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/BundledSaneInstallation.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/BundledSaneInstallation.cs new file mode 100644 index 0000000000..f4465615c4 --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/BundledSaneInstallation.cs @@ -0,0 +1,48 @@ +using NAPS2.Unmanaged; + +namespace NAPS2.Scan.Internal.Sane.Native; + +/// +/// Indicates a custom build of SANE bundled with the application (i.e. NAPS2 on Mac). +/// +internal class BundledSaneInstallation : ISaneInstallation +{ + private string? _configDir; + private string? _libraryPath; + private string[]? _libraryDeps; + + public bool CanStreamDevices => true; + + public void Initialize() + { + var testRoot = Environment.GetEnvironmentVariable("NAPS2_TEST_DEPS"); + _libraryPath = NativeLibrary.FindLibraryPath(PlatformCompat.System.SaneLibraryName, testRoot); + _libraryDeps = PlatformCompat.System.SaneLibraryDeps + ?.Select(path => NativeLibrary.FindLibraryPath(path, testRoot)).ToArray(); + if (_libraryDeps != null) + { + // If we're using a bundled SANE, we will need to manually set the environment + // variables to the appropriate folders. + var backendsFolder = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(_libraryPath)!, "sane")); + _configDir = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(_libraryPath)!, "..", "_config", "sane")); + // We can't use Environment.SetEnvironmentVariable as that will just change the .NET + // env and won't be visible to SANE. Instead we use setenv which is technically not + // thread-safe but in practice should be fine here. + PlatformCompat.System.SetEnv("LD_LIBRARY_PATH", backendsFolder); + PlatformCompat.System.SetEnv("SANE_CONFIG_DIR", _configDir); + // Note: We can add SANE debug variables here + // PlatformCompat.System.SetEnv("SANE_DEBUG_DLL", "255"); + } + } + + public void SetCustomConfigDir(string? configDir) + { + PlatformCompat.System.SetEnv("SANE_CONFIG_DIR", $"{configDir}:{DefaultConfigDir}"); + } + + public string DefaultConfigDir => _configDir ?? throw new InvalidOperationException(); + + public string LibraryPath => _libraryPath ?? throw new InvalidOperationException(); + + public string[]? LibraryDeps => _libraryDeps; +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/FlatpakSaneInstallation.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/FlatpakSaneInstallation.cs new file mode 100644 index 0000000000..b31e610d10 --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/FlatpakSaneInstallation.cs @@ -0,0 +1,25 @@ +namespace NAPS2.Scan.Internal.Sane.Native; + +/// +/// Indicates a custom build of SANE inside the Flatpak sandbox. +/// +internal class FlatpakSaneInstallation : ISaneInstallation +{ + public bool CanStreamDevices => true; + + public void Initialize() + { + } + + public void SetCustomConfigDir(string configDir) + { + // The trailing ":" means we'll search the default dirs if this dir doesn't exist + PlatformCompat.System.SetEnv("SANE_CONFIG_DIR", $"{configDir}:"); + } + + public string DefaultConfigDir => "/app/etc/sane.d"; + + public string LibraryPath => "libsane.so.1"; + + public string[]? LibraryDeps => null; +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/ISaneDevice.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/ISaneDevice.cs new file mode 100644 index 0000000000..a8b1e32074 --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/ISaneDevice.cs @@ -0,0 +1,14 @@ +namespace NAPS2.Scan.Internal.Sane.Native; + +internal interface ISaneDevice +{ + void Cancel(); + void Start(); + SaneReadParameters GetParameters(); + bool Read(byte[] buffer, out int len); + IEnumerable GetOptions(); + void SetOption(SaneOption option, bool value, out SaneOptionSetInfo info); + void SetOption(SaneOption option, double value, out SaneOptionSetInfo info); + void SetOption(SaneOption option, string value, out SaneOptionSetInfo info); + void GetOption(SaneOption option, out double value); +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/ISaneInstallation.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/ISaneInstallation.cs new file mode 100644 index 0000000000..d04e50ca67 --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/ISaneInstallation.cs @@ -0,0 +1,34 @@ +namespace NAPS2.Scan.Internal.Sane.Native; + +internal interface ISaneInstallation +{ + /// + /// Whether the SANE installation has a patch to add the sane_stream_devices method. + /// + bool CanStreamDevices { get; } + + /// + /// Performs any initialization work needed to use the SANE installation (e.g. generating config files). + /// + void Initialize(); + + /// + /// Sets the path to a custom sane.d config directory. + /// + void SetCustomConfigDir(string configDir); + + /// + /// Gets the path to the default sane.d config directory. + /// + string DefaultConfigDir { get; } + + /// + /// Gets the path to the libsane library. + /// + string LibraryPath { get; } + + /// + /// Gets the paths to any libraries that need to be manually loaded before loading the libsane library. + /// + string[]? LibraryDeps { get; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneCapabilities.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneCapabilities.cs index 5f60e3130b..840fd6ac38 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneCapabilities.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneCapabilities.cs @@ -1,7 +1,7 @@ namespace NAPS2.Scan.Internal.Sane.Native; [Flags] -public enum SaneCapabilities +internal enum SaneCapabilities { None = 0, SoftSelect = 1, diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneClient.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneClient.cs index 87ada9983c..daa89051df 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneClient.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneClient.cs @@ -1,12 +1,34 @@ using System.Runtime.InteropServices; +using System.Threading; namespace NAPS2.Scan.Internal.Sane.Native; -public class SaneClient : SaneNativeObject +internal class SaneClient : SaneNativeObject { - public SaneClient() : base(IntPtr.Zero) + private static readonly object SaneLock = new(); + private static bool _isInitialized; + + private readonly bool _keepInitialized; + + private static SaneNativeLibrary GetNativeLibrary(ISaneInstallation saneInstallation) + { + lock (SaneLock) + { + return new SaneNativeLibrary(saneInstallation.LibraryPath, saneInstallation.LibraryDeps); + } + } + + public SaneClient(ISaneInstallation saneInstallation, bool keepInitialized) + : base(GetNativeLibrary(saneInstallation), IntPtr.Zero) { - Native.sane_init(out _, IntPtr.Zero); + _keepInitialized = keepInitialized; + + Monitor.Enter(SaneLock); + if (!_isInitialized) + { + Native.sane_init(out _, IntPtr.Zero); + _isInitialized = true; + } } public IEnumerable GetDevices() @@ -21,17 +43,34 @@ public IEnumerable GetDevices() } } + // This calls sane_stream_devices which is not a normal part of SANE and is patched into + // NAPS2 SANE builds. It will fail when using a SANE installation without the patch. + public void StreamDevices(Action callback, CancellationToken cancelToken) + { + HandleStatus(Native.sane_stream_devices(devicePtr => + { + if (devicePtr != IntPtr.Zero) + { + var device = Marshal.PtrToStructure(devicePtr); + callback(device); + } + return cancelToken.IsCancellationRequested ? 0 : 1; + }, 0)); + } + public SaneDevice OpenDevice(string deviceName) { HandleStatus(Native.sane_open(deviceName, out var handle)); - return new SaneDevice(handle); + return new SaneDevice(Native, handle); } protected override void Dispose(bool disposing) { - if (disposing) + if (disposing && !_keepInitialized) { Native.sane_exit(); + _isInitialized = false; } + Monitor.Exit(SaneLock); } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneConstraintType.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneConstraintType.cs index 97aeb333a4..750249eec9 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneConstraintType.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneConstraintType.cs @@ -1,6 +1,6 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public enum SaneConstraintType +internal enum SaneConstraintType { None = 0, Range = 1, diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneDevice.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneDevice.cs index 1cc0a93667..0bd0047645 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneDevice.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneDevice.cs @@ -2,9 +2,9 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public class SaneDevice : SaneNativeObject +internal class SaneDevice : SaneNativeObject, ISaneDevice { - public SaneDevice(IntPtr handle) : base(handle) + public SaneDevice(SaneNativeLibrary native, IntPtr handle) : base(native, handle) { } @@ -58,6 +58,13 @@ public IEnumerable GetOptions() } } + public unsafe void SetOption(SaneOption option, bool value, out SaneOptionSetInfo info) + { + int word = value ? 1 : 0; + int* ptr = &word; + HandleStatus(Native.sane_control_option(Handle, option.Index, SaneOptionAction.SetValue, (IntPtr) ptr, out info)); + } + public unsafe void SetOption(SaneOption option, double value, out SaneOptionSetInfo info) { int word = option.Type == SaneValueType.Fixed ? SaneFixedPoint.ToFixed(value) : (int) value; diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneDeviceInfo.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneDeviceInfo.cs index 2483ad80f9..80331d9d4d 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneDeviceInfo.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneDeviceInfo.cs @@ -1,6 +1,6 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public struct SaneDeviceInfo +internal struct SaneDeviceInfo { public string Name; public string Vendor; diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneException.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneException.cs index a248469537..82b72813ca 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneException.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneException.cs @@ -1,6 +1,6 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public class SaneException : Exception +internal class SaneException : Exception { public SaneException(SaneStatus status) : base($"SANE error: {status}") { diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneFrameType.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneFrameType.cs index f9975d2685..62f7aa38f4 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneFrameType.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneFrameType.cs @@ -1,6 +1,6 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public enum SaneFrameType +internal enum SaneFrameType { Gray = 0, Rgb = 1, diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneNativeLibrary.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneNativeLibrary.cs index 7657b5f1d4..ba31de404f 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneNativeLibrary.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneNativeLibrary.cs @@ -1,37 +1,12 @@ // ReSharper disable InconsistentNaming +using System.Runtime.InteropServices; + namespace NAPS2.Scan.Internal.Sane.Native; -public class SaneNativeLibrary : Unmanaged.NativeLibrary +internal class SaneNativeLibrary : Unmanaged.NativeLibrary { - private static readonly Lazy LazyInstance = new(() => - { - var testRoot = Environment.GetEnvironmentVariable("NAPS2_TEST_ROOT"); - var libraryPath = FindLibraryPath(PlatformCompat.System.SaneLibraryName, testRoot); - var libraryDeps = PlatformCompat.System.SaneLibraryDeps - ?.Select(path => FindLibraryPath(path, testRoot)).ToArray(); - if (libraryDeps != null) - { - // If we're using a bundled SANE, we will need to manually set the environment - // variables to the appropriate folders. - var backendsFolder = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(libraryPath)!, "sane")); - var configFolder = - Path.GetFullPath(Path.Combine(Path.GetDirectoryName(libraryPath)!, "..", "_config", "sane")); - // We can't use Environment.SetEnvironmentVariable as that will just change the .NET - // env and won't be visible to SANE. Instead we use setenv which is technically not - // thread-safe but in practice should be fine here. - PlatformCompat.System.SetEnv("LD_LIBRARY_PATH", backendsFolder); - PlatformCompat.System.SetEnv("SANE_CONFIG_DIR", configFolder); - // Note: We can add SANE debug variables here - // PlatformCompat.System.SetEnv("SANE_DEBUG_DLL", "255"); - } - var nativeLib = new SaneNativeLibrary(libraryPath, libraryDeps); - return nativeLib; - }); - - public static SaneNativeLibrary Instance => LazyInstance.Value; - - private SaneNativeLibrary(string libraryPath, string[]? libraryDeps) + public SaneNativeLibrary(string libraryPath, string[]? libraryDeps) : base(libraryPath, libraryDeps) { } @@ -42,6 +17,9 @@ private SaneNativeLibrary(string libraryPath, string[]? libraryDeps) public delegate SaneStatus sane_get_devices_delegate(out IntPtr device_list, int local_only); + // sane_stream_devices is not a normal part of SANE and is patched into NAPS2 SANE builds + public delegate SaneStatus sane_stream_devices_delegate(SaneDeviceCallback callback, int local_only); + public delegate SaneStatus sane_open_delegate(string name, out IntPtr handle); public delegate void sane_close_delegate(IntPtr handle); @@ -62,6 +40,8 @@ public delegate SaneStatus sane_control_option_delegate(IntPtr handle, int n, Sa public sane_init_delegate sane_init => Load(); public sane_exit_delegate sane_exit => Load(); public sane_get_devices_delegate sane_get_devices => Load(); + // sane_stream_devices is not a normal part of SANE and is patched into NAPS2 SANE builds + public sane_stream_devices_delegate sane_stream_devices => Load(); public sane_open_delegate sane_open => Load(); public sane_close_delegate sane_close => Load(); @@ -73,4 +53,8 @@ public delegate SaneStatus sane_control_option_delegate(IntPtr handle, int n, Sa public sane_start_delegate sane_start => Load(); public sane_read_delegate sane_read => Load(); public sane_cancel_delegate sane_cancel => Load(); + + // SaneDeviceCallback is not a normal part of SANE and is patched into NAPS2 SANE builds + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate int SaneDeviceCallback(IntPtr device); } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneNativeObject.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneNativeObject.cs index ec7857a97e..baa3f723a5 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneNativeObject.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneNativeObject.cs @@ -3,28 +3,18 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public abstract class SaneNativeObject : IDisposable +internal abstract class SaneNativeObject : IDisposable { private bool _disposed; private IntPtr _handle; - protected SaneNativeObject(IntPtr handle) + protected SaneNativeObject(SaneNativeLibrary native, IntPtr handle) { + Native = native; Handle = handle; } - protected static SaneNativeLibrary Native - { - get - { - var value = SaneNativeLibrary.Instance; - if (!Monitor.IsEntered(value)) - { - throw new InvalidOperationException("Sane operations must be locked"); - } - return value; - } - } + protected SaneNativeLibrary Native { get; } protected internal IntPtr Handle { diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneOption.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneOption.cs index cdd2cb9523..4e23b346f3 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneOption.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneOption.cs @@ -2,16 +2,15 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public class SaneOption +internal class SaneOption { private static IEnumerable ParseStringArray(IntPtr arrayPtr) { - for (int i = 0; ; i++) + for (int i = 0;; i++) { var ptr = Marshal.ReadIntPtr(arrayPtr + IntPtr.Size * i); var str = Marshal.PtrToStringAnsi(ptr); if (str == null) break; - Console.WriteLine($"Reading constraint value {str}"); yield return str; } } @@ -34,6 +33,30 @@ private static IEnumerable ParseFixedArray(IntPtr arrayPtr) } } + internal static SaneOption CreateStringListForTesting(int index, string name, string[] stringList) + { + return new SaneOption(index, name, "", "", SaneValueType.String, SaneUnit.None, 0, SaneCapabilities.SoftSelect, + SaneConstraintType.StringList, stringList.ToList(), null, null); + } + + internal static SaneOption CreateWordListForTesting(int index, string name, double[] wordList) + { + return new SaneOption(index, name, "", "", SaneValueType.Int, SaneUnit.None, 0, SaneCapabilities.SoftSelect, + SaneConstraintType.WordList, null, wordList.ToList(), null); + } + + internal static SaneOption CreateBooleanForTesting(int index, string name) + { + return new SaneOption(index, name, "", "", SaneValueType.Bool, SaneUnit.None, 0, SaneCapabilities.SoftSelect, + SaneConstraintType.None, null, null, null); + } + + internal static SaneOption CreateFixedForTesting(int index, string name, SaneRange range) + { + return new SaneOption(index, name, "", "", SaneValueType.Fixed, SaneUnit.Mm, 0, SaneCapabilities.SoftSelect, + SaneConstraintType.Range, null, null, range); + } + internal SaneOption(SaneOptionDescriptor descriptor, int index) { Index = index; @@ -45,7 +68,6 @@ internal SaneOption(SaneOptionDescriptor descriptor, int index) Size = descriptor.Size; Capabilities = descriptor.Capabilities; ConstraintType = descriptor.ConstraintType; - Console.WriteLine($"Creating option {Index} {Name} {Title} {Desc} {Type} {Unit} {Size} {Capabilities} {ConstraintType} {descriptor.Constraint}"); if (descriptor.ConstraintType == SaneConstraintType.StringList) { StringList = ParseStringArray(descriptor.Constraint).ToList(); @@ -77,6 +99,24 @@ internal SaneOption(SaneOptionDescriptor descriptor, int index) } } + private SaneOption(int index, string name, string title, string desc, SaneValueType type, SaneUnit unit, int size, + SaneCapabilities caps, SaneConstraintType constraintType, List? stringList, List? wordList, + SaneRange? range) + { + Index = index; + Name = name; + Title = title; + Desc = desc; + Type = type; + Unit = unit; + Size = size; + Capabilities = caps; + ConstraintType = constraintType; + StringList = stringList; + WordList = wordList; + Range = range; + } + public int Index { get; } public string? Name { get; } @@ -104,4 +144,13 @@ internal SaneOption(SaneOptionDescriptor descriptor, int index) public bool IsActive => !Capabilities.HasFlag(SaneCapabilities.Inactive); public bool IsSettable => Capabilities.HasFlag(SaneCapabilities.SoftSelect); + + public override string ToString() + { + var constraint = StringList != null ? string.Join(",", StringList) + : WordList != null ? string.Join(",", WordList) + : Range != null ? $"{Range.Min}-{Range.Max}/{Range.Quant}" + : ""; + return $"{Index} {Name} {Title} {Desc} {Type} {Unit} {Size} {Capabilities} {ConstraintType} {constraint}"; + } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneOptionAction.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneOptionAction.cs index ca55d60e2e..656f6e5ff7 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneOptionAction.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneOptionAction.cs @@ -1,6 +1,6 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public enum SaneOptionAction +internal enum SaneOptionAction { GetValue, SetValue, diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneOptionSetInfo.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneOptionSetInfo.cs index 25abc08236..e34ee9180b 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneOptionSetInfo.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneOptionSetInfo.cs @@ -1,6 +1,6 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public enum SaneOptionSetInfo +internal enum SaneOptionSetInfo { None = 0, Inexact = 1, diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneRange.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneRange.cs index 7aa4d94013..3889158ffe 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneRange.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneRange.cs @@ -1,6 +1,6 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public class SaneRange +internal class SaneRange { public double Min { get; init; } diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneReadParameters.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneReadParameters.cs index 6f4be35030..15515c9ddb 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneReadParameters.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneReadParameters.cs @@ -1,6 +1,6 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public struct SaneReadParameters +internal struct SaneReadParameters { public SaneFrameType Frame; public int LastFrame; diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneStatus.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneStatus.cs index fdc4e80ea8..cddd44fcb9 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneStatus.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneStatus.cs @@ -1,6 +1,6 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public enum SaneStatus +internal enum SaneStatus { Good = 0, Unsupported = 1, diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneUnit.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneUnit.cs index 70e1ba4669..5a8f3030ff 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneUnit.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneUnit.cs @@ -1,6 +1,6 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public enum SaneUnit +internal enum SaneUnit { None = 0, Pixel = 1, diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneValueType.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneValueType.cs index b772b296e2..9cb9489e9f 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneValueType.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SaneValueType.cs @@ -1,6 +1,6 @@ namespace NAPS2.Scan.Internal.Sane.Native; -public enum SaneValueType +internal enum SaneValueType { Bool = 0, Int = 1, diff --git a/NAPS2.Sdk/Scan/Internal/Sane/Native/SystemSaneInstallation.cs b/NAPS2.Sdk/Scan/Internal/Sane/Native/SystemSaneInstallation.cs new file mode 100644 index 0000000000..80fea3eea1 --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/Sane/Native/SystemSaneInstallation.cs @@ -0,0 +1,35 @@ +namespace NAPS2.Scan.Internal.Sane.Native; + +/// +/// Indicates the system-provided SANE build. +/// +internal class SystemSaneInstallation : ISaneInstallation +{ + private string? _libraryPath; + + public bool CanStreamDevices => false; + + public void Initialize() + { +#if NET6_0_OR_GREATER + _libraryPath = OperatingSystem.IsMacOS() + ? "libsane.1.dylib" + : "libsane.so.1"; +#else + _libraryPath = null; +#endif + } + + public void SetCustomConfigDir(string? configDir) + { + // The trailing ":" means we'll search the default dirs if this dir doesn't exist + PlatformCompat.System.SetEnv("SANE_CONFIG_DIR", $"{configDir}:"); + } + + // There may be some distros where this is incorrect, but if the path doesn't exist we just disable optimizations + public string DefaultConfigDir => "/etc/sane.d"; + + public string LibraryPath => _libraryPath ?? throw new InvalidOperationException(); + + public string[]? LibraryDeps => null; +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionController.cs b/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionController.cs index 97d993208f..7005f8cd30 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionController.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionController.cs @@ -1,36 +1,70 @@ +using Microsoft.Extensions.Logging; using NAPS2.Scan.Internal.Sane.Native; namespace NAPS2.Scan.Internal.Sane; internal class SaneOptionController { - private readonly SaneDevice _device; + private readonly ISaneDevice _device; + // TODO: Move exception handling + logging out if we split NAPS2.Sane off into a separate library + private readonly ILogger _logger; private Dictionary _options = null!; - public SaneOptionController(SaneDevice device) + public SaneOptionController(ISaneDevice device, ILogger logger) { _device = device; + _logger = logger; LoadOptions(); + foreach (var opt in _options.Values.OrderBy(x => x.Index)) + { + _logger.LogDebug($"Option: {opt}"); + } } private void LoadOptions() { _options = _device.GetOptions() .Where(x => x.Name != null && x.Type != SaneValueType.Group) - .ToDictionary(x => x.Name!); + .GroupBy(x => x.Name!) + .ToDictionary(x => x.Key, x => x.First()); + } + + public bool TrySet(string name, bool value) + { + _logger.LogDebug($"Maybe setting {name}"); + if (!_options.ContainsKey(name)) + return false; + var opt = _options[name]; + if (!opt.IsSettable || opt.Type != SaneValueType.Bool) + return false; + try + { + _logger.LogDebug($"Setting {name} to {value}"); + _device.SetOption(_options[name], value, out var info); + if (info.HasFlag(SaneOptionSetInfo.ReloadOptions)) + { + LoadOptions(); + } + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error writing SANE option {OptionName}", name); + return false; + } } public bool TrySet(string name, double value) { - Console.WriteLine($"Maybe setting {name}"); + _logger.LogDebug($"Maybe setting {name}"); if (!_options.ContainsKey(name)) return false; var opt = _options[name]; - if (!opt.IsActive || !opt.IsSettable || opt.Type is not (SaneValueType.Int or SaneValueType.Fixed)) + if (!opt.IsSettable || opt.Type is not (SaneValueType.Int or SaneValueType.Fixed)) return false; try { - Console.WriteLine($"Setting {name} to {value}"); + _logger.LogDebug($"Setting {name} to {value}"); _device.SetOption(_options[name], value, out var info); if (info.HasFlag(SaneOptionSetInfo.ReloadOptions)) { @@ -40,27 +74,33 @@ public bool TrySet(string name, double value) } catch (Exception ex) { - Log.ErrorException($"Error writing SANE option {name}", ex); + _logger.LogError(ex, "Error writing SANE option {OptionName}", name); return false; } } - public bool TrySet(string name, IEnumerable valueSet) + public bool TrySet(string name, SaneOptionMatcher matcher) + { + return TrySet(name, matcher, out _); + } + + public bool TrySet(string name, SaneOptionMatcher matcher, out string? matchedValue) { - Console.WriteLine($"Maybe setting {name}"); + _logger.LogDebug($"Maybe setting {name}"); + matchedValue = null; if (!_options.ContainsKey(name)) return false; var opt = _options[name]; - if (!opt.IsActive || !opt.IsSettable || opt.Type != SaneValueType.String) + if (!opt.IsSettable || opt.Type != SaneValueType.String) return false; try { - var valueHashSet = valueSet.ToHashSet(); foreach (var value in opt.StringList!) { - if (valueHashSet.Contains(value)) + if (matcher.Matches(value)) { - Console.WriteLine($"Setting {name} to {value}"); + matchedValue = value; + _logger.LogDebug($"Setting {name} to {value}"); _device.SetOption(opt, value, out var info); if (info.HasFlag(SaneOptionSetInfo.ReloadOptions)) { @@ -72,7 +112,7 @@ public bool TrySet(string name, IEnumerable valueSet) } catch (Exception ex) { - Log.ErrorException($"Error writing SANE option {name}", ex); + _logger.LogError(ex, "Error writing SANE option {OptionName}", name); } return false; } @@ -88,7 +128,7 @@ public bool TryGet(string name, out double value) if (!_options.ContainsKey(name)) return false; var opt = _options[name]; - if (!opt.IsActive || opt.Type is not (SaneValueType.Int or SaneValueType.Fixed)) + if (opt.Type is not (SaneValueType.Int or SaneValueType.Fixed)) return false; try { @@ -97,7 +137,7 @@ public bool TryGet(string name, out double value) } catch (Exception ex) { - Log.ErrorException($"Error reading SANE option {name}", ex); + _logger.LogError(ex, "Error writing SANE option {OptionName}", name); return false; } } diff --git a/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionMatcher.cs b/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionMatcher.cs new file mode 100644 index 0000000000..f8db4ceb1f --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionMatcher.cs @@ -0,0 +1,26 @@ +namespace NAPS2.Scan.Internal.Sane; + +internal class SaneOptionMatcher +{ + private readonly HashSet _knownValues; + private readonly string[] _substrings; + private readonly List _excludes = new(); + + public SaneOptionMatcher(IEnumerable knownValues, params string[] substrings) + { + _knownValues = knownValues.ToHashSet(); + _substrings = substrings; + } + + public SaneOptionMatcher Exclude(SaneOptionMatcher other) + { + _excludes.Add(other); + return this; + } + + public bool Matches(string value) + { + return !_excludes.Any(x => x.Matches(value)) && + (_knownValues.Contains(value) || _substrings.Any(value.ContainsInvariantIgnoreCase)); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionMatchers.cs b/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionMatchers.cs new file mode 100644 index 0000000000..349ed301dd --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionMatchers.cs @@ -0,0 +1,45 @@ +namespace NAPS2.Scan.Internal.Sane; + +internal static class SaneOptionMatchers +{ + private static readonly IEnumerable FlatbedStrs = new[] + { + SaneOptionTranslations.Flatbed, + SaneOptionTranslations.FB, + SaneOptionTranslations.fb + }.SelectMany(x => x); + + private static readonly IEnumerable FeederStrs = new[] + { + SaneOptionTranslations.ADF, + SaneOptionTranslations.adf, + SaneOptionTranslations.Automatic_Document_Feeder, + SaneOptionTranslations.ADF_Front + }.SelectMany(x => x); + + private static readonly IEnumerable DuplexStrs = new[] + { + SaneOptionTranslations.Duplex, + SaneOptionTranslations.ADF_Duplex + }.SelectMany(x => x); + + public static readonly SaneOptionMatcher Duplex = + new SaneOptionMatcher(DuplexStrs, "duplex"); + + public static readonly SaneOptionMatcher Feeder = + new SaneOptionMatcher(FeederStrs, "feeder", "adf").Exclude(Duplex); + + public static readonly SaneOptionMatcher Flatbed = + new SaneOptionMatcher(FlatbedStrs, "flatbed"); + + public static readonly SaneOptionMatcher BlackAndWhite = + new SaneOptionMatcher(SaneOptionTranslations.Lineart, "black and white", "black & white", "black/white"); + + public static readonly SaneOptionMatcher Grayscale = + new SaneOptionMatcher(SaneOptionTranslations.Gray, "gray", "grey") + // Error diffusion isn't "real" grayscale + .Exclude(new SaneOptionMatcher([], "Error Diffusion")); + + public static readonly SaneOptionMatcher Color = + new SaneOptionMatcher(SaneOptionTranslations.Color, "color", "colour"); +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionNames.cs b/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionNames.cs index f36d5b1177..eef651188d 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionNames.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/SaneOptionNames.cs @@ -9,7 +9,10 @@ internal static class SaneOptionNames public const string SOURCE = "source"; public const string ADF_MODE1 = "adf_mode"; public const string ADF_MODE2 = "adf-mode"; + public const string DUPLEX = "duplex"; // Not commonly used (usually adf_mode is used instead) + public const string PAGE_WIDTH = "page-width"; + public const string PAGE_HEIGHT = "page-height"; public const string TOP_LEFT_X = "tl-x"; public const string TOP_LEFT_Y = "tl-y"; public const string BOT_RIGHT_X = "br-x"; diff --git a/NAPS2.Sdk/Scan/Internal/Sane/SaneScanAreaController.cs b/NAPS2.Sdk/Scan/Internal/Sane/SaneScanAreaController.cs index 2358553986..9bae19832b 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/SaneScanAreaController.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/SaneScanAreaController.cs @@ -5,6 +5,8 @@ namespace NAPS2.Scan.Internal.Sane; internal class SaneScanAreaController { private readonly SaneOptionController _optionController; + private readonly SaneOption? _pageW; + private readonly SaneOption? _pageH; private readonly SaneOption? _tlx; private readonly SaneOption? _tly; private readonly SaneOption? _brx; @@ -16,6 +18,8 @@ public SaneScanAreaController(SaneOptionController optionController) { _optionController = optionController; + _pageW = _optionController.GetOption(SaneOptionNames.PAGE_WIDTH); + _pageH = _optionController.GetOption(SaneOptionNames.PAGE_HEIGHT); _tlx = _optionController.GetOption(SaneOptionNames.TOP_LEFT_X); _tly = _optionController.GetOption(SaneOptionNames.TOP_LEFT_Y); _brx = _optionController.GetOption(SaneOptionNames.BOT_RIGHT_X); @@ -65,6 +69,8 @@ private bool IsNumericRange(SaneOption opt) private double GetMaxMm(SaneOption opt, double? dpi) => ToMm(opt, GetNumericRangeMax(opt), dpi); + private double? MaybeGetMaxMm(SaneOption? opt, double? res) => opt == null ? null : GetMaxMm(opt, res); + private double ToMm(SaneOption opt, double value, double? dpi) { if (opt.Unit == SaneUnit.Pixel) @@ -77,14 +83,22 @@ private double ToMm(SaneOption opt, double value, double? dpi) public (double minX, double minY, double maxX, double maxY) GetBounds() { if (!CanSetArea) throw new InvalidOperationException(); + // Checking just tl/br should be enough, but some backends have an issue where we need to check width/height too + // https://gitlab.com/sane-project/backends/-/issues/730 return ( - GetMinMm(_tlx!, _xres), GetMinMm(_tly!, _yres), - GetMaxMm(_brx!, _xres), GetMaxMm(_bry!, _yres)); + GetMinMm(_tlx!, _xres), + GetMinMm(_tly!, _yres), + Math.Max(GetMaxMm(_brx!, _xres), MaybeGetMaxMm(_pageW, _xres) ?? 0), + Math.Max(GetMaxMm(_bry!, _yres), MaybeGetMaxMm(_pageH, _xres) ?? 0)); } public void SetArea(double x1, double y1, double x2, double y2) { if (!CanSetArea) throw new InvalidOperationException(); + // Setting just tl/br should be enough, but some backends have an issue where we need to set width/height too + // https://gitlab.com/sane-project/backends/-/issues/730 + _optionController.TrySet(SaneOptionNames.PAGE_WIDTH, x2 - x1); + _optionController.TrySet(SaneOptionNames.PAGE_HEIGHT, y2 - y1); _optionController.TrySet(SaneOptionNames.TOP_LEFT_X, x1); _optionController.TrySet(SaneOptionNames.TOP_LEFT_Y, y1); _optionController.TrySet(SaneOptionNames.BOT_RIGHT_X, x2); diff --git a/NAPS2.Sdk/Scan/Internal/Sane/SaneScanDriver.cs b/NAPS2.Sdk/Scan/Internal/Sane/SaneScanDriver.cs index ebe76b0d08..6afb3714df 100644 --- a/NAPS2.Sdk/Scan/Internal/Sane/SaneScanDriver.cs +++ b/NAPS2.Sdk/Scan/Internal/Sane/SaneScanDriver.cs @@ -1,4 +1,6 @@ -using System.Threading; +using System.Collections.Immutable; +using System.Threading; +using Microsoft.Extensions.Logging; using NAPS2.Images.Bitwise; using NAPS2.Scan.Exceptions; using NAPS2.Scan.Internal.Sane.Native; @@ -7,159 +9,411 @@ namespace NAPS2.Scan.Internal.Sane; internal class SaneScanDriver : IScanDriver { - private static readonly HashSet FlatbedStrs = new[] - { - SaneOptionTranslations.Flatbed, - SaneOptionTranslations.FB, - SaneOptionTranslations.fb - }.SelectMany(x => x).ToHashSet(); - - private static readonly HashSet FeederStrs = new[] - { - SaneOptionTranslations.ADF, - SaneOptionTranslations.adf, - SaneOptionTranslations.Automatic_Document_Feeder, - SaneOptionTranslations.ADF_Front - }.SelectMany(x => x).ToHashSet(); - - private static readonly HashSet DuplexStrs = new[] - { - SaneOptionTranslations.Duplex, - SaneOptionTranslations.ADF_Duplex - }.SelectMany(x => x).ToHashSet(); + private static string? _customConfigDir; private readonly ScanningContext _scanningContext; public SaneScanDriver(ScanningContext scanningContext) { _scanningContext = scanningContext; + +#if NET6_0_OR_GREATER + Installation = OperatingSystem.IsMacOS() + ? new BundledSaneInstallation() + : File.Exists("/.flatpak-info") + ? new FlatpakSaneInstallation() + : new SystemSaneInstallation(); +#else + Installation = null!; +#endif } - public Task> GetDeviceList(ScanOptions options) + private ISaneInstallation Installation { get; } + + public Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback) { + var localIPsTask = options.ExcludeLocalIPs ? LocalIPsHelper.Get() : null; + + void MaybeCallback(SaneDeviceInfo device) + { + if (options.ExcludeLocalIPs && GetIP(device) is { } ip && localIPsTask!.Result.Contains(ip)) + { + return; + } + callback(GetScanDevice(device)); + } + return Task.Run(() => { - // TODO: Run SANE in a worker process so we can parallelize - // TODO: Maybe use a mutex in SaneClient instead of needing manual locking? - lock (SaneNativeLibrary.Instance) + Installation.Initialize(); + string? tempConfigDir = MaybeCreateTempConfigDirForSingleBackend(options.SaneOptions.Backend); + try { // TODO: This is crashing after a delay for no apparent reason. - // Presumably it's a bad backend. - // TODO: Run SANE in a worker process for added stability. - using var client = new SaneClient(); + // That's okay because we're in a worker process, but ideally we could fix it in SANE. + using var client = new SaneClient(Installation, options.SaneOptions.KeepInitialized); // TODO: We can use device.type and .vendor to help pick an icon etc. // https://sane-project.gitlab.io/standard/api.html#device-descriptor-type - return client.GetDevices() - .Select(device => + if (Installation.CanStreamDevices) + { + client.StreamDevices(MaybeCallback, cancelToken); + } + else + { + foreach (var device in client.GetDevices()) { - // TODO: The backend should be part of the device metadata, e.g. DriverSubType - var backend = device.Name.Split(':')[0]; - return new ScanDevice(device.Name, $"{device.Model} ({backend})"); - }) - .ToList(); + MaybeCallback(device); + } + } + } + finally + { + if (tempConfigDir != null) + { + try + { + Directory.Delete(tempConfigDir, true); + } + catch (Exception ex) + { + _scanningContext.Logger.LogDebug(ex, "Error cleaning up temp SANE config dir"); + } + } + } + }); + } + + public Task GetCaps(ScanOptions options, CancellationToken cancelToken) + { + return Task.Run(() => + { + try + { + Installation.Initialize(); + using var client = new SaneClient(Installation, options.SaneOptions.KeepInitialized); + if (cancelToken.IsCancellationRequested) return new ScanCaps(); + _scanningContext.Logger.LogDebug("Opening SANE Device \"{ID}\" for caps", options.Device!.ID); + using var device = client.OpenDevice(options.Device.ID); + if (cancelToken.IsCancellationRequested) return new ScanCaps(); + return GetSaneCaps(device, GetBackend(options.Device)); + } + catch (SaneException ex) + { + switch (ex.Status) + { + case SaneStatus.Good: + case SaneStatus.Cancelled: + return new ScanCaps(); + case SaneStatus.DeviceBusy: + throw new DeviceBusyException(); + case SaneStatus.Invalid: + _scanningContext.Logger.LogDebug(ex, "Sane invalid error"); + throw new DeviceOfflineException(); + case SaneStatus.IoError: + throw new DeviceCommunicationException(); + default: + throw new DeviceException($"SANE error: {ex.Status}"); + } } }); } + internal ScanCaps GetSaneCaps(ISaneDevice device, string backend) + { + var controller = new SaneOptionController(device, _scanningContext.Logger); + + PerSourceCaps? flatbed = null; + PerSourceCaps? feeder = null; + PerSourceCaps? duplex = null; + + if (controller.TrySet(SaneOptionNames.SOURCE, SaneOptionMatchers.Flatbed)) + { + flatbed = GetPerSourceCaps(controller); + } + if (controller.TrySet(SaneOptionNames.SOURCE, SaneOptionMatchers.Feeder)) + { + feeder = GetPerSourceCaps(controller); + } + if (controller.TrySet(SaneOptionNames.SOURCE, SaneOptionMatchers.Duplex) || + controller.TrySet(SaneOptionNames.ADF_MODE1, SaneOptionMatchers.Duplex) || + controller.TrySet(SaneOptionNames.ADF_MODE2, SaneOptionMatchers.Duplex) || + controller.TrySet(SaneOptionNames.DUPLEX, true)) + { + duplex = GetPerSourceCaps(controller); + } + + return new ScanCaps + { + MetadataCaps = new MetadataCaps + { + DriverSubtype = backend + }, + PaperSourceCaps = flatbed != null || feeder != null || duplex != null + ? new PaperSourceCaps + { + SupportsFlatbed = flatbed != null, + SupportsFeeder = feeder != null, + SupportsDuplex = duplex != null + } + : null, + FlatbedCaps = flatbed, + FeederCaps = feeder, + DuplexCaps = duplex + }; + } + + private PerSourceCaps GetPerSourceCaps(SaneOptionController controller) + { + var resOpt = controller.GetOption(SaneOptionNames.RESOLUTION); + var xResOpt = controller.GetOption(SaneOptionNames.X_RESOLUTION); + var yResOpt = controller.GetOption(SaneOptionNames.Y_RESOLUTION); + var resValues = resOpt != null + ? GetValues(resOpt) + : GetValues(xResOpt) is { } xValues && GetValues(yResOpt) is { } yValues + ? xValues.Intersect(yValues).OrderBy(dpi => dpi).ToImmutableList() + : null; + + var scanAreaController = new SaneScanAreaController(controller); + PageSize? scanArea = null; + if (scanAreaController.CanSetArea) + { + var (minX, minY, maxX, maxY) = scanAreaController.GetBounds(); + scanArea = new PageSize((decimal) (maxX - minX), (decimal) (maxY - minY), PageSizeUnit.Millimetre); + } + + return new PerSourceCaps + { + BitDepthCaps = controller.GetOption(SaneOptionNames.MODE) != null + ? new BitDepthCaps + { + SupportsColor = controller.TrySet(SaneOptionNames.MODE, SaneOptionMatchers.Color), + SupportsGrayscale = controller.TrySet(SaneOptionNames.MODE, SaneOptionMatchers.Grayscale), + SupportsBlackAndWhite = controller.TrySet(SaneOptionNames.MODE, SaneOptionMatchers.BlackAndWhite) + } + : null, + DpiCaps = new DpiCaps + { + Values = resValues + }, + PageSizeCaps = new PageSizeCaps + { + ScanArea = scanArea + } + }; + } + + private ImmutableList? GetValues(SaneOption? option) + { + if (option == null) return null; + if (option.WordList != null) + { + return option.WordList.Select(x => (int) x).ToImmutableList(); + } + if (option.Range != null) + { + return DpiCaps.ForRange((int) option.Range.Min, (int) option.Range.Max, (int) option.Range.Quant).Values; + } + return null; + } + + private static ScanDevice GetScanDevice(SaneDeviceInfo device) => + new(Driver.Sane, device.Name, GetName(device)); + + private static string GetName(SaneDeviceInfo device) + { + var backend = GetBackend(device.Name); + // Special cases for sane-escl and sane-airscan. + if (backend == "escl") + { + // We include the vendor as it's excluded from the model, and we include the full name instead of + // just the backend as that has the IP address. + return $"{device.Vendor} {device.Model} ({device.Name})"; + } + if (backend == "airscan") + { + // We include the device type which has the IP address. + return $"{device.Model} ({backend}:{device.Type})"; + } + return $"{device.Model} ({backend})"; + } + + private string? GetIP(SaneDeviceInfo device) + { + var backend = GetBackend(device.Name); + if (backend == "escl") + { + // Name is in the form "escl:http://xx.xx.xx.xx:yy" + var uri = new Uri(device.Name.Substring(device.Name.IndexOf(":", StringComparison.InvariantCulture) + 1)); + return uri.Host; + } + if (backend == "airscan") + { + // Type is in the form "ip=xx.xx.xx.xx" + return device.Type.Substring(3); + } + return null; + } + + public static string GetBackend(ScanDevice device) => GetBackend(device.ID); + + private static string GetBackend(string saneDeviceName) => saneDeviceName.Split(':')[0]; + public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) { return Task.Run(() => { - lock (SaneNativeLibrary.Instance) + bool hasAtLeastOneImage = false; + try { - bool hasAtLeastOneImage = false; + Installation.Initialize(); + using var client = new SaneClient(Installation, options.SaneOptions.KeepInitialized); + if (cancelToken.IsCancellationRequested) return; + _scanningContext.Logger.LogDebug("Opening SANE Device \"{ID}\"", options.Device!.ID); + using var device = client.OpenDevice(options.Device.ID); + if (cancelToken.IsCancellationRequested) return; + var optionData = SetOptions(device, options); + var cancelOnce = new Once(device.Cancel); + cancelToken.Register(cancelOnce.Run); try { - using var client = new SaneClient(); - if (cancelToken.IsCancellationRequested) return; - using var device = client.OpenDevice(options.Device!.ID!); - if (cancelToken.IsCancellationRequested) return; - SetOptions(device, options); - // TODO: We apparently need to cancel even upon normal completion, i.e. one sane_cancel per sane_start - cancelToken.Register(device.Cancel); - - // TODO: Can we validate whether it's really an adf? - if (options.PaperSource == PaperSource.Flatbed) + if (!optionData.IsFeeder) { - var image = ScanPage(device, scanEvents) ?? + var image = ScanPage(device, scanEvents, optionData) ?? throw new DeviceException("SANE expected image"); callback(image); } else { - while (ScanPage(device, scanEvents) is { } image) + while (ScanPage(device, scanEvents, optionData) is { } image) { hasAtLeastOneImage = true; callback(image); } } } - catch (SaneException ex) + finally { - switch (ex.Status) - { - case SaneStatus.Good: - case SaneStatus.Cancelled: - return; - case SaneStatus.NoDocs: - if (!hasAtLeastOneImage) - { - throw new NoPagesException(); - } - break; - case SaneStatus.DeviceBusy: - throw new DeviceException(SdkResources.DeviceBusy); - case SaneStatus.Invalid: - // TODO: Maybe not always correct? e.g. when setting options - throw new DeviceException(SdkResources.DeviceOffline); - case SaneStatus.Jammed: - throw new DeviceException(SdkResources.DevicePaperJam); - case SaneStatus.CoverOpen: - throw new DeviceException(SdkResources.DeviceCoverOpen); - default: - throw new DeviceException($"SANE error: {ex.Status}"); - } + cancelOnce.Run(); + } + } + catch (SaneException ex) + { + switch (ex.Status) + { + case SaneStatus.Good: + case SaneStatus.Cancelled: + return; + case SaneStatus.NoDocs: + if (!hasAtLeastOneImage) + { + throw new DeviceFeederEmptyException(); + } + + break; + case SaneStatus.DeviceBusy: + throw new DeviceBusyException(); + case SaneStatus.Invalid: + // TODO: Maybe not always correct? e.g. when setting options + throw new DeviceOfflineException(); + case SaneStatus.Jammed: + throw new DevicePaperJamException(); + case SaneStatus.CoverOpen: + throw new DeviceCoverOpenException(); + case SaneStatus.IoError: + throw new DeviceCommunicationException(); + default: + throw new DeviceException($"SANE error: {ex.Status}"); } } }); } - private void SetOptions(SaneDevice device, ScanOptions options) + private string? MaybeCreateTempConfigDirForSingleBackend(string? backendName) { - // TODO: How to handle brightness, contrast, etc.? - var controller = new SaneOptionController(device); + if (string.IsNullOrEmpty(backendName)) + { + return null; + } + if (!Directory.Exists(Installation.DefaultConfigDir)) + { + // Non-typical SANE installation where we don't know the config dir and can't do this optimization + return null; + } + if (_customConfigDir == null) + { + // SANE caches the SANE_CONFIG_DIR environment variable process-wide, which means that we can't willy-nilly + // change the config dir. However, if we use a static directory name and only create the actual directory + // when we want to use it, SANE will (without caching) use the directory when it exists, and fall back to + // the default config dir otherwise. + _customConfigDir = Path.Combine(_scanningContext.TempFolderPath, Path.GetRandomFileName()); + Installation.SetCustomConfigDir(_customConfigDir); + } + // By using a custom config dir with a dll.conf file that only has a single backend specified, we can force SANE + // to only check that backend + Directory.CreateDirectory(_customConfigDir); + // Copy the backend.conf file in case there's any important backend-specific configuration + var backendConfFile = $"{backendName}.conf"; + if (File.Exists(Path.Combine(Installation.DefaultConfigDir, backendConfFile))) + { + File.Copy( + Path.Combine(Installation.DefaultConfigDir, backendConfFile), + Path.Combine(_customConfigDir, backendConfFile)); + } + // Create a dll.conf file with only the single backend name (normally it's all backends, one per line) + File.WriteAllText(Path.Combine(_customConfigDir, "dll.conf"), backendName); + // Create an empty dll.d dir so SANE doesn't use the default one + Directory.CreateDirectory(Path.Combine(_customConfigDir, "dll.d")); + _scanningContext.Logger.LogDebug("Created temp SANE config dir {Dir}", _customConfigDir); + return _customConfigDir; + } - if (options.PaperSource == PaperSource.Flatbed) + internal OptionData SetOptions(ISaneDevice device, ScanOptions options) + { + var controller = new SaneOptionController(device, _scanningContext.Logger); + var optionData = new OptionData { - controller.TrySet(SaneOptionNames.SOURCE, FlatbedStrs); + IsFeeder = options.PaperSource is PaperSource.Feeder or PaperSource.Duplex + }; + + if (options.PaperSource == PaperSource.Auto) + { + if (!controller.TrySet(SaneOptionNames.SOURCE, SaneOptionMatchers.Flatbed)) + { + optionData.IsFeeder = controller.TrySet(SaneOptionNames.SOURCE, SaneOptionMatchers.Feeder); + } + } + else if (options.PaperSource == PaperSource.Flatbed) + { + controller.TrySet(SaneOptionNames.SOURCE, SaneOptionMatchers.Flatbed); } else if (options.PaperSource == PaperSource.Feeder) { // We could throw NoFeederSupportException on failure, except this might be a feeder-only scanner. - controller.TrySet(SaneOptionNames.SOURCE, FeederStrs); + controller.TrySet(SaneOptionNames.SOURCE, SaneOptionMatchers.Feeder); } else if (options.PaperSource == PaperSource.Duplex) { - controller.TrySet(SaneOptionNames.SOURCE, DuplexStrs); - controller.TrySet(SaneOptionNames.ADF_MODE1, DuplexStrs); - controller.TrySet(SaneOptionNames.ADF_MODE2, DuplexStrs); + if (!controller.TrySet(SaneOptionNames.SOURCE, SaneOptionMatchers.Duplex)) + { + // If we can't set the source to Duplex, set it to Feeder instead. + // We can then set AdfMode to Duplex. + controller.TrySet(SaneOptionNames.SOURCE, SaneOptionMatchers.Feeder); + } + controller.TrySet(SaneOptionNames.ADF_MODE1, SaneOptionMatchers.Duplex); + controller.TrySet(SaneOptionNames.ADF_MODE2, SaneOptionMatchers.Duplex); + controller.TrySet(SaneOptionNames.DUPLEX, true); } var mode = options.BitDepth switch { - BitDepth.BlackAndWhite => SaneOptionTranslations.Lineart, - BitDepth.Grayscale => SaneOptionTranslations.Gray, - _ => SaneOptionTranslations.Color + BitDepth.BlackAndWhite => SaneOptionMatchers.BlackAndWhite, + BitDepth.Grayscale => SaneOptionMatchers.Grayscale, + _ => SaneOptionMatchers.Color }; - controller.TrySet(SaneOptionNames.MODE, mode); + controller.TrySet(SaneOptionNames.MODE, mode, out optionData.Mode); - // TODO: Get closest resolution value - if (!controller.TrySet(SaneOptionNames.RESOLUTION, options.Dpi)) - { - controller.TrySet(SaneOptionNames.X_RESOLUTION, options.Dpi); - controller.TrySet(SaneOptionNames.Y_RESOLUTION, options.Dpi); - } + SetResolution(options, controller, optionData); var scanAreaController = new SaneScanAreaController(controller); if (scanAreaController.CanSetArea) @@ -176,21 +430,99 @@ private void SetOptions(SaneDevice device, ScanOptions options) }; scanAreaController.SetArea(minX + offsetX, minY, minX + offsetX + width, minY + height); } + + foreach (var kvp in options.KeyValueOptions) + { + string name = kvp.Key; + string value = kvp.Value; + var opt = controller.GetOption(name); + if (opt != null) + { + // TODO: Also implement bool value type + if (opt.Type == SaneValueType.String) + { + controller.TrySet(name, new SaneOptionMatcher([value])); + } + if (opt.Type is SaneValueType.Int or SaneValueType.Fixed && double.TryParse(value, out var doubleValue)) + { + controller.TrySet(name, doubleValue); + } + } + } + + return optionData; } - private IMemoryImage? ScanPage(SaneDevice device, IScanEvents scanEvents) + private void SetResolution(ScanOptions options, SaneOptionController controller, OptionData optionData) + { + var targetDpi = GetClosestResolution(options.Dpi, controller); + + if (controller.TrySet(SaneOptionNames.RESOLUTION, targetDpi)) + { + if (controller.TryGet(SaneOptionNames.RESOLUTION, out var res)) + { + optionData.XRes = res; + optionData.YRes = res; + } + } + else + { + controller.TrySet(SaneOptionNames.X_RESOLUTION, targetDpi); + controller.TrySet(SaneOptionNames.Y_RESOLUTION, targetDpi); + if (controller.TryGet(SaneOptionNames.X_RESOLUTION, out var xRes)) + { + optionData.XRes = xRes; + } + if (controller.TryGet(SaneOptionNames.Y_RESOLUTION, out var yRes)) + { + optionData.YRes = yRes; + } + } + if (optionData.XRes <= 0) optionData.XRes = targetDpi; + if (optionData.YRes <= 0) optionData.YRes = targetDpi; + } + + private double GetClosestResolution(int dpi, SaneOptionController controller) + { + var targetDpi = (double) dpi; + var opt = controller.GetOption(SaneOptionNames.RESOLUTION) ?? + controller.GetOption(SaneOptionNames.X_RESOLUTION) ?? + controller.GetOption(SaneOptionNames.Y_RESOLUTION); + if (opt != null) + { + if (opt.ConstraintType == SaneConstraintType.Range) + { + targetDpi = targetDpi.Clamp(opt.Range!.Min, opt.Range.Max); + if (opt.Range.Quant != 0) + { + targetDpi -= (targetDpi - opt.Range.Min) % opt.Range.Quant; + } + } + if (opt.ConstraintType == SaneConstraintType.WordList && opt.WordList!.Any()) + { + targetDpi = opt.WordList!.OrderBy(x => Math.Abs(x - targetDpi)).First(); + } + } + if ((int) targetDpi != dpi) + { + _scanningContext.Logger.LogDebug("Correcting DPI from {InDpi} to {OutDpi}", dpi, targetDpi); + } + return targetDpi; + } + + internal IMemoryImage? ScanPage(ISaneDevice device, IScanEvents scanEvents, OptionData optionData) { - // TODO: Fix up events var data = ScanFrame(device, scanEvents, 0, out var p); if (data == null) { return null; } - if (p.Frame is SaneFrameType.Red or SaneFrameType.Green or SaneFrameType.Blue) - { - return ProcessMultiFrameImage(device, scanEvents, p, data); - } - return ProcessSingleFrameImage(p, data); + + var page = p.Frame is SaneFrameType.Red or SaneFrameType.Green or SaneFrameType.Blue + ? ProcessMultiFrameImage(device, scanEvents, p, data.GetBuffer()) + : ProcessSingleFrameImage(p, data.GetBuffer()); + page.SetResolution((float) optionData.XRes, (float) optionData.YRes); + return page; } private IMemoryImage ProcessSingleFrameImage(SaneReadParameters p, byte[] data) @@ -204,16 +536,16 @@ private IMemoryImage ProcessSingleFrameImage(SaneReadParameters p, byte[] data) $"Unsupported transfer format: {p.Depth} bits per sample, {p.Frame} frame") }; var image = _scanningContext.ImageContext.Create(p.PixelsPerLine, p.Lines, pixelFormat); - var pixelInfo = new PixelInfo(p.PixelsPerLine, p.Lines, subPixelType); + var pixelInfo = new PixelInfo(p.PixelsPerLine, p.Lines, subPixelType, p.BytesPerLine); new CopyBitwiseImageOp().Perform(data, pixelInfo, image); return image; } - private IMemoryImage ProcessMultiFrameImage(SaneDevice device, IScanEvents scanEvents, SaneReadParameters p, + private IMemoryImage ProcessMultiFrameImage(ISaneDevice device, IScanEvents scanEvents, SaneReadParameters p, byte[] data) { var image = _scanningContext.ImageContext.Create(p.PixelsPerLine, p.Lines, ImagePixelFormat.RGB24); - var pixelInfo = new PixelInfo(p.PixelsPerLine, p.Lines, SubPixelType.Gray); + var pixelInfo = new PixelInfo(p.PixelsPerLine, p.Lines, SubPixelType.Gray, p.BytesPerLine); // Use the first buffer, then read two more buffers and use them so we get all 3 channels new CopyBitwiseImageOp { DestChannel = ToChannel(p.Frame) }.Perform(data, pixelInfo, image); @@ -222,12 +554,12 @@ private IMemoryImage ProcessMultiFrameImage(SaneDevice device, IScanEvents scanE return image; } - private void ReadSingleChannelFrame(SaneDevice device, IScanEvents scanEvents, int frame, PixelInfo pixelInfo, + private void ReadSingleChannelFrame(ISaneDevice device, IScanEvents scanEvents, int frame, PixelInfo pixelInfo, IMemoryImage image) { var data = ScanFrame(device, scanEvents, frame, out var p) ?? throw new DeviceException("SANE unexpected last frame"); - new CopyBitwiseImageOp { DestChannel = ToChannel(p.Frame) }.Perform(data, pixelInfo, image); + new CopyBitwiseImageOp { DestChannel = ToChannel(p.Frame) }.Perform(data.GetBuffer(), pixelInfo, image); } private ColorChannel ToChannel(SaneFrameType frame) => frame switch @@ -238,125 +570,55 @@ private void ReadSingleChannelFrame(SaneDevice device, IScanEvents scanEvents, i _ => throw new ArgumentException() }; - private byte[]? ScanFrame(SaneDevice device, IScanEvents scanEvents, int frame, out SaneReadParameters p) + internal MemoryStream? ScanFrame(ISaneDevice device, IScanEvents scanEvents, int frame, out SaneReadParameters p) { device.Start(); if (frame == 0) { scanEvents.PageStart(); } + p = device.GetParameters(); bool isMultiFrame = p.Frame is SaneFrameType.Red or SaneFrameType.Green or SaneFrameType.Blue; - var frameSize = p.BytesPerLine * p.Lines; + // p.Lines can be -1, in which case we don't know the frame size ahead of time + var frameSize = p.Lines == -1 ? 0 : p.BytesPerLine * p.Lines; var currentProgress = frame * frameSize; var totalProgress = isMultiFrame ? frameSize * 3 : frameSize; - var data = new byte[frameSize]; - var index = 0; var buffer = new byte[65536]; - scanEvents.PageProgress(currentProgress / (double) totalProgress); + if (totalProgress > 0) + { + scanEvents.PageProgress(currentProgress / (double) totalProgress); + } + var dataStream = new MemoryStream(frameSize); while (device.Read(buffer, out var len)) { - Array.Copy(buffer, 0, data, index, len); - index += len; + dataStream.Write(buffer, 0, len); currentProgress += len; - scanEvents.PageProgress(currentProgress / (double) totalProgress); + if (totalProgress > 0) + { + scanEvents.PageProgress(currentProgress / (double) totalProgress); + } } - if (index == 0) + + if (dataStream.Length == 0) { return null; } - if (index != frameSize) - { - throw new DeviceException($"SANE unexpected data length, got {index}, expected {frameSize}"); - } - return data; + // Now that we've read the data we know the exact frame size and can work backwards to get the number of lines. + p.Lines = (int) dataStream.Length / p.BytesPerLine; + + return dataStream; + } + + internal class OptionData + { + public bool IsFeeder; + public double XRes; + public double YRes; + public string? Mode; } - // private KeyValueScanOptions GetKeyValueOptions(ScanOptions options) - // { - // var availableOptions = - // SaneOptionCache.GetOrSet(options.Device!.ID!, () => GetAvailableOptions(options.Device!.ID!)); - // var keyValueOptions = new KeyValueScanOptions(options.SaneOptions.KeyValueOptions ?? new KeyValueScanOptions()); - // - // bool ChooseStringOption(string name, Func match) - // { - // var opt = availableOptions.Get(name); - // var choice = opt?.StringList?.FirstOrDefault(match); - // if (choice != null) - // { - // keyValueOptions[name] = choice; - // return true; - // } - // return false; - // } - // - // bool ChooseNumericOption(string name, decimal value) - // { - // var opt = availableOptions.Get(name); - // if (opt?.ConstraintType == SaneConstraintType.WordList) - // { - // var choice = opt.WordList?.OrderBy(x => Math.Abs(x - value)).FirstOrDefault(); - // if (choice != null) - // { - // keyValueOptions[name] = choice.Value.ToString(CultureInfo.InvariantCulture); - // return true; - // } - // } - // else if (opt?.ConstraintType == SaneConstraintType.Range) - // { - // if (value < opt.Range!.Min) - // { - // value = opt.Range.Min; - // } - // if (value > opt.Range.Max) - // { - // value = opt.Range.Max; - // } - // if (opt.Range.Quant != 0) - // { - // var mod = (value - opt.Range.Min) % opt.Range.Quant; - // if (mod != 0) - // { - // value = mod < opt.Range.Quant / 2 ? value - mod : value + opt.Range.Quant - mod; - // } - // } - // keyValueOptions[name] = value.ToString("0.#####", CultureInfo.InvariantCulture); - // return true; - // } - // return false; - // } - // - // bool IsFlatbedChoice(string choice) => - // choice.IndexOf("flatbed", StringComparison.InvariantCultureIgnoreCase) >= 0; - // - // bool IsFeederChoice(string choice) => new[] { "adf", "feeder", "simplex" }.Any(x => - // choice.IndexOf(x, StringComparison.InvariantCultureIgnoreCase) >= 0); - // - // bool IsDuplexChoice(string choice) => - // choice.IndexOf("duplex", StringComparison.InvariantCultureIgnoreCase) >= 0; - // - // if (options.PaperSource == PaperSource.Flatbed) - // { - // ChooseStringOption("--source", IsFlatbedChoice); - // } - // else if (options.PaperSource == PaperSource.Feeder) - // { - // if (!ChooseStringOption("--source", x => IsFeederChoice(x) && !IsDuplexChoice(x)) && - // !ChooseStringOption("--source", IsFeederChoice) && - // !ChooseStringOption("--source", IsDuplexChoice)) - // { - // throw new NoFeederSupportException(); - // } - // } - // else if (options.PaperSource == PaperSource.Duplex) - // { - // if (!ChooseStringOption("--source", IsDuplexChoice)) - // { - // throw new NoDuplexSupportException(); - // } - // } - // // if (options.BitDepth == BitDepth.Color) // { // ChooseStringOption("--mode", x => x == "Color"); @@ -376,39 +638,4 @@ private void ReadSingleChannelFrame(SaneDevice device, IScanEvents scanEvents, i // ChooseNumericOption("--depth", 1); // ChooseNumericOption("--threshold", (-options.Brightness + 1000) / 20m); // } - // - // var width = options.PageSize!.WidthInMm; - // var height = options.PageSize.HeightInMm; - // ChooseNumericOption("-x", width); - // ChooseNumericOption("-y", height); - // var maxWidth = availableOptions.Get("-l")?.Range?.Max; - // var maxHeight = availableOptions.Get("-t")?.Range?.Max; - // if (maxWidth != null) - // { - // if (options.PageAlign == HorizontalAlign.Center) - // { - // ChooseNumericOption("-l", (maxWidth.Value - width) / 2); - // } - // else if (options.PageAlign == HorizontalAlign.Right) - // { - // ChooseNumericOption("-l", maxWidth.Value - width); - // } - // else - // { - // ChooseNumericOption("-l", 0); - // } - // } - // if (maxHeight != null) - // { - // ChooseNumericOption("-t", 0); - // } - // - // if (!ChooseNumericOption("--resolution", options.Dpi)) - // { - // ChooseNumericOption("--x-resolution", options.Dpi); - // ChooseNumericOption("--y-resolution", options.Dpi); - // } - // - // return keyValueOptions; - // } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/ScanBridgeFactory.cs b/NAPS2.Sdk/Scan/Internal/ScanBridgeFactory.cs index 7d6914a6fc..72235d9b6b 100644 --- a/NAPS2.Sdk/Scan/Internal/ScanBridgeFactory.cs +++ b/NAPS2.Sdk/Scan/Internal/ScanBridgeFactory.cs @@ -1,37 +1,34 @@ -namespace NAPS2.Scan.Internal; +using NAPS2.Remoting.Worker; + +namespace NAPS2.Scan.Internal; internal class ScanBridgeFactory : IScanBridgeFactory { - private readonly InProcScanBridge _inProcScanBridge; - private readonly WorkerScanBridge _workerScanBridge; - private readonly NetworkScanBridge _networkScanBridge; + private readonly ScanningContext _scanningContext; public ScanBridgeFactory(ScanningContext scanningContext) - : this(new InProcScanBridge(scanningContext), new WorkerScanBridge(scanningContext), - new NetworkScanBridge(scanningContext)) - { - } - - public ScanBridgeFactory(InProcScanBridge inProcScanBridge, WorkerScanBridge workerScanBridge, - NetworkScanBridge networkScanBridge) { - _inProcScanBridge = inProcScanBridge; - _workerScanBridge = workerScanBridge; - _networkScanBridge = networkScanBridge; + _scanningContext = scanningContext; } public IScanBridge Create(ScanOptions options) { - if (!string.IsNullOrEmpty(options.NetworkOptions.Ip)) + if (_scanningContext.WorkerFactory == null) + { + // Worker processes generally aren't required, just preferred for stability. + // Where applicable, the driver (i.e. Twain) will throw an error if we're running on the wrong arch. + return new InProcScanBridge(_scanningContext); + } + if (options.Driver == Driver.Apple) { - // The physical scanner is connected to a different computer, so we connect to a NAPS2 server process over the network - return _networkScanBridge; + // Run ImageCaptureCore in a worker process for added stability + return new WorkerScanBridge(_scanningContext, WorkerType.Native); } if (options.Driver == Driver.Sane) { // Run SANE in a worker process for added stability - return _workerScanBridge; + return new WorkerScanBridge(_scanningContext, WorkerType.Native); } - return _inProcScanBridge; + return new InProcScanBridge(_scanningContext); } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/ScanDriverFactory.cs b/NAPS2.Sdk/Scan/Internal/ScanDriverFactory.cs index 155356f1cc..88af5e8c3e 100644 --- a/NAPS2.Sdk/Scan/Internal/ScanDriverFactory.cs +++ b/NAPS2.Sdk/Scan/Internal/ScanDriverFactory.cs @@ -1,4 +1,6 @@ -namespace NAPS2.Scan.Internal; +using NAPS2.Scan.Exceptions; + +namespace NAPS2.Scan.Internal; internal class ScanDriverFactory : IScanDriverFactory { @@ -18,18 +20,24 @@ public IScanDriver Create(ScanOptions options) return new Apple.AppleScanDriver(_scanningContext); #else case Driver.Wia: +#if NET6_0_OR_GREATER + if (!OperatingSystem.IsWindows()) throw new NotSupportedException(); +#endif return new Wia.WiaScanDriver(_scanningContext); case Driver.Twain: - return options.TwainOptions.Adapter == TwainAdapter.Legacy - ? new Twain.LegacyTwainScanDriver() - : new Twain.TwainScanDriver(_scanningContext); +#if NET6_0_OR_GREATER + if (!OperatingSystem.IsWindows()) throw new NotSupportedException(); +#endif + return new Twain.TwainScanDriver(_scanningContext); #endif case Driver.Sane: return new Sane.SaneScanDriver(_scanningContext); + case Driver.Escl: + return new Escl.EsclScanDriver(_scanningContext); default: - throw new NotSupportedException( + throw new DriverNotSupportedException( $"Unsupported driver: {options.Driver}. " + - "Make sure you're using the right framework target (e.g. net6-macos10.15 for the Apple driver)."); + "Make sure you're using the right framework target (e.g. net8-macos for the Apple driver)."); } } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/ScanOptionsValidator.cs b/NAPS2.Sdk/Scan/Internal/ScanOptionsValidator.cs index 45aaa526b0..c45da80c9f 100644 --- a/NAPS2.Sdk/Scan/Internal/ScanOptionsValidator.cs +++ b/NAPS2.Sdk/Scan/Internal/ScanOptionsValidator.cs @@ -1,16 +1,24 @@ -using NAPS2.Serialization; +using NAPS2.Scan.Exceptions; +using NAPS2.Serialization; namespace NAPS2.Scan.Internal; // TODO: Add tests for this and/or scanperformer -public class ScanOptionsValidator +internal class ScanOptionsValidator { public ScanOptions ValidateAll(ScanOptions options, ScanningContext scanningContext, bool requireDevice) { - // Easy deep copy. Ideally we'd do this in a more efficient way. - options = options.ToXml().FromXml(); + options = options.Clone(); - options.Driver = ValidateDriver(options.Driver); + if (options.Device != null && options.Driver != Driver.Default) + { + // Verify driver consistency + if (options.Driver != options.Device.Driver) + { + throw new ArgumentException("ScanOptions.Device.Driver must match ScanOptions.Driver"); + } + } + options.Driver = ValidateDriver(options.Device?.Driver ?? options.Driver); if (options.Driver == Driver.Sane) { options.UseNativeUI = false; @@ -58,21 +66,40 @@ public ScanOptions ValidateAll(ScanOptions options, ScanningContext scanningCont throw new ArgumentException("OCR is enabled but no OCR engine is set on ScanningContext."); } - if (string.IsNullOrEmpty(options.NetworkOptions.Ip) != (options.NetworkOptions.Port == null)) - { - throw new ArgumentException("NetworkOptions.Ip and .Port must both be either set or unset."); - } - // TODO: Do we need to validate the presence of a device? // TODO: Probably more things as well. return options; } - public Driver ValidateDriver(Driver driver) => - driver == Driver.Default - ? SystemDefaultDriver - : driver; + public Driver ValidateDriver(Driver driver) + { + if (driver == Driver.Default) + { + return SystemDefaultDriver; + } + if (driver == Driver.Wia && !PlatformCompat.System.IsWiaDriverSupported || + driver == Driver.Twain && !PlatformCompat.System.IsTwainDriverSupported || + driver == Driver.Escl && !PlatformCompat.System.IsEsclDriverSupported || + driver == Driver.Sane && !PlatformCompat.System.IsSaneDriverSupported || + driver == Driver.Apple && !PlatformCompat.System.IsAppleDriverSupported) + { + throw new DriverNotSupportedException($"The \"{driver}\" driver is not supported on this platform."); + } + return driver; + } + + public static IEnumerable SupportedDrivers + { + get + { + if (PlatformCompat.System.IsWiaDriverSupported) yield return Driver.Wia; + if (PlatformCompat.System.IsTwainDriverSupported) yield return Driver.Twain; + if (PlatformCompat.System.IsEsclDriverSupported) yield return Driver.Escl; + if (PlatformCompat.System.IsSaneDriverSupported) yield return Driver.Sane; + if (PlatformCompat.System.IsAppleDriverSupported) yield return Driver.Apple; + } + } public static Driver SystemDefaultDriver { @@ -80,8 +107,6 @@ public static Driver SystemDefaultDriver { if (PlatformCompat.System.IsWiaDriverSupported) { - // TODO: Maybe default to TWAIN - // TODO: Also in general "default driver" handling should change return Driver.Wia; } if (PlatformCompat.System.IsAppleDriverSupported) diff --git a/NAPS2.Sdk/Scan/Internal/Twain/DefaultTwainHandleManager.cs b/NAPS2.Sdk/Scan/Internal/Twain/DefaultTwainHandleManager.cs new file mode 100644 index 0000000000..b0f39436b7 --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/Twain/DefaultTwainHandleManager.cs @@ -0,0 +1,65 @@ +#if !MAC +using System.Threading; +using NAPS2.Platform.Windows; +using NTwain; + +namespace NAPS2.Scan.Internal.Twain; + +/// +/// TwainHandleManager implementation that lazily starts a Win32MessagePump and delegates to a Win32TwainHandleManager. +/// +[System.Runtime.Versioning.SupportedOSPlatform("windows")] +internal class DefaultTwainHandleManager : TwainHandleManager +{ + private Win32MessagePump? _messagePump; + private Win32TwainHandleManager? _inner; + + private Win32TwainHandleManager CreateInner() + { + var mre = new ManualResetEvent(false); + var messageThread = new Thread(() => + { + _messagePump = Win32MessagePump.Create(); + mre.Set(); + _messagePump.RunMessageLoop(); + }); + messageThread.SetApartmentState(ApartmentState.STA); + messageThread.Start(); + mre.WaitOne(); + return new Win32TwainHandleManager(_messagePump!); + } + + public override IntPtr GetDsmHandle(IntPtr dialogParent, bool useNativeUi) + { + _inner ??= CreateInner(); + return _inner.GetDsmHandle(dialogParent, useNativeUi); + } + + public override IntPtr GetEnableHandle(IntPtr dialogParent, bool useNativeUi) + { + _inner ??= CreateInner(); + return _inner.GetEnableHandle(dialogParent, useNativeUi); + } + + public override MessageLoopHook CreateMessageLoopHook(IntPtr dialogParent = default, bool useNativeUi = false) + { + _inner ??= CreateInner(); + return _inner.CreateMessageLoopHook(dialogParent, useNativeUi); + } + + public override IInvoker Invoker + { + get + { + _inner ??= CreateInner(); + return _inner.Invoker; + } + } + + public override void Dispose() + { + _inner?.Dispose(); + _messagePump?.Dispose(); + } +} +#endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/ITwainSessionController.cs b/NAPS2.Sdk/Scan/Internal/Twain/ITwainController.cs similarity index 82% rename from NAPS2.Sdk/Scan/Internal/Twain/ITwainSessionController.cs rename to NAPS2.Sdk/Scan/Internal/Twain/ITwainController.cs index 0331a2c61e..ad02520a47 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/ITwainSessionController.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/ITwainController.cs @@ -6,8 +6,9 @@ namespace NAPS2.Scan.Internal.Twain; /// Interface for interacting with a Twain session that might happen in the current (local) process or a worker (remote) /// process. /// -public interface ITwainSessionController +internal interface ITwainController { Task> GetDeviceList(ScanOptions options); + Task GetCaps(ScanOptions options); Task StartScan(ScanOptions options, ITwainEvents twainEvents, CancellationToken cancelToken); } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/ITwainEvents.cs b/NAPS2.Sdk/Scan/Internal/Twain/ITwainEvents.cs index 809b5e0e29..e5f2b47002 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/ITwainEvents.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/ITwainEvents.cs @@ -2,11 +2,13 @@ namespace NAPS2.Scan.Internal.Twain; -public interface ITwainEvents +internal interface ITwainEvents { void PageStart(TwainPageStart pageStart); void NativeImageTransferred(TwainNativeImage nativeImage); void MemoryBufferTransferred(TwainMemoryBuffer memoryBuffer); + + void TransferCanceled(TwainTransferCanceled transferCanceled); } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/LegacyTwainScanDriver.cs b/NAPS2.Sdk/Scan/Internal/Twain/LegacyTwainScanDriver.cs deleted file mode 100644 index 1a3e1f398a..0000000000 --- a/NAPS2.Sdk/Scan/Internal/Twain/LegacyTwainScanDriver.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Threading; - -namespace NAPS2.Scan.Internal.Twain; - -internal class LegacyTwainScanDriver : IScanDriver -{ - public Task> GetDeviceList(ScanOptions options) => throw new NotImplementedException(); - - public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/LocalTwainController.cs b/NAPS2.Sdk/Scan/Internal/Twain/LocalTwainController.cs new file mode 100644 index 0000000000..3fcebd5a3c --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/Twain/LocalTwainController.cs @@ -0,0 +1,244 @@ +#if !MAC +using System.Collections.Immutable; +using System.Threading; +using Microsoft.Extensions.Logging; +using NAPS2.Scan.Exceptions; +using NAPS2.Unmanaged; +using NTwain; +using NTwain.Data; + +namespace NAPS2.Scan.Internal.Twain; + +/// +/// Real implementation of ITwainController that interacts with a Twain session in the current process. +/// +internal class LocalTwainController : ITwainController +{ + public static readonly TWIdentity TwainAppId; + + static LocalTwainController() + { + PlatformInfo.Current.Log.IsDebugEnabled = true; + TwainAppId = TWIdentity.Create(DataGroups.Image | DataGroups.Control, AssemblyHelper.Version, + AssemblyHelper.Company, AssemblyHelper.Product, AssemblyHelper.Product, AssemblyHelper.Description); + } + + private static readonly Once TwainDsmSetup = new(() => + { + var twainDsmPath = NativeLibrary.FindLibraryPath("twaindsm.dll"); + PlatformCompat.System.LoadLibrary(twainDsmPath); + PlatformInfo.Current.NewDsmPath = twainDsmPath; + }); + + private readonly ILogger _logger; + + public LocalTwainController(ScanningContext scanningContext) + { + _logger = scanningContext.Logger; + } + + public Task> GetDeviceList(ScanOptions options) + { + if (options.TwainOptions.Dsm != TwainDsm.Old) + { + TwainDsmSetup.Run(); + } + return Task.Run(() => + { + var deviceList = InternalGetDeviceList(options); + if (options.TwainOptions.Dsm != TwainDsm.Old && deviceList.Count == 0) + { + // Fall back to OldDsm in case of no devices + // This is primarily for Citrix support, which requires using twain_32.dll for TWAIN passthrough + deviceList = InternalGetDeviceList(options); + } + + return deviceList; + }); + } + + private List InternalGetDeviceList(ScanOptions options) + { + PlatformInfo.Current.PreferNewDSM = options.TwainOptions.Dsm != TwainDsm.Old; + var session = new TwainSession(TwainAppId); + using var handleManager = TwainHandleManager.Factory(); + session.Open(handleManager.CreateMessageLoopHook()); + try + { + return session.GetSources().Select(ds => new ScanDevice(Driver.Twain, ds.Name, ds.Name)).ToList(); + } + finally + { + try + { + session.Close(); + } + catch (Exception e) + { + _logger.LogError(e, "Error closing TWAIN session"); + } + } + } + + public Task GetCaps(ScanOptions options) + { + if (options.TwainOptions.Dsm != TwainDsm.Old) + { + TwainDsmSetup.Run(); + } + return Task.Run(() => + { + var caps = InternalGetCaps(options); + if (options.TwainOptions.Dsm != TwainDsm.Old && caps == null) + { + // Fall back to OldDsm in case of no devices + // This is primarily for Citrix support, which requires using twain_32.dll for TWAIN passthrough + caps = InternalGetCaps(options); + } + + return caps; + }); + } + + private ScanCaps InternalGetCaps(ScanOptions options) + { + PlatformInfo.Current.PreferNewDSM = options.TwainOptions.Dsm != TwainDsm.Old; + var session = new TwainSession(TwainAppId); + using var handleManager = TwainHandleManager.Factory(); + session.Open(handleManager.CreateMessageLoopHook()); + try + { + var ds = session.GetSources().FirstOrDefault(ds => ds.Name == options.Device!.ID); + if (ds == null) throw new DeviceNotFoundException(); + try + { + var rc = ds.Open(); + if (rc != ReturnCode.Success) + { + _logger.LogDebug("Couldn't open TWAIN data source for capabilities, return code {RC}", rc); + return new ScanCaps(); + } + try + { + var feederCap = ds.Capabilities.CapFeederEnabled; + + feederCap.SetValue(BoolType.False); + bool supportsFlatbed = feederCap.GetCurrent() == BoolType.False; + var flatbedCaps = supportsFlatbed ? GetPerSourceCaps(ds) : null; + + feederCap.SetValue(BoolType.True); + bool supportsFeeder = feederCap.GetCurrent() == BoolType.True; + var feederCaps = supportsFeeder ? GetPerSourceCaps(ds) : null; + + bool supportsDuplex = supportsFeeder && ds.Capabilities.CapDuplex.GetCurrent() != Duplex.None; + + return new ScanCaps + { + MetadataCaps = new MetadataCaps + { + Manufacturer = ds.Manufacturer, + Model = ds.Name, + SerialNumber = ds.Capabilities.CapSerialNumber.GetCurrent() + }, + PaperSourceCaps = new PaperSourceCaps + { + SupportsFlatbed = supportsFlatbed, + SupportsFeeder = supportsFeeder, + SupportsDuplex = supportsDuplex, + CanCheckIfFeederHasPaper = + ds.Capabilities.CapAutomaticSenseMedium.IsSupported || + ds.Capabilities.CapFeederLoaded.IsSupported + }, + FlatbedCaps = flatbedCaps, + FeederCaps = feederCaps, + DuplexCaps = supportsDuplex ? feederCaps : null + }; + } + finally + { + ds.Close(); + } + } + catch (Exception e) + { + _logger.LogError(e, "Error getting TWAIN capabilities"); + if (e is DeviceException) + { + throw; + } + throw new ScanDriverUnknownException(e); + } + } + finally + { + try + { + session.Close(); + } + catch (Exception e) + { + _logger.LogError(e, "Error closing TWAIN session for capabilities"); + } + } + } + + private PerSourceCaps GetPerSourceCaps(DataSource ds) + { + var xRes = ds.Capabilities.ICapXResolution.GetValues().Select(x => (int) x.Whole); + var yRes = ds.Capabilities.ICapYResolution.GetValues().Select(x => (int) x.Whole); + var dpiCaps = new DpiCaps { Values = xRes.Intersect(yRes).ToImmutableList() }; + var pixelTypes = ds.Capabilities.ICapPixelType.GetValues().ToList(); + var bitDepthCaps = new BitDepthCaps + { + SupportsColor = pixelTypes.Contains(PixelType.RGB), + SupportsGrayscale = pixelTypes.Contains(PixelType.Gray), + SupportsBlackAndWhite = pixelTypes.Contains(PixelType.BlackWhite) + }; + var w = ds.Capabilities.ICapPhysicalWidth.GetCurrent(); + var h = ds.Capabilities.ICapPhysicalHeight.GetCurrent(); + var scanArea = new PageSize( + decimal.Round(w.Whole + w.Fraction / 65536m, 4), + decimal.Round(h.Whole + h.Fraction / 65536m, 4), + PageSizeUnit.Inch); + var pageSizeCaps = new PageSizeCaps { ScanArea = scanArea }; + return new PerSourceCaps + { + DpiCaps = dpiCaps, + BitDepthCaps = bitDepthCaps, + PageSizeCaps = pageSizeCaps + }; + } + + public async Task StartScan(ScanOptions options, ITwainEvents twainEvents, CancellationToken cancelToken) + { + if (options.TwainOptions.Dsm != TwainDsm.Old) + { + TwainDsmSetup.Run(); + } + try + { + await InternalScan(options.TwainOptions.Dsm, options, cancelToken, twainEvents); + } + catch (DeviceNotFoundException) + { + if (options.TwainOptions.Dsm != TwainDsm.Old) + { + // Fall back to OldDsm in case of no devices + // This is primarily for Citrix support, which requires using twain_32.dll for TWAIN passthrough + await InternalScan(TwainDsm.Old, options, cancelToken, twainEvents); + } + else + { + throw; + } + } + } + + private async Task InternalScan(TwainDsm dsm, ScanOptions options, CancellationToken cancelToken, + ITwainEvents twainEvents) + { + var runner = new TwainScanRunner(_logger, TwainAppId, dsm, options, cancelToken, twainEvents); + await runner.Run(); + } +} +#endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/LocalTwainSessionController.cs b/NAPS2.Sdk/Scan/Internal/Twain/LocalTwainSessionController.cs deleted file mode 100644 index 0fe0416ee7..0000000000 --- a/NAPS2.Sdk/Scan/Internal/Twain/LocalTwainSessionController.cs +++ /dev/null @@ -1,82 +0,0 @@ -#if !MAC -using System.Threading; -using NAPS2.Scan.Exceptions; -using NTwain; - -namespace NAPS2.Scan.Internal.Twain; - -/// -/// Real implementation of ITwainSessionController that interacts with a Twain session in the current process. -/// -public class LocalTwainSessionController : ITwainSessionController -{ - public Task> GetDeviceList(ScanOptions options) - { - return Task.Run(() => - { - var deviceList = InternalGetDeviceList(options); - if (options.TwainOptions.Dsm != TwainDsm.Old && deviceList.Count == 0) - { - // Fall back to OldDsm in case of no devices - // This is primarily for Citrix support, which requires using twain_32.dll for TWAIN passthrough - deviceList = InternalGetDeviceList(options); - } - - return deviceList; - }); - } - - private static List InternalGetDeviceList(ScanOptions options) - { - PlatformInfo.Current.PreferNewDSM = options.TwainOptions.Dsm != TwainDsm.Old; - var session = new TwainSession(TwainScanDriver.TwainAppId); - session.Open(); - try - { - return session.GetSources().Select(ds => new ScanDevice(ds.Name, ds.Name)).ToList(); - } - finally - { - try - { - session.Close(); - } - catch (Exception e) - { - Log.ErrorException("Error closing TWAIN session", e); - } - } - } - - public async Task StartScan(ScanOptions options, ITwainEvents twainEvents, CancellationToken cancelToken) - { - // TODO: An error in NTwain doesn't seem to be logged or propagated back to the parent process correctly - // TODO: Specifically, in TwainSessionRunner.Init - // TODO: Cancelling twain shows a cancellation error - // TODO: There seems to be some issue with the UI getting locked; probably event-loop related, inconsistent. Unsure if related to the new NTWAIN or the new implementation or what. - try - { - await InternalScan(options.TwainOptions.Dsm, options, cancelToken, twainEvents); - } - catch (DeviceNotFoundException) - { - if (options.TwainOptions.Dsm != TwainDsm.Old) - { - // Fall back to OldDsm in case of no devices - // This is primarily for Citrix support, which requires using twain_32.dll for TWAIN passthrough - await InternalScan(TwainDsm.Old, options, cancelToken, twainEvents); - } - else - { - throw; - } - } - } - - private async Task InternalScan(TwainDsm dsm, ScanOptions options, CancellationToken cancelToken, ITwainEvents twainEvents) - { - var runner = new TwainSessionScanRunner(dsm, options, cancelToken, twainEvents); - await runner.Run(); - } -} -#endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/RemoteTwainController.cs b/NAPS2.Sdk/Scan/Internal/Twain/RemoteTwainController.cs new file mode 100644 index 0000000000..6d997278a8 --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/Twain/RemoteTwainController.cs @@ -0,0 +1,58 @@ +using System.Threading; +using Microsoft.Extensions.Logging; +using NAPS2.Remoting.Worker; + +namespace NAPS2.Scan.Internal.Twain; + +/// +/// Proxy implementation of ITwainController that interacts with a Twain session in a worker process. +/// +internal class RemoteTwainController : ITwainController +{ + private readonly ScanningContext _scanningContext; + + public RemoteTwainController(ScanningContext scanningContext) + { + _scanningContext = scanningContext; + } + + public async Task> GetDeviceList(ScanOptions options) + { + using var workerContext = CreateWorker(options); + return await workerContext.Service.TwainGetDeviceList(options); + } + + public async Task GetCaps(ScanOptions options) + { + using var workerContext = CreateWorker(options); + return await workerContext.Service.TwainGetCaps(options); + } + + public async Task StartScan(ScanOptions options, ITwainEvents twainEvents, CancellationToken cancelToken) + { + using var workerContext = CreateWorker(options); + await workerContext.Service.TwainScan(options, cancelToken, twainEvents); + if (cancelToken.IsCancellationRequested) + { + // We need to report cancellation so that TwainImageProcessor doesn't return a partial image + _scanningContext.Logger.LogDebug("NAPS2.TW - Sending cancel event"); + twainEvents.TransferCanceled(new TwainTransferCanceled()); + // We also want to wait until the worker closes so we guarantee the parent process doesn't die before the + // TWAIN process has a chance to clean up + await workerContext.Stop(); + } + } + + private WorkerContext CreateWorker(ScanOptions options) + { + if (_scanningContext.WorkerFactory == null) + { + // Shouldn't hit this case + throw new InvalidOperationException(); + } + return _scanningContext.CreateWorker( + options.TwainOptions.Dsm == TwainDsm.NewX64 || !PlatformCompat.System.SupportsWinX86Worker + ? WorkerType.Native + : WorkerType.WinX86)!; + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/RemoteTwainSessionController.cs b/NAPS2.Sdk/Scan/Internal/Twain/RemoteTwainSessionController.cs deleted file mode 100644 index d12c6ff284..0000000000 --- a/NAPS2.Sdk/Scan/Internal/Twain/RemoteTwainSessionController.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Threading; -using NAPS2.Platform.Windows; - -namespace NAPS2.Scan.Internal.Twain; - -/// -/// Proxy implementation of ITwainSessionController that interacts with a Twain session in a worker process. -/// -public class RemoteTwainSessionController : ITwainSessionController -{ - private readonly ScanningContext _scanningContext; - - public RemoteTwainSessionController(ScanningContext scanningContext) - { - _scanningContext = scanningContext; - } - - public async Task> GetDeviceList(ScanOptions options) - { - if (_scanningContext.WorkerFactory == null) - { - throw new InvalidOperationException( - "ScanningContext.WorkerFactory must be set to use TWAIN from a 64-bit process."); - } - using var workerContext = _scanningContext.WorkerFactory.Create(); - return await workerContext.Service.TwainGetDeviceList(options); - } - - public async Task StartScan(ScanOptions options, ITwainEvents twainEvents, CancellationToken cancelToken) - { - if (_scanningContext.WorkerFactory == null) - { - throw new InvalidOperationException( - "ScanningContext.WorkerFactory must be set to use TWAIN from a 64-bit process."); - } - using var workerContext = _scanningContext.WorkerFactory.Create(); - try - { - await workerContext.Service.TwainScan(options, cancelToken, twainEvents); - } - finally - { - EnableWindow(options.DialogParent); - } - } - - private void EnableWindow(IntPtr dialogParent) - { - if (dialogParent != IntPtr.Zero) - { - // At the Windows API level, a modal window is implemented by doing two things: - // 1. Setting the parent on the child window - // 2. Disabling the parent window - // The worker is supposed to re-enable the window before returning, but in case the process dies or - // some other problem occurs, here we make sure that happens. - Win32.EnableWindow(dialogParent, true); - } - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/StubTwainSessionController.cs b/NAPS2.Sdk/Scan/Internal/Twain/StubTwainController.cs similarity index 61% rename from NAPS2.Sdk/Scan/Internal/Twain/StubTwainSessionController.cs rename to NAPS2.Sdk/Scan/Internal/Twain/StubTwainController.cs index 326db77d2c..cefca0b188 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/StubTwainSessionController.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/StubTwainController.cs @@ -3,15 +3,20 @@ namespace NAPS2.Scan.Internal.Twain; /// -/// Stub implementation of ITwainSessionController for unsupported platforms. +/// Stub implementation of ITwainController for unsupported platforms. /// -public class StubTwainSessionController : ITwainSessionController +internal class StubTwainController : ITwainController { public Task> GetDeviceList(ScanOptions options) { throw new NotSupportedException(); } + public Task GetCaps(ScanOptions options) + { + throw new NotSupportedException(); + } + public Task StartScan(ScanOptions options, ITwainEvents twainEvents, CancellationToken cancelToken) { throw new NotSupportedException(); diff --git a/NAPS2.Sdk/Scan/Internal/Twain/TwainEvents.cs b/NAPS2.Sdk/Scan/Internal/Twain/TwainEvents.cs index 2122a80296..97bc03c2ff 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/TwainEvents.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/TwainEvents.cs @@ -2,18 +2,20 @@ namespace NAPS2.Scan.Internal.Twain; -class TwainEvents : ITwainEvents +internal class TwainEvents : ITwainEvents { private readonly Action _pageStartCallback; private readonly Action _nativeImageCallback; private readonly Action _memoryBufferCallback; + private readonly Action _transferCanceledCallback; public TwainEvents(Action pageStartCallback, Action nativeImageCallback, - Action memoryBufferCallback) + Action memoryBufferCallback, Action transferCanceledCallback) { _pageStartCallback = pageStartCallback; _nativeImageCallback = nativeImageCallback; _memoryBufferCallback = memoryBufferCallback; + _transferCanceledCallback = transferCanceledCallback; } public void PageStart(TwainPageStart pageStart) @@ -30,4 +32,9 @@ public void MemoryBufferTransferred(TwainMemoryBuffer memoryBuffer) { _memoryBufferCallback(memoryBuffer); } + + public void TransferCanceled(TwainTransferCanceled transferCanceled) + { + _transferCanceledCallback(transferCanceled); + } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/TwainHandleManager.cs b/NAPS2.Sdk/Scan/Internal/Twain/TwainHandleManager.cs index fd51d1c6c9..61836a5072 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/TwainHandleManager.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/TwainHandleManager.cs @@ -1,24 +1,33 @@ +#if !MAC +using NTwain; + namespace NAPS2.Scan.Internal.Twain; -public class TwainHandleManager : IDisposable +/// +/// Abstracts how HWND handles are obtained for use with TWAIN. +/// +internal abstract class TwainHandleManager : IDisposable { - public static Func Factory { get; set; } = () => new TwainHandleManager(); + public static Func Factory { get; set; } = () => + { +#if NET6_0_OR_GREATER + if (!OperatingSystem.IsWindows()) throw new NotSupportedException(); +#endif + return new DefaultTwainHandleManager(); + }; protected TwainHandleManager() { } - public virtual IntPtr GetDsmHandle(IntPtr dialogParent, bool useNativeUi) - { - return dialogParent; - } + public abstract IntPtr GetDsmHandle(IntPtr dialogParent, bool useNativeUi); - public virtual IntPtr GetEnableHandle(IntPtr dialogParent, bool useNativeUi) - { - return dialogParent; - } + public abstract IntPtr GetEnableHandle(IntPtr dialogParent, bool useNativeUi); - public virtual void Dispose() - { - } -} \ No newline at end of file + public abstract MessageLoopHook CreateMessageLoopHook(IntPtr dialogParent = default, bool useNativeUi = false); + + public abstract IInvoker Invoker { get; } + + public abstract void Dispose(); +} +#endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/TwainImageProcessor.cs b/NAPS2.Sdk/Scan/Internal/Twain/TwainImageProcessor.cs index f84b2eb5a8..4f864129bc 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/TwainImageProcessor.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/TwainImageProcessor.cs @@ -1,4 +1,6 @@ #if !MAC +using Microsoft.Extensions.Logging; +using NAPS2.Images.Bitwise; using NAPS2.Remoting.Worker; namespace NAPS2.Scan.Internal.Twain; @@ -10,9 +12,12 @@ namespace NAPS2.Scan.Internal.Twain; internal class TwainImageProcessor : ITwainEvents, IDisposable { private readonly ScanningContext _scanningContext; + private readonly ILogger _logger; private readonly Action _callback; private TwainImageData? _currentImageData; - private IMemoryImage? _currentMemoryImage; + private IMemoryImage? _currentImage; + private int _transferredWidth; + private int _transferredHeight; private long _transferredPixels; private long _totalPixels; private readonly TwainProgressEstimator _progressEstimator; @@ -21,15 +26,19 @@ public TwainImageProcessor(ScanningContext scanningContext, ScanOptions options, Action callback) { _scanningContext = scanningContext; + _logger = scanningContext.Logger; _callback = callback; _progressEstimator = new TwainProgressEstimator(options, scanEvents); } public void PageStart(TwainPageStart pageStart) { + Flush(); _currentImageData = pageStart.ImageData; - _currentMemoryImage?.Dispose(); - _currentMemoryImage = null; + _currentImage?.Dispose(); + _currentImage = null; + _transferredWidth = 0; + _transferredHeight = 0; _transferredPixels = 0; _totalPixels = _currentImageData == null ? 0 : _currentImageData.Width * (long) _currentImageData.Height; _progressEstimator.MarkStart(_totalPixels); @@ -48,29 +57,94 @@ public void MemoryBufferTransferred(TwainMemoryBuffer memoryBuffer) { throw new InvalidOperationException(); } + if (memoryBuffer.Columns == 0 && memoryBuffer.BytesPerRow > 0) + { + // Workaround for bug with Kyocera drivers where Columns is unspecified + memoryBuffer.Columns = memoryBuffer.BytesPerRow * 8 / _currentImageData.BitsPerPixel; + _logger.LogDebug( + "NAPS2.TW - Correcting memory buffer columns to {w} based on bytes/row {bpr}, bits/pixel {bpp}", + memoryBuffer.Columns, memoryBuffer.BytesPerRow, _currentImageData.BitsPerPixel); + } + if (memoryBuffer.Columns <= 0 || memoryBuffer.Rows <= 0 || memoryBuffer.BytesPerRow <= 0) + { + var b = memoryBuffer; + _logger.LogError( + "NAPS2.TW - Invalid memory buffer: w {w}, h {h}, bpr {bpr}, len {len}, x {x}, y {y}", + b.Columns, b.Rows, b.BytesPerRow, b.Buffer.Length, b.XOffset, b.YOffset); + return; + } + + _transferredPixels += memoryBuffer.Columns * (long) memoryBuffer.Rows; + _transferredWidth = Math.Max(_transferredWidth, memoryBuffer.Columns + memoryBuffer.XOffset); + _transferredHeight = Math.Max(_transferredHeight, memoryBuffer.Rows + memoryBuffer.YOffset); var pixelFormat = _currentImageData.BitsPerPixel == 1 ? ImagePixelFormat.BW1 : ImagePixelFormat.RGB24; - _currentMemoryImage ??= _scanningContext.ImageContext.Create( - _currentImageData.Width, _currentImageData.Height, pixelFormat); - _currentMemoryImage.SetResolution((float) _currentImageData.XRes, (float) _currentImageData.YRes); + _currentImage ??= _scanningContext.ImageContext.Create( + Math.Max(_currentImageData.Width, _transferredWidth), + Math.Max(_currentImageData.Height, _transferredHeight), + pixelFormat); + _currentImage.SetResolution((float) _currentImageData.XRes, (float) _currentImageData.YRes); - _transferredPixels += memoryBuffer.Columns * (long) memoryBuffer.Rows; + // In case the real image dimensions don't match the specified image dimensions, we may need to get more memory. + // The image will be realloc'd to the real size once we're done and know what that is. + if (_transferredWidth > _currentImage.Width) + { + ReallocImage(Math.Max(_currentImage.Width * 2, _transferredWidth), _currentImage.Height); + } + if (_transferredHeight > _currentImage.Height) + { + ReallocImage(_currentImage.Width, Math.Max(_currentImage.Height * 2, _transferredHeight)); + } - TwainMemoryBufferReader.CopyBufferToImage(memoryBuffer, _currentImageData, _currentMemoryImage); - _progressEstimator.MarkProgress(_transferredPixels, _totalPixels); + TwainMemoryBufferReader.CopyBufferToImage(memoryBuffer, _currentImageData, _currentImage); + _progressEstimator.MarkProgress(Math.Min(_transferredPixels, _totalPixels), _totalPixels); + } - if (_transferredPixels == _totalPixels) + private void ReallocImage(int width, int height) + { + _logger.LogDebug($"NAPS2.TW - Realloc image {_currentImage!.Width}x{_currentImage.Height} -> {width}x{height}"); + var copy = _scanningContext.ImageContext.Create(width, height, _currentImage!.PixelFormat); + new CopyBitwiseImageOp { + Columns = Math.Min(width, _currentImage.Width), + Rows = Math.Min(height, _currentImage.Height) + }.Perform(_currentImage, copy); + _currentImage.Dispose(); + _currentImage = copy; + } + + public void TransferCanceled(TwainTransferCanceled transferCanceled) + { + _currentImage?.Dispose(); + _currentImage = null; + } + + public void Flush() + { + if (_currentImage != null && _transferredWidth > 0 && _transferredHeight > 0) + { + if (_transferredWidth != _currentImage.Width || _transferredHeight != _currentImage.Height) + { + // The real image dimensions don't match the specified image dimensions, so we have to realloc. + ReallocImage(_transferredWidth, _transferredHeight); + } _progressEstimator.MarkCompletion(); - // TODO: Throw an error if there's a pixel mismatch, i.e. we go to the next page / finish with too few, or have too many - _callback(_currentMemoryImage); - _currentMemoryImage = null; + _callback(_currentImage); + _currentImage = null; } } public void Dispose() { - _currentMemoryImage?.Dispose(); + if (_currentImage != null && _transferredPixels == _totalPixels && + _transferredWidth == _currentImageData?.Width && _transferredHeight == _currentImageData?.Height) + { + // If we have an error after a successful scan (so Flush isn't called normally) we still want to flush. + // Obviously this won't work if the image dimensions are off (as we can't tell if the scan is complete or + // not) but that should be a rare case. + Flush(); + } + _currentImage?.Dispose(); } } #endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/TwainMemoryBufferReader.cs b/NAPS2.Sdk/Scan/Internal/Twain/TwainMemoryBufferReader.cs index ee247693a9..0b866d4fce 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/TwainMemoryBufferReader.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/TwainMemoryBufferReader.cs @@ -1,26 +1,31 @@ #if !MAC using NAPS2.Images.Bitwise; using NAPS2.Remoting.Worker; -using NTwain.Data; namespace NAPS2.Scan.Internal.Twain; /// /// For Twain MemXfer, this class reads the raw buffer data and copies it into an image object. /// -public static class TwainMemoryBufferReader +internal static class TwainMemoryBufferReader { + private const int BLACK_WHITE = 0; + private const int GRAY = 1; + private const int RGB = 2; + public static void CopyBufferToImage(TwainMemoryBuffer memoryBuffer, TwainImageData imageData, IMemoryImage outputImage) { - var subPixelType = ((PixelType) imageData.PixelType, imageData.BitsPerPixel, imageData.SamplesPerPixel) switch - { - (PixelType.RGB, 24, 3) when CheckBitsPerSample(imageData, 8, 8, 8) => SubPixelType.Rgb, - (PixelType.Gray, 8, 1) when CheckBitsPerSample(imageData, 8) => SubPixelType.Gray, - (PixelType.BlackWhite, 1, 1) when CheckBitsPerSample(imageData, 1) => SubPixelType.Bit, - _ => throw new ArgumentException( - $"Unsupported pixel type: {imageData.BitsPerPixel} {imageData.PixelType} {imageData.SamplesPerPixel} {string.Join(",", imageData.BitsPerSample)}") - }; + var subPixelType = (imageData.PixelType, imageData.BitsPerPixel, imageData.SamplesPerPixel, + imageData.BitsPerSample) switch + { + // Technically for RGB we should check for [8, 8, 8, ...] but some scanners only set the first value + (RGB, 24, 3, [8, ..]) => SubPixelType.Rgb, + (GRAY, 8, 1, [8, ..]) => SubPixelType.Gray, + (BLACK_WHITE, 1, 1, [1, ..]) => SubPixelType.Bit, + _ => throw new ArgumentException( + $"Unsupported pixel type: {imageData.BitsPerPixel} {imageData.PixelType} {imageData.SamplesPerPixel} {string.Join(",", imageData.BitsPerSample)}") + }; var pixelInfo = new PixelInfo(memoryBuffer.Columns, memoryBuffer.Rows, subPixelType, memoryBuffer.BytesPerRow); new CopyBitwiseImageOp { @@ -28,17 +33,5 @@ public static void CopyBufferToImage(TwainMemoryBuffer memoryBuffer, TwainImageD DestYOffset = memoryBuffer.YOffset }.Perform(memoryBuffer.Buffer.ToByteArray(), pixelInfo, outputImage); } - - private static bool CheckBitsPerSample(TwainImageData imageData, params int[] expected) - { - for (int i = 0; i < expected.Length; i++) - { - if (imageData.BitsPerSample[i] != expected[i]) - { - return false; - } - } - return true; - } } #endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/TwainProgressEstimator.cs b/NAPS2.Sdk/Scan/Internal/Twain/TwainProgressEstimator.cs index 74a53c5ee8..42dc5cc15e 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/TwainProgressEstimator.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/TwainProgressEstimator.cs @@ -29,7 +29,7 @@ public static bool HasTimingInfo(ScanOptions options) private static TimingKey GetTimingKey(ScanOptions options) { - return new TimingKey(options.Device!.ID!, options.BitDepth, options.PageSize!); + return new TimingKey(options.Device?.ID, options.BitDepth, options.Dpi, options.PageSize); } public TwainProgressEstimator(ScanOptions options, IScanEvents scanEvents) @@ -111,7 +111,7 @@ public void MarkProgress(long transferredPixels, long totalPixels) _pixelsAtFirstBuffer = transferredPixels; } var progress = transferredPixels / (double) totalPixels; - if (_previousTimingInfo != null) + if (_previousTimingInfo != null && _previousTimingInfo.TotalMillis > 0) { var overheadDone = _previousTimingInfo.OverheadMillis / (double) _previousTimingInfo.TotalMillis; var nonOverheadRatio = (_previousTimingInfo.TotalMillis - _previousTimingInfo.OverheadMillis) / @@ -145,7 +145,7 @@ public void Add(TimingKey key, TimingInfo value) private readonly Dictionary _cache = new(); } - public record TimingKey(string DeviceId, BitDepth BitDepth, PageSize PageSize); + public record TimingKey(string? DeviceId, BitDepth BitDepth, int Dpi, PageSize? PageSize); public record TimingInfo(long OverheadMillis, long TotalMillis); } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/TwainScanDriver.cs b/NAPS2.Sdk/Scan/Internal/Twain/TwainScanDriver.cs index 22e3ac1de1..1157b853d7 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/TwainScanDriver.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/TwainScanDriver.cs @@ -1,63 +1,116 @@ #if !MAC -using System.Reflection; -using System.Runtime.InteropServices; using System.Threading; using NAPS2.Platform.Windows; -using NTwain; -using NTwain.Data; namespace NAPS2.Scan.Internal.Twain; /// -/// Implementation of IScanDriver for Twain. Delegates to RemoteTwainSessionController in most cases which runs Twain +/// Implementation of IScanDriver for Twain. Delegates to RemoteTwainController in most cases which runs Twain /// in a 32-bit worker process as Twain drivers are generally 32-bit only. /// internal class TwainScanDriver : IScanDriver { - public static readonly TWIdentity TwainAppId = - TWIdentity.CreateFromAssembly(DataGroups.Image | DataGroups.Control, Assembly.GetEntryAssembly()); + private readonly ScanningContext _scanningContext; - static TwainScanDriver() + public TwainScanDriver(ScanningContext scanningContext) { - // Path to the folder containing the 64-bit twaindsm.dll relative to NAPS2.Core.dll - if (PlatformCompat.System.CanUseWin32) + _scanningContext = scanningContext; + } + + public Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback) + { + CheckArch(options); + return Task.Run(async () => { - string libDir = Environment.Is64BitProcess ? "_win64" : "_win32"; - Win32.SetDllDirectory(Path.Combine(AssemblyHelper.LibFolder, libDir)); - } -#if DEBUG - PlatformInfo.Current.Log.IsDebugEnabled = true; -#endif + var controller = GetTwainController(options); + foreach (var device in await controller.GetDeviceList(options)) + { + callback(device); + } + }); } - private readonly ScanningContext _scanningContext; + public Task GetCaps(ScanOptions options, CancellationToken cancelToken) + { + CheckArch(options); + return Task.Run(async () => + { + var controller = GetTwainController(options); + return await controller.GetCaps(options); + }); + } - public TwainScanDriver(ScanningContext scanningContext) + public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, + Action callback) { - _scanningContext = scanningContext; + CheckArch(options); + return Task.Run(async () => + { + var controller = GetTwainController(options); + using var state = new TwainImageProcessor(_scanningContext, options, scanEvents, callback); + try + { + await controller.StartScan(options, state, cancelToken); + } + finally + { + EnableWindow(options); + } + state.Flush(); + }); } - public async Task> GetDeviceList(ScanOptions options) + private void EnableWindow(ScanOptions options) { - var controller = GetSessionController(options); - return await controller.GetDeviceList(options); + if (options.DialogParent != IntPtr.Zero && (options.UseNativeUI || options.TwainOptions.ShowProgress)) + { + // At the Windows API level, a modal window is implemented by doing two things: + // 1. Setting the parent on the child window + // 2. Disabling the parent window + // The worker is supposed to re-enable the window before returning, but in case the process dies or + // some other problem occurs, here we make sure that happens. + Win32.EnableWindow(options.DialogParent, true); + // We also want to make sure the main NAPS2 window is in the foreground + Win32.SetForegroundWindow(options.DialogParent); + } } - public async Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, - Action callback) + private void CheckArch(ScanOptions options) { - var controller = GetSessionController(options); - using var state = new TwainImageProcessor(_scanningContext, options, scanEvents, callback); - await controller.StartScan(options, state, cancelToken); + if (_scanningContext.WorkerFactory != null) + { + // Arch doesn't matter if we can run in a worker process of the correct arch. + return; + } + if (!PlatformCompat.System.SupportsWinX86Worker) + { + // No need for an x86 worker (i.e. if we're on arm64) + return; + } + var dsm = options.TwainOptions.Dsm; + if (dsm is TwainDsm.New or TwainDsm.Old && Environment.Is64BitProcess) + { + throw new InvalidOperationException( + "Tried to run TWAIN from a 64-bit process. " + + "If this is intentional, set ScanOptions.TwainOptions.Dsm to TwainDsm.NewX64. " + + "Otherwise you can set up a worker process with ScanningContext.SetUpWin32Worker()."); + } + if (dsm == TwainDsm.NewX64 && !Environment.Is64BitProcess) + { + throw new InvalidOperationException("Tried to run 64-bit TWAIN from a 32-bit process."); + } } - private ITwainSessionController GetSessionController(ScanOptions options) + private ITwainController GetTwainController(ScanOptions options) { - if (options.TwainOptions.Dsm != TwainDsm.NewX64 && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (_scanningContext.WorkerFactory == null) { - return new RemoteTwainSessionController(_scanningContext); + // If we don't have a worker, we assume the configuration has already been validated by CheckArch and will + // run in the current process. + // In general we always prefer to run TWAIN in a worker though. + return new LocalTwainController(_scanningContext); } - return new LocalTwainSessionController(); + return new RemoteTwainController(_scanningContext); } } #endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/TwainSessionScanRunner.cs b/NAPS2.Sdk/Scan/Internal/Twain/TwainScanRunner.cs similarity index 68% rename from NAPS2.Sdk/Scan/Internal/Twain/TwainSessionScanRunner.cs rename to NAPS2.Sdk/Scan/Internal/Twain/TwainScanRunner.cs index e41d423e7b..002efe9ce6 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/TwainSessionScanRunner.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/TwainScanRunner.cs @@ -1,6 +1,7 @@ #if !MAC using System.Threading; using Google.Protobuf; +using Microsoft.Extensions.Logging; using NAPS2.Remoting.Worker; using NAPS2.Scan.Exceptions; using NTwain; @@ -13,8 +14,9 @@ namespace NAPS2.Scan.Internal.Twain; /// ITwainEvents interface. This logic involves quite a bit of complicated state management related to the Twain spec. /// https://twain.org/wp-content/uploads/2015/05/TWAIN-2.3-Specification.pdf /// -internal class TwainSessionScanRunner +internal class TwainScanRunner { + private readonly ILogger _logger; private readonly TwainDsm _dsm; private readonly ScanOptions _options; private readonly CancellationToken _cancelToken; @@ -22,11 +24,13 @@ internal class TwainSessionScanRunner private readonly TwainHandleManager _handleManager; private readonly TwainSession _session; private readonly TaskCompletionSource _tcs; + private readonly TaskCompletionSource _sourceDisabledTcs; private DataSource? _source; - public TwainSessionScanRunner(TwainDsm dsm, ScanOptions options, CancellationToken cancelToken, - ITwainEvents twainEvents) + public TwainScanRunner(ILogger logger, TWIdentity twainAppId, TwainDsm dsm, ScanOptions options, + CancellationToken cancelToken, ITwainEvents twainEvents) { + _logger = logger; _dsm = dsm; _options = options; _cancelToken = cancelToken; @@ -34,19 +38,21 @@ public TwainSessionScanRunner(TwainDsm dsm, ScanOptions options, CancellationTok _handleManager = TwainHandleManager.Factory(); PlatformInfo.Current.PreferNewDSM = dsm != TwainDsm.Old; - Debug.WriteLine($"Using TWAIN DSM: {PlatformInfo.Current.ExpectedDsmPath}"); - _session = new TwainSession(TwainScanDriver.TwainAppId); + _logger.LogDebug($"Using TWAIN DSM: {PlatformInfo.Current.ExpectedDsmPath}"); + _session = new TwainSession(twainAppId); _session.TransferReady += TransferReady; _session.DataTransferred += DataTransferred; + _session.TransferCanceled += TransferCanceled; _session.TransferError += TransferError; _session.SourceDisabled += SourceDisabled; _session.StateChanged += StateChanged; - _tcs = new TaskCompletionSource(); + _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _sourceDisabledTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } public Task Run() { - Invoker.Current.Invoke(Init); + _handleManager.Invoker.InvokeDispatch(Init); return _tcs.Task; } @@ -54,38 +60,37 @@ private void Init() { try { - Debug.WriteLine("NAPS2.TW - Opening session"); - var dsmHandle = _handleManager.GetDsmHandle(_options.DialogParent, _options.UseNativeUI); -#if NET6_0_OR_GREATER - var rc = _session.Open(); -#else - var rc = _session.Open(new WindowsFormsMessageLoopHook(dsmHandle)); -#endif + _logger.LogDebug("NAPS2.TW - Opening session"); + bool useNativeUi = _options.UseNativeUI || _options.TwainOptions.ShowProgress; + var rc = _session.Open(_handleManager.CreateMessageLoopHook(_options.DialogParent, useNativeUi)); if (rc != ReturnCode.Success) { throw new DeviceException($"TWAIN session open error: {rc}"); } - Debug.WriteLine("NAPS2.TW - Finding source"); + _logger.LogDebug("NAPS2.TW - Finding source"); _source = _session.FirstOrDefault(x => x.Name == _options.Device!.ID); if (_source == null) { throw new DeviceNotFoundException(); } - Debug.WriteLine("NAPS2.TW - Opening source"); + _logger.LogDebug("NAPS2.TW - Opening source"); + _logger.LogDebug( + "NAPS2.TW - Name: {Name}; Manu: {Manu}; Family: {Family}; Version: {Version}; Protocol: {Protocol}", + _source.Name, _source.Manufacturer, _source.ProductFamily, _source.Version, _source.ProtocolVersion); rc = _source.Open(); if (rc != ReturnCode.Success) { throw GetExceptionForStatus(_session.GetStatus()); } - Debug.WriteLine("NAPS2.TW - Configuring source"); + _logger.LogDebug("NAPS2.TW - Configuring source"); ConfigureSource(_source); - Debug.WriteLine("NAPS2.TW - Enabling source"); + _logger.LogDebug("NAPS2.TW - Enabling source"); var ui = _options.UseNativeUI ? SourceEnableMode.ShowUI : SourceEnableMode.NoUI; - var enableHandle = _handleManager.GetEnableHandle(_options.DialogParent, _options.UseNativeUI); + var enableHandle = _handleManager.GetEnableHandle(_options.DialogParent, useNativeUi); // Note that according to the twain spec, on Windows it is recommended to set the modal parameter to false rc = _source.Enable(ui, false, enableHandle); if (rc != ReturnCode.Success) @@ -93,7 +98,9 @@ private void Init() throw GetExceptionForStatus(_source.GetStatus()); } - _cancelToken.Register(FinishWithCancellation); + _cancelToken.Register(() => _handleManager.Invoker.Invoke(FinishWithCancellation)); + _sourceDisabledTcs.Task.ContinueWith(_ => _handleManager.Invoker.Invoke(FinishWithCompletion)) + .AssertNoAwait(); } catch (Exception ex) { @@ -103,11 +110,11 @@ private void Init() private void FinishWithCancellation() { - Debug.WriteLine("NAPS2.TW - Finishing with cancellation"); + _logger.LogDebug("NAPS2.TW - Finishing with cancellation"); if (_session.State != 5) { // If we're in state 6 or 7, this will abort the ongoing transfer via ForceStepDown. - // If we're in state 4 or lower, then we're not transferring and this will just clean up the source/session. + // If we're in state 4 or lower, then we're not transferring and this will just clean up the source/session. UnloadTwain(); _tcs.TrySetResult(false); } @@ -117,13 +124,13 @@ private void FinishWithCancellation() // (Or if we're in state 5 in the process of finishing all transfers, then we don't need to cancel anyway.) // This will result in FinishWithCompletion being called when the source disables itself. // The alternative of calling ForceStepDown from state 5 seems to produce an error message from the scanner. - Debug.WriteLine("NAPS2.TW - Will cancel via TransferReady"); + _logger.LogDebug("NAPS2.TW - Will cancel via TransferReady"); } } private void FinishWithError(Exception ex) { - Debug.WriteLine("NAPS2.TW - Finishing with error"); + _logger.LogDebug(ex, "NAPS2.TW - Finishing with error"); // If we're in state 5 or higher, we'll call ForceStepDown, which could potentially produce additional errors, // but what alternative is there? // If we're in state 4 or lower, this will just clean up the source/session. @@ -133,7 +140,7 @@ private void FinishWithError(Exception ex) private void FinishWithCompletion() { - Debug.WriteLine("NAPS2.TW - Finishing with completion"); + _logger.LogDebug("NAPS2.TW - Finishing with completion"); // At this point we should be in state 4 and this will clean up the source/session. UnloadTwain(); _tcs.TrySetResult(true); @@ -167,18 +174,24 @@ private void UnloadTwain() private void StateChanged(object? sender, EventArgs e) { - Debug.WriteLine($"NAPS2.TW - StateChanged (to {_session.State})"); + _logger.LogDebug($"NAPS2.TW - StateChanged (to {_session.State})"); } private void SourceDisabled(object? sender, EventArgs e) { - Debug.WriteLine("NAPS2.TW - SourceDisabled"); - FinishWithCompletion(); + _logger.LogDebug("NAPS2.TW - SourceDisabled"); + _sourceDisabledTcs.TrySetResult(true); + } + + private void TransferCanceled(object? sender, TransferCanceledEventArgs e) + { + _logger.LogDebug("NAPS2.TW - TransferCanceled"); + _twainEvents.TransferCanceled(new TwainTransferCanceled()); } private void TransferError(object? sender, TransferErrorEventArgs e) { - Debug.WriteLine("NAPS2.TW - TransferError"); + _logger.LogDebug("NAPS2.TW - TransferError"); FinishWithError(e.Exception ?? GetExceptionForStatus(e.SourceStatus)); } @@ -193,9 +206,11 @@ private Exception GetExceptionForStatus(TWStatus status) // regardless. return new AlreadyHandledDriverException(); case ConditionCode.PaperJam: - return new DeviceException(SdkResources.DevicePaperJam); - case ConditionCode.CheckDeviceOnline: - return new DeviceException(SdkResources.DeviceOffline); + return new DevicePaperJamException(); + case ConditionCode.CheckDeviceOnline when _session.State <= 3: + return new DeviceOfflineException(); + case ConditionCode.CheckDeviceOnline when _session.State >= 4: + return new DeviceCommunicationException(); default: return new DeviceException($"TWAIN error: {status.ConditionCode}"); } @@ -203,14 +218,14 @@ private Exception GetExceptionForStatus(TWStatus status) private void DataTransferred(object? sender, DataTransferredEventArgs e) { - Debug.WriteLine("NAPS2.TW - DataTransferred"); + _logger.LogDebug("NAPS2.TW - DataTransferred"); try { - // TODO: We probably want to support native transfer for mac/net6 -#if NET6_0_OR_GREATER - _twainEvents.MemoryBufferTransferred(ToMemoryBuffer(e.MemoryData, e.MemoryInfo)); -#else - if (_options.TwainOptions.TransferMode == TwainTransferMode.Memory) + if (_options.TwainOptions.TransferMode == TwainTransferMode.Memory && e.MemoryData == null) + { + _logger.LogDebug("NAPS2.TW - Expected memory transfer, but got native transfer?"); + } + if (e.MemoryData != null) { _twainEvents.MemoryBufferTransferred(ToMemoryBuffer(e.MemoryData, e.MemoryInfo)); } @@ -221,28 +236,23 @@ private void DataTransferred(object? sender, DataTransferredEventArgs e) Buffer = ByteString.FromStream(e.GetNativeImageStream()) }); } -#endif } catch (Exception ex) { - Log.ErrorException("Error sending TWAIN data transfer event", ex); + _logger.LogError(ex, "Error sending TWAIN data transfer event"); } } private void TransferReady(object? sender, TransferReadyEventArgs e) { - Debug.WriteLine("NAPS2.TW - TransferReady"); + _logger.LogDebug("NAPS2.TW - TransferReady"); try { var pageStart = new TwainPageStart(); -#if NET6_0_OR_GREATER - pageStart.ImageData = ToImageData(e.PendingImageInfo); -#else if (_options.TwainOptions.TransferMode == TwainTransferMode.Memory) { pageStart.ImageData = ToImageData(e.PendingImageInfo); } -#endif _twainEvents.PageStart(pageStart); if (_cancelToken.IsCancellationRequested) { @@ -251,7 +261,7 @@ private void TransferReady(object? sender, TransferReadyEventArgs e) } catch (Exception ex) { - Log.ErrorException("Error sending TWAIN transfer ready event", ex); + _logger.LogError(ex, "Error sending TWAIN transfer ready event"); } } @@ -286,15 +296,31 @@ private static TwainImageData ToImageData(TWImageInfo imageInfo) private void ConfigureSource(DataSource source) { -#if NET6_0_OR_GREATER - source.Capabilities.ICapXferMech.SetValue(XferMech.Memory); -#else // Transfer Mode + if (_options.TwainOptions.TransferMode == TwainTransferMode.Default) + { + if (source.Manufacturer.Contains("Kyocera", StringComparison.InvariantCultureIgnoreCase) && + (source.ProductFamily.Contains("Ecosys", StringComparison.InvariantCultureIgnoreCase) || + source.Name.Contains("Ecosys", StringComparison.InvariantCultureIgnoreCase))) + { + _logger.LogDebug("Detected Kyocera Ecosys scanner. Defaulting to Native transfer mode."); + _options.TwainOptions.TransferMode = TwainTransferMode.Native; + } + else + { + _logger.LogDebug("Defaulting to Memory transfer mode."); + _options.TwainOptions.TransferMode = TwainTransferMode.Memory; + } + } if (_options.TwainOptions.TransferMode == TwainTransferMode.Memory) { + _logger.LogDebug("Transfer mode: Memory"); source.Capabilities.ICapXferMech.SetValue(XferMech.Memory); } -#endif + else + { + _logger.LogDebug("Transfer mode: Native"); + } if (_options.UseNativeUI) { @@ -310,6 +336,7 @@ private void ConfigureSource(DataSource source) // Paper Source switch (_options.PaperSource) { + case PaperSource.Auto: // Assume the data source will ignore if unsupported case PaperSource.Flatbed: source.Capabilities.CapFeederEnabled.SetValue(BoolType.False); source.Capabilities.CapDuplexEnabled.SetValue(BoolType.False); @@ -324,17 +351,35 @@ private void ConfigureSource(DataSource source) break; } + // TODO: Should we add an "Automatic" option in the NAPS2 GUI instead of making "Glass" = Auto? + // For "Auto", choose the feeder if it has paper, otherwise the flatbed. + if (_options.PaperSource == PaperSource.Auto) + { + if (source.Capabilities.CapAutomaticSenseMedium.IsSupported) + { + source.Capabilities.CapAutomaticSenseMedium.SetValue(BoolType.True); + } + else if (source.Capabilities.CapFeederLoaded.IsSupported && + source.Capabilities.CapFeederLoaded.GetCurrent() == BoolType.True) + { + source.Capabilities.CapFeederEnabled.SetValue(BoolType.True); + } + } + // Bit Depth switch (_options.BitDepth) { case BitDepth.Color: source.Capabilities.ICapPixelType.SetValue(PixelType.RGB); + source.Capabilities.ICapBitDepth.SetValue(24); break; case BitDepth.Grayscale: source.Capabilities.ICapPixelType.SetValue(PixelType.Gray); + source.Capabilities.ICapBitDepth.SetValue(8); break; case BitDepth.BlackAndWhite: source.Capabilities.ICapPixelType.SetValue(PixelType.BlackWhite); + source.Capabilities.ICapBitDepth.SetValue(1); break; } @@ -391,4 +436,5 @@ private void SetClosest(ICapWrapper cap, int value) cap.SetValue(closest); } } + #endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/Win32MessageLoopHook.cs b/NAPS2.Sdk/Scan/Internal/Twain/Win32MessageLoopHook.cs new file mode 100644 index 0000000000..857fee0827 --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/Twain/Win32MessageLoopHook.cs @@ -0,0 +1,29 @@ +#if !MAC +using NAPS2.Platform.Windows; +using NTwain; + +namespace NAPS2.Scan.Internal.Twain; + +/// +/// A MessageLoopHook implementation that uses Win32 methods directly, with no dependencies on WinForms or WPF. +/// +[System.Runtime.Versioning.SupportedOSPlatform("windows")] +internal class Win32MessageLoopHook : MessageLoopHook +{ + private readonly Win32MessagePump _messagePump; + + public Win32MessageLoopHook(Win32MessagePump messagePump, IntPtr dsmHandle) + { + _messagePump = messagePump; + Handle = dsmHandle; + } + + public override void Invoke(Action action) => _messagePump.Invoke(action); + + public override void BeginInvoke(Action action) => _messagePump.InvokeDispatch(action); + + protected override void Start(IWinMessageFilter filter) => _messagePump.Filter = filter.IsTwainMessage; + + protected override void Stop() => _messagePump.Filter = null; +} +#endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Wia/WiaConfiguration.cs b/NAPS2.Sdk/Scan/Internal/Wia/WiaConfiguration.cs index ddf60521bc..fc9e56df2d 100644 --- a/NAPS2.Sdk/Scan/Internal/Wia/WiaConfiguration.cs +++ b/NAPS2.Sdk/Scan/Internal/Wia/WiaConfiguration.cs @@ -1,6 +1,6 @@ namespace NAPS2.Scan.Internal.Wia; -public record WiaConfiguration +internal record WiaConfiguration { public Dictionary DeviceProps { get; init; } = new(); diff --git a/NAPS2.Sdk/Scan/Internal/Wia/WiaExtensions.cs b/NAPS2.Sdk/Scan/Internal/Wia/WiaExtensions.cs deleted file mode 100644 index c7b11e6da2..0000000000 --- a/NAPS2.Sdk/Scan/Internal/Wia/WiaExtensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -#if !MAC -using NAPS2.Wia; - -namespace NAPS2.Scan.Internal.Wia; - -public static class WiaExtensions -{ - public static void SafeSetProperty(this WiaItemBase item, int propId, int value) - { - try - { - item.SetProperty(propId, value); - } - catch (Exception e) - { - Log.ErrorException("Error setting property", e); - } - } - - public static void SafeSetPropertyClosest(this WiaItemBase item, int propId, ref int value) - { - try - { - item.SetPropertyClosest(propId, ref value); - } - catch (Exception e) - { - Log.ErrorException("Error setting property", e); - } - } - - public static void SafeSetPropertyRange(this WiaItemBase item, int propId, int value, int expectedMin, int expectedMax) - { - try - { - item.SetPropertyRange(propId, value, expectedMin, expectedMax); - } - catch (Exception e) - { - Log.ErrorException("Error setting property", e); - } - } -} -#endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Wia/WiaScanDriver.cs b/NAPS2.Sdk/Scan/Internal/Wia/WiaScanDriver.cs index bb1916e5b7..bdac05f97f 100644 --- a/NAPS2.Sdk/Scan/Internal/Wia/WiaScanDriver.cs +++ b/NAPS2.Sdk/Scan/Internal/Wia/WiaScanDriver.cs @@ -1,10 +1,14 @@ #if !MAC +using System.Collections.Immutable; using System.Threading; +using Microsoft.Extensions.Logging; +using NAPS2.Remoting.Worker; using NAPS2.Scan.Exceptions; using NAPS2.Wia; namespace NAPS2.Scan.Internal.Wia; +[System.Runtime.Versioning.SupportedOSPlatform("windows")] internal class WiaScanDriver : IScanDriver { private readonly ScanningContext _scanningContext; @@ -14,21 +18,100 @@ public WiaScanDriver(ScanningContext scanningContext) _scanningContext = scanningContext; } - public Task> GetDeviceList(ScanOptions options) + public Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback) { return Task.Run(() => { using var deviceManager = new WiaDeviceManager((WiaVersion) options.WiaOptions.WiaApiVersion); - return deviceManager.GetDeviceInfos().Select(deviceInfo => + foreach (var deviceInfo in deviceManager.GetDeviceInfos()) { using (deviceInfo) { - return new ScanDevice(deviceInfo.Id(), deviceInfo.Name()); + string id = deviceInfo.Id(); + string name = deviceInfo.Name(); + if (name.Equals(@"No friendly name", StringComparison.InvariantCultureIgnoreCase)) + { + // Some Windows/driver issues can result in the scanner name appearing as "No friendly name". + // Better to replace with a generic "Unknown Scanner" string. + name = SdkResources.UnknownScanner; + } + callback(new ScanDevice(Driver.Wia, id, name)); } - }).ToList(); + } + }); + } + + public Task GetCaps(ScanOptions options, CancellationToken cancelToken) + { + return Task.Run(() => + { + try + { + using var deviceManager = new WiaDeviceManager((WiaVersion) options.WiaOptions.WiaApiVersion); + using var device = deviceManager.FindDevice(options.Device!.ID); + using var items = device.GetSubItems().ToDisposableList(); + var flatbed = items.FirstOrDefault(x => x.Name() == "Flatbed"); + var feeder = items.FirstOrDefault(x => x.Name() == "Feeder"); + var flatbedCaps = flatbed != null ? GetItemCaps(device, flatbed, true) : null; + var feederCaps = feeder != null ? GetItemCaps(device, feeder, false) : null; + return new ScanCaps + { + MetadataCaps = new MetadataCaps + { + Manufacturer = device.Properties.GetOrNull(WiaPropertyId.DIP_VEND_DESC)?.Value as string, + Model = device.Properties.GetOrNull(WiaPropertyId.DIP_DEV_DESC)?.Value as string + }, + PaperSourceCaps = new PaperSourceCaps + { + SupportsFlatbed = device.SupportsFlatbed(), + SupportsFeeder = device.SupportsFeeder(), + SupportsDuplex = device.SupportsDuplex(), + CanCheckIfFeederHasPaper = true + }, + FlatbedCaps = device.SupportsFlatbed() ? flatbedCaps : null, + FeederCaps = device.SupportsFeeder() ? feederCaps : null, + DuplexCaps = device.SupportsDuplex() ? feederCaps : null + }; + } + catch (WiaException e) + { + WiaScanErrors.ThrowDeviceError(e); + throw; + } }); } + private PerSourceCaps GetItemCaps(WiaDevice device, WiaItem item, bool flatbed) + { + var xRes = item.Properties.GetOrNull(WiaPropertyId.IPS_XRES); + var dpiCaps = xRes != null + ? xRes.Attributes.Flags.HasFlag(WiaPropertyFlags.Range) + ? DpiCaps.ForRange(xRes.Attributes.Min, xRes.Attributes.Max, xRes.Attributes.Step) + : new DpiCaps + { + Values = xRes.Attributes.Values?.Cast().ToImmutableList(), + } + : null; + var dataType = item.Properties.GetOrNull(WiaPropertyId.IPA_DATATYPE); + var validDataTypes = dataType?.Attributes.Values; + var bitDepthCaps = validDataTypes != null + ? new BitDepthCaps + { + SupportsColor = validDataTypes.Contains(3), + SupportsGrayscale = validDataTypes.Contains(2), + SupportsBlackAndWhite = validDataTypes.Contains(0) + } + : null; + var (horizontalSize, verticalSize) = GetScanArea(device, item, flatbed); + var scanArea = new PageSize(horizontalSize / 1000m, verticalSize / 1000m, PageSizeUnit.Inch); + return new PerSourceCaps + { + DpiCaps = dpiCaps, + BitDepthCaps = bitDepthCaps, + PageSizeCaps = new PageSizeCaps { ScanArea = scanArea } + }; + } + public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) { @@ -48,7 +131,7 @@ public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents NativeWiaObject.DefaultWiaVersion == WiaVersion.Wia20 && !options.UseNativeUI) { - Debug.WriteLine("Falling back to WIA 1.0 due to E_INVALIDARG"); + _scanningContext.Logger.LogDebug("Falling back to WIA 1.0 due to E_INVALIDARG"); await context.Scan(WiaVersion.Wia10); } } @@ -62,14 +145,17 @@ public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents private class WiaScanContext { private readonly ScanningContext _scanningContext; + private readonly ILogger _logger; private readonly ScanOptions _options; private readonly CancellationToken _cancelToken; private readonly IScanEvents _scanEvents; private readonly Action _callback; - public WiaScanContext(ScanningContext scanningContext, ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) + public WiaScanContext(ScanningContext scanningContext, ScanOptions options, CancellationToken cancelToken, + IScanEvents scanEvents, Action callback) { _scanningContext = scanningContext; + _logger = scanningContext.Logger; _options = options; _cancelToken = cancelToken; _scanEvents = scanEvents; @@ -79,13 +165,21 @@ public WiaScanContext(ScanningContext scanningContext, ScanOptions options, Canc public async Task Scan(WiaVersion wiaVersion) { using var deviceManager = new WiaDeviceManager(wiaVersion); - using var device = deviceManager.FindDevice(_options.Device!.ID!); + using var device = deviceManager.FindDevice(_options.Device!.ID); if (device.Version == WiaVersion.Wia20 && _options.UseNativeUI) { await DoWia20NativeTransfer(deviceManager, device); return; } + if (_options.PaperSource == PaperSource.Auto) + { + // Default to flatbed if supported (or if both support checks fail) + _options.PaperSource = device.SupportsFlatbed() || !device.SupportsFeeder() + ? PaperSource.Flatbed + : PaperSource.Feeder; + } + using var item = GetItem(device); if (item == null) { @@ -111,11 +205,10 @@ private async Task DoWia20NativeTransfer(WiaDeviceManager deviceManager, WiaDevi { foreach (var path in paths) { - await foreach(var image in _scanningContext.ImageContext.LoadFrames(path)) + await foreach (var image in _scanningContext.ImageContext.LoadFrames(path)) { using (image) { - // TODO: Might still need to do some work on ownership for in-memory ScannedImage storage _callback(image); } } @@ -131,7 +224,7 @@ private async Task DoWia20NativeTransfer(WiaDeviceManager deviceManager, WiaDevi } catch (Exception e) { - Log.ErrorException("Error deleting WIA 2.0 native transferred file", e); + _logger.LogError(e, "Error deleting WIA 2.0 native transferred file"); } } } @@ -152,15 +245,34 @@ private void DoTransfer(WiaDevice device, WiaItem item) using var transfer = item.StartTransfer(); Exception? scanException = null; + bool hasAtLeastOneImage = false; transfer.PageScanned += (sender, args) => { try { - using (args.Stream) - using (var image = _scanningContext.ImageContext.Load(args.Stream)) + using var stream = args.Stream; + if (stream.Length == 0) + { + _logger.LogError("Ignoring empty stream from WIA"); + return; + } + hasAtLeastOneImage = true; + IMemoryImage image; + try + { + image = _scanningContext.ImageContext.Load(stream); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading stream from WIA"); + // Assume the problem is an incomplete stream due to some kind of communication failure + throw new DeviceCommunicationException(); + } + using (image) { _callback(image); } + _scanEvents.PageStart(); } catch (Exception e) { @@ -172,7 +284,14 @@ private void DoTransfer(WiaDevice device, WiaItem item) using (_cancelToken.Register(transfer.Cancel)) { _scanEvents.PageStart(); - transfer.Download(); + try + { + transfer.Download(); + } + catch (WiaException e) when (e.ErrorCode == 0x210001) + { + // This error code is undocumented but seems to mean "no more pages" which can be ignored + } if (device.Version == WiaVersion.Wia10 && _options.PaperSource != PaperSource.Flatbed) { @@ -181,7 +300,6 @@ private void DoTransfer(WiaDevice device, WiaItem item) { while (!_cancelToken.IsCancellationRequested && scanException == null) { - _scanEvents.PageStart(); transfer.Download(); } } @@ -194,22 +312,28 @@ private void DoTransfer(WiaDevice device, WiaItem item) { throw scanException; } + if (!hasAtLeastOneImage && !_cancelToken.IsCancellationRequested && + _options.PaperSource != PaperSource.Flatbed) + { + throw new DeviceFeederEmptyException(); + } } private WiaItem? GetItem(WiaDevice device) { if (_options.UseNativeUI) { - bool useWorker = Environment.Is64BitProcess && device.Version == WiaVersion.Wia10; + bool useWorker = PlatformCompat.System.SupportsWinX86Worker && + device.Version == WiaVersion.Wia10; if (useWorker) { if (_scanningContext.WorkerFactory == null) { throw new InvalidOperationException( - "ScanningContext.WorkerFactory must be set to use WIA 1.0 Native UI from a 64-bit process"); + "ScanningContext.SetUpWin32Worker() must be called to use WIA 1.0 Native UI from a 64-bit process"); } WiaConfiguration? config; - using (var worker = _scanningContext.WorkerFactory.Create()) + using (var worker = _scanningContext.CreateWorker(WorkerType.WinX86)!) { config = worker.Service.Wia10NativeUI(device.Id(), _options.DialogParent); } @@ -218,6 +342,11 @@ private void DoTransfer(WiaDevice device, WiaItem item) return null; } var item = device.FindSubItem(config.ItemName); + if (item == null) + { + _logger.LogError("Could not find WIA item {Item}", config.ItemName); + return null; + } device.Properties.DeserializeEditable(device.Properties.Delta(config.DeviceProps)); item.Properties.DeserializeEditable(item.Properties.Delta(config.ItemProps)); return item; @@ -256,11 +385,11 @@ private void ConfigureProps(WiaDevice device, WiaItem item) { if (device.Version == WiaVersion.Wia10) { - device.SafeSetProperty(WiaPropertyId.DPS_PAGES, 1); + SafeSetProperty(device, WiaPropertyId.DPS_PAGES, 1); } else { - item.SafeSetProperty(WiaPropertyId.IPS_PAGES, 0); + SafeSetProperty(item, WiaPropertyId.IPS_PAGES, 0); } } @@ -269,13 +398,13 @@ private void ConfigureProps(WiaDevice device, WiaItem item) switch (_options.PaperSource) { case PaperSource.Flatbed: - device.SafeSetProperty(WiaPropertyId.DPS_DOCUMENT_HANDLING_SELECT, WiaPropertyValue.FLATBED); + SafeSetProperty(device, WiaPropertyId.DPS_DOCUMENT_HANDLING_SELECT, WiaPropertyValue.FLATBED); break; case PaperSource.Feeder: - device.SafeSetProperty(WiaPropertyId.DPS_DOCUMENT_HANDLING_SELECT, WiaPropertyValue.FEEDER); + SafeSetProperty(device, WiaPropertyId.DPS_DOCUMENT_HANDLING_SELECT, WiaPropertyValue.FEEDER); break; case PaperSource.Duplex: - device.SafeSetProperty(WiaPropertyId.DPS_DOCUMENT_HANDLING_SELECT, + SafeSetProperty(device, WiaPropertyId.DPS_DOCUMENT_HANDLING_SELECT, WiaPropertyValue.FEEDER | WiaPropertyValue.DUPLEX); break; } @@ -285,10 +414,10 @@ private void ConfigureProps(WiaDevice device, WiaItem item) switch (_options.PaperSource) { case PaperSource.Feeder: - item.SafeSetProperty(WiaPropertyId.IPS_DOCUMENT_HANDLING_SELECT, WiaPropertyValue.FRONT_ONLY); + SafeSetProperty(item, WiaPropertyId.IPS_DOCUMENT_HANDLING_SELECT, WiaPropertyValue.FRONT_ONLY); break; case PaperSource.Duplex: - item.SafeSetProperty(WiaPropertyId.IPS_DOCUMENT_HANDLING_SELECT, WiaPropertyValue.DUPLEX); + SafeSetProperty(item, WiaPropertyId.IPS_DOCUMENT_HANDLING_SELECT, WiaPropertyValue.DUPLEX); break; } } @@ -296,41 +425,29 @@ private void ConfigureProps(WiaDevice device, WiaItem item) switch (_options.BitDepth) { case BitDepth.Grayscale: - item.SafeSetProperty(WiaPropertyId.IPA_DATATYPE, 2); + SafeSetProperty(item, WiaPropertyId.IPA_DATATYPE, 2); break; case BitDepth.Color: - item.SafeSetProperty(WiaPropertyId.IPA_DATATYPE, 3); + SafeSetProperty(item, WiaPropertyId.IPA_DATATYPE, 3); break; case BitDepth.BlackAndWhite: - item.SafeSetProperty(WiaPropertyId.IPA_DATATYPE, 0); + SafeSetProperty(item, WiaPropertyId.IPA_DATATYPE, 0); break; } int xRes = _options.Dpi; int yRes = _options.Dpi; - item.SafeSetPropertyClosest(WiaPropertyId.IPS_XRES, ref xRes); - item.SafeSetPropertyClosest(WiaPropertyId.IPS_YRES, ref yRes); + SafeSetPropertyClosest(item, WiaPropertyId.IPS_XRES, ref xRes); + SafeSetPropertyClosest(item, WiaPropertyId.IPS_YRES, ref yRes); + if (xRes != _options.Dpi || yRes != _options.Dpi) + { + _logger.LogDebug($"Correcting DPI from {_options.Dpi}x{_options.Dpi} to {xRes}x{yRes}"); + } int pageWidth = _options.PageSize!.WidthInThousandthsOfAnInch * xRes / 1000; int pageHeight = _options.PageSize.HeightInThousandthsOfAnInch * yRes / 1000; - int horizontalSize, verticalSize; - if (device.Version == WiaVersion.Wia10) - { - horizontalSize = - (int) device.Properties[_options.PaperSource == PaperSource.Flatbed - ? WiaPropertyId.DPS_HORIZONTAL_BED_SIZE - : WiaPropertyId.DPS_HORIZONTAL_SHEET_FEED_SIZE].Value; - verticalSize = - (int) device.Properties[_options.PaperSource == PaperSource.Flatbed - ? WiaPropertyId.DPS_VERTICAL_BED_SIZE - : WiaPropertyId.DPS_VERTICAL_SHEET_FEED_SIZE].Value; - } - else - { - horizontalSize = (int) item.Properties[WiaPropertyId.IPS_MAX_HORIZONTAL_SIZE].Value; - verticalSize = (int) item.Properties[WiaPropertyId.IPS_MAX_VERTICAL_SIZE].Value; - } + var (horizontalSize, verticalSize) = GetScanArea(device, item, _options.PaperSource == PaperSource.Flatbed); int pagemaxwidth = horizontalSize * xRes / 1000; int pagemaxheight = verticalSize * yRes / 1000; @@ -346,22 +463,80 @@ private void ConfigureProps(WiaDevice device, WiaItem item) if (_options.WiaOptions.OffsetWidth) { - item.SafeSetProperty(WiaPropertyId.IPS_XEXTENT, pageWidth + horizontalPos); - item.SafeSetProperty(WiaPropertyId.IPS_XPOS, horizontalPos); + SafeSetProperty(item, WiaPropertyId.IPS_XEXTENT, pageWidth + horizontalPos); + SafeSetProperty(item, WiaPropertyId.IPS_XPOS, horizontalPos); } else { - item.SafeSetProperty(WiaPropertyId.IPS_XEXTENT, pageWidth); - item.SafeSetProperty(WiaPropertyId.IPS_XPOS, horizontalPos); + SafeSetProperty(item, WiaPropertyId.IPS_XEXTENT, pageWidth); + SafeSetProperty(item, WiaPropertyId.IPS_XPOS, horizontalPos); } - item.SafeSetProperty(WiaPropertyId.IPS_YEXTENT, pageHeight); + SafeSetProperty(item, WiaPropertyId.IPS_YEXTENT, pageHeight); if (!_options.BrightnessContrastAfterScan) { - item.SafeSetPropertyRange(WiaPropertyId.IPS_CONTRAST, _options.Contrast, -1000, 1000); - item.SafeSetPropertyRange(WiaPropertyId.IPS_BRIGHTNESS, _options.Brightness, -1000, 1000); + SafeSetPropertyRange(item, WiaPropertyId.IPS_CONTRAST, _options.Contrast, -1000, 1000); + SafeSetPropertyRange(item, WiaPropertyId.IPS_BRIGHTNESS, _options.Brightness, -1000, 1000); } } + + private void SafeSetProperty(WiaItemBase item, int propId, int value) + { + try + { + item.SetProperty(propId, value); + } + catch (Exception e) + { + _logger.LogError(e, "Error setting property {PropId}", propId); + } + } + + private void SafeSetPropertyClosest(WiaItemBase item, int propId, ref int value) + { + try + { + item.SetPropertyClosest(propId, ref value); + } + catch (Exception e) + { + _logger.LogError(e, "Error setting property {PropId}", propId); + } + } + + private void SafeSetPropertyRange(WiaItemBase item, int propId, int value, int expectedMin, int expectedMax) + { + try + { + item.SetPropertyRange(propId, value, expectedMin, expectedMax); + } + catch (Exception e) + { + _logger.LogError(e, "Error setting property {PropId}", propId); + } + } + } + + private static (int, int) GetScanArea(WiaDevice device, WiaItem item, bool flatbed) + { + int horizontalSize, verticalSize; + if (device.Version == WiaVersion.Wia10) + { + horizontalSize = + (int) device.Properties[flatbed + ? WiaPropertyId.DPS_HORIZONTAL_BED_SIZE + : WiaPropertyId.DPS_HORIZONTAL_SHEET_FEED_SIZE].Value; + verticalSize = + (int) device.Properties[flatbed + ? WiaPropertyId.DPS_VERTICAL_BED_SIZE + : WiaPropertyId.DPS_VERTICAL_SHEET_FEED_SIZE].Value; + } + else + { + horizontalSize = (int) item.Properties[WiaPropertyId.IPS_MAX_HORIZONTAL_SIZE].Value; + verticalSize = (int) item.Properties[WiaPropertyId.IPS_MAX_VERTICAL_SIZE].Value; + } + return (horizontalSize, verticalSize); } } #endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Wia/WiaScanErrors.cs b/NAPS2.Sdk/Scan/Internal/Wia/WiaScanErrors.cs index 5daa1a6603..3474d43aa4 100644 --- a/NAPS2.Sdk/Scan/Internal/Wia/WiaScanErrors.cs +++ b/NAPS2.Sdk/Scan/Internal/Wia/WiaScanErrors.cs @@ -4,39 +4,22 @@ namespace NAPS2.Scan.Internal.Wia; -public class WiaScanErrors +internal class WiaScanErrors { public static void ThrowDeviceError(WiaException e) { - if (e.ErrorCode == WiaErrorCodes.NO_DEVICE_AVAILABLE) + throw e.ErrorCode switch { - throw new DeviceNotFoundException(); - } - if (e.ErrorCode == WiaErrorCodes.PAPER_EMPTY) - { - throw new NoPagesException(); - } - if (e.ErrorCode == WiaErrorCodes.OFFLINE) - { - throw new DeviceException(SdkResources.DeviceOffline); - } - if (e.ErrorCode == WiaErrorCodes.BUSY) - { - throw new DeviceException(SdkResources.DeviceBusy); - } - if (e.ErrorCode == WiaErrorCodes.COVER_OPEN) - { - throw new DeviceException(SdkResources.DeviceCoverOpen); - } - if (e.ErrorCode == WiaErrorCodes.PAPER_JAM) - { - throw new DeviceException(SdkResources.DevicePaperJam); - } - if (e.ErrorCode == WiaErrorCodes.WARMING_UP) - { - throw new DeviceException(SdkResources.DeviceWarmingUp); - } - throw new ScanDriverUnknownException(e); + WiaErrorCodes.NO_DEVICE_AVAILABLE => new DeviceNotFoundException(), + WiaErrorCodes.PAPER_EMPTY => new DeviceFeederEmptyException(), + WiaErrorCodes.OFFLINE => new DeviceOfflineException(), + WiaErrorCodes.COMMUNICATION => new DeviceCommunicationException(), + WiaErrorCodes.BUSY => new DeviceBusyException(), + WiaErrorCodes.COVER_OPEN => new DeviceCoverOpenException(), + WiaErrorCodes.PAPER_JAM => new DevicePaperJamException(), + WiaErrorCodes.WARMING_UP => new DeviceWarmingUpException(), + _ => new ScanDriverUnknownException(e) + }; } } #endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/WorkerScanBridge.cs b/NAPS2.Sdk/Scan/Internal/WorkerScanBridge.cs index 4c13b7ac51..220b2ee72f 100644 --- a/NAPS2.Sdk/Scan/Internal/WorkerScanBridge.cs +++ b/NAPS2.Sdk/Scan/Internal/WorkerScanBridge.cs @@ -1,28 +1,40 @@ using System.Threading; +using NAPS2.Remoting.Worker; namespace NAPS2.Scan.Internal; /// /// Represents scanning in a worker process on the same machine. /// -// TODO: Since implementing TwainScan as a distinct operation, this class is unused. We could consider removing it, but it may come in handy at some point. internal class WorkerScanBridge : IScanBridge { private readonly ScanningContext _scanningContext; + private readonly WorkerType _workerType; - public WorkerScanBridge(ScanningContext scanningContext) + public WorkerScanBridge(ScanningContext scanningContext, WorkerType workerType) { _scanningContext = scanningContext; + _workerType = workerType; } - public async Task> GetDeviceList(ScanOptions options) + public async Task GetDevices(ScanOptions options, CancellationToken cancelToken, Action callback) { if (_scanningContext.WorkerFactory == null) { - throw new InvalidOperationException("ScanningContext.WorkerFactory must be set to scan with a worker"); + throw new InvalidOperationException("ScanningContext must have a worker set up."); } - using var ctx = _scanningContext.WorkerFactory.Create(); - return await ctx.Service.GetDeviceList(options); + using var ctx = _scanningContext.CreateWorker(_workerType)!; + await ctx.Service.GetDevices(options, cancelToken, callback); + } + + public async Task GetCaps(ScanOptions options, CancellationToken cancelToken) + { + if (_scanningContext.WorkerFactory == null) + { + throw new InvalidOperationException("ScanningContext must have a worker set up."); + } + using var ctx = _scanningContext.CreateWorker(_workerType)!; + return await ctx.Service.GetCaps(options, cancelToken); } public async Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, @@ -30,9 +42,9 @@ public async Task Scan(ScanOptions options, CancellationToken cancelToken, IScan { if (_scanningContext.WorkerFactory == null) { - throw new InvalidOperationException("ScanningContext.WorkerFactory must be set to scan with a worker"); + throw new InvalidOperationException("ScanningContext must have a worker set up."); } - using var ctx = _scanningContext.WorkerFactory.Create(); + using var ctx = _scanningContext.CreateWorker(_workerType)!; await ctx.Service.Scan(_scanningContext, options, cancelToken, scanEvents, (image, tempPath) => { callback(image, new PostProcessingContext { TempPath = tempPath }); }); } diff --git a/NAPS2.Sdk/Scan/MetadataCaps.cs b/NAPS2.Sdk/Scan/MetadataCaps.cs new file mode 100644 index 0000000000..842a3786f1 --- /dev/null +++ b/NAPS2.Sdk/Scan/MetadataCaps.cs @@ -0,0 +1,32 @@ +namespace NAPS2.Scan; + +/// +/// Represents scanner metadata as part of ScanCaps. +/// +public class MetadataCaps +{ + /// + /// For SANE, this is the backend name. + /// + public string? DriverSubtype { get; init; } + + /// + /// The device manufacturer. + /// + public string? Manufacturer { get; init; } + + /// + /// The device model name. + /// + public string? Model { get; init; } + + /// + /// The device serial number. + /// + public string? SerialNumber { get; init; } + + /// + /// The URI for an icon associated with the device. + /// + public string? IconUri { get; init; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/NetworkOptions.cs b/NAPS2.Sdk/Scan/NetworkOptions.cs deleted file mode 100644 index 826036dd3d..0000000000 --- a/NAPS2.Sdk/Scan/NetworkOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NAPS2.Scan; - -public class NetworkOptions -{ - public string? Ip { get; set; } - - public int? Port { get; set; } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/PageEndEventArgs.cs b/NAPS2.Sdk/Scan/PageEndEventArgs.cs index 20e53be636..209156ad33 100644 --- a/NAPS2.Sdk/Scan/PageEndEventArgs.cs +++ b/NAPS2.Sdk/Scan/PageEndEventArgs.cs @@ -8,7 +8,13 @@ public PageEndEventArgs(int pageNumber, ProcessedImage image) Image = image; } + /// + /// Gets the page number currently being scanned (starting from 1). + /// public int PageNumber { get; } + /// + /// Gets the scanned image. + /// public ProcessedImage Image { get; set; } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/PageProgressEventArgs.cs b/NAPS2.Sdk/Scan/PageProgressEventArgs.cs index 8c88e751aa..7ca794aaa5 100644 --- a/NAPS2.Sdk/Scan/PageProgressEventArgs.cs +++ b/NAPS2.Sdk/Scan/PageProgressEventArgs.cs @@ -8,7 +8,13 @@ public PageProgressEventArgs(int pageNumber, double progress) Progress = progress; } + /// + /// Gets the page number currently being scanned (starting from 1). + /// public int PageNumber { get; } + /// + /// Gets the progress of the scan (between 0.0 and 1.0). + /// public double Progress { get; } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/PageSizeCaps.cs b/NAPS2.Sdk/Scan/PageSizeCaps.cs new file mode 100644 index 0000000000..5b95777338 --- /dev/null +++ b/NAPS2.Sdk/Scan/PageSizeCaps.cs @@ -0,0 +1,27 @@ +namespace NAPS2.Scan; + +/// +/// Represents valid values for ScanOptions.PageSize as part of PerSourceCaps. +/// +public class PageSizeCaps +{ + /// + /// Gets the size of the full scan area (i.e. maximum page size). + /// + public PageSize? ScanArea { get; init; } + + /// + /// Determines whether the provided page size fits within the full scan area (with 1% margin of error). + /// + public bool Fits(PageSize pageSize) + { + if (ScanArea == null) + { + // If no scan area is specified, we consider it as unbounded + return true; + } + // Allow 1% margin + return pageSize.WidthInInches <= ScanArea.WidthInInches * 1.01m && + pageSize.HeightInInches <= ScanArea.HeightInInches * 1.01m; + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/PageStartEventArgs.cs b/NAPS2.Sdk/Scan/PageStartEventArgs.cs index e77bfcb2de..9faf4310d4 100644 --- a/NAPS2.Sdk/Scan/PageStartEventArgs.cs +++ b/NAPS2.Sdk/Scan/PageStartEventArgs.cs @@ -7,5 +7,8 @@ public PageStartEventArgs(int pageNumber) PageNumber = pageNumber; } + /// + /// Gets the page number currently being scanned (starting from 1). + /// public int PageNumber { get; } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/PaperSource.cs b/NAPS2.Sdk/Scan/PaperSource.cs index 2dd1189b6e..35a3e3f0e5 100644 --- a/NAPS2.Sdk/Scan/PaperSource.cs +++ b/NAPS2.Sdk/Scan/PaperSource.cs @@ -1,8 +1,29 @@ namespace NAPS2.Scan; +/// +/// Specifies the physical paper source for devices that have multiple options (Flatbed, Feeder, Duplex). +/// public enum PaperSource { + /// + /// Use a supported paper source for the device. Generally this prioritizes Flatbed -> Feeder -> Duplex, but it + /// depends on the driver. This may choose between Feeder and Flatbed based on whether there is paper in the feeder. + /// + Auto, + + /// + /// Use the flatbed component of the scanner to scan a single page. + /// Flatbed, + + /// + /// Use the automatic document feeder component of the scanner, potentially scanning multiple pages. + /// Feeder, + + /// + /// Use the automatic document feeder component of the scanner with double-sided scanning, potentially scanning + /// multiple pages. + /// Duplex } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/PaperSourceCaps.cs b/NAPS2.Sdk/Scan/PaperSourceCaps.cs new file mode 100644 index 0000000000..35acb28b47 --- /dev/null +++ b/NAPS2.Sdk/Scan/PaperSourceCaps.cs @@ -0,0 +1,27 @@ +namespace NAPS2.Scan; + +/// +/// Represents valid values for ScanOptions.PaperSource as part of ScanCaps. +/// +public class PaperSourceCaps +{ + /// + /// Whether the scanner supports PaperSource.Flatbed. + /// + public bool SupportsFlatbed { get; init; } + + /// + /// Whether the scanner supports PaperSource.Feeder. + /// + public bool SupportsFeeder { get; init; } + + /// + /// Whether the scanner supports PaperSource.Duplex. + /// + public bool SupportsDuplex { get; init; } + + /// + /// Whether the scanner has the ability to detect if paper is in the feeder for use with PaperSource.Auto. + /// + public bool CanCheckIfFeederHasPaper { get; init; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/PerSourceCaps.cs b/NAPS2.Sdk/Scan/PerSourceCaps.cs new file mode 100644 index 0000000000..4ecf19d4ac --- /dev/null +++ b/NAPS2.Sdk/Scan/PerSourceCaps.cs @@ -0,0 +1,70 @@ +using System.Collections.Immutable; + +namespace NAPS2.Scan; + +/// +/// Represents capabilities specific to a single PaperSource as part of ScanCaps. +/// +public class PerSourceCaps +{ + /// + /// Gets an object representing the union of all possible option values allowed by the provided objects. + /// This can be helpful when presenting the user with a single set of possible options for multiple sources. + /// + public static PerSourceCaps UnionAll(IEnumerable caps) + { + var capsColl = caps as ICollection ?? caps.ToList(); + DpiCaps? dpiCaps = null; + foreach (var dpiValues in capsColl.Select(x => x.DpiCaps?.Values).WhereNotNull()) + { + dpiCaps = new DpiCaps + { + Values = (dpiCaps?.Values ?? []).Union(dpiValues).OrderBy(x => x).ToImmutableList() + }; + } + BitDepthCaps? bitDepthCaps = null; + foreach (var bd in capsColl.Select(x => x.BitDepthCaps).WhereNotNull()) + { + bitDepthCaps = new BitDepthCaps + { + SupportsColor = (bitDepthCaps?.SupportsColor ?? false) || bd.SupportsColor, + SupportsGrayscale = (bitDepthCaps?.SupportsGrayscale ?? false) || bd.SupportsGrayscale, + SupportsBlackAndWhite = (bitDepthCaps?.SupportsBlackAndWhite ?? false) || bd.SupportsBlackAndWhite, + }; + } + PageSizeCaps? pageSizeCaps = null; + foreach (var area in capsColl.Select(x => x.PageSizeCaps?.ScanArea).WhereNotNull()) + { + pageSizeCaps = new PageSizeCaps + { + ScanArea = pageSizeCaps?.ScanArea == null + ? area + : new PageSize( + Math.Max(pageSizeCaps.ScanArea.WidthInInches, area.WidthInInches), + Math.Max(pageSizeCaps.ScanArea.HeightInInches, area.HeightInInches), + PageSizeUnit.Inch) + }; + } + return new PerSourceCaps + { + DpiCaps = dpiCaps, + BitDepthCaps = bitDepthCaps, + PageSizeCaps = pageSizeCaps + }; + } + + /// + /// Valid values for ScanOptions.Dpi. + /// + public DpiCaps? DpiCaps { get; init; } + + /// + /// Valid values for ScanOptions.BitDepth. + /// + public BitDepthCaps? BitDepthCaps { get; init; } + + /// + /// Valid values for ScanOptions.PageSize. + /// + public PageSizeCaps? PageSizeCaps { get; init; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/SaneOptions.cs b/NAPS2.Sdk/Scan/SaneOptions.cs index 1895b26391..a46e077157 100644 --- a/NAPS2.Sdk/Scan/SaneOptions.cs +++ b/NAPS2.Sdk/Scan/SaneOptions.cs @@ -1,7 +1,18 @@ namespace NAPS2.Scan; +/// +/// Scanning options specific to the SANE driver. +/// public class SaneOptions { - // TODO: Probably move this to top-level ScanOptions (as e.g. twain might have extra options too) - public KeyValueScanOptions? KeyValueOptions { get; set; } + /// + /// Limit the devices queried by GetDevices/GetDeviceList to the given SANE backend. + /// + public string? Backend { get; set; } + + /// + /// Whether to keep SANE initialized in memory after the operation is complete. This improves stability when + /// doing multiple operations in a single process. Defaults to true. + /// + public bool KeepInitialized { get; set; } = true; } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/ScanCaps.cs b/NAPS2.Sdk/Scan/ScanCaps.cs new file mode 100644 index 0000000000..5e5ab7c508 --- /dev/null +++ b/NAPS2.Sdk/Scan/ScanCaps.cs @@ -0,0 +1,33 @@ +namespace NAPS2.Scan; + +/// +/// Represents scanner capabilities. This includes valid values for scanning options and extra metadata beyond just the +/// device name and id. +/// +public class ScanCaps +{ + /// + /// Metadata for the device. + /// + public MetadataCaps? MetadataCaps { get; init; } + + /// + /// Valid values for ScanOptions.PaperSource. + /// + public PaperSourceCaps? PaperSourceCaps { get; init; } + + /// + /// Capabilities specific to the Flatbed paper source. + /// + public PerSourceCaps? FlatbedCaps { get; init; } + + /// + /// Capabilities specific to the Feeder paper source. + /// + public PerSourceCaps? FeederCaps { get; init; } + + /// + /// Capabilities specific to the Duplex paper source. + /// + public PerSourceCaps? DuplexCaps { get; init; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/ScanController.cs b/NAPS2.Sdk/Scan/ScanController.cs index ec6bec644b..e9d3a8e2f1 100644 --- a/NAPS2.Sdk/Scan/ScanController.cs +++ b/NAPS2.Sdk/Scan/ScanController.cs @@ -1,16 +1,26 @@ using System.Threading; +using Microsoft.Extensions.Logging; using NAPS2.Ocr; +using NAPS2.Scan.Exceptions; using NAPS2.Scan.Internal; +using NAPS2.Scan.Internal.Sane; namespace NAPS2.Scan; -public class ScanController : IScanController +/// +/// The main entry point for scanning with NAPS2. +/// +public class ScanController { private readonly ScanningContext _scanningContext; private readonly ILocalPostProcessor _localPostProcessor; private readonly ScanOptionsValidator _scanOptionsValidator; private readonly IScanBridgeFactory _scanBridgeFactory; + /// + /// Initializes a new instance of the ScanController class with the specified ScanningContext. + /// + /// public ScanController(ScanningContext scanningContext) : this(scanningContext, new LocalPostProcessor(scanningContext, new OcrController(scanningContext)), new ScanOptionsValidator(), @@ -18,9 +28,9 @@ public ScanController(ScanningContext scanningContext) { } - public ScanController(ScanningContext scanningContext, OcrController ocrController) - : this(scanningContext, new LocalPostProcessor(scanningContext, ocrController), new ScanOptionsValidator(), - new ScanBridgeFactory(scanningContext)) + internal ScanController(ScanningContext scanningContext, IScanBridgeFactory scanBridgeFactory) + : this(scanningContext, new LocalPostProcessor(scanningContext, new OcrController(scanningContext)), + new ScanOptionsValidator(), scanBridgeFactory) { } @@ -33,49 +43,193 @@ internal ScanController(ScanningContext scanningContext, ILocalPostProcessor loc _scanBridgeFactory = scanBridgeFactory; } + /// + /// Gets a list of devices using the default driver for the system (WIA on Windows, Apple on Mac, SANE on Linux). + /// + /// The device list. public Task> GetDeviceList() => GetDeviceList(new ScanOptions()); + /// + /// Gets a list of devices using the specified driver. + /// + /// The driver to use. + /// The device list. public Task> GetDeviceList(Driver driver) => GetDeviceList(new ScanOptions { Driver = driver }); + /// + /// Gets a list of devices using the specified options. This is mainly just the driver but some other properties + /// may affect the list of devices (e.g. TwainOptions.Dsm). + /// + /// The options to use. + /// The device list. public async Task> GetDeviceList(ScanOptions options) { options = _scanOptionsValidator.ValidateAll(options, _scanningContext, false); var bridge = _scanBridgeFactory.Create(options); - return await bridge.GetDeviceList(options); + var devices = new List(); + await bridge.GetDevices(options, CancellationToken.None, devices.Add); + return devices; + } + + /// + /// Gets an enumerable of devices using the default driver for the system (WIA on Windows, Apple on Mac, SANE on + /// Linux). Depending on the driver this may yield devices incrementally or all at once. + /// + /// A token used to cancel the operation. + /// The device enumerable. + public IAsyncEnumerable GetDevices(CancellationToken cancelToken = default) => + GetDevices(new ScanOptions(), cancelToken); + + /// + /// Gets an enumerable of devices using the specified driver. Depending on the driver this may yield devices + /// incrementally or all at once. + /// + /// The driver to use. + /// A token used to cancel the operation. + /// The device enumerable. + public IAsyncEnumerable GetDevices(Driver driver, CancellationToken cancelToken = default) => + GetDevices(new ScanOptions { Driver = driver }, cancelToken); + + /// + /// Gets an enumerable of devices using the specified options. This is mainly just the driver but some other + /// properties may affect the list of devices (e.g. TwainOptions.Dsm). Depending on the driver this may yield + /// devices incrementally or all at once. + /// + /// The options to use. + /// A token used to cancel the operation. + /// The device enumerable. + public IAsyncEnumerable GetDevices(ScanOptions options, CancellationToken cancelToken = default) + { + options = _scanOptionsValidator.ValidateAll(options, _scanningContext, false); + var bridge = _scanBridgeFactory.Create(options); + return AsyncProducers.RunProducer(async produce => + { + await bridge.GetDevices(options, cancelToken, produce); + }); } + /// + /// Gets the capabilities of the scanning device. This includes valid values for scanning options and extra metadata + /// beyond just the device name and id. + /// + /// The device to query for capabilities. + /// A token used to cancel the operation. + /// + public Task GetCaps(ScanDevice device, CancellationToken cancelToken = default) => + GetCaps(new ScanOptions { Device = device }, cancelToken); + + /// + /// Gets the capabilities of the scanning device. This includes valid values for scanning options and extra metadata + /// beyond just the device name and id. + /// + /// The options to use. + /// A token used to cancel the operation. + /// + public async Task GetCaps(ScanOptions options, CancellationToken cancelToken = default) + { + options = _scanOptionsValidator.ValidateAll(options, _scanningContext, true); + var bridge = _scanBridgeFactory.Create(options); + return await bridge.GetCaps(options, cancelToken); + } + + /// + /// Scans using the specified options and returns an enumerable of the scanned images. + /// + /// If PropagateErrors is true (the default), enumerating the result will result in an error if there was a problem + /// scanning. For example, if one page was successfully scanned and then there was a paper jam, the first scanned + /// image will be yielded and then an exception will be thrown for the paper jam. Or if the scanner was offline an + /// exception will be thrown as soon as the caller tries to start enumerating the results. + /// + /// You can get detailed progress information by subscribing to the relevant ScanController events. + /// + /// The options to use. + /// A token used to cancel the operation. + /// The scanned images enumerable. public IAsyncEnumerable Scan(ScanOptions options, CancellationToken cancelToken = default) { options = _scanOptionsValidator.ValidateAll(options, _scanningContext, true); int pageNumber = 0; + Exception? scanError = null; void ScanStartCallback() => ScanStart?.Invoke(this, EventArgs.Empty); - void ScanEndCallback() => ScanEnd?.Invoke(this, EventArgs.Empty); - void ScanErrorCallback(Exception ex) => ScanError?.Invoke(this, new ScanErrorEventArgs(ex)); - void PageStartCallback() => PageStart?.Invoke(this, new PageStartEventArgs(++pageNumber)); - + void ScanEndCallback() => ScanEnd?.Invoke(this, new ScanEndEventArgs(scanError)); + void PageStartCallback() + { + pageNumber++; + PageStart?.Invoke(this, new PageStartEventArgs(pageNumber)); + } void PageProgressCallback(double progress) => PageProgress?.Invoke(this, new PageProgressEventArgs(pageNumber, progress)); - void PageEndCallback(ProcessedImage image) => PageEnd?.Invoke(this, new PageEndEventArgs(pageNumber, image)); + void ConnectionUriChangedCallback(string? iconUri, string? connectionUri) => + DeviceUriChanged?.Invoke(this, new DeviceUriChangedEventArgs(iconUri, connectionUri)); + _scanningContext.Logger.LogDebug("Scanning with {Device}", options.Device); + _scanningContext.Logger.LogDebug( + "Scan source: {Source}; bit depth: {BitDepth}; dpi: {Dpi}; page size: {PageSize}", + options.PaperSource, options.BitDepth, options.Dpi, options.PageSize); ScanStartCallback(); return AsyncProducers.RunProducer(async produceImage => { - try + var bridge = _scanBridgeFactory.Create(options); + + async Task DoScan(ScanOptions actualOptions) { - var bridge = _scanBridgeFactory.Create(options); - await bridge.Scan(options, cancelToken, new ScanEvents(PageStartCallback, PageProgressCallback), + await bridge.Scan(actualOptions, cancelToken, + new ScanEvents(PageStartCallback, PageProgressCallback, ConnectionUriChangedCallback), (image, postProcessingContext) => { - image = _localPostProcessor.PostProcess(image, options, postProcessingContext); + image = _localPostProcessor.PostProcess(image, actualOptions, postProcessingContext); produceImage(image); PageEndCallback(image); }); } + + try + { + try + { + await DoScan(options); + } + catch (DeviceOfflineException) when + (options.Driver == Driver.Sane && + (_scanningContext.WorkerFactory != null || PlatformCompat.System.IsLibUsbReliable)) + { + // Some SANE backends (e.g. airscan, genesys) have inconsistent IDs so "device offline" might actually + // just mean "device id has changed". We can query for a device that matches the name of the + // original device, and assume it's the same physical device, which should generally be correct. + // + // TODO: Ideally this would be contained within SaneScanDriver, but due to libusb's unreliability on + // macOS, we have to make sure each call is in a separate worker process. Makes me wonder if the + // scanning pipeline could be redesigned so that drivers have more control over worker processes. + _scanningContext.Logger.LogDebug( + "SANE Device appears offline; re-querying in case of ID change for name \"{Name}\"", + options.Device!.Name); + var getDevicesOptions = options.Clone(); + getDevicesOptions.SaneOptions.Backend = SaneScanDriver.GetBackend(options.Device!); + ScanDevice? matchingDevice = null; + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken); + await bridge.GetDevices(getDevicesOptions, cts.Token, device => + { + if (device.Name == options.Device!.Name) + { + matchingDevice = device; + cts.Cancel(); + } + }); + if (matchingDevice == null) + { + _scanningContext.Logger.LogDebug("No matching device found"); + throw; + } + var actualOptions = options.Clone(); + actualOptions.Device = matchingDevice; + await DoScan(actualOptions); + } + } catch (Exception ex) { - ScanErrorCallback(ex); + scanError = ex; if (PropagateErrors) { throw; @@ -89,20 +243,42 @@ void PageProgressCallback(double progress) => } /// - /// Whether scan errors should be thrown when enumerating the IAsyncEnumerable result. If false (the default), you - /// will need to listen for the ScanError event to handle errors. + /// Whether scan errors should be thrown when enumerating the IAsyncEnumerable result. True by default. If you set + /// this to false, you will need to listen for the ScanError event to handle errors. /// - public bool PropagateErrors { get; set; } + public bool PropagateErrors { get; set; } = true; + /// + /// Occurs when a Scan operation is about to start. + /// public event EventHandler? ScanStart; - public event EventHandler? ScanEnd; - - public event EventHandler? ScanError; + /// + /// Occurs when a Scan operation has completed. If the scan ends due to an error, the Error property will be set on + /// the event args. For more detailed diagnostics, set ScanningContext.Logger. + /// + public event EventHandler? ScanEnd; + /// + /// Occurs when scanning starts for a page. This can be called multiple times during a single Scan operation if it + /// is a feeder scanner. + /// public event EventHandler? PageStart; + /// + /// Occurs when the progress changes for scanning a page. + /// public event EventHandler? PageProgress; + /// + /// Occurs when scanning is done for a page and the scanned image is available. This can be called multiple times + /// during a single Scan operation if it is a feeder scanner. + /// public event EventHandler? PageEnd; + + /// + /// Occurs when an ESCL scan occurs and the device has a new icon or connection URI (e.g. the IP has changed). This + /// can be used to update the ScanDevice object for future scans. + /// + public event EventHandler? DeviceUriChanged; } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/ScanDevice.cs b/NAPS2.Sdk/Scan/ScanDevice.cs index 46529b82e5..cbb8a6ef42 100644 --- a/NAPS2.Sdk/Scan/ScanDevice.cs +++ b/NAPS2.Sdk/Scan/ScanDevice.cs @@ -1,27 +1,34 @@ -using System.Diagnostics.CodeAnalysis; - namespace NAPS2.Scan; /// -/// The representation of a scanning device identified by a driver. +/// The representation of a scanning device and its corresponding driver. /// -public class ScanDevice +public sealed record ScanDevice( + Driver Driver, + string ID, + string Name, + string? IconUri = null, + string? ConnectionUri = null) { - // Need an empty constructor for XML compatibility - public ScanDevice() + private ScanDevice() : this(Driver.Default, "", "") { } - [SetsRequiredMembers] - public ScanDevice(string id, string name) + public bool Equals(ScanDevice? other) { - ID = id; - Name = name; + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Driver == other.Driver && ID == other.ID && Name == other.Name; } - - // Keeping old naming scheme for XML compatibility - // ReSharper disable once InconsistentNaming - public required string ID { get; init; } - public required string Name { get; init; } + public override int GetHashCode() + { + unchecked + { + var hashCode = (int) Driver; + hashCode = (hashCode * 397) ^ ID.GetHashCode(); + hashCode = (hashCode * 397) ^ Name.GetHashCode(); + return hashCode; + } + } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/ScanEndEventArgs.cs b/NAPS2.Sdk/Scan/ScanEndEventArgs.cs new file mode 100644 index 0000000000..cb0bccedfc --- /dev/null +++ b/NAPS2.Sdk/Scan/ScanEndEventArgs.cs @@ -0,0 +1,16 @@ +namespace NAPS2.Scan; + +public class ScanEndEventArgs : EventArgs +{ + public ScanEndEventArgs(Exception? error = null) + { + Error = error; + } + + /// + /// Gets the exception that ended the scan, if any. + /// + public Exception? Error { get; } + + public bool HasError => Error != null; +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/ScanErrorEventArgs.cs b/NAPS2.Sdk/Scan/ScanErrorEventArgs.cs deleted file mode 100644 index dd8239fd38..0000000000 --- a/NAPS2.Sdk/Scan/ScanErrorEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace NAPS2.Scan; - -public class ScanErrorEventArgs : EventArgs -{ - public ScanErrorEventArgs(Exception exception) - { - Exception = exception; - } - - public Exception Exception { get; } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/ScanEvents.cs b/NAPS2.Sdk/Scan/ScanEvents.cs index 56b6988fd7..cd46e1b95e 100644 --- a/NAPS2.Sdk/Scan/ScanEvents.cs +++ b/NAPS2.Sdk/Scan/ScanEvents.cs @@ -4,15 +4,18 @@ namespace NAPS2.Scan; internal class ScanEvents : IScanEvents { - public static readonly IScanEvents Stub = new ScanEvents(() => { }, _ => { }); + public static readonly IScanEvents Stub = new ScanEvents(() => { }, _ => { }, (_, _) => { }); private readonly Action _pageStartCallback; private readonly Action _pageProgressCallback; + private readonly Action _deviceUriChangedCallback; - public ScanEvents(Action pageStartCallback, Action pageProgressCallback) + public ScanEvents(Action pageStartCallback, Action pageProgressCallback, + Action deviceUriChangedCallback) { _pageStartCallback = pageStartCallback; _pageProgressCallback = pageProgressCallback; + _deviceUriChangedCallback = deviceUriChangedCallback; } public void PageStart() @@ -24,4 +27,9 @@ public void PageProgress(double progress) { _pageProgressCallback(progress); } + + public void DeviceUriChanged(string? iconUri, string? connectionUri) + { + _deviceUriChangedCallback(iconUri, connectionUri); + } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/ScanOptions.cs b/NAPS2.Sdk/Scan/ScanOptions.cs index 5461356319..8125889042 100644 --- a/NAPS2.Sdk/Scan/ScanOptions.cs +++ b/NAPS2.Sdk/Scan/ScanOptions.cs @@ -1,35 +1,86 @@ using NAPS2.Ocr; +using NAPS2.Serialization; namespace NAPS2.Scan; -// TODO: We can probably make this an immutable record +/// +/// Represents the options used for scanning. +/// public class ScanOptions { + public const int DEFAULT_QUALITY = 75; + + /// + /// The driver type used for scanning. Supported drivers depend on the platform (Windows/Mac/Linux). This usually + /// doesn't need to be set as it can be determined from the Device property. + /// public Driver Driver { get; set; } + /// + /// The physical device to scan with. + /// public ScanDevice? Device { get; set; } + /// + /// For scanners with multiple ways to scan (flatbed/feeder/duplex), specifies which source to use. + /// public PaperSource PaperSource { get; set; } + /// + /// The resolution to scan with, in dots-per-inch (DPI). Typical values include 100, 300, 600, 1200. + /// public int Dpi { get; set; } + /// + /// A factor used to shrink the scanned image. A factor of 2 means to shrink the image by 2x. + /// public int ScaleRatio { get; set; } + /// + /// The size of the page to be scanned. + /// public PageSize? PageSize { get; set; } // TODO: Use this as threshold for B/W scans + /// + /// A brightness adjustment to be used for the scan, in the range -1000 (all black) to +1000 (all white). + /// public int Brightness { get; set; } + /// + /// A contrast adjustment to be used for the scan, in the range -1000 (no contrast) to +1000 (max contrast). + /// public int Contrast { get; set; } - public NetworkOptions NetworkOptions { get; set; } = new(); - + /// + /// Options specific to the WIA driver. + /// public WiaOptions WiaOptions { get; set; } = new(); + /// + /// Options specific to the TWAIN driver. + /// public TwainOptions TwainOptions { get; set; } = new(); + /// + /// Options specific to the SANE driver. + /// public SaneOptions SaneOptions { get; set; } = new(); + /// + /// Options specific to the ESCL driver. + /// + public EsclOptions EsclOptions { get; set; } = new(); + + /// + /// When querying devices, ignore networked devices that are at the same IP as the current computer (so that devices + /// shared with ScanServer aren't given duplicate entries). + /// + public bool ExcludeLocalIPs { get; set; } + + /// + /// Options for detecting barcodes during the scan. + /// public BarcodeDetectionOptions BarcodeDetectionOptions { get; set; } = new(); // TODO: Add another option (maybe default on) to wait for the OCR results and include them in postprocessingdata @@ -54,24 +105,49 @@ public class ScanOptions public bool ExcludeBlankPages { get; set; } - public int BlankPageWhiteThreshold { get; set; } + public int BlankPageWhiteThreshold { get; set; } = 70; - public int BlankPageCoverageThreshold { get; set; } + public int BlankPageCoverageThreshold { get; set; } = 15; + /// + /// Whether scanned images should be stored using maximum (lossless) quality. Otherwise images are generally stored + /// using JPEG compression. + /// public bool MaxQuality { get; set; } - public int Quality { get; set; } + /// + /// The JPEG compression quality used for storing images. Ignored if MaxQuality is true. + /// + public int Quality { get; set; } = DEFAULT_QUALITY; + /// + /// If non-null, generates thumbnails of the specified size and provides them in the PostProcessingData of each + /// scanned image. + /// public int? ThumbnailSize { get; set; } + /// + /// Whether scanned images should go through automatic deskewing to straighten pages that are at a slight angle. + /// public bool AutoDeskew { get; set; } + /// + /// A fixed number of degrees to rotate each scanned page clockwise. If AutoDeskew is true, the fixed rotation + /// happens first. + /// + public double RotateDegrees { get; set; } + + /// + /// Compatibility option to correct problems with some scanners. If this is true and the PaperSource is Duplex, + /// even-numbered pages are flipped vertically. + /// public bool FlipDuplexedPages { get; set; } -} -public enum HorizontalAlign -{ - Right, - Center, - Left + public KeyValueScanOptions KeyValueOptions { get; set; } = new(); + + public ScanOptions Clone() + { + // Easy deep copy. Ideally we'd do this in a more efficient way. + return this.ToXml().FromXml(); + } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/ScanningContext.cs b/NAPS2.Sdk/Scan/ScanningContext.cs index daeea05bd7..aea542e06e 100644 --- a/NAPS2.Sdk/Scan/ScanningContext.cs +++ b/NAPS2.Sdk/Scan/ScanningContext.cs @@ -1,91 +1,107 @@ using System.Collections.Immutable; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using NAPS2.Ocr; using NAPS2.Remoting.Worker; namespace NAPS2.Scan; +/// +/// A ScanningContext object is needed for most NAPS2 operations. Set it up with the corresponding ImageContext type +/// for image type you expect (e.g. GdiImageContext for System.Drawing.Bitmap, if you're using Windows Forms). You can +/// also set various other properties that affect scanning and image processing. +/// +/// When the ScanningContext is disposed, all ProcessedImage objects that were generating from scanning or importing +/// with that ScanningContext object will be automatically disposed. +/// public class ScanningContext : IDisposable { private readonly ProcessedImageOwner _processedImageOwner = new(); - // TODO: Make sure properties are initialized by callers (or something equivalent) + /// + /// Initializes a new instance of the ScanningContext class with the specified ImageContext. + /// + /// The corresponding ImageContext type for the image type you expect (e.g. + /// GdiImageContext for System.Drawing.Bitmap, if you're using Windows Forms). public ScanningContext(ImageContext imageContext) { ImageContext = imageContext; } - public ScanningContext(ImageContext imageContext, FileStorageManager fileStorageManager) - { - ImageContext = imageContext; - FileStorageManager = fileStorageManager; - } - - public ScanningContext(ImageContext imageContext, FileStorageManager fileStorageManager, IOcrEngine ocrEngine, - IWorkerFactory workerFactory) - { - ImageContext = imageContext; - FileStorageManager = fileStorageManager; - OcrEngine = ocrEngine; - WorkerFactory = workerFactory; - } - - // TODO: Figure out initialization etc. + /// + /// Gets the context's ImageContext. This corresponds to the image type used (e.g. GdiImageContext for + /// System.Drawing.Bitmap, if you're using Windows Forms). + /// public ImageContext ImageContext { get; } + /// + /// Gets or sets the context's FileStorageManager. If non-null, ProcessedImage objects from scanning or importing + /// with this ScanningContext will store the actual image data on disk instead of in memory. + /// public FileStorageManager? FileStorageManager { get; set; } - public string TempFolderPath { get; set; } = Path.GetTempPath(); + /// + /// Gets or sets the context's WorkerFactory. This is required for some operations that need to happen in a worker + /// process (e.g. to scan with 32-bit TWAIN from a 64-bit process). + /// + internal IWorkerFactory? WorkerFactory { get; set; } - public string? RecoveryPath { get; set; } + /// + /// Gets or sets the context's OcrEngine. This is used to perform the OCR (optical character recognition) operation + /// if OCR is requested for PDF export. + /// + public IOcrEngine? OcrEngine { get; set; } - public IWorkerFactory? WorkerFactory { get; set; } + /// + /// Gets or sets the path to a temp folder where transient files can be stored. Defaults to Path.GetTempPath(). + /// + public string TempFolderPath { get; set; } = Path.GetTempPath(); - public OcrRequestQueue OcrRequestQueue { get; } = new(); + /// + /// Gets or sets the logger used for detailed diagnostics. + /// + public ILogger Logger { get; set; } = NullLogger.Instance; - public IOcrEngine? OcrEngine { get; set; } + internal string? RecoveryPath { get; set; } - public ProcessedImage CreateProcessedImage(IImageStorage storage) - { - return CreateProcessedImage(storage, Enumerable.Empty()); - } + internal OcrRequestQueue OcrRequestQueue { get; } = new(); - public ProcessedImage CreateProcessedImage(IImageStorage storage, IEnumerable transforms) + public void Dispose() { - var bitDepth = storage switch - { - IMemoryImage { LogicalPixelFormat: ImagePixelFormat.BW1 } => BitDepth.BlackAndWhite, - IMemoryImage { LogicalPixelFormat: ImagePixelFormat.Gray8 } => BitDepth.Grayscale, - _ => BitDepth.Color - }; - return CreateProcessedImage(storage, bitDepth, false, -1, transforms); + _processedImageOwner.Dispose(); + FileStorageManager?.Dispose(); } - public ProcessedImage CreateProcessedImage(IImageStorage storage, BitDepth bitDepth, bool lossless, int quality) + internal WorkerContext? CreateWorker(WorkerType workerType) { - return CreateProcessedImage(storage, bitDepth, lossless, quality, Enumerable.Empty()); + return WorkerFactory?.Create(this, workerType); } - public ProcessedImage CreateProcessedImage(IImageStorage storage, BitDepth bitDepth, bool lossless, int quality, - IEnumerable transforms) + internal ProcessedImage CreateProcessedImage(IImageStorage storage, bool lossless = false, int quality = -1, + PageSize? pageSize = null, IEnumerable? transforms = null, + RefCount? refCount = null) { - var convertedStorage = ConvertStorageIfNeeded(storage, bitDepth, lossless, quality); - var metadata = new ImageMetadata(bitDepth, lossless); + var convertedStorage = ConvertStorageIfNeeded(storage, lossless, quality); + var metadata = new ImageMetadata(lossless, pageSize); + var postProcessingData = new PostProcessingData(); + refCount ??= + new RefCount( + new ProcessedImage.InternalDisposer(convertedStorage, postProcessingData, _processedImageOwner)); var image = new ProcessedImage( ImageContext, convertedStorage, metadata, - new PostProcessingData(), - new TransformState(transforms.ToImmutableList()), - _processedImageOwner); + postProcessingData, + new TransformState(transforms?.ToImmutableList() ?? []), + refCount); return image; } - private IImageStorage ConvertStorageIfNeeded(IImageStorage storage, BitDepth bitDepth, bool lossless, int quality) + private IImageStorage ConvertStorageIfNeeded(IImageStorage storage, bool lossless, int quality) { - // TODO: We should revisit existing tests and make sure we have coverage for both filestorage and non if (FileStorageManager != null) { - return ConvertToFileStorage(storage, bitDepth, lossless, quality); + return ConvertToFileStorage(storage, lossless, quality); } return ConvertToMemoryStorage(storage); } @@ -110,12 +126,12 @@ private IImageStorage ConvertToMemoryStorage(IImageStorage storage) } } - private IImageStorage ConvertToFileStorage(IImageStorage storage, BitDepth bitDepth, bool lossless, int quality) + private IImageStorage ConvertToFileStorage(IImageStorage storage, bool lossless, int quality) { switch (storage) { case IMemoryImage image: - return WriteImageToBackingFile(image, bitDepth, lossless, quality); + return WriteImageToBackingFile(image, lossless, quality); case ImageFileStorage fileStorage: return fileStorage; case ImageMemoryStorage memoryStorage: @@ -125,7 +141,7 @@ private IImageStorage ConvertToFileStorage(IImageStorage storage, BitDepth bitDe } // TODO: Can we just write this to a file directly? Is there any case where SaveSmallestFormat is really needed? var loadedImage = ImageContext.Load(memoryStorage.Stream); - return WriteImageToBackingFile(loadedImage, bitDepth, lossless, quality); + return WriteImageToBackingFile(loadedImage, lossless, quality); default: // The only case that should hit this is a test with a mock return storage; @@ -144,35 +160,27 @@ private ImageFileStorage WriteDataToBackingFile(MemoryStream stream, string ext) return new ImageFileStorage(path, false); } - private IImageStorage WriteImageToBackingFile(IMemoryImage image, BitDepth bitDepth, bool lossless, int quality) + private IImageStorage WriteImageToBackingFile(IMemoryImage image, bool lossless, int quality) { if (FileStorageManager == null) { throw new InvalidOperationException(); } var path = FileStorageManager.NextFilePath(); - var fullPath = new ImageExportHelper() - .SaveSmallestFormat(path, image, bitDepth, lossless, quality, out _); + var fullPath = ImageExportHelper.SaveSmallestFormat(path, image, lossless, quality, out _); return new ImageFileStorage(fullPath, false); } - public string SaveToTempFile(IMemoryImage image, BitDepth bitDepth = BitDepth.Color) + internal string SaveToTempFile(IMemoryImage image) { var path = Path.Combine(TempFolderPath, Path.GetRandomFileName()); - return new ImageExportHelper() - .SaveSmallestFormat(path, image, bitDepth, false, -1, out _); + return ImageExportHelper.SaveSmallestFormat(path, image, false, -1, out _); } - public string SaveToTempFile(ProcessedImage image, BitDepth bitDepth = BitDepth.Color) + internal string SaveToTempFile(ProcessedImage image) { using var rendered = image.Render(); - return SaveToTempFile(rendered, bitDepth); - } - - public void Dispose() - { - _processedImageOwner.Dispose(); - FileStorageManager?.Dispose(); + return SaveToTempFile(rendered); } private class ProcessedImageOwner : IProcessedImageOwner, IDisposable diff --git a/NAPS2.Sdk/Scan/ScanningContextExtensions.cs b/NAPS2.Sdk/Scan/ScanningContextExtensions.cs new file mode 100644 index 0000000000..113bb8655b --- /dev/null +++ b/NAPS2.Sdk/Scan/ScanningContextExtensions.cs @@ -0,0 +1,22 @@ +using NAPS2.Remoting.Worker; + +namespace NAPS2.Scan; + +public static class ScanningContextExtensions +{ + /// + /// If you have installed the NAPS2.Sdk.Worker.Win32 Nuget package, call this method to set up the worker on the + /// ScanningContext. This will allow 32-bit TWAIN drivers to be used. + /// + public static void SetUpWin32Worker(this ScanningContext scanningContext) + { + var workerFactory = WorkerFactory.CreateDefault(); + if (!File.Exists(workerFactory.WinX86WorkerExePath)) + { + throw new InvalidOperationException( + "Could not find NAPS2.Worker.exe; have you installed the NAPS2.Sdk.Worker.Win32 Nuget package?"); + } + workerFactory.Init(scanningContext); + scanningContext.WorkerFactory = workerFactory; + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/TwainOptions.cs b/NAPS2.Sdk/Scan/TwainOptions.cs index 07f521fa66..869f32c6d8 100644 --- a/NAPS2.Sdk/Scan/TwainOptions.cs +++ b/NAPS2.Sdk/Scan/TwainOptions.cs @@ -1,5 +1,8 @@ namespace NAPS2.Scan; +/// +/// Scanning options specific to the TWAIN driver. +/// public class TwainOptions { /// @@ -10,18 +13,10 @@ public class TwainOptions public TwainDsm Dsm { get; set; } /// - /// The adapter used for TWAIN, either the modern NTwain or the Legacy implementation used in very old versions of - /// NAPS2. NTwain is better for the vast majority of scanners, but a few (e.g. Kyocera brand) may only work with - /// Legacy for unknown reasons. - /// - public TwainAdapter Adapter { get; set; } - - /// - /// The transfer mode used for TWAIN, either Native or Memory. By default Native is used, but Memory might have - /// work better with some scanners. + /// The transfer mode used for TWAIN, either Native or Memory. By default Memory is used. /// public TwainTransferMode TransferMode { get; set; } - + /// /// Whether to show the TWAIN progress UI. This only matters when ScanOptions.UseNativeUI is false (otherwise the /// full UI is shown regardless). @@ -36,22 +31,46 @@ public class TwainOptions public bool IncludeWiaDevices { get; set; } } -public enum TwainAdapter -{ - NTwain, - Legacy -} - +/// +/// The data source manager (DSM) to use for TWAIN. +/// public enum TwainDsm { + /// + /// The modern 32-bit twaindsm.dll. Recommended. + /// New, - // TODO: Consider dropping support for x64 twain, it's not tested and I don't think anyone can use it anyway + + /// + /// The modern 64-bit twaindsm.dll. Choose this if you want to use a 64-bit TWAIN data source. + /// NewX64, + + /// + /// The old 32-bit twain32.dll. Some data sources have compatibility issues with the newer DSM. + /// Old } +/// +/// The transfer mode to use for TWAIN. +/// public enum TwainTransferMode { - Native, - Memory + /// + /// Transfers the image using the recommended mode. Usually this is Memory, but some scanner drivers have known bugs + /// with that mode, and where we can detect that we use Native instead. + /// + Default, + + /// + /// Transfers the image in strips. + /// + Memory, + + /// + /// Transfers the entire image at once. This may fail with very high-resolution images if they exceed the memory + /// limits of the 32-bit worker. + /// + Native } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/WiaOptions.cs b/NAPS2.Sdk/Scan/WiaOptions.cs index 656b4a4dfa..09c32d6837 100644 --- a/NAPS2.Sdk/Scan/WiaOptions.cs +++ b/NAPS2.Sdk/Scan/WiaOptions.cs @@ -1,5 +1,8 @@ namespace NAPS2.Scan; +/// +/// Scanning options specific to the WIA driver. +/// public class WiaOptions { public WiaApiVersion WiaApiVersion { get; set; } diff --git a/NAPS2.Sdk/Scan/WiaVersion.cs b/NAPS2.Sdk/Scan/WiaVersion.cs index d1fb4aa178..2792b8175b 100644 --- a/NAPS2.Sdk/Scan/WiaVersion.cs +++ b/NAPS2.Sdk/Scan/WiaVersion.cs @@ -1,5 +1,8 @@ namespace NAPS2.Scan; +/// +/// WIA version used for scanning (1.0 or 2.0). Generally 2.0 is preferred as it has better support for feeders. +/// public enum WiaApiVersion { Default, diff --git a/NAPS2.Sdk/Serialization/DeserializeImageOptions.cs b/NAPS2.Sdk/Serialization/DeserializeImageOptions.cs index 5e6c3d5be9..04ef40d18f 100644 --- a/NAPS2.Sdk/Serialization/DeserializeImageOptions.cs +++ b/NAPS2.Sdk/Serialization/DeserializeImageOptions.cs @@ -1,6 +1,6 @@ namespace NAPS2.Serialization; -public class DeserializeImageOptions +internal class DeserializeImageOptions { /// /// If true, the Deserialize caller guarantees that the file storage will not be used for longer than the duration of the RPC call. diff --git a/NAPS2.Sdk/Serialization/ImageSerializer.cs b/NAPS2.Sdk/Serialization/ImageSerializer.cs index b1cae97da9..3fc0b4cfd3 100644 --- a/NAPS2.Sdk/Serialization/ImageSerializer.cs +++ b/NAPS2.Sdk/Serialization/ImageSerializer.cs @@ -3,7 +3,7 @@ namespace NAPS2.Serialization; -public static class ImageSerializer +internal static class ImageSerializer { public static SerializedImage Serialize(ProcessedImage image, SerializeImageOptions options) { @@ -37,11 +37,11 @@ public static SerializedImage Serialize(ProcessedImage image, SerializeImageOpti Metadata = new SerializedImageMetadata { TransformListXml = image.TransformState.Transforms.ToXml(), - BitDepth = (SerializedImageMetadata.Types.BitDepth) image.Metadata.BitDepth, - Lossless = image.Metadata.Lossless + Lossless = image.Metadata.Lossless, + PageSize = image.Metadata.PageSize?.ToString() ?? "" }, Thumbnail = thumbStream != null ? ByteString.FromStream(thumbStream) : ByteString.Empty, - BarcodeDetectionXml = image.PostProcessingData.BarcodeDetection?.ToXml(), + BarcodeDetectionXml = image.PostProcessingData.Barcode?.ToXml(), RenderedFilePath = options.RenderedFilePath ?? "" }; @@ -65,7 +65,7 @@ public static SerializedImage Serialize(ProcessedImage image, SerializeImageOpti result.TypeHint = memoryStorage.TypeHint; break; case IMemoryImage imageStorage: - var fileFormat = imageStorage.OriginalFileFormat == ImageFileFormat.Unspecified + var fileFormat = imageStorage.OriginalFileFormat == ImageFileFormat.Unknown ? ImageFileFormat.Jpeg : imageStorage.OriginalFileFormat; result.FileContent = ByteString.FromStream(imageStorage.SaveToMemoryStream(fileFormat)); @@ -141,9 +141,9 @@ public static ProcessedImage Deserialize(ScanningContext scanningContext, Serial var processedImage = scanningContext.CreateProcessedImage( storage, - (BitDepth) serializedImage.Metadata.BitDepth, serializedImage.Metadata.Lossless, -1, + PageSize.Parse(serializedImage.Metadata.PageSize), serializedImage.Metadata.TransformListXml.FromXml>()); var thumbnail = serializedImage.Thumbnail.ToByteArray(); @@ -159,7 +159,7 @@ public static ProcessedImage Deserialize(ScanningContext scanningContext, Serial { processedImage = processedImage.WithPostProcessingData(processedImage.PostProcessingData with { - BarcodeDetection = serializedImage.BarcodeDetectionXml.FromXml() + Barcode = serializedImage.BarcodeDetectionXml.FromXml() }, true); } return processedImage; diff --git a/NAPS2.Sdk/Serialization/SerializeImageOptions.cs b/NAPS2.Sdk/Serialization/SerializeImageOptions.cs index d9ea617515..8853223638 100644 --- a/NAPS2.Sdk/Serialization/SerializeImageOptions.cs +++ b/NAPS2.Sdk/Serialization/SerializeImageOptions.cs @@ -1,6 +1,6 @@ namespace NAPS2.Serialization; -public class SerializeImageOptions +internal class SerializeImageOptions { /// /// Indicates that, when the serialized image is transferred with file-based storage, the file should be considered diff --git a/NAPS2.Sdk/Serialization/SerializedImage.proto b/NAPS2.Sdk/Serialization/SerializedImage.proto index 5990fea761..4eb52bd590 100644 --- a/NAPS2.Sdk/Serialization/SerializedImage.proto +++ b/NAPS2.Sdk/Serialization/SerializedImage.proto @@ -22,6 +22,8 @@ message SerializedImageMetadata { Grayscale = 1; BlackAndWhite = 2; } - BitDepth bitDepth = 2; bool lossless = 3; + string pageSize = 4; + + reserved 2; } diff --git a/NAPS2.Sdk/Threading/DefaultInvoker.cs b/NAPS2.Sdk/Threading/DefaultInvoker.cs index 1b7a89690c..2a012a5456 100644 --- a/NAPS2.Sdk/Threading/DefaultInvoker.cs +++ b/NAPS2.Sdk/Threading/DefaultInvoker.cs @@ -3,11 +3,11 @@ /// /// A default implementation for synchronized access to the UI thread that assumes there is no privileged thread. /// -public class DefaultInvoker : IInvoker +internal class DefaultInvoker : IInvoker { public void Invoke(Action action) => action(); - public void SafeInvoke(Action action) => action(); + public void InvokeDispatch(Action action) => Task.Run(action); public T InvokeGet(Func func) => func(); } \ No newline at end of file diff --git a/NAPS2.Sdk/Threading/IInvoker.cs b/NAPS2.Sdk/Threading/IInvoker.cs index d754293fa9..562266e252 100644 --- a/NAPS2.Sdk/Threading/IInvoker.cs +++ b/NAPS2.Sdk/Threading/IInvoker.cs @@ -3,11 +3,25 @@ /// /// An interface for synchronized access to the UI thread. /// -public interface IInvoker +internal interface IInvoker { + /// + /// Run an action on the UI thread, waiting for completion before returning. + /// + /// void Invoke(Action action); - void SafeInvoke(Action action); + /// + /// Start running an action on the UI thread and immediately return. + /// + /// + void InvokeDispatch(Action action); + /// + /// Run a function on the UI thread, wait for its result, then return that result. + /// + /// + /// + /// T InvokeGet(Func func); } \ No newline at end of file diff --git a/NAPS2.Sdk/Threading/Invoker.cs b/NAPS2.Sdk/Threading/Invoker.cs index 92ed7433ab..4f11e85c52 100644 --- a/NAPS2.Sdk/Threading/Invoker.cs +++ b/NAPS2.Sdk/Threading/Invoker.cs @@ -4,7 +4,7 @@ /// /// Synchronized access to the UI thread. /// -public static class Invoker +internal static class Invoker { private static IInvoker _current = new DefaultInvoker(); diff --git a/NAPS2.Sdk/Threading/Pipeline.cs b/NAPS2.Sdk/Threading/Pipeline.cs index 628e51deed..6c82963184 100644 --- a/NAPS2.Sdk/Threading/Pipeline.cs +++ b/NAPS2.Sdk/Threading/Pipeline.cs @@ -16,7 +16,7 @@ namespace NAPS2.Threading; /// .Run(OutputText); /// /// -public static class Pipeline +internal static class Pipeline { private const int DEFAULT_MAX_PARALLELISM = 8; diff --git a/NAPS2.Sdk/Threading/SyncContextInvoker.cs b/NAPS2.Sdk/Threading/SyncContextInvoker.cs deleted file mode 100644 index 2bf6464ce0..0000000000 --- a/NAPS2.Sdk/Threading/SyncContextInvoker.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Threading; - -namespace NAPS2.Threading; - -public class SyncContextInvoker : IInvoker -{ - private readonly SynchronizationContext _current; - - public SyncContextInvoker(SynchronizationContext current) - { - _current = current; - } - - public void Invoke(Action action) - { - _current.Send(_ => action(), null); - } - - public void SafeInvoke(Action action) - { - _current.Send(_ => action(), null); - } - - public T InvokeGet(Func func) - { - T value = default!; - _current.Send(_ => value = func(), null); - return value; - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Unmanaged/NativeLibrary.cs b/NAPS2.Sdk/Unmanaged/NativeLibrary.cs index 738be846d8..85cc4b4179 100644 --- a/NAPS2.Sdk/Unmanaged/NativeLibrary.cs +++ b/NAPS2.Sdk/Unmanaged/NativeLibrary.cs @@ -2,7 +2,7 @@ namespace NAPS2.Unmanaged; -public class NativeLibrary +internal class NativeLibrary { public static string FindLibraryPath(string libraryName, string? baseFolder = null) => FindPath(libraryName, baseFolder, PlatformCompat.System.LibrarySearchPaths); @@ -26,10 +26,11 @@ private static string FindPath(string libraryName, string? baseFolder, string[] } } } - // TODO: Maybe do this for some platforms? - // var expectedPath = - // Path.Combine(AssemblyHelper.LibFolder, PlatformCompat.System.LibrarySearchPaths[0], libraryName); - // throw new Exception($"Library does not exist: {expectedPath}"); + if (baseFolder != null) + { + // In tests we definitely expect to find this. + throw new Exception($"Could not find '{libraryName}' in '{baseFolder}'"); + } // Just the library name so it uses the system search paths return libraryName; } diff --git a/NAPS2.Sdk/Util/AssemblyHelper.cs b/NAPS2.Sdk/Util/AssemblyHelper.cs index dcd4403cb3..a99a961944 100644 --- a/NAPS2.Sdk/Util/AssemblyHelper.cs +++ b/NAPS2.Sdk/Util/AssemblyHelper.cs @@ -2,9 +2,9 @@ namespace NAPS2.Util; -public class AssemblyHelper +internal class AssemblyHelper { - public const string COPYRIGHT_YEARS = "2009, 2012-2022"; + public const string COPYRIGHT_YEARS = "2009-2025"; public static string GetFolder(Assembly? assembly) { @@ -20,12 +20,28 @@ public static string LibFolder } } - // We can't use the assembly location, see - // https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview?tabs=cli#api-incompatibility - public static string EntryFolder { get; } = AppContext.BaseDirectory; + public static string EntryFolder { get; } + + static AssemblyHelper() + { + // We can't use the assembly location, see + // https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview?tabs=cli#api-incompatibility + EntryFolder = AppContext.BaseDirectory; + + // If this is inside the "lib" subfolder, the base path needs to be the parent folder + if (EntryFolder.EndsWith(@"\lib\")) + { + EntryFolder = EntryFolder.Substring(0, EntryFolder.Length - 4); + } + } public static string EntryFile => - Assembly.GetEntryAssembly()?.Location ?? throw new InvalidOperationException("No entry file"); +#if NET6_0_OR_GREATER + Environment.ProcessPath +#else + Assembly.GetEntryAssembly()?.Location +#endif + ?? throw new InvalidOperationException("No entry file"); private static string GetAssemblyAttributeValue(Func selector) { @@ -39,7 +55,7 @@ private static string GetAssemblyAttributeValue(Func selector) public static string Title => GetAssemblyAttributeValue(x => x.Title); - public static string Version => Assembly.GetEntryAssembly()!.GetName().Version!.ToString(); + public static Version Version => Assembly.GetEntryAssembly()!.GetName().Version!; public static string Description => GetAssemblyAttributeValue(x => x.Description); diff --git a/NAPS2.Sdk/Util/CompilerAttributes.cs b/NAPS2.Sdk/Util/CompilerAttributes.cs deleted file mode 100644 index 8230a289b1..0000000000 --- a/NAPS2.Sdk/Util/CompilerAttributes.cs +++ /dev/null @@ -1,61 +0,0 @@ -// https://sergiopedri.medium.com/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb -// ReSharper disable once CheckNamespace - -namespace System.Runtime.CompilerServices -{ - internal static class IsExternalInit - { - } - - /// Specifies that a type has required members or that a member is required. - [AttributeUsage( - AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, - AllowMultiple = false, Inherited = false)] - internal sealed class RequiredMemberAttribute : Attribute - { - } - - /// - /// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. - /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] - internal sealed class CompilerFeatureRequiredAttribute : Attribute - { - public CompilerFeatureRequiredAttribute(string featureName) - { - FeatureName = featureName; - } - - /// - /// The name of the compiler feature. - /// - public string FeatureName { get; } - - /// - /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . - /// - public bool IsOptional { get; init; } - - /// - /// The used for the ref structs C# feature. - /// - public const string RefStructs = nameof(RefStructs); - - /// - /// The used for the required members C# feature. - /// - public const string RequiredMembers = nameof(RequiredMembers); - } -} - -namespace System.Diagnostics.CodeAnalysis -{ - /// - /// Specifies that this constructor sets all required members for the current type, and callers - /// do not need to set any required members themselves. - /// - [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] - internal sealed class SetsRequiredMembersAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Util/DebugTimer.cs b/NAPS2.Sdk/Util/DebugTimer.cs index 28f97bae42..5c7a1c24bf 100644 --- a/NAPS2.Sdk/Util/DebugTimer.cs +++ b/NAPS2.Sdk/Util/DebugTimer.cs @@ -1,6 +1,6 @@ namespace NAPS2.Util; -public class DebugTimer : IDisposable +internal class DebugTimer : IDisposable { private readonly string? _label; private readonly Stopwatch _stopwatch; diff --git a/NAPS2.Sdk/Util/DeferredAction.cs b/NAPS2.Sdk/Util/DeferredAction.cs index 4c68d8256b..712357df73 100644 --- a/NAPS2.Sdk/Util/DeferredAction.cs +++ b/NAPS2.Sdk/Util/DeferredAction.cs @@ -1,6 +1,6 @@ namespace NAPS2.Util; -public class DeferredAction +internal class DeferredAction { private readonly Action _action; private int _counter; diff --git a/NAPS2.Sdk/Util/EndianReader.cs b/NAPS2.Sdk/Util/EndianReader.cs new file mode 100644 index 0000000000..067db8a0d1 --- /dev/null +++ b/NAPS2.Sdk/Util/EndianReader.cs @@ -0,0 +1,27 @@ +using System.Buffers.Binary; + +namespace NAPS2.Util; + +internal class EndianReader +{ + private readonly bool _reverseEndianness; + + public EndianReader(bool reverseEndianness) + { + _reverseEndianness = reverseEndianness; + } + + public int ReadInt16(byte[] buf, int offset) + { + var value = BitConverter.ToInt16(buf, offset); + if (_reverseEndianness) value = BinaryPrimitives.ReverseEndianness(value); + return value; + } + + public int ReadInt32(byte[] buf, int offset) + { + var value = BitConverter.ToInt32(buf, offset); + if (_reverseEndianness) value = BinaryPrimitives.ReverseEndianness(value); + return value; + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Util/EventThrottle.cs b/NAPS2.Sdk/Util/EventThrottle.cs index 11dea0e29f..d036349ee0 100644 --- a/NAPS2.Sdk/Util/EventThrottle.cs +++ b/NAPS2.Sdk/Util/EventThrottle.cs @@ -2,7 +2,7 @@ namespace NAPS2.Util; -public class EventThrottle +internal class EventThrottle { private readonly Action _eventCallback; @@ -15,6 +15,11 @@ public EventThrottle(Action eventCallback) _eventCallback = eventCallback; } + public void Reset() + { + _hasLastValue = false; + } + public void OnlyIfChanged(T value) { if (!_hasLastValue) diff --git a/NAPS2.Sdk/Util/ExpFallback.cs b/NAPS2.Sdk/Util/ExpFallback.cs index bfec53348d..ff635e79ea 100644 --- a/NAPS2.Sdk/Util/ExpFallback.cs +++ b/NAPS2.Sdk/Util/ExpFallback.cs @@ -2,7 +2,7 @@ namespace NAPS2.Util; -public class ExpFallback +internal class ExpFallback { public ExpFallback(int min, int max) { @@ -17,6 +17,8 @@ public ExpFallback(int min, int max) public int Value { get; private set; } + public bool IsAtMax => Value == Max; + public void Reset() { Value = Min; diff --git a/NAPS2.Sdk/Util/FileSystemHelper.cs b/NAPS2.Sdk/Util/FileSystemHelper.cs index b0dc23e985..57c3217d4d 100644 --- a/NAPS2.Sdk/Util/FileSystemHelper.cs +++ b/NAPS2.Sdk/Util/FileSystemHelper.cs @@ -1,6 +1,6 @@ namespace NAPS2.Util; -public static class FileSystemHelper +internal static class FileSystemHelper { /// /// Creates the parent directory for the provided path if needed. @@ -34,4 +34,38 @@ public static bool IsFileInUse(string filePath, out Exception? exception) exception = null; return false; } + + // https://learn.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories + public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive) + { + // Get information about the source directory + var dir = new DirectoryInfo(sourceDir); + + // Check if the source directory exists + if (!dir.Exists) + throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); + + // Cache directories before we start copying + DirectoryInfo[] dirs = dir.GetDirectories(); + + // Create the destination directory + Directory.CreateDirectory(destinationDir); + + // Get the files in the source directory and copy to the destination directory + foreach (FileInfo file in dir.GetFiles()) + { + string targetFilePath = Path.Combine(destinationDir, file.Name); + file.CopyTo(targetFilePath); + } + + // If recursive and copying subdirectories, recursively call this method + if (recursive) + { + foreach (DirectoryInfo subDir in dirs) + { + string newDestinationDir = Path.Combine(destinationDir, subDir.Name); + CopyDirectory(subDir.FullName, newDestinationDir, true); + } + } + } } \ No newline at end of file diff --git a/NAPS2.Sdk/Util/NaturalStringComparer.cs b/NAPS2.Sdk/Util/NaturalStringComparer.cs index 30c37a77dd..ab36b596ba 100644 --- a/NAPS2.Sdk/Util/NaturalStringComparer.cs +++ b/NAPS2.Sdk/Util/NaturalStringComparer.cs @@ -1,6 +1,6 @@ namespace NAPS2.Util; -public class NaturalStringComparer : IComparer +internal class NaturalStringComparer : IComparer { public int Compare(string? x, string? y) { diff --git a/NAPS2.Sdk/Util/NumberExtensions.cs b/NAPS2.Sdk/Util/NumberExtensions.cs deleted file mode 100644 index 973f9c0876..0000000000 --- a/NAPS2.Sdk/Util/NumberExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace NAPS2.Util; - -public static class NumberExtensions -{ - /// - /// Ensures the provided value is within the provided range (inclusive). - /// - /// - /// - /// - /// - /// - public static T Clamp(this T val, T min, T max) where T : IComparable - { - if (val.CompareTo(min) < 0) - { - return min; - } - if (val.CompareTo(max) > 0) - { - return max; - } - return val; - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Util/TimedCache.cs b/NAPS2.Sdk/Util/TimedCache.cs new file mode 100644 index 0000000000..e1a9b5edd5 --- /dev/null +++ b/NAPS2.Sdk/Util/TimedCache.cs @@ -0,0 +1,124 @@ +using System.Threading; + +namespace NAPS2.Util; + +/// +/// Implements a cache where items are expired after a given timespan. Item references keep an item alive and refresh +/// its expiry time. Once an item has no references and expires, it will be disposed. +/// +/// +/// +internal class TimedCache : IDisposable where TKey : notnull +{ + private readonly Dictionary _items = new(); + private Timer? _timer; + + public TimeSpan ExpiryTime { get; init; } = TimeSpan.FromSeconds(5); + + public TimeSpan ExpiryCheckInterval { get; init; } = TimeSpan.FromSeconds(1); + + internal Func NowSource { get; init; } = () => DateTime.Now; + + public IItemRef UseCacheItem(TKey key, Func itemFactory) + { + lock (this) + { + if (!_items.ContainsKey(key)) + { + _items[key] = new Item { Value = itemFactory() }; + RefreshExpiry(_items[key]); + } + return new ItemRef(_items[key], this); + } + } + + private void RefreshExpiry(Item item) + { + item.Expiry = NowSource() + ExpiryTime; + ResetTimer(); + } + + private void ResetTimer() + { + if (_timer == null && _items.Count > 0) + { + var interval = (int) ExpiryCheckInterval.TotalMilliseconds; + _timer = new Timer(_ => ExpireItems(), null, interval, interval); + } + else if (_timer != null && _items.Count == 0) + { + _timer.Dispose(); + _timer = null; + } + } + + internal void ExpireItems() + { + lock (this) + { + foreach (var kvp in _items) + { + if (kvp.Value.RefCount == 0 && kvp.Value.Expiry < NowSource()) + { + _items.Remove(kvp.Key); + (kvp.Value.Value as IDisposable)?.Dispose(); + } + } + ResetTimer(); + } + } + + public void Dispose() + { + lock (this) + { + foreach (var kvp in _items) + { + _items.Remove(kvp.Key); + (kvp.Value.Value as IDisposable)?.Dispose(); + } + } + } + + private class Item + { + public required TItem Value { get; init; } + + public int RefCount { get; set; } + + public DateTime Expiry { get; set; } + } + + private class ItemRef : IItemRef + { + private bool _disposed; + + public ItemRef(Item item, TimedCache cache) + { + Item = item; + Cache = cache; + Item.RefCount++; + } + + public Item Item { get; } + public TimedCache Cache { get; } + + public TItem Value => Item.Value; + + public void Dispose() + { + lock (Cache) + { + if (_disposed) return; + _disposed = true; + Item.RefCount--; + Cache.RefreshExpiry(Item); + } + } + } + + public interface IItemRef : IDisposable + { + public TItem Value { get; } + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Util/WebClientExtensions.cs b/NAPS2.Sdk/Util/WebClientExtensions.cs index 854cfa46bc..758dd6e27a 100644 --- a/NAPS2.Sdk/Util/WebClientExtensions.cs +++ b/NAPS2.Sdk/Util/WebClientExtensions.cs @@ -3,7 +3,7 @@ namespace NAPS2.Util; -public static class WebClientExtensions +internal static class WebClientExtensions { public static Task DownloadStringTaskAsync(this WebClient client, string address, CancellationToken cancelToken = default) { diff --git a/NAPS2.Sdk/_doc/.gitignore b/NAPS2.Sdk/_doc/.gitignore new file mode 100644 index 0000000000..b98b7d5f54 --- /dev/null +++ b/NAPS2.Sdk/_doc/.gitignore @@ -0,0 +1,10 @@ +############### +# folder # +############### +/**/DROP/ +/**/TEMP/ +/**/packages/ +/**/bin/ +/**/obj/ +_site +index.md diff --git a/NAPS2.Sdk/_doc/api/.gitignore b/NAPS2.Sdk/_doc/api/.gitignore new file mode 100644 index 0000000000..582a710b5d --- /dev/null +++ b/NAPS2.Sdk/_doc/api/.gitignore @@ -0,0 +1,6 @@ +############### +# temp file # +############### +*.yml +.manifest +index.md diff --git a/NAPS2.Sdk/_doc/docfx.json b/NAPS2.Sdk/_doc/docfx.json new file mode 100644 index 0000000000..109db2ae1b --- /dev/null +++ b/NAPS2.Sdk/_doc/docfx.json @@ -0,0 +1,109 @@ +{ + "metadata": [ + { + "src": [ + { + "files": [ + "NAPS2.Sdk.csproj" + ], + "src": "../../NAPS2.Sdk" + }, + { + "files": [ + "NAPS2.Images.csproj" + ], + "src": "../../NAPS2.Images" + }, + { + "files": [ + "NAPS2.Images.Gdi.csproj" + ], + "src": "../../NAPS2.Images.Gdi" + }, + { + "files": [ + "NAPS2.Images.Wpf.csproj" + ], + "src": "../../NAPS2.Images.Wpf" + }, + { + "files": [ + "NAPS2.Images.Mac.csproj" + ], + "src": "../../NAPS2.Images.Mac" + }, + { + "files": [ + "NAPS2.Images.Gtk.csproj" + ], + "src": "../../NAPS2.Images.Gtk" + }, + { + "files": [ + "NAPS2.Images.ImageSharp.csproj" + ], + "src": "../../NAPS2.Images.ImageSharp" + } + ], + "dest": "api", + "disableGitFeatures": false, + "disableDefaultFilter": false, + "allowCompilationErrors": true + } + ], + "build": { + "globalMetadata": { + "_appName": "NAPS2 SDK", + "_appTitle": "NAPS2 SDK", + "_appLogoPath": "scanner-48-rev2.png", + "_appFaviconPath": "favicon.ico", + "_enableSearch": true, + "_disableContribution": true + }, + "content": [ + { + "files": [ + "api/**.yml", + "api/index.md" + ] + }, + { + "files": [ + "toc.yml", + "*.md" + ] + } + ], + "resource": [ + { + "files": [ + "favicon.ico", + "scanner-48-rev2.png" + ], + "src": "../../NAPS2.Lib/Icons" + } + ], + "overwrite": [ + { + "files": [ + "apidoc/**.md" + ], + "exclude": [ + "obj/**", + "_site/**" + ] + } + ], + "dest": "_site", + "globalMetadataFiles": [], + "fileMetadataFiles": [], + "template": [ + "default", "modern" + ], + "postProcessors": [], + "noLangKeyword": false, + "keepFileLink": false, + "cleanupCacheHistory": false, + "disableGitFeatures": false + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/_doc/toc.yml b/NAPS2.Sdk/_doc/toc.yml new file mode 100644 index 0000000000..64073b9322 --- /dev/null +++ b/NAPS2.Sdk/_doc/toc.yml @@ -0,0 +1,8 @@ +- name: Api Docs + href: api/ + homepage: api/ +- name: Nuget + href: https://www.nuget.org/packages/NAPS2.Sdk + homepage: https://www.nuget.org/packages/NAPS2.Sdk +- name: Github + href: https://github.com/cyanfish/naps2/tree/master/NAPS2.Sdk \ No newline at end of file diff --git a/NAPS2.Setup/NAPS2.Setup.csproj b/NAPS2.Setup/NAPS2.Setup.csproj index 485a9cd349..7b6e65b9a8 100644 --- a/NAPS2.Setup/NAPS2.Setup.csproj +++ b/NAPS2.Setup/NAPS2.Setup.csproj @@ -1,9 +1,13 @@  - net6;net462 + net8;net462 + + + + \ No newline at end of file diff --git a/NAPS2.Setup/appsettings.xml b/NAPS2.Setup/appsettings.xml index 64da4155e3..32614be201 100644 --- a/NAPS2.Setup/appsettings.xml +++ b/NAPS2.Setup/appsettings.xml @@ -5,24 +5,33 @@ Information - SaveAll + Default + false + false + true + ScanWithDefaultProfile + SaveAll + false + false + false false false false false false false + false false + false false false false false false - false false - false + false + false false - false 600 UserConfig @@ -30,6 +39,8 @@ Fast true Default + None + None @@ -75,7 +86,7 @@ false Default - + Ctrl+Enter F2 F3 @@ -89,20 +100,21 @@ F11 F12 - + Ctrl+N Ctrl+B - - + Ctrl+L + Ctrl+G + Ctrl+Alt+O Ctrl+O - Ctrl+S - - - - - - - - + Ctrl+S + Ctrl+Shift+S + Ctrl+Alt+P + Ctrl+I + Ctrl+Shift+I + Ctrl+Alt+I + Ctrl+E + Ctrl+Shift+E + Ctrl+Alt+E Ctrl+P @@ -112,10 +124,15 @@ + + + + - - - + Ctrl+Shift+Left + Ctrl+Shift+Right + Ctrl+Shift+Down + Ctrl+Up Ctrl+Down @@ -127,6 +144,7 @@ Ctrl+Shift+Del + F1 Ctrl+Oemplus Ctrl+OemMinus diff --git a/NAPS2.Setup/config/linux/com.naps2.Naps2.desktop b/NAPS2.Setup/config/linux/com.naps2.Naps2.desktop index ff60f0d310..b13e3adfea 100644 --- a/NAPS2.Setup/config/linux/com.naps2.Naps2.desktop +++ b/NAPS2.Setup/config/linux/com.naps2.Naps2.desktop @@ -3,7 +3,7 @@ Type=Application Version=1.0 Name=NAPS2 Comment=Not Another PDF Scanner -Path=/app/bin/naps2 Exec=naps2 Icon=com.naps2.Naps2 -Categories=Graphics;Office \ No newline at end of file +Categories=Graphics;Office;Scanning;OCR; +MimeType=application/pdf;image/jpeg;image/png;image/tiff;image/bmp; \ No newline at end of file diff --git a/NAPS2.Setup/config/linux/com.naps2.Naps2.metainfo.xml b/NAPS2.Setup/config/linux/com.naps2.Naps2.metainfo.xml index c9c504d0a8..ebbaa192bc 100644 --- a/NAPS2.Setup/config/linux/com.naps2.Naps2.metainfo.xml +++ b/NAPS2.Setup/config/linux/com.naps2.Naps2.metainfo.xml @@ -5,7 +5,7 @@ GPL-2.0-or-later NAPS2 - Not Another PDF Scanner - Scan documents to PDF and other file types, as simply as possible. + Scan documents to PDF and more, as simply as possible.

NAPS2 on Linux allows you to scan from USB (using SANE) and network scanners. Use your chosen settings, or diff --git a/NAPS2.Setup/config/linux/com.naps2.Naps2.yml b/NAPS2.Setup/config/linux/com.naps2.Naps2.yml index db64d62bd9..f15390fef5 100644 --- a/NAPS2.Setup/config/linux/com.naps2.Naps2.yml +++ b/NAPS2.Setup/config/linux/com.naps2.Naps2.yml @@ -1,14 +1,15 @@ app-id: com.naps2.Naps2 runtime: org.freedesktop.Platform -runtime-version: '22.08' +runtime-version: '24.08' sdk: org.freedesktop.Sdk sdk-extensions: - - org.freedesktop.Sdk.Extension.dotnet6 + - org.freedesktop.Sdk.Extension.dotnet9 command: naps2 copy-icon: true cleanup: - /include - /lib/*.a + - /lib64/*.a - /lib/*.la - /lib/cmake - /lib/debug @@ -22,16 +23,16 @@ modules: sources: - type: git url: https://github.com/libusb/libusb - tag: v1.0.26 - commit: 4239bc3a50014b8e6a5a2a59df1fff3b7469543b + tag: v1.0.27 + commit: d52e355daa09f17ce64819122cb067b8a2ee0d4b - name: leptonica buildsystem: cmake-ninja builddir: true sources: - type: git url: https://github.com/DanBloomberg/leptonica - tag: 1.82.0 - commit: f4138265b390f1921b9891d6669674d3157887d8 + tag: 1.85.0 + commit: 63aef18d98432b8582a1565e241f7bd2ee9cc8d9 - name: tesseract buildsystem: cmake-ninja builddir: true @@ -44,22 +45,47 @@ modules: sources: - type: git url: https://github.com/tesseract-ocr/tesseract - tag: 5.2.0 - commit: 5ad5325a0aa8effc47ca033625b6a51682f82767 + tag: 5.5.0 + commit: 64eab6c457b2337dd690746a5fde5c222b40d5f8 + - name: avahi + buildsystem: autotools + config-opts: + - --with-distro=debian + - --disable-libevent + - --disable-qt4 + - --disable-qt5 + - --disable-libdaemon + - --disable-python + - --disable-mono + - --disable-manpages + sources: + - type: git + url: https://github.com/lathiat/avahi + tag: v0.8 + commit: f060abee2807c943821d88839c013ce15db17b58 - name: sane buildsystem: autotools sources: - type: git url: https://gitlab.com/sane-project/backends - tag: 1.1.1 - commit: 332edc8b7ce642bb06132cf204a8c2dd57720bce + tag: 1.3.1 + commit: 3ff55fd8ee04ae459e199088ebe11ac979671d0e + - type: patch + path: sane-streamdevices.patch + - name: sane-airscan + buildsystem: meson + sources: + - type: git + url: https://github.com/alexpevzner/sane-airscan + tag: 0.99.31 + commit: a086d81a1222194804a075cac496c821023ae23a - name: main buildsystem: simple build-options: - append-path: /usr/lib/sdk/dotnet6/bin - append-ld-library-path: /usr/lib/sdk/dotnet6/lib + append-path: /usr/lib/sdk/dotnet9/bin + append-ld-library-path: /usr/lib/sdk/dotnet9/lib env: - PKG_CONFIG_PATH: /app/lib/pkgconfig:/app/share/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig:/usr/lib/dotnet6/lib/pkgconfig + PKG_CONFIG_PATH: /app/lib/pkgconfig:/app/share/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig:/usr/lib/dotnet9/lib/pkgconfig DOTNET_CLI_TELEMETRY_OPTOUT: 'true' DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true' arch: @@ -71,13 +97,14 @@ modules: RUNTIME: linux-x64 build-commands: - mkdir -p /app/bin - - dotnet publish NAPS2.App.Gtk -c Release -r $RUNTIME --self-contained --source ./nuget-sources - - cp -r --remove-destination /run/build/main/NAPS2.App.Gtk/bin/Release/net6/$RUNTIME/publish/. /app/bin/ + - dotnet publish NAPS2.App.Gtk -c Release -r $RUNTIME --self-contained /p:DebugType=None /p:DebugSymbols=false --source ./nuget-sources + - cp -r --remove-destination /run/build/main/NAPS2.App.Gtk/bin/Release/net9/$RUNTIME/publish/. /app/bin/ - install -Dm644 com.naps2.Naps2.png /app/share/icons/hicolor/128x128/apps/com.naps2.Naps2.png - install -Dm644 com.naps2.Naps2.metainfo.xml /app/share/metainfo/com.naps2.Naps2.metainfo.xml - install -Dm644 com.naps2.Naps2.desktop /app/share/applications/com.naps2.Naps2.desktop cleanup: - - /bin/_linux + - /bin/_linuxarm/tesseract + - /bin/_linux/tesseract sources: - type: git path: ../../../../../.. @@ -105,3 +132,5 @@ finish-args: # GVfs - --filesystem=xdg-run/gvfsd - --talk-name=org.gtk.vfs.* + # mDNS discovery for ESCL scanners + - --system-talk-name=org.freedesktop.Avahi diff --git a/NAPS2.Setup/config/linux/debian-control b/NAPS2.Setup/config/linux/debian-control new file mode 100644 index 0000000000..cd7df09fcb --- /dev/null +++ b/NAPS2.Setup/config/linux/debian-control @@ -0,0 +1,18 @@ +Package: naps2 +Source: naps2 +Depends: libsane1 +Recommends: sane-airscan +Priority: optional +Section: graphics +Maintainer: Ben Olden-Cooligan +Architecture: {!arch} +Version: {!version} +Description: Scan documents to PDF and more, as simply as possible. + NAPS2 on Linux allows you to scan from USB (using SANE) and network scanners. Use your chosen settings, or + set up multiple profiles for different devices and configurations. Once you've finished scanning, you can + save, email, or print with only a couple clicks. Save to PDF, TIFF, JPEG, PNG, or other file types. + . + Easily rotate, crop, and rearrange scanned pages. Adjust brightness, contrast, and apply automatic document + corrections to make your scanned pages look great. Use OCR to add searchable text to your PDFs in any of + over 100 languages. Use batch scanning, advanced profile settings, and an optional CLI to automate tedious + tasks. diff --git a/NAPS2.Setup/config/linux/flatpak-dotnet-generator.py b/NAPS2.Setup/config/linux/flatpak-dotnet-generator.py index 9b644219f2..e67e6a50dc 100644 --- a/NAPS2.Setup/config/linux/flatpak-dotnet-generator.py +++ b/NAPS2.Setup/config/linux/flatpak-dotnet-generator.py @@ -35,11 +35,11 @@ def runCommand(cmd): 'flatpak', 'run', '--env=DOTNET_CLI_TELEMETRY_OPTOUT=true', '--env=DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true', - '--command=sh', '--runtime=org.freedesktop.Sdk//22.08', '--share=network', - '--filesystem=host', 'org.freedesktop.Sdk.Extension.dotnet6//22.08', '-c', - 'PATH="${PATH}:/usr/lib/sdk/dotnet6/bin" NUGET_PACKAGES="' + + '--command=sh', '--runtime=org.freedesktop.Sdk//24.08', '--share=network', + '--filesystem=host', 'org.freedesktop.Sdk.Extension.dotnet9//24.08', '-c', + 'PATH="${PATH}:/usr/lib/sdk/dotnet9/bin" NUGET_PACKAGES="' + str(Path(tmp).resolve()) + - '" LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/sdk/dotnet6/lib" exec dotnet ' + cmd, + '" LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/sdk/dotnet9/lib" exec dotnet ' + cmd, '--', args.project] + runtime_args) runCommand('restore -r linux-x64 "$@"') diff --git a/NAPS2.Setup/config/linux/rpm-spec b/NAPS2.Setup/config/linux/rpm-spec new file mode 100644 index 0000000000..31530fea9d --- /dev/null +++ b/NAPS2.Setup/config/linux/rpm-spec @@ -0,0 +1,44 @@ +%define __spec_install_post %{nil} +%define debug_package %{nil} +%define __os_install_post %{_dbpath}/brp-compress + +Summary: Scan documents to PDF and more, as simply as possible. +Name: naps2 +Version: {!version} +Release: 1 +License: GPLv2+ +Group: Graphics +SOURCE0: %{name}-%{version}.tar.gz +URL: https://www.naps2.com/ +AutoReqProv: no + +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root + +%description +%{summary} + +%prep +%setup -q + +%build +# Empty section. + +%install +rm -rf %{buildroot} +mkdir -p %{buildroot} + +# in builddir +cp -a * %{buildroot} + + +%clean +rm -rf %{buildroot} + + +%files +%defattr(-,root,root,-) +/usr/lib/naps2/* +/usr/bin/naps2 +/usr/share/applications/naps2.desktop +/usr/share/icons/hicolor/128x128/apps/com.naps2.Naps2.png +/usr/share/metainfo/com.naps2.Naps2.metainfo.xml \ No newline at end of file diff --git a/NAPS2.Setup/config/linux/sane-streamdevices.patch b/NAPS2.Setup/config/linux/sane-streamdevices.patch new file mode 100644 index 0000000000..d12d0d5aa9 --- /dev/null +++ b/NAPS2.Setup/config/linux/sane-streamdevices.patch @@ -0,0 +1,121 @@ +diff --git a/backend/dll.c b/backend/dll.c +index bf34c4f6d..3cd4e53f0 100644 +--- a/backend/dll.c ++++ b/backend/dll.c +@@ -1169,6 +1169,96 @@ sane_get_devices (const SANE_Device *** device_list, SANE_Bool local_only) + return SANE_STATUS_GOOD; + } + ++SANE_Status ++sane_stream_devices (SANE_Device_Callback callback, SANE_Bool local_only) ++{ ++ const SANE_Device **be_list; ++ struct backend *be; ++ SANE_Status status; ++ char *full_name; ++ int i, num_devs; ++ size_t len; ++ ++ DBG (3, "sane_stream_devices\n"); ++ ++ for (be = first_backend; be; be = be->next) ++ { ++ if (!be->inited) ++ if (init (be) != SANE_STATUS_GOOD) ++ continue; ++ ++ status = (*(op_get_devs_t)be->op[OP_GET_DEVS]) (&be_list, local_only); ++ if (status != SANE_STATUS_GOOD || !be_list) ++ continue; ++ ++ /* count the number of devices for this backend: */ ++ for (num_devs = 0; be_list[num_devs]; ++num_devs); ++ ++ for (i = 0; i < num_devs; ++i) ++ { ++ SANE_Device *dev; ++ char *mem; ++ struct alias *alias; ++ ++ for (alias = first_alias; alias != NULL; alias = alias->next) ++ { ++ len = strlen (be->name); ++ if (strlen (alias->oldname) <= len) ++ continue; ++ if (strncmp (alias->oldname, be->name, len) == 0 ++ && alias->oldname[len] == ':' ++ && strcmp (&alias->oldname[len + 1], be_list[i]->name) == 0) ++ break; ++ } ++ ++ if (alias) ++ { ++ if (!alias->newname) /* hidden device */ ++ continue; ++ ++ len = strlen (alias->newname); ++ mem = malloc (sizeof (*dev) + len + 1); ++ if (!mem) ++ return SANE_STATUS_NO_MEM; ++ ++ full_name = mem + sizeof (*dev); ++ strcpy (full_name, alias->newname); ++ } ++ else ++ { ++ /* create a new device entry with a device name that is the ++ sum of the backend name a colon and the backend's device ++ name: */ ++ len = strlen (be->name) + 1 + strlen (be_list[i]->name); ++ mem = malloc (sizeof (*dev) + len + 1); ++ if (!mem) ++ return SANE_STATUS_NO_MEM; ++ ++ full_name = mem + sizeof (*dev); ++ strcpy (full_name, be->name); ++ strcat (full_name, ":"); ++ strcat (full_name, be_list[i]->name); ++ } ++ ++ dev = (SANE_Device *) mem; ++ dev->name = full_name; ++ dev->vendor = be_list[i]->vendor; ++ dev->model = be_list[i]->model; ++ dev->type = be_list[i]->type; ++ ++ callback(dev); ++ free ((void *) dev); ++ } ++ if (!callback(NULL)) ++ { ++ break; ++ } ++ } ++ ++ DBG (3, "sane_stream_devices: done streaming\n"); ++ return SANE_STATUS_GOOD; ++} ++ + SANE_Status + sane_open (SANE_String_Const full_name, SANE_Handle * meta_handle) + { +diff --git a/include/sane/sane.h b/include/sane/sane.h +index 494ee891b..09487280d 100644 +--- a/include/sane/sane.h ++++ b/include/sane/sane.h +@@ -215,11 +215,15 @@ typedef void (*SANE_Auth_Callback) (SANE_String_Const resource, + SANE_Char *username, + SANE_Char *password); + ++typedef SANE_Bool (*SANE_Device_Callback) (SANE_Device* device); ++ + extern SANE_Status sane_init (SANE_Int * version_code, + SANE_Auth_Callback authorize); + extern void sane_exit (void); + extern SANE_Status sane_get_devices (const SANE_Device *** device_list, + SANE_Bool local_only); ++extern SANE_Status sane_stream_devices (SANE_Device_Callback callback, ++ SANE_Bool local_only); + extern SANE_Status sane_open (SANE_String_Const devicename, + SANE_Handle * handle); + extern void sane_close (SANE_Handle handle); diff --git a/NAPS2.Setup/config/public_signing_key.snk b/NAPS2.Setup/config/public_signing_key.snk new file mode 100644 index 0000000000..6d05eaba49 Binary files /dev/null and b/NAPS2.Setup/config/public_signing_key.snk differ diff --git a/NAPS2.Setup/config/windows/CodeDependencies.iss b/NAPS2.Setup/config/windows/CodeDependencies.iss deleted file mode 100644 index 4994021c7b..0000000000 --- a/NAPS2.Setup/config/windows/CodeDependencies.iss +++ /dev/null @@ -1,855 +0,0 @@ -; -- CodeDependencies.iss -- -; -; This script shows how to download and install any dependency such as .NET, -; Visual C++ or SQL Server during your application's installation process. -; -; contribute: https://github.com/DomGries/InnoDependencyInstaller - - -; ----------- -; SHARED CODE -; ----------- -[Code] -// types and variables -type - TDependency_Entry = record - Filename: String; - Parameters: String; - Title: String; - URL: String; - Checksum: String; - ForceSuccess: Boolean; - RestartAfter: Boolean; - end; - -var - Dependency_Memo: String; - Dependency_List: array of TDependency_Entry; - Dependency_NeedRestart, Dependency_ForceX86: Boolean; - Dependency_DownloadPage: TDownloadWizardPage; - -procedure Dependency_Add(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess, RestartAfter: Boolean); -var - Dependency: TDependency_Entry; - DependencyCount: Integer; -begin - Dependency_Memo := Dependency_Memo + #13#10 + '%1' + Title; - - Dependency.Filename := Filename; - Dependency.Parameters := Parameters; - Dependency.Title := Title; - - if FileExists(ExpandConstant('{tmp}{\}') + Filename) then begin - Dependency.URL := ''; - end else begin - Dependency.URL := URL; - end; - - Dependency.Checksum := Checksum; - Dependency.ForceSuccess := ForceSuccess; - Dependency.RestartAfter := RestartAfter; - - DependencyCount := GetArrayLength(Dependency_List); - SetArrayLength(Dependency_List, DependencyCount + 1); - Dependency_List[DependencyCount] := Dependency; -end; - - -procedure Dependency_Internal1; -begin - Dependency_DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil); -end; - - -function Dependency_Internal2(var NeedsRestart: Boolean): String; -var - DependencyCount, DependencyIndex, ResultCode: Integer; - Retry: Boolean; - TempValue: String; -begin - DependencyCount := GetArrayLength(Dependency_List); - - if DependencyCount > 0 then begin - Dependency_DownloadPage.Show; - - for DependencyIndex := 0 to DependencyCount - 1 do begin - if Dependency_List[DependencyIndex].URL <> '' then begin - Dependency_DownloadPage.Clear; - Dependency_DownloadPage.Add(Dependency_List[DependencyIndex].URL, Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Checksum); - - Retry := True; - while Retry do begin - Retry := False; - - try - Dependency_DownloadPage.Download; - except - if Dependency_DownloadPage.AbortedByUser then begin - Result := Dependency_List[DependencyIndex].Title; - DependencyIndex := DependencyCount; - end else begin - case SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of - IDABORT: begin - Result := Dependency_List[DependencyIndex].Title; - DependencyIndex := DependencyCount; - end; - IDRETRY: begin - Retry := True; - end; - end; - end; - end; - end; - end; - end; - - if Result = '' then begin - for DependencyIndex := 0 to DependencyCount - 1 do begin - Dependency_DownloadPage.SetText(Dependency_List[DependencyIndex].Title, ''); - Dependency_DownloadPage.SetProgress(DependencyIndex + 1, DependencyCount + 1); - - while True do begin - ResultCode := 0; - if ShellExec('', ExpandConstant('{tmp}{\}') + Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then begin - if Dependency_List[DependencyIndex].RestartAfter then begin - if DependencyIndex = DependencyCount - 1 then begin - Dependency_NeedRestart := True; - end else begin - NeedsRestart := True; - Result := Dependency_List[DependencyIndex].Title; - end; - break; - end else if (ResultCode = 0) or Dependency_List[DependencyIndex].ForceSuccess then begin // ERROR_SUCCESS (0) - break; - end else if ResultCode = 1641 then begin // ERROR_SUCCESS_REBOOT_INITIATED (1641) - NeedsRestart := True; - Result := Dependency_List[DependencyIndex].Title; - break; - end else if ResultCode = 3010 then begin // ERROR_SUCCESS_REBOOT_REQUIRED (3010) - Dependency_NeedRestart := True; - break; - end; - end; - - case SuppressibleMsgBox(FmtMessage(SetupMessage(msgErrorFunctionFailed), [Dependency_List[DependencyIndex].Title, IntToStr(ResultCode)]), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of - IDABORT: begin - Result := Dependency_List[DependencyIndex].Title; - break; - end; - IDIGNORE: begin - break; - end; - end; - end; - - if Result <> '' then begin - break; - end; - end; - - if NeedsRestart then begin - TempValue := '"' + ExpandConstant('{srcexe}') + '" /restart=1 /LANG="' + ExpandConstant('{language}') + '" /DIR="' + WizardDirValue + '" /GROUP="' + WizardGroupValue + '" /TYPE="' + WizardSetupType(False) + '" /COMPONENTS="' + WizardSelectedComponents(False) + '" /TASKS="' + WizardSelectedTasks(False) + '"'; - if WizardNoIcons then begin - TempValue := TempValue + ' /NOICONS'; - end; - RegWriteStringValue(HKA, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', '{#SetupSetting("AppName")}', TempValue); - end; - end; - - Dependency_DownloadPage.Hide; - end; -end; - - -function Dependency_Internal3(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String; -begin - Result := ''; - if MemoUserInfoInfo <> '' then begin - Result := Result + MemoUserInfoInfo + Newline + NewLine; - end; - if MemoDirInfo <> '' then begin - Result := Result + MemoDirInfo + Newline + NewLine; - end; - if MemoTypeInfo <> '' then begin - Result := Result + MemoTypeInfo + Newline + NewLine; - end; - if MemoComponentsInfo <> '' then begin - Result := Result + MemoComponentsInfo + Newline + NewLine; - end; - if MemoGroupInfo <> '' then begin - Result := Result + MemoGroupInfo + Newline + NewLine; - end; - if MemoTasksInfo <> '' then begin - Result := Result + MemoTasksInfo; - end; - - if Dependency_Memo <> '' then begin - if MemoTasksInfo = '' then begin - Result := Result + SetupMessage(msgReadyMemoTasks); - end; - Result := Result + FmtMessage(Dependency_Memo, [Space]); - end; -end; - - -function Dependency_Internal4: Boolean; -begin - Result := Dependency_NeedRestart; -end; - -function Dependency_IsX64: Boolean; -begin - Result := not Dependency_ForceX86 and Is64BitInstallMode; -end; - -function Dependency_String(const x86, x64: String): String; -begin - if Dependency_IsX64 then begin - Result := x64; - end else begin - Result := x86; - end; -end; - -function Dependency_ArchSuffix: String; -begin - Result := Dependency_String('', '_x64'); -end; - -function Dependency_ArchTitle: String; -begin - Result := Dependency_String(' (x86)', ' (x64)'); -end; - -function Dependency_IsNetCoreInstalled(const Version: String): Boolean; -var - ResultCode: Integer; -begin - // source code: https://github.com/dotnet/deployment-tools/tree/master/src/clickonce/native/projects/NetCoreCheck - if not FileExists(ExpandConstant('{tmp}{\}') + 'netcorecheck' + Dependency_ArchSuffix + '.exe') then begin - ExtractTemporaryFile('netcorecheck' + Dependency_ArchSuffix + '.exe'); - end; - Result := ShellExec('', ExpandConstant('{tmp}{\}') + 'netcorecheck' + Dependency_ArchSuffix + '.exe', Version, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0); -end; - -procedure Dependency_AddDotNet35; -begin - // https://dotnet.microsoft.com/download/dotnet-framework/net35-sp1 - if not IsDotNetInstalled(net35, 1) then begin - Dependency_Add('dotnetfx35.exe', - '/lang:enu /passive /norestart', - '.NET Framework 3.5 Service Pack 1', - 'https://download.microsoft.com/download/2/0/E/20E90413-712F-438C-988E-FDAA79A8AC3D/dotnetfx35.exe', - '', False, False); - end; -end; - -procedure Dependency_AddDotNet40; -begin - // https://dotnet.microsoft.com/download/dotnet-framework/net40 - if not IsDotNetInstalled(net4full, 0) then begin - Dependency_Add('dotNetFx40_Full_setup.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Framework 4.0', - 'https://download.microsoft.com/download/1/B/E/1BE39E79-7E39-46A3-96FF-047F95396215/dotNetFx40_Full_setup.exe', - '', False, False); - end; -end; - -procedure Dependency_AddDotNet45; -begin - // https://dotnet.microsoft.com/download/dotnet-framework/net452 - if not IsDotNetInstalled(net452, 0) then begin - Dependency_Add('dotnetfx45.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Framework 4.5.2', - 'https://go.microsoft.com/fwlink/?LinkId=397707', - '', False, False); - end; -end; - -procedure Dependency_AddDotNet46; -begin - // https://dotnet.microsoft.com/download/dotnet-framework/net462 - if not IsDotNetInstalled(net462, 0) then begin - Dependency_Add('dotnetfx46.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Framework 4.6.2', - 'https://go.microsoft.com/fwlink/?linkid=780596', - '', False, False); - end; -end; - -procedure Dependency_AddDotNet47; -begin - // https://dotnet.microsoft.com/download/dotnet-framework/net472 - if not IsDotNetInstalled(net472, 0) then begin - Dependency_Add('dotnetfx47.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Framework 4.7.2', - 'https://go.microsoft.com/fwlink/?LinkId=863262', - '', False, False); - end; -end; - -procedure Dependency_AddDotNet48; -begin - // https://dotnet.microsoft.com/download/dotnet-framework/net48 - if not IsDotNetInstalled(net48, 0) then begin - Dependency_Add('dotnetfx48.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Framework 4.8', - 'https://go.microsoft.com/fwlink/?LinkId=2085155', - '', False, False); - end; -end; - -procedure Dependency_AddNetCore31; -begin - // https://dotnet.microsoft.com/download/dotnet-core/3.1 - if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App 3.1.22') then begin - Dependency_Add('netcore31' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Core Runtime 3.1.22' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/c2437aed-8cc4-41d0-a239-d6c7cf7bddae/062c37e8b06df740301c0bca1b0b7b9a/dotnet-runtime-3.1.22-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/4e95705e-1bb6-4764-b899-1b97eb70ea1d/dd311e073bd3e25b2efe2dcf02727e81/dotnet-runtime-3.1.22-win-x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddNetCore31Asp; -begin - // https://dotnet.microsoft.com/download/dotnet-core/3.1 - if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App 3.1.22') then begin - Dependency_Add('netcore31asp' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - 'ASP.NET Core Runtime 3.1.22' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/0a1a2ee5-b8ed-4f0d-a4af-a7bce9a9ac2b/d452039b49d79e8897f272c3ab34b875/aspnetcore-runtime-3.1.22-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/80e52143-31e8-450e-aa94-b3f8484aaba9/4b69e5c77d50e7b367960a0079c90a99/aspnetcore-runtime-3.1.22-win-x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddNetCore31Desktop; -begin - // https://dotnet.microsoft.com/download/dotnet-core/3.1 - if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App 3.1.22') then begin - Dependency_Add('netcore31desktop' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Desktop Runtime 3.1.22' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/e4fcd574-4487-4b4b-8ca8-c23177c6f59f/c6d67a04956169dc21895cdcb42bf344/windowsdesktop-runtime-3.1.22-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/1c14e24b-7f31-42dc-ba3c-83295a2d6f7e/41b93591162dfe556cc160ae44fbe75e/windowsdesktop-runtime-3.1.22-win-x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddDotNet50; -begin - // https://dotnet.microsoft.com/download/dotnet/5.0 - if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App 5.0.13') then begin - Dependency_Add('dotnet50' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Runtime 5.0.13' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/4a79fcd5-d61b-4606-8496-68071c8099c6/2bf770ca40521e8c4563072592eadd06/dotnet-runtime-5.0.13-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/fccf43d2-3e62-4ede-b5a5-592a7ccded7b/6339f1fdfe3317df5b09adf65f0261ab/dotnet-runtime-5.0.13-win-x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddDotNet50Asp; -begin - // https://dotnet.microsoft.com/download/dotnet/5.0 - if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App 5.0.13') then begin - Dependency_Add('dotnet50asp' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - 'ASP.NET Core Runtime 5.0.13' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/340f9482-fc43-4ef7-b434-e2ed57f55cb3/c641b805cef3823769409a6dbac5746b/aspnetcore-runtime-5.0.13-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/aac560f3-eac8-437e-aebd-9830119deb10/6a3880161cf527e4ec71f67efe4d91ad/aspnetcore-runtime-5.0.13-win-x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddDotNet50Desktop; -begin - // https://dotnet.microsoft.com/download/dotnet/5.0 - if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App 5.0.13') then begin - Dependency_Add('dotnet50desktop' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Desktop Runtime 5.0.13' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/c8125c6b-d399-4be3-b201-8f1394fc3b25/724758f754fc7b67daba74db8d6d91d9/windowsdesktop-runtime-5.0.13-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/2bfb80f2-b8f2-44b0-90c1-d3c8c1c8eac8/409dd3d3367feeeda048f4ff34b32e82/windowsdesktop-runtime-5.0.13-win-x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddDotNet60; -begin - // https://dotnet.microsoft.com/download/dotnet/6.0 - if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App 6.0.11') then begin - Dependency_Add('dotnet60' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Runtime 6.0.11' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/719bfd7c-bce2-4e73-937c-cbd7a7ace3cb/d4f570d461711d22e277f1e3487ea9c2/dotnet-runtime-6.0.11-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/8cf88855-ed09-4002-95db-8bb0f0eff051/f9006645511830bd3b840be132423768/dotnet-runtime-6.0.11-win-x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddDotNet60Asp; -begin - // https://dotnet.microsoft.com/download/dotnet/6.0 - if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App 6.0.11') then begin - Dependency_Add('dotnet60asp' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - 'ASP.NET Core Runtime 6.0.11' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/94504599-143a-4d53-b518-74aee0ebecca/dac4a7b1f7bdc7b4e8441d6befa4941a/aspnetcore-runtime-6.0.11-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/e874914f-d43d-4b61-8479-f6a5536e44b1/7043adfe896aa9f980ce23e884aae37d/aspnetcore-runtime-6.0.11-win-x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddDotNet60Desktop; -begin - // https://dotnet.microsoft.com/download/dotnet/6.0 - if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App 6.0.11') then begin - Dependency_Add('dotnet60desktop' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Desktop Runtime 6.0.11' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/2a392287-fd51-4ee8-9c15-a672ab9bc55d/03d4784b3a543a0fb9ce5677ed13a9a3/windowsdesktop-runtime-6.0.11-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/0192a249-3ec8-4374-a827-e186dd58d55d/cec046575f3eb2247a10ba3d50f5cf6c/windowsdesktop-runtime-6.0.11-win-x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddDotNet70; -begin - // https://dotnet.microsoft.com/download/dotnet/7.0 - if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App 7.0.0') then begin - Dependency_Add('dotnet70' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Runtime 7.0.0' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/75c0d7c7-9f30-46fd-9675-a301f0e051f4/ec04d5cc40aa6537a4af21fad6bf8ba9/dotnet-runtime-7.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/87bc5966-97cc-498c-8381-bff4c43aafc6/baca88b989e7d2871e989d33a667d8e9/dotnet-runtime-7.0.0-win-x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddDotNet70Asp; -begin - // https://dotnet.microsoft.com/download/dotnet/7.0 - if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App 7.0.0') then begin - Dependency_Add('dotnet70asp' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - 'ASP.NET Core Runtime 7.0.0' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/aa4da7f2-fa27-47b1-9ad0-ac07dcecb730/00101e955bae403e5a2a424b3c29fb78/aspnetcore-runtime-7.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/388543cf-e110-4425-be62-2dfa1635586c/fab629ebe2c7b2edfa0f2ee9171de26b/aspnetcore-runtime-7.0.0-win-x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddDotNet70Desktop; -begin - // https://dotnet.microsoft.com/download/dotnet/7.0 - if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App 7.0.0') then begin - Dependency_Add('dotnet70desktop' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Desktop Runtime 7.0.0' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/d05a833c-2cf9-4d06-89ae-a0f3e10c5c91/c668ff42e23c2f67aa3d80227860585f/windowsdesktop-runtime-7.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/5b2fbe00-507e-450e-8b52-43ab052aadf2/79d54c3a19ce3fce314f2367cf4e3b21/windowsdesktop-runtime-7.0.0-win-x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddVC2005; -begin - // https://www.microsoft.com/en-us/download/details.aspx?id=26347 - if not IsMsiProductInstalled(Dependency_String('{86C9D5AA-F00C-4921-B3F2-C60AF92E2844}', '{A8D19029-8E5C-4E22-8011-48070F9E796E}'), PackVersionComponents(8, 0, 61000, 0)) then begin - Dependency_Add('vcredist2005' + Dependency_ArchSuffix + '.exe', - '/q', - 'Visual C++ 2005 Service Pack 1 Redistributable' + Dependency_ArchTitle, - Dependency_String('https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x86.EXE', 'https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x64.EXE'), - '', False, False); - end; -end; - -procedure Dependency_AddVC2008; -begin - // https://www.microsoft.com/en-us/download/details.aspx?id=26368 - if not IsMsiProductInstalled(Dependency_String('{DE2C306F-A067-38EF-B86C-03DE4B0312F9}', '{FDA45DDF-8E17-336F-A3ED-356B7B7C688A}'), PackVersionComponents(9, 0, 30729, 6161)) then begin - Dependency_Add('vcredist2008' + Dependency_ArchSuffix + '.exe', - '/q', - 'Visual C++ 2008 Service Pack 1 Redistributable' + Dependency_ArchTitle, - Dependency_String('https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x86.exe', 'https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddVC2010; -begin - // https://www.microsoft.com/en-us/download/details.aspx?id=26999 - if not IsMsiProductInstalled(Dependency_String('{1F4F1D2A-D9DA-32CF-9909-48485DA06DD5}', '{5B75F761-BAC8-33BC-A381-464DDDD813A3}'), PackVersionComponents(10, 0, 40219, 0)) then begin - Dependency_Add('vcredist2010' + Dependency_ArchSuffix + '.exe', - '/passive /norestart', - 'Visual C++ 2010 Service Pack 1 Redistributable' + Dependency_ArchTitle, - Dependency_String('https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddVC2012; -begin - // https://www.microsoft.com/en-us/download/details.aspx?id=30679 - if not IsMsiProductInstalled(Dependency_String('{4121ED58-4BD9-3E7B-A8B5-9F8BAAE045B7}', '{EFA6AFA1-738E-3E00-8101-FD03B86B29D1}'), PackVersionComponents(11, 0, 61030, 0)) then begin - Dependency_Add('vcredist2012' + Dependency_ArchSuffix + '.exe', - '/passive /norestart', - 'Visual C++ 2012 Update 4 Redistributable' + Dependency_ArchTitle, - Dependency_String('https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddVC2013; -begin - // https://support.microsoft.com/en-us/help/4032938 - if not IsMsiProductInstalled(Dependency_String('{B59F5BF1-67C8-3802-8E59-2CE551A39FC5}', '{20400CF0-DE7C-327E-9AE4-F0F38D9085F8}'), PackVersionComponents(12, 0, 40664, 0)) then begin - Dependency_Add('vcredist2013' + Dependency_ArchSuffix + '.exe', - '/passive /norestart', - 'Visual C++ 2013 Update 5 Redistributable' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/10912113/5da66ddebb0ad32ebd4b922fd82e8e25/vcredist_x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/10912041/cee5d6bca2ddbcd039da727bf4acb48a/vcredist_x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddVC2015To2022; -begin - // https://docs.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist - if not IsMsiProductInstalled(Dependency_String('{65E5BD06-6392-3027-8C26-853107D3CF1A}', '{36F68A90-239C-34DF-B58C-64B30153CE35}'), PackVersionComponents(14, 30, 30704, 0)) then begin - Dependency_Add('vcredist2022' + Dependency_ArchSuffix + '.exe', - '/passive /norestart', - 'Visual C++ 2015-2022 Redistributable' + Dependency_ArchTitle, - Dependency_String('https://aka.ms/vs/17/release/vc_redist.x86.exe', 'https://aka.ms/vs/17/release/vc_redist.x64.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddDirectX; -begin - // https://www.microsoft.com/en-us/download/details.aspx?id=35 - Dependency_Add('dxwebsetup.exe', - '/q', - 'DirectX Runtime', - 'https://download.microsoft.com/download/1/7/1/1718CCC4-6315-4D8E-9543-8E28A4E18C4C/dxwebsetup.exe', - '', True, False); -end; - -procedure Dependency_AddSql2008Express; -var - Version: String; - PackedVersion: Int64; -begin - // https://www.microsoft.com/en-us/download/details.aspx?id=30438 - if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(10, 50, 4000, 0)) < 0) then begin - Dependency_Add('sql2008express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', - 'SQL Server 2008 R2 Service Pack 2 Express', - Dependency_String('https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR_x64_ENU.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddSql2012Express; -var - Version: String; - PackedVersion: Int64; -begin - // https://www.microsoft.com/en-us/download/details.aspx?id=56042 - if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(11, 0, 7001, 0)) < 0) then begin - Dependency_Add('sql2012express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', - 'SQL Server 2012 Service Pack 4 Express', - Dependency_String('https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR_x64_ENU.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddSql2014Express; -var - Version: String; - PackedVersion: Int64; -begin - // https://www.microsoft.com/en-us/download/details.aspx?id=57473 - if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL12.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(12, 0, 6024, 0)) < 0) then begin - Dependency_Add('sql2014express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', - 'SQL Server 2014 Service Pack 3 Express', - Dependency_String('https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR_x64_ENU.exe'), - '', False, False); - end; -end; - -procedure Dependency_AddSql2016Express; -var - Version: String; - PackedVersion: Int64; -begin - // https://www.microsoft.com/en-us/download/details.aspx?id=56840 - if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(13, 0, 5026, 0)) < 0) then begin - Dependency_Add('sql2016express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', - 'SQL Server 2016 Service Pack 2 Express', - 'https://download.microsoft.com/download/3/7/6/3767D272-76A1-4F31-8849-260BD37924E4/SQLServer2016-SSEI-Expr.exe', - '', False, False); - end; -end; - -procedure Dependency_AddSql2017Express; -var - Version: String; - PackedVersion: Int64; -begin - // https://www.microsoft.com/en-us/download/details.aspx?id=55994 - if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL14.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(14, 0, 0, 0)) < 0) then begin - Dependency_Add('sql2017express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', - 'SQL Server 2017 Express', - 'https://download.microsoft.com/download/5/E/9/5E9B18CC-8FD5-467E-B5BF-BADE39C51F73/SQLServer2017-SSEI-Expr.exe', - '', False, False); - end; -end; - -procedure Dependency_AddSql2019Express; -var - Version: String; - PackedVersion: Int64; -begin - // https://www.microsoft.com/en-us/download/details.aspx?id=101064 - if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(15, 0, 0, 0)) < 0) then begin - Dependency_Add('sql2019express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', - 'SQL Server 2019 Express', - 'https://download.microsoft.com/download/7/f/8/7f8a9c43-8c8a-4f7c-9f92-83c18d96b681/SQL2019-SSEI-Expr.exe', - '', False, False); - end; -end; - -procedure Dependency_AddWebView2; -begin - if not RegValueExists(HKLM, Dependency_String('SOFTWARE', 'SOFTWARE\WOW6432Node') + '\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv') then begin - Dependency_Add('MicrosoftEdgeWebview2Setup.exe', - '/silent /install', - 'WebView2 Runtime', - 'https://go.microsoft.com/fwlink/p/?LinkId=2124703', - '', False, False); - end; -end; - - -[Setup] -; ------------- -; EXAMPLE SETUP -; ------------- -#ifndef Dependency_NoExampleSetup - -; comment out dependency defines to disable installing them -#define UseDotNet35 -#define UseDotNet40 -#define UseDotNet45 -#define UseDotNet46 -#define UseDotNet47 -#define UseDotNet48 - -; requires netcorecheck.exe and netcorecheck_x64.exe (see download link below) -#define UseNetCoreCheck -#ifdef UseNetCoreCheck - #define UseNetCore31 - #define UseNetCore31Asp - #define UseNetCore31Desktop - #define UseDotNet50 - #define UseDotNet50Asp - #define UseDotNet50Desktop - #define UseDotNet60 - #define UseDotNet60Asp - #define UseDotNet60Desktop - #define UseDotNet70 - #define UseDotNet70Asp - #define UseDotNet70Desktop -#endif - -#define UseVC2005 -#define UseVC2008 -#define UseVC2010 -#define UseVC2012 -#define UseVC2013 -#define UseVC2015To2022 - -; requires dxwebsetup.exe (see download link below) -;#define UseDirectX - -#define UseSql2008Express -#define UseSql2012Express -#define UseSql2014Express -#define UseSql2016Express -#define UseSql2017Express -#define UseSql2019Express - -#define UseWebView2 - -#define MyAppSetupName 'MyProgram' -#define MyAppVersion '1.0' -#define MyAppPublisher 'Inno Setup' -#define MyAppCopyright 'Copyright © Inno Setup' -#define MyAppURL 'https://jrsoftware.org/isinfo.php' - -AppName={#MyAppSetupName} -AppVersion={#MyAppVersion} -AppVerName={#MyAppSetupName} {#MyAppVersion} -AppCopyright={#MyAppCopyright} -VersionInfoVersion={#MyAppVersion} -VersionInfoCompany={#MyAppPublisher} -AppPublisher={#MyAppPublisher} -AppPublisherURL={#MyAppURL} -AppSupportURL={#MyAppURL} -AppUpdatesURL={#MyAppURL} -OutputBaseFilename={#MyAppSetupName}-{#MyAppVersion} -DefaultGroupName={#MyAppSetupName} -DefaultDirName={autopf}\{#MyAppSetupName} -UninstallDisplayIcon={app}\MyProgram.exe -SourceDir=src -OutputDir={#SourcePath}\bin -AllowNoIcons=yes -PrivilegesRequired=admin - -; remove next line if you only deploy 32-bit binaries and dependencies -ArchitecturesInstallIn64BitMode=x64 - -[Languages] -Name: en; MessagesFile: "compiler:Default.isl" -Name: nl; MessagesFile: "compiler:Languages\Dutch.isl" -Name: de; MessagesFile: "compiler:Languages\German.isl" - -[Files] -#ifdef UseNetCoreCheck -; download netcorecheck.exe: https://go.microsoft.com/fwlink/?linkid=2135256 -; download netcorecheck_x64.exe: https://go.microsoft.com/fwlink/?linkid=2135504 -Source: "netcorecheck.exe"; Flags: dontcopy noencryption -Source: "netcorecheck_x64.exe"; Flags: dontcopy noencryption -#endif - -#ifdef UseDirectX -Source: "dxwebsetup.exe"; Flags: dontcopy noencryption -#endif - -Source: "MyProg-x64.exe"; DestDir: "{app}"; DestName: "MyProg.exe"; Check: Dependency_IsX64; Flags: ignoreversion -Source: "MyProg.exe"; DestDir: "{app}"; Check: not Dependency_IsX64; Flags: ignoreversion - -[Icons] -Name: "{group}\{#MyAppSetupName}"; Filename: "{app}\MyProg.exe" -Name: "{group}\{cm:UninstallProgram,{#MyAppSetupName}}"; Filename: "{uninstallexe}" -Name: "{commondesktop}\{#MyAppSetupName}"; Filename: "{app}\MyProg.exe"; Tasks: desktopicon - -[Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}" - -[Run] -Filename: "{app}\MyProg.exe"; Description: "{cm:LaunchProgram,{#MyAppSetupName}}"; Flags: nowait postinstall skipifsilent - -[Code] -function InitializeSetup: Boolean; -begin -#ifdef UseDotNet35 - Dependency_AddDotNet35; -#endif -#ifdef UseDotNet40 - Dependency_AddDotNet40; -#endif -#ifdef UseDotNet45 - Dependency_AddDotNet45; -#endif -#ifdef UseDotNet46 - Dependency_AddDotNet46; -#endif -#ifdef UseDotNet47 - Dependency_AddDotNet47; -#endif -#ifdef UseDotNet48 - Dependency_AddDotNet48; -#endif - -#ifdef UseNetCore31 - Dependency_AddNetCore31; -#endif -#ifdef UseNetCore31Asp - Dependency_AddNetCore31Asp; -#endif -#ifdef UseNetCore31Desktop - Dependency_AddNetCore31Desktop; -#endif -#ifdef UseDotNet50 - Dependency_AddDotNet50; -#endif -#ifdef UseDotNet50Asp - Dependency_AddDotNet50Asp; -#endif -#ifdef UseDotNet50Desktop - Dependency_AddDotNet50Desktop; -#endif -#ifdef UseDotNet60 - Dependency_AddDotNet60; -#endif -#ifdef UseDotNet60Asp - Dependency_AddDotNet60Asp; -#endif -#ifdef UseDotNet60Desktop - Dependency_AddDotNet60Desktop; -#endif -#ifdef UseDotNet70 - Dependency_AddDotNet70; -#endif -#ifdef UseDotNet70Asp - Dependency_AddDotNet70Asp; -#endif -#ifdef UseDotNet70Desktop - Dependency_AddDotNet70Desktop; -#endif - -#ifdef UseVC2005 - Dependency_AddVC2005; -#endif -#ifdef UseVC2008 - Dependency_AddVC2008; -#endif -#ifdef UseVC2010 - Dependency_AddVC2010; -#endif -#ifdef UseVC2012 - Dependency_AddVC2012; -#endif -#ifdef UseVC2013 - //Dependency_ForceX86 := True; // force 32-bit install of next dependencies - Dependency_AddVC2013; - //Dependency_ForceX86 := False; // disable forced 32-bit install again -#endif -#ifdef UseVC2015To2022 - Dependency_AddVC2015To2022; -#endif - -#ifdef UseDirectX - ExtractTemporaryFile('dxwebsetup.exe'); - Dependency_AddDirectX; -#endif - -#ifdef UseSql2008Express - Dependency_AddSql2008Express; -#endif -#ifdef UseSql2012Express - Dependency_AddSql2012Express; -#endif -#ifdef UseSql2014Express - Dependency_AddSql2014Express; -#endif -#ifdef UseSql2016Express - Dependency_AddSql2016Express; -#endif -#ifdef UseSql2017Express - Dependency_AddSql2017Express; -#endif -#ifdef UseSql2019Express - Dependency_AddSql2019Express; -#endif - -#ifdef UseWebView2 - Dependency_AddWebView2; -#endif - - Result := True; -end; - -#endif diff --git a/NAPS2.Setup/config/windows/msix/Assets/scanner-150.png b/NAPS2.Setup/config/windows/msix/Assets/scanner-150.png new file mode 100644 index 0000000000..7a6bb31d91 Binary files /dev/null and b/NAPS2.Setup/config/windows/msix/Assets/scanner-150.png differ diff --git a/NAPS2.Setup/config/windows/msix/Assets/scanner-44.targetsize-44.png b/NAPS2.Setup/config/windows/msix/Assets/scanner-44.targetsize-44.png new file mode 100644 index 0000000000..41551a8a1c Binary files /dev/null and b/NAPS2.Setup/config/windows/msix/Assets/scanner-44.targetsize-44.png differ diff --git a/NAPS2.Setup/config/windows/msix/Assets/scanner-44.targetsize-44_altform-lightunplated.png b/NAPS2.Setup/config/windows/msix/Assets/scanner-44.targetsize-44_altform-lightunplated.png new file mode 100644 index 0000000000..41551a8a1c Binary files /dev/null and b/NAPS2.Setup/config/windows/msix/Assets/scanner-44.targetsize-44_altform-lightunplated.png differ diff --git a/NAPS2.Setup/config/windows/msix/Assets/scanner-44.targetsize-44_altform-unplated.png b/NAPS2.Setup/config/windows/msix/Assets/scanner-44.targetsize-44_altform-unplated.png new file mode 100644 index 0000000000..41551a8a1c Binary files /dev/null and b/NAPS2.Setup/config/windows/msix/Assets/scanner-44.targetsize-44_altform-unplated.png differ diff --git a/NAPS2.Setup/config/windows/msix/appxmanifest.xml b/NAPS2.Setup/config/windows/msix/appxmanifest.xml new file mode 100644 index 0000000000..7f84cd0b6b --- /dev/null +++ b/NAPS2.Setup/config/windows/msix/appxmanifest.xml @@ -0,0 +1,102 @@ + + + + + NAPS2 - Not Another PDF Scanner + NAPS2 Software + Scan documents to PDF and more, as simply as possible. + Assets\scanner-150.png + + + + + + + + + + + + + + + + + + + + + + + + .pdf + + PDF Files + Assets\scanner-44.png + + + + + + .jpg + .jpeg + .png + .tiff + .tif + .bmp + + Image Files + Assets\scanner-44.png + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NAPS2.Setup/config/windows/msix/priconfig.xml b/NAPS2.Setup/config/windows/msix/priconfig.xml new file mode 100644 index 0000000000..1c041eaa7c --- /dev/null +++ b/NAPS2.Setup/config/windows/msix/priconfig.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NAPS2.Setup/config/windows/setup.languages.iss b/NAPS2.Setup/config/windows/setup.languages.iss new file mode 100644 index 0000000000..4a6c5a4aba --- /dev/null +++ b/NAPS2.Setup/config/windows/setup.languages.iss @@ -0,0 +1,49 @@ +; Only NAPS2-translated languages are included (to avoid misleading users) +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" +Name: "Afrikaans"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Afrikaans.isl"; +Name: "Albanian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Albanian.isl"; +Name: "Arabic"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Arabic.isl"; +; Name: "Bengali"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Bengali.isl"; +Name: "BrazilianPortuguese"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\BrazilianPortuguese.isl"; +Name: "Bulgarian"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Bulgarian.isl"; +Name: "Catalan"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Catalan.isl"; +Name: "ChineseSimplified"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\ChineseSimplified.isl"; +Name: "ChineseTraditional"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\ChineseTraditional.isl"; +Name: "Croatian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Croatian.isl"; +Name: "Czech"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Czech.isl"; +Name: "Danish"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Danish.isl"; +Name: "Dutch"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Dutch.isl"; +Name: "Estonian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Estonian.isl"; +Name: "Farsi"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Farsi.isl"; +Name: "Finnish"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Finnish.isl"; +Name: "French"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\French.isl"; +Name: "German"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\German.isl"; +Name: "Greek"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Greek.isl"; +Name: "Hebrew"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Hebrew.isl"; +Name: "Hindi"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Hindi.isl"; +Name: "Hungarian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Hungarian.isl"; +Name: "Indonesian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Indonesian.isl"; +Name: "Italian"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Italian.isl"; +Name: "Japanese"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Japanese.isl"; +Name: "Korean"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Korean.isl"; +Name: "Latvian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Latvian.isl"; +Name: "Lithuanian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Lithuanian.isl"; +Name: "Norwegian"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Norwegian.isl"; +Name: "NorwegianNynorsk"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\NorwegianNynorsk.isl"; +Name: "Polish"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Polish.isl"; +Name: "Portuguese"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Portuguese.isl"; +Name: "Romanian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Romanian.isl"; +Name: "Russian"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Russian.isl"; +Name: "SerbianCyrillic"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\SerbianCyrillic.isl"; +Name: "SerbianLatin"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\SerbianLatin.isl"; +Name: "Sinhala"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Sinhala.isl"; +Name: "Slovak"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Slovak.isl"; +Name: "Slovenian"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Slovenian.isl"; +Name: "Spanish"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Spanish.isl"; +Name: "Swedish"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Swedish.isl"; +Name: "Thai"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Thai.isl"; +Name: "Turkish"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Turkish.isl"; +Name: "Ukrainian"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Ukrainian.isl"; +; Name: "Urdu"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Urdu.isl"; +Name: "Vietnamese"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Vietnamese.isl"; \ No newline at end of file diff --git a/NAPS2.Setup/config/windows/setup.template.iss b/NAPS2.Setup/config/windows/setup.template.iss index e4f0d32282..e19d26e78e 100644 --- a/NAPS2.Setup/config/windows/setup.template.iss +++ b/NAPS2.Setup/config/windows/setup.template.iss @@ -1,93 +1,60 @@ ; !defs -; Set up for InnoDependencyInstaller -#define public Dependency_NoExampleSetup -#include "..\config\windows\CodeDependencies.iss" +#include "..\config\windows\setup.languages.iss" + +#define AppShortName "NAPS2" +#define AppLongName "NAPS2 - Not Another PDF Scanner" +#define AppCompany "NAPS2 Software" +#define AppCopyrightStartYear "2009" +#define AppCopyrightEndYear GetDateTimeString('yyyy','','') +#define AppCopyrightCompany "NAPS2 Contributors" +#define ExeName "NAPS2.exe" [Setup] -AppName=NAPS2 - Not Another PDF Scanner +AppName={#AppLongName} AppVersion={#AppVersion} -AppVerName=NAPS2 {#AppVersion} -AppPublisher=Ben Olden-Cooligan +AppVerName={#AppShortName} {#AppVersionName} +AppPublisher={#AppCompany} AppPublisherURL=https://www.naps2.com AppSupportURL=https://www.naps2.com/support AppUpdatesURL=https://www.naps2.com/download -DefaultDirName={commonpf}\NAPS2 -DefaultGroupName=NAPS2 -OutputDir=../publish/{#AppVersion} -OutputBaseFilename=naps2-{#AppVersion}-{#AppPlatform} -Compression=lzma2/ultra + +VersionInfoDescription={#AppShortName} installer +VersionInfoVersion={#AppVersion} +VersionInfoProductName={#AppShortName} +VersionInfoProductVersion={#AppVersion} +VersionInfoCompany={#AppCompany} +VersionInfoCopyright=(c) {#AppCopyrightStartYear}-{#AppCopyrightEndYear} + +ShowLanguageDialog=yes +UsePreviousLanguage=no +LanguageDetectionMethod=uilanguage +WizardStyle=modern +; Require Windows 10 1607+ +MinVersion=10.0.14393 + +DefaultDirName={commonpf}\{#AppShortName} +DefaultGroupName={#AppShortName} +LicenseFile=..\..\LICENSE + +UninstallDisplayName={#AppShortName} +UninstallDisplayIcon={app}\{#ExeName} + +OutputDir=../publish/{#AppVersionName} +OutputBaseFilename=naps2-{#AppVersionName}-{#AppPlatform} +Compression=lzma2/ultra64 LZMAUseSeparateProcess=yes SolidCompression=yes ; !arch -LicenseFile=..\..\LICENSE -UninstallDisplayIcon={app}\NAPS2.exe +ChangesAssociations=yes [Run] -Filename: "{app}\NAPS2.exe"; Flags: nowait postinstall - -; Only NAPS2-translated languages are included (to avoid misleading users) -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl" -Name: "Afrikaans"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Afrikaans.isl"; -Name: "Albanian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Albanian.isl"; -Name: "Arabic"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Arabic.isl"; -; Name: "Bengali"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Bengali.isl"; -Name: "BrazilianPortuguese"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\BrazilianPortuguese.isl"; -Name: "Bulgarian"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Bulgarian.isl"; -Name: "Catalan"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Catalan.isl"; -Name: "ChineseSimplified"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\ChineseSimplified.isl"; -Name: "ChineseTraditional"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\ChineseTraditional.isl"; -Name: "Croatian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Croatian.isl"; -Name: "Czech"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Czech.isl"; -Name: "Danish"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Danish.isl"; -Name: "Dutch"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Dutch.isl"; -Name: "Estonian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Estonian.isl"; -Name: "Farsi"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Farsi.isl"; -Name: "Finnish"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Finnish.isl"; -Name: "French"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\French.isl"; -Name: "German"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\German.isl"; -Name: "Greek"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Greek.isl"; -Name: "Hebrew"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Hebrew.isl"; -; Name: "Hindi"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Hindi.isl"; -Name: "Hungarian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Hungarian.isl"; -; Name: "Indonesian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Indonesian.isl"; -Name: "Italian"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Italian.isl"; -Name: "Japanese"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Japanese.isl"; -Name: "Korean"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Korean.isl"; -Name: "Latvian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Latvian.isl"; -Name: "Lithuanian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Lithuanian.isl"; -Name: "Norwegian"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Norwegian.isl"; -Name: "NorwegianNynorsk"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\NorwegianNynorsk.isl"; -Name: "Polish"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Polish.isl"; -Name: "Portuguese"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Portuguese.isl"; -Name: "Romanian"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Romanian.isl"; -Name: "Russian"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Russian.isl"; -Name: "SerbianCyrillic"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\SerbianCyrillic.isl"; -Name: "SerbianLatin"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\SerbianLatin.isl"; -Name: "Sinhala"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Sinhala.isl"; -Name: "Slovak"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Slovak.isl"; -Name: "Slovenian"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Slovenian.isl"; -Name: "Spanish"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Spanish.isl"; -Name: "Swedish"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Swedish.isl"; -; Name: "Thai"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Thai.isl"; -Name: "Turkish"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Turkish.isl"; -Name: "Ukrainian"; MessagesFile: "C:\Program Files (x86)\Inno Setup 6\Languages\Ukrainian.isl"; -; Name: "Urdu"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Urdu.isl"; -Name: "Vietnamese"; MessagesFile: "..\..\NAPS2.Setup\config\windows\inno-lang\Vietnamese.isl"; +Filename: "{app}\{#ExeName}"; Flags: nowait postinstall [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked -; Impl for InnoDependencyInstaller -[Code] -function InitializeSetup: Boolean; -begin - Dependency_AddVC2015To2022; - Result := True; -end; - [Files] ; !files @@ -95,26 +62,38 @@ end; [InstallDelete] Type: files; Name: "{app}\*.exe" Type: files; Name: "{app}\*.exe.config" +Type: files; Name: "{app}\*.dll" +Type: files; Name: "{app}\*.json" Type: filesandordirs; Name: "{app}\lib" ; !clean32 [Icons] -Name: "{group}\NAPS2"; Filename: "{app}\NAPS2.exe" -Name: "{commondesktop}\NAPS2"; Filename: "{app}\NAPS2.exe"; Tasks: desktopicon +Name: "{group}\NAPS2"; Filename: "{app}\{#ExeName}" +Name: "{commondesktop}\NAPS2"; Filename: "{app}\{#ExeName}"; Tasks: desktopicon [Registry] Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers\Handlers\WIA_{{1c3a7177-f3a7-439e-be47-e304a185f932}"; Flags: uninsdeletekey Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers\Handlers\WIA_{{1c3a7177-f3a7-439e-be47-e304a185f932}"; ValueType: string; ValueName: "Action"; ValueData: "Scan with NAPS2" Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers\Handlers\WIA_{{1c3a7177-f3a7-439e-be47-e304a185f932}"; ValueType: string; ValueName: "CLSID"; ValueData: "WIACLSID" Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers\Handlers\WIA_{{1c3a7177-f3a7-439e-be47-e304a185f932}"; ValueType: string; ValueName: "DefaultIcon"; ValueData: "sti.dll,0" -Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers\Handlers\WIA_{{1c3a7177-f3a7-439e-be47-e304a185f932}"; ValueType: string; ValueName: "InitCmdLine"; ValueData: "/WiaCmd;{app}\NAPS2.exe /StiDevice:%1 /StiEvent:%2;" +Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers\Handlers\WIA_{{1c3a7177-f3a7-439e-be47-e304a185f932}"; ValueType: string; ValueName: "InitCmdLine"; ValueData: "/WiaCmd;{app}\{#ExeName} /StiDevice:%1 /StiEvent:%2;" Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers\Handlers\WIA_{{1c3a7177-f3a7-439e-be47-e304a185f932}"; ValueType: string; ValueName: "Provider"; ValueData: "NAPS2" -Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\StillImage\Registered Applications"; Flags:uninsdeletevalue; ValueType: string; ValueName: "NAPS2"; ValueData: "{app}\NAPS2.exe" +Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\StillImage\Registered Applications"; Flags:uninsdeletevalue; ValueType: string; ValueName: "NAPS2"; ValueData: "{app}\{#ExeName}" Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\StillImage\Events\STIProxyEvent\{{1c3a7177-f3a7-439e-be47-e304a185f932}"; Flags: uninsdeletekey -Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\StillImage\Events\STIProxyEvent\{{1c3a7177-f3a7-439e-be47-e304a185f932}"; ValueType: string; ValueName: "Cmdline"; ValueData: "{app}\NAPS2.exe /StiDevice:%1 /StiEvent:%2" +Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\StillImage\Events\STIProxyEvent\{{1c3a7177-f3a7-439e-be47-e304a185f932}"; ValueType: string; ValueName: "Cmdline"; ValueData: "{app}\{#ExeName} /StiDevice:%1 /StiEvent:%2" Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\StillImage\Events\STIProxyEvent\{{1c3a7177-f3a7-439e-be47-e304a185f932}"; ValueType: string; ValueName: "Desc"; ValueData: "Scan with NAPS2" -Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\StillImage\Events\STIProxyEvent\{{1c3a7177-f3a7-439e-be47-e304a185f932}"; ValueType: string; ValueName: "Icon"; ValueData: "{app}\NAPS2.exe,0" +Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\StillImage\Events\STIProxyEvent\{{1c3a7177-f3a7-439e-be47-e304a185f932}"; ValueType: string; ValueName: "Icon"; ValueData: "{app}\{#ExeName},0" Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\StillImage\Events\STIProxyEvent\{{1c3a7177-f3a7-439e-be47-e304a185f932}"; ValueType: string; ValueName: "Name"; ValueData: "NAPS2" +Root: HKCR; Subkey: ".pdf\OpenWithProgids"; ValueType: string; ValueName: "{#AppShortName}"; ValueData: ""; Flags: uninsdeletevalue +Root: HKCR; Subkey: ".jpg\OpenWithProgids"; ValueType: string; ValueName: "{#AppShortName}"; ValueData: ""; Flags: uninsdeletevalue +Root: HKCR; Subkey: ".jpeg\OpenWithProgids"; ValueType: string; ValueName: "{#AppShortName}"; ValueData: ""; Flags: uninsdeletevalue +Root: HKCR; Subkey: ".png\OpenWithProgids"; ValueType: string; ValueName: "{#AppShortName}"; ValueData: ""; Flags: uninsdeletevalue +Root: HKCR; Subkey: ".tiff\OpenWithProgids"; ValueType: string; ValueName: "{#AppShortName}"; ValueData: ""; Flags: uninsdeletevalue +Root: HKCR; Subkey: ".tif\OpenWithProgids"; ValueType: string; ValueName: "{#AppShortName}"; ValueData: ""; Flags: uninsdeletevalue +Root: HKCR; Subkey: ".bmp\OpenWithProgids"; ValueType: string; ValueName: "{#AppShortName}"; ValueData: ""; Flags: uninsdeletevalue +Root: HKCR; Subkey: "{#AppShortName}"; ValueType: string; ValueName: ""; ValueData: "{#AppShortName}"; Flags: uninsdeletekey; +Root: HKCR; Subkey: "{#AppShortName}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#ExeName},0" +Root: HKCR; Subkey: "{#AppShortName}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeName}"" ""%1""" \ No newline at end of file diff --git a/NAPS2.Setup/config/windows/setup.template.wxs b/NAPS2.Setup/config/windows/setup.template.wxs index a285b57a25..fccd520f4b 100644 --- a/NAPS2.Setup/config/windows/setup.template.wxs +++ b/NAPS2.Setup/config/windows/setup.template.wxs @@ -5,7 +5,7 @@ Name="NAPS2" Language="1033" Version="{{ !version }}" - Manufacturer="Ben Olden-Cooligan" + Manufacturer="NAPS2 Software" UpgradeCode="FEB82971-B3E6-4F19-9684-1D543E644D73"> @@ -24,18 +24,28 @@ - + + + + + + + + + + + - + - + @@ -62,6 +72,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -70,7 +110,7 @@ - + @@ -83,11 +123,11 @@ - + - + diff --git a/NAPS2.Setup/lib/PdfSharpCore.dll b/NAPS2.Setup/lib/PdfSharpCore.dll deleted file mode 100644 index 68982c1b7d..0000000000 Binary files a/NAPS2.Setup/lib/PdfSharpCore.dll and /dev/null differ diff --git a/NAPS2.Setup/lib/linux/libpdfium.so b/NAPS2.Setup/lib/linux/libpdfium.so deleted file mode 100644 index 17df14c2e2..0000000000 Binary files a/NAPS2.Setup/lib/linux/libpdfium.so and /dev/null differ diff --git a/NAPS2.Setup/lib/mac/libpdfium.dylib b/NAPS2.Setup/lib/mac/libpdfium.dylib deleted file mode 100644 index 17bd273a8b..0000000000 Binary files a/NAPS2.Setup/lib/mac/libpdfium.dylib and /dev/null differ diff --git a/NAPS2.Setup/lib/macarm/libpdfium.dylib b/NAPS2.Setup/lib/macarm/libpdfium.dylib deleted file mode 100755 index cb2b424ef1..0000000000 Binary files a/NAPS2.Setup/lib/macarm/libpdfium.dylib and /dev/null differ diff --git a/NAPS2.Setup/lib/win32/pdfium.dll b/NAPS2.Setup/lib/win32/pdfium.dll deleted file mode 100644 index 5e4acf62f3..0000000000 Binary files a/NAPS2.Setup/lib/win32/pdfium.dll and /dev/null differ diff --git a/NAPS2.Setup/lib/win64/pdfium.dll b/NAPS2.Setup/lib/win64/pdfium.dll deleted file mode 100644 index 12b80318dd..0000000000 Binary files a/NAPS2.Setup/lib/win64/pdfium.dll and /dev/null differ diff --git a/NAPS2.Setup/license.rtf b/NAPS2.Setup/license.rtf index 1c8763ccbc..dc7a5c177e 100644 --- a/NAPS2.Setup/license.rtf +++ b/NAPS2.Setup/license.rtf @@ -48,7 +48,7 @@ Normal Table;}}{\*\pgptbl {\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}}{\*\rsidtbl \rsid67 NAPS2 - Not Another PDF Scanner \par \hich\af42\dbch\af31505\loch\f42 http://sourceforge.net/projects/naps2/ \par \hich\af42\dbch\af31505\loch\f42 -\par }{\rtlch\fcs1 \ab\af42\afs16 \ltrch\fcs0 \f42\fs16\lang1060\langfe1033\kerning36\langnp1060\insrsid67871\charrsid67871 \hich\af42\dbch\af31505\loch\f42 Copyright 2009, 2012-2022 NAPS2 Contributors}{\rtlch\fcs1 \ab\af42\afs16 \ltrch\fcs0 +\par }{\rtlch\fcs1 \ab\af42\afs16 \ltrch\fcs0 \f42\fs16\lang1060\langfe1033\kerning36\langnp1060\insrsid67871\charrsid67871 \hich\af42\dbch\af31505\loch\f42 Copyright 2009-2024 NAPS2 Contributors}{\rtlch\fcs1 \ab\af42\afs16 \ltrch\fcs0 \f42\fs16\lang1060\langfe1033\kerning36\langnp1060\insrsid14093636 \par }{\rtlch\fcs1 \ab\af42\afs16 \ltrch\fcs0 \f42\fs16\lang1060\langfe1033\kerning36\langnp1060\insrsid67871\charrsid14093636 \par }{\rtlch\fcs1 \ab\af42\afs16 \ltrch\fcs0 \f42\fs16\lang1060\langfe1033\kerning36\langnp1060\insrsid14093636\charrsid14093636 \hich\af42\dbch\af31505\loch\f42 This program is free software; you can redistribute it and/or diff --git a/NAPS2.Setup/targets/CommonTargets.targets b/NAPS2.Setup/targets/CommonTargets.targets index 3bc4397678..da3698cc84 100644 --- a/NAPS2.Setup/targets/CommonTargets.targets +++ b/NAPS2.Setup/targets/CommonTargets.targets @@ -1,8 +1,9 @@ - 11 + 12 true + Copyright 2009-2024 NAPS2 Contributors diff --git a/NAPS2.Setup/targets/ImageUsers.targets b/NAPS2.Setup/targets/ImageUsers.targets index 2207eb1d94..ff17895b1c 100644 --- a/NAPS2.Setup/targets/ImageUsers.targets +++ b/NAPS2.Setup/targets/ImageUsers.targets @@ -2,7 +2,6 @@ - diff --git a/NAPS2.Setup/targets/LibUsers.targets b/NAPS2.Setup/targets/LibUsers.targets index da8c38b293..a8830fe9b0 100644 --- a/NAPS2.Setup/targets/LibUsers.targets +++ b/NAPS2.Setup/targets/LibUsers.targets @@ -5,6 +5,7 @@ + diff --git a/NAPS2.Setup/targets/NativeLibs.Linux.targets b/NAPS2.Setup/targets/NativeLibs.Linux.targets deleted file mode 100644 index c6ddf207a7..0000000000 --- a/NAPS2.Setup/targets/NativeLibs.Linux.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - PreserveNewest - lib\linux\libpdfium.so - _linux\libpdfium.so - - - diff --git a/NAPS2.Setup/targets/NativeLibs.Mac.targets b/NAPS2.Setup/targets/NativeLibs.Mac.targets deleted file mode 100644 index 33531afb3e..0000000000 --- a/NAPS2.Setup/targets/NativeLibs.Mac.targets +++ /dev/null @@ -1,16 +0,0 @@ - - - - PreserveNewest - lib\mac\libpdfium.dylib - libpdfium.dylib - - - PreserveNewest - lib\macarm\libpdfium.dylib - libpdfium.dylib - - - diff --git a/NAPS2.Setup/targets/NativeLibs.targets b/NAPS2.Setup/targets/NativeLibs.targets index fc42a94c3b..365984530e 100644 --- a/NAPS2.Setup/targets/NativeLibs.targets +++ b/NAPS2.Setup/targets/NativeLibs.targets @@ -1,31 +1,5 @@ - - PreserveNewest - lib\win64\pdfium.dll - _win64\pdfium.dll - - - PreserveNewest - lib\win32\pdfium.dll - _win32\pdfium.dll - - - PreserveNewest - lib\linux\libpdfium.so - _linux\libpdfium.so - - - PreserveNewest - lib\mac\libpdfium.dylib - _mac\libpdfium.dylib - - - PreserveNewest - lib\macarm\libpdfium.dylib - _macarm\libpdfium.dylib - - PreserveNewest lib\win64\twaindsm.dll diff --git a/NAPS2.Setup/targets/SdkPackageTargets.targets b/NAPS2.Setup/targets/SdkPackageTargets.targets new file mode 100644 index 0000000000..a3ae6ba57e --- /dev/null +++ b/NAPS2.Setup/targets/SdkPackageTargets.targets @@ -0,0 +1,19 @@ + + + + 1.2.1 + + true + Ben Olden-Cooligan + NAPS2 Software + LICENSE + https://github.com/cyanfish/naps2 + https://www.naps2.com/sdk + git + + + + + + + diff --git a/NAPS2.Setup/targets/SdkUsers.targets b/NAPS2.Setup/targets/SdkUsers.targets index c3f23a4436..59e96d8412 100644 --- a/NAPS2.Setup/targets/SdkUsers.targets +++ b/NAPS2.Setup/targets/SdkUsers.targets @@ -4,7 +4,6 @@ - diff --git a/NAPS2.Setup/targets/VersionTargets.targets b/NAPS2.Setup/targets/VersionTargets.targets index 48539ce5d6..e378efef16 100644 --- a/NAPS2.Setup/targets/VersionTargets.targets +++ b/NAPS2.Setup/targets/VersionTargets.targets @@ -1,5 +1,6 @@ - 7.0.0 + 8.2.1 + 8.2.1 diff --git a/NAPS2.Tools/Cli.cs b/NAPS2.Tools/Cli.cs index 9b86a119fd..022cd8188b 100644 --- a/NAPS2.Tools/Cli.cs +++ b/NAPS2.Tools/Cli.cs @@ -5,8 +5,9 @@ namespace NAPS2.Tools; public static class Cli { - public static void Run(string command, string args, Dictionary? env = null, - CancellationToken cancel = default, bool noVerbose = false, string? ignoreErrorIfOutputContains = null) + public static int Run(string command, string args, Dictionary? env = null, + CancellationToken cancel = default, bool noVerbose = false, bool alwaysVerbose = false, + string? ignoreErrorIfOutputContains = null, string? workingDir = null) { var startInfo = new ProcessStartInfo { @@ -16,7 +17,7 @@ public static void Run(string command, string args, Dictionary? RedirectStandardError = true, RedirectStandardOutput = true, CreateNoWindow = true, - WorkingDirectory = Paths.SolutionRoot + WorkingDirectory = workingDir ?? Paths.SolutionRoot }; if (env != null) { @@ -48,9 +49,9 @@ void Save(object sender, DataReceivedEventArgs e) savedOutput.AppendLine(e.Data); } - bool print = Output.EnableVerbose && !noVerbose; + bool print = (Output.EnableVerbose && !noVerbose) || alwaysVerbose; proc.OutputDataReceived += print ? Print : Save; - proc.ErrorDataReceived += Print; + proc.ErrorDataReceived += print ? Print : Save; bool ignoreError = false; if (ignoreErrorIfOutputContains != null) @@ -74,6 +75,7 @@ void Save(object sender, DataReceivedEventArgs e) } throw new Exception($"Command failed: {command} {args}"); } + return proc.ExitCode; } finally { diff --git a/NAPS2.Tools/ICommand.cs b/NAPS2.Tools/ICommand.cs index 2b2d80ebd7..f602ff6d99 100644 --- a/NAPS2.Tools/ICommand.cs +++ b/NAPS2.Tools/ICommand.cs @@ -1,4 +1,4 @@ -namespace NAPS2.Tools.Project; +namespace NAPS2.Tools; public interface ICommand where TOptions : OptionsBase { diff --git a/NAPS2.Tools/Localization/CrowdinHelper.cs b/NAPS2.Tools/Localization/CrowdinHelper.cs index 0fd7d0c1c9..d61a56ae00 100644 --- a/NAPS2.Tools/Localization/CrowdinHelper.cs +++ b/NAPS2.Tools/Localization/CrowdinHelper.cs @@ -9,7 +9,7 @@ public static class CrowdinHelper public static CrowdinApiClient GetClient() { - var key = File.ReadAllText(Path.Combine(Paths.Naps2UserFolder, "crowdin")); + var key = File.ReadAllText(Path.Combine(Paths.Naps2UserFolder, "crowdin")).Trim(); var client = new CrowdinApiClient(new CrowdinCredentials { AccessToken = key }); return client; } diff --git a/NAPS2.Tools/Localization/LangCommand.cs b/NAPS2.Tools/Localization/LangCommand.cs new file mode 100644 index 0000000000..15f7d3f436 --- /dev/null +++ b/NAPS2.Tools/Localization/LangCommand.cs @@ -0,0 +1,15 @@ +using NAPS2.Tools.Project; + +namespace NAPS2.Tools.Localization; + +public class LangCommand : ICommand +{ + public int Run(LangOptions opts) + { + new TemplatesCommand().Run(new TemplatesOptions()); + new PushTemplatesCommand().Run(new PushTemplatesOptions()); + new PullTranslationsCommand().Run(new PullTranslationsOptions()); + new ResxCommand().Run(new ResxOptions()); + return 0; + } +} \ No newline at end of file diff --git a/NAPS2.Tools/Localization/LangOptions.cs b/NAPS2.Tools/Localization/LangOptions.cs new file mode 100644 index 0000000000..f8b418b20e --- /dev/null +++ b/NAPS2.Tools/Localization/LangOptions.cs @@ -0,0 +1,9 @@ +using CommandLine; +using NAPS2.Tools.Project; + +namespace NAPS2.Tools.Localization; + +[Verb("lang", HelpText = "Run templates+pushpot+pullpo+resx commands")] +public class LangOptions : OptionsBase +{ +} \ No newline at end of file diff --git a/NAPS2.Tools/Localization/PullTranslationsCommand.cs b/NAPS2.Tools/Localization/PullTranslationsCommand.cs index e7a6e537c5..bbde9cd454 100644 --- a/NAPS2.Tools/Localization/PullTranslationsCommand.cs +++ b/NAPS2.Tools/Localization/PullTranslationsCommand.cs @@ -16,15 +16,6 @@ public int Run(PullTranslationsOptions opts) { var client = CrowdinHelper.GetClient(); - await using var templatesFile = File.OpenRead(Paths.TemplatesFile); - var storage = await client.Storage.AddStorage(templatesFile, "templates.pot"); - - await client.SourceFiles.UpdateOrRestoreFile(CrowdinHelper.PROJECT_ID, CrowdinHelper.TEMPLATES_FILE_ID, - new ReplaceFileRequest - { - StorageId = storage.Id - }); - Output.Verbose("Building Crowdin project translations"); var build = await client.Translations.BuildProjectTranslation(CrowdinHelper.PROJECT_ID, new BuildProjectTranslationRequest()); @@ -75,7 +66,7 @@ await client.SourceFiles.UpdateOrRestoreFile(CrowdinHelper.PROJECT_ID, CrowdinHe } } var outputPath = Path.Combine(Paths.PoFolder, $"{outputLocale}.po"); - await using var outputStream = File.OpenWrite(outputPath); + await using var outputStream = new FileStream(outputPath, FileMode.Create); await entry.Open().CopyToAsync(outputStream); } diff --git a/NAPS2.Tools/Localization/ResxContext.cs b/NAPS2.Tools/Localization/ResxContext.cs index a61c2ee20a..dad50503e2 100644 --- a/NAPS2.Tools/Localization/ResxContext.cs +++ b/NAPS2.Tools/Localization/ResxContext.cs @@ -34,6 +34,7 @@ public void Load(string poFile) { original += line.Substring(1, line.Length - 2); } + original = original.Replace("\\\"", "\""); if (line == null || !line.StartsWith("msgstr", StringComparison.InvariantCulture)) { @@ -45,6 +46,7 @@ public void Load(string poFile) { translated += line.Substring(1, line.Length - 2); } + translated = translated.Replace("\\\"", "\""); Strings[original] = new TranslatableString(original, translated); if (!string.IsNullOrWhiteSpace(translated)) diff --git a/NAPS2.Tools/Localization/TemplatesCommand.cs b/NAPS2.Tools/Localization/TemplatesCommand.cs index 8af6d28c2f..8a9d6c12ff 100644 --- a/NAPS2.Tools/Localization/TemplatesCommand.cs +++ b/NAPS2.Tools/Localization/TemplatesCommand.cs @@ -1,14 +1,12 @@ -using NAPS2.Tools.Project; - -namespace NAPS2.Tools.Localization; +namespace NAPS2.Tools.Localization; public class TemplatesCommand : ICommand { public int Run(TemplatesOptions opts) { var ctx = new TemplatesContext(); - ctx.Load(Path.Combine(Paths.SolutionRoot, @"NAPS2.Sdk\Lang\Resources"), false); - ctx.Load(Path.Combine(Paths.SolutionRoot, @"NAPS2.Lib\Lang\Resources"), false); + ctx.Load(Path.Combine(Paths.SolutionRoot, "NAPS2.Sdk", "Lang", "Resources"), false); + ctx.Load(Path.Combine(Paths.SolutionRoot, "NAPS2.Lib", "Lang", "Resources"), false); ctx.Save(Paths.TemplatesFile); return 0; } diff --git a/NAPS2.Tools/Localization/TranslatableString.cs b/NAPS2.Tools/Localization/TranslatableString.cs index 5e974bbf29..0d8a881532 100644 --- a/NAPS2.Tools/Localization/TranslatableString.cs +++ b/NAPS2.Tools/Localization/TranslatableString.cs @@ -12,5 +12,5 @@ public TranslatableString(string original, string? translation = null) public string? Translation { get; } - public List Context { get; } = new List(); + public List Context { get; } = []; } \ No newline at end of file diff --git a/NAPS2.Tools/N2Config.cs b/NAPS2.Tools/N2Config.cs index 028ddf0009..b47bb06812 100644 --- a/NAPS2.Tools/N2Config.cs +++ b/NAPS2.Tools/N2Config.cs @@ -3,6 +3,7 @@ namespace NAPS2.Tools; +// TODO: N2Config vs files in Naps2UserFolder? public static class N2Config { public static string ShareDir @@ -18,17 +19,25 @@ public static string ShareDir } } + public static string? AutoUpdateCert => EnsureConfigFile().Value("auto-update-cert") ?? ""; + + public static string? WindowsIdentity => EnsureConfigFile().Value("windows-identity") ?? ""; + public static string? MacApplicationIdentity => EnsureConfigFile().Value("mac-application-identity") ?? ""; public static string? MacInstallerIdentity => EnsureConfigFile().Value("mac-installer-identity") ?? ""; public static string? MacNotarizationArgs => EnsureConfigFile().Value("mac-notarization-args") ?? ""; + public static string? FlatpakGpgKey => EnsureConfigFile().Value("flatpak-gpg-key") ?? ""; + + public static string? FlatpakRepo => EnsureConfigFile().Value("flatpak-repo") ?? ""; + private static JToken EnsureConfigFile() { if (!File.Exists(Paths.ConfigFile)) { - File.WriteAllText(Paths.ConfigFile, "{\n \"share-dir\": \"\",\n \"mac-application-identity\": \"\", \"mac-installer-identity\": \"\",\n \"mac-notarization-args\": \"\"\n}\n"); + File.WriteAllText(Paths.ConfigFile, "{\n \"share-dir\": \"\",\\n \\\"windows-identity\\\": \\\"\\\",\n \"mac-application-identity\": \"\", \"mac-installer-identity\": \"\",\n \"mac-notarization-args\": \"\",\n \"flatpak-gpg-key\": \"\",\n \"flatpak-repo\": \"\",\n \"auto-update-cert\": \"\"\n}\n"); } using var file = File.OpenText(Paths.ConfigFile); using var reader = new JsonTextReader(file); diff --git a/NAPS2.Tools/NAPS2.Tools.csproj b/NAPS2.Tools/NAPS2.Tools.csproj index cefaea68be..981756f1ef 100644 --- a/NAPS2.Tools/NAPS2.Tools.csproj +++ b/NAPS2.Tools/NAPS2.Tools.csproj @@ -1,7 +1,7 @@  - net7 + net9 enable Exe NAPS2.Tools @@ -13,10 +13,13 @@ - - + + + + + - + diff --git a/NAPS2.Tools/OptionsBase.cs b/NAPS2.Tools/OptionsBase.cs index 4628804bd8..e1f6949463 100644 --- a/NAPS2.Tools/OptionsBase.cs +++ b/NAPS2.Tools/OptionsBase.cs @@ -1,6 +1,6 @@ using CommandLine; -namespace NAPS2.Tools.Project; +namespace NAPS2.Tools; public class OptionsBase { diff --git a/NAPS2.Tools/Program.cs b/NAPS2.Tools/Program.cs index aa488d9cb4..1977c5bb95 100644 --- a/NAPS2.Tools/Program.cs +++ b/NAPS2.Tools/Program.cs @@ -3,8 +3,10 @@ using NAPS2.Tools.Project; using NAPS2.Tools.Project.Installation; using NAPS2.Tools.Project.Packaging; +using NAPS2.Tools.Project.Releasing; using NAPS2.Tools.Project.Verification; using NAPS2.Tools.Project.Workflows; +using NAPS2.Tools.Sdk; namespace NAPS2.Tools; @@ -17,8 +19,6 @@ public static class Program // - Updates language resources for that language // - Possibly then runs "pkg zip --name test-{lang}" - // TODO: Add a "setver" command that updates version targets, Info.plist, and anything else that needs a version - public static int Main(string[] args) { var commands = new CommandList() @@ -35,7 +35,13 @@ public static int Main(string[] args) .Add() .Add() .Add() - .Add(); + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add(); var result = Parser.Default.ParseArguments(args, commands.OptionTypes); if (result.Errors.Any()) @@ -53,7 +59,7 @@ public static int Main(string[] args) public class CommandList { - private readonly List _optionTypes = new(); + private readonly List _optionTypes = []; private readonly Dictionary _optionTypeToCommandType = new(); public CommandList Add() where TOption : OptionsBase where TCommand : ICommand diff --git a/NAPS2.Tools/Project/BuildCommand.cs b/NAPS2.Tools/Project/BuildCommand.cs index b74503547e..1de2bbb7dc 100644 --- a/NAPS2.Tools/Project/BuildCommand.cs +++ b/NAPS2.Tools/Project/BuildCommand.cs @@ -6,15 +6,32 @@ public class BuildCommand : ICommand { public int Run(BuildOptions opts) { - var constraints = new TargetConstraints + if (opts.BuildType?.ToLowerInvariant() == "sdk") { - AllowDebug = true - }; - foreach (var target in TargetsHelper.Enumerate(opts.BuildType, null, constraints)) + Cli.Run("dotnet", "publish NAPS2.Sdk.Worker.Build/NAPS2.Sdk.Worker.Build.csproj -c Release"); + } + foreach (var target in TargetsHelper.EnumerateBuildTargets(opts.BuildType)) { - var config = GetConfig(target.BuildType); + var config = GetConfig(target); + if (opts.Debug) + { + config += " /p:AddDebugConstant=1"; + } Output.Info($"Building: {config}"); - Cli.Run("dotnet", $"build -c {config}"); + try + { + Cli.Run("dotnet", $"build -c {config}"); + if (OperatingSystem.IsMacOS()) + { + // TODO: Figure out why we need to build twice to get native deps in the output + Cli.Run("dotnet", $"build -c {config}"); + } + } + catch (Exception) + { + Output.Info("Build failed, retrying once"); + Cli.Run("dotnet", $"build -c {config}"); + } Output.OperationEnd($"Built: {config}"); } return 0; @@ -22,10 +39,19 @@ public int Run(BuildOptions opts) private static string GetConfig(BuildType buildType) => buildType switch { - BuildType.Debug => "Debug", - BuildType.Exe => "Release", + BuildType.Debug => OperatingSystem.IsMacOS() + ? "Debug-Mac" + : OperatingSystem.IsLinux() + ? "Debug-Linux" + : "Debug-Windows", + BuildType.Release => OperatingSystem.IsMacOS() + ? "Release-Mac" + : OperatingSystem.IsLinux() + ? "Release-Linux" + : "Release-Windows", BuildType.Msi => "Release-Msi", BuildType.Zip => "Release-Zip", + BuildType.Sdk => "Sdk", _ => throw new ArgumentException() }; } \ No newline at end of file diff --git a/NAPS2.Tools/Project/BuildOptions.cs b/NAPS2.Tools/Project/BuildOptions.cs index 79fd99ee59..c5e38f113f 100644 --- a/NAPS2.Tools/Project/BuildOptions.cs +++ b/NAPS2.Tools/Project/BuildOptions.cs @@ -2,9 +2,12 @@ namespace NAPS2.Tools.Project; -[Verb("build", HelpText = "Builds the project, 'build {all|debug|exe|msi|zip}'")] +[Verb("build", HelpText = "Builds the project, 'build {all|debug|release|msi|zip}'")] public class BuildOptions : OptionsBase { - [Value(0, MetaName = "build type", Required = true, HelpText = "all|debug|exe|msi|zip")] + [Value(0, MetaName = "build type", Required = true, HelpText = "all|debug|release|msi|zip|sdk")] public string? BuildType { get; set; } + + [Option("debug", Required = false, HelpText = "Set DEBUG compile-time constant")] + public bool Debug { get; set; } } \ No newline at end of file diff --git a/NAPS2.Tools/Project/CleanCommand.cs b/NAPS2.Tools/Project/CleanCommand.cs index 914e258950..243a746c54 100644 --- a/NAPS2.Tools/Project/CleanCommand.cs +++ b/NAPS2.Tools/Project/CleanCommand.cs @@ -5,6 +5,7 @@ public class CleanCommand : ICommand public int Run(CleanOptions opts) { Output.Info("Starting clean"); + bool hasError = false; foreach (var projectDir in new DirectoryInfo(Paths.SolutionRoot).EnumerateDirectories("NAPS2.*") .Where(x => x.Name.ToLower() != "naps2.tools")) { @@ -20,11 +21,29 @@ public int Run(CleanOptions opts) catch (Exception ex) { Output.Info($"Could not delete {projectDir.Name}/{cleanDir.Name}/{subDir.Name}: {ex.Message}"); + hasError = true; } } } Output.Verbose($"Cleaned {projectDir.Name}"); } + try + { + var docObj = new DirectoryInfo(Path.Combine(Paths.SolutionRoot, "NAPS2.Sdk", "_doc", "obj")); + if (docObj.Exists) + { + docObj.Delete(true); + } + } + catch (Exception ex) + { + Output.Info($"Could not delete NAPS2.Sdk/doc/obj: {ex.Message}"); + hasError = true; + } + if (hasError) + { + throw new Exception("Cleaned with failures."); + } Output.Info("Cleaned."); return 0; } diff --git a/NAPS2.Tools/Project/DocCommand.cs b/NAPS2.Tools/Project/DocCommand.cs new file mode 100644 index 0000000000..2ee6922db3 --- /dev/null +++ b/NAPS2.Tools/Project/DocCommand.cs @@ -0,0 +1,37 @@ +namespace NAPS2.Tools.Project; + +public class DocCommand : ICommand +{ + public int Run(DocOptions options) + { + // Clean up old build + Directory.Delete(Path.Combine(Paths.SolutionRoot, "NAPS2.Sdk", "_doc", "_site"), true); + foreach (var file in new DirectoryInfo(Path.Combine(Paths.SolutionRoot, "NAPS2.Sdk", "_doc", "api")) + .EnumerateFiles().Where(x => x.Name != ".gitignore")) + { + file.Delete(); + } + + // Copy the SDK readme as index.html + File.Copy( + Path.Combine(Paths.SolutionRoot, "NAPS2.Sdk", "README.md"), + Path.Combine(Paths.SolutionRoot, "NAPS2.Sdk", "_doc", "api", "index.md"), + true); + + if (options.DocCommand == "build") + { + Cli.Run("docfx", "NAPS2.Sdk/_doc/docfx.json", alwaysVerbose: true); + } + if (options.DocCommand == "serve") + { + Cli.Run("docfx", "NAPS2.Sdk/_doc/docfx.json --serve", alwaysVerbose: true); + } + if (options.DocCommand == "push") + { + Cli.Run("docfx", "NAPS2.Sdk/_doc/docfx.json", alwaysVerbose: true); + var deployScriptPath = Path.Combine(Paths.Naps2UserFolder, "deploy-docs.ps1"); + Cli.Run("powershell", deployScriptPath, alwaysVerbose: true); + } + return 0; + } +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/DocOptions.cs b/NAPS2.Tools/Project/DocOptions.cs new file mode 100644 index 0000000000..492b8a1234 --- /dev/null +++ b/NAPS2.Tools/Project/DocOptions.cs @@ -0,0 +1,10 @@ +using CommandLine; + +namespace NAPS2.Tools.Project; + +[Verb("doc", HelpText = "Docfx control")] +public class DocOptions : OptionsBase +{ + [Value(0, MetaName = "doc command", Required = true, HelpText = "build|serve|push")] + public string? DocCommand { get; set; } +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Installation/ExeInstaller.cs b/NAPS2.Tools/Project/Installation/ExeInstaller.cs index 471ddf5eef..a94425e3a1 100644 --- a/NAPS2.Tools/Project/Installation/ExeInstaller.cs +++ b/NAPS2.Tools/Project/Installation/ExeInstaller.cs @@ -6,7 +6,7 @@ public static class ExeInstaller { public static void Install(Platform platform, string version, bool run) { - ProjectHelper.DeleteInstallationFolder(platform); + ProjectHelper.DeleteInstallationFolder(Platform.Win64); var exePath = ProjectHelper.GetPackagePath("exe", platform, version); Output.Info($"Starting exe installer: {exePath}"); diff --git a/NAPS2.Tools/Project/Installation/InstallCommand.cs b/NAPS2.Tools/Project/Installation/InstallCommand.cs index e2b2fb0e08..efa7c83ed7 100644 --- a/NAPS2.Tools/Project/Installation/InstallCommand.cs +++ b/NAPS2.Tools/Project/Installation/InstallCommand.cs @@ -6,33 +6,21 @@ public class InstallCommand : ICommand { public int Run(InstallOptions opts) { - var version = ProjectHelper.GetDefaultProjectVersion(); + var version = ProjectHelper.GetCurrentVersionName(); - var constraints = new TargetConstraints + foreach (var target in TargetsHelper.EnumeratePackageTargets(opts.PackageType, opts.Platform, true)) { - InstallersOnly = true - }; - foreach (var target in TargetsHelper.Enumerate(opts.BuildType, opts.Platform, constraints)) - { - switch (target.BuildType) + switch (target.Type) { - case BuildType.Exe: - if (target.Platform.IsLinux()) - { - FlatpakInstaller.Install(target.Platform, version, opts.Run); - } - else if (target.Platform.IsMac()) - { - // TODO: Mac install? - } - else if (target.Platform.IsWindows()) - { - ExeInstaller.Install(target.Platform, version, opts.Run); - } + case PackageType.Exe: + ExeInstaller.Install(target.Platform, version, opts.Run); break; - case BuildType.Msi: + case PackageType.Msi: MsiInstaller.Install(target.Platform, version, opts.Run); break; + case PackageType.Flatpak: + FlatpakInstaller.Install(target.Platform, version, opts.Run); + break; } } return 0; diff --git a/NAPS2.Tools/Project/Installation/InstallOptions.cs b/NAPS2.Tools/Project/Installation/InstallOptions.cs index bcc002e450..02c9cc99f1 100644 --- a/NAPS2.Tools/Project/Installation/InstallOptions.cs +++ b/NAPS2.Tools/Project/Installation/InstallOptions.cs @@ -2,11 +2,11 @@ namespace NAPS2.Tools.Project.Installation; -[Verb("install", HelpText = "Install the packaged app, 'install {exe|msi}'")] +[Verb("install", HelpText = "Install the packaged app, 'install {exe|msi|flatpak|pkg|deb|rpm}'")] public class InstallOptions : OptionsBase { - [Value(0, MetaName = "build type", Required = true, HelpText = "exe|msi")] - public string? BuildType { get; set; } + [Value(0, MetaName = "package type", Required = true, HelpText = "exe|msi|flatpak|pkg|deb|rpm")] + public string? PackageType { get; set; } [Option('p', "platform", Required = false, HelpText = "win|win32|win64|mac|macintel|macarm|linux")] public string? Platform { get; set; } diff --git a/NAPS2.Tools/Project/Installation/MsiInstaller.cs b/NAPS2.Tools/Project/Installation/MsiInstaller.cs index f1a58abdf1..66d7954565 100644 --- a/NAPS2.Tools/Project/Installation/MsiInstaller.cs +++ b/NAPS2.Tools/Project/Installation/MsiInstaller.cs @@ -6,7 +6,7 @@ public static class MsiInstaller { public static void Install(Platform platform, string version, bool run) { - ProjectHelper.DeleteInstallationFolder(platform); + ProjectHelper.DeleteInstallationFolder(Platform.Win64); var msiPath = ProjectHelper.GetPackagePath("msi", platform, version); Output.Info($"Starting msi installer: {msiPath}"); diff --git a/NAPS2.Tools/Project/Packaging/DebPackager.cs b/NAPS2.Tools/Project/Packaging/DebPackager.cs new file mode 100644 index 0000000000..da7491741a --- /dev/null +++ b/NAPS2.Tools/Project/Packaging/DebPackager.cs @@ -0,0 +1,79 @@ +using NAPS2.Tools.Project.Targets; + +namespace NAPS2.Tools.Project.Packaging; + +public static class DebPackager +{ + public static void PackageDeb(PackageInfo pkgInfo, bool noSign) + { + var debPath = pkgInfo.GetPath("deb"); + Output.Info($"Packaging deb: {debPath}"); + + Output.Verbose("Building binaries"); + var runtimeId = pkgInfo.Platform == Platform.LinuxArm ? "linux-arm64" : "linux-x64"; + Cli.Run("dotnet", $"clean NAPS2.App.Gtk -c Release -r {runtimeId}"); + Cli.Run("dotnet", $"publish NAPS2.App.Gtk -c Release -r {runtimeId} --self-contained /p:DebugType=None /p:DebugSymbols=false"); + + Output.Verbose("Creating package"); + + var workingDir = Path.Combine(Paths.SetupObj, "deb"); + if (Directory.Exists(workingDir)) + { + Directory.Delete(workingDir, true); + } + + Directory.CreateDirectory(workingDir); + + var controlDir = Path.Combine(workingDir, "DEBIAN"); + Directory.CreateDirectory(controlDir); + + // Create control files + var template = File.ReadAllText(Path.Combine(Paths.SetupLinux, "debian-control")); + template = template.Replace("{!arch}", pkgInfo.Platform == Platform.LinuxArm ? "arm64" : "amd64"); + template = template.Replace("{!version}", pkgInfo.VersionNumber); + File.WriteAllText(Path.Combine(controlDir, "control"), template); + + // Copy binary files + var publishDir = Path.Combine(Paths.SolutionRoot, "NAPS2.App.Gtk", "bin", "Release", "net9", runtimeId, + "publish"); + var targetDir = Path.Combine(workingDir, "usr/lib/naps2"); + ProjectHelper.CopyDirectory(publishDir, targetDir); + + // Copy metadata files + var iconDir = Path.Combine(workingDir, "usr/share/icons/hicolor/128x128/apps"); + Directory.CreateDirectory(iconDir); + var appsDir = Path.Combine(workingDir, "usr/share/applications"); + Directory.CreateDirectory(appsDir); + var metainfoDir = Path.Combine(workingDir, "usr/share/metainfo"); + Directory.CreateDirectory(metainfoDir); + File.Copy( + Path.Combine(Paths.SolutionRoot, "NAPS2.Lib", "Icons", "scanner-128.png"), + Path.Combine(iconDir, "com.naps2.Naps2.png")); + File.Copy( + Path.Combine(Paths.SetupLinux, "com.naps2.Naps2.desktop"), + Path.Combine(appsDir, "naps2.desktop")); + File.WriteAllText(Path.Combine(metainfoDir, "com.naps2.Naps2.metainfo.xml"), + ProjectHelper.GetLinuxMetaInfo(pkgInfo)); + File.Copy( + Path.Combine(Paths.SolutionRoot, "LICENSE"), + Path.Combine(targetDir, "LICENSE.txt")); + + // Create symlinks + var binDir = Path.Combine(workingDir, "usr/bin"); + Directory.CreateDirectory(binDir); + Cli.Run("ln", $"-s /usr/lib/naps2/naps2 {Path.Combine(binDir, "naps2")}"); + + // Fix permissions + var nativeLibsFolder = pkgInfo.Platform == Platform.LinuxArm ? "_linuxarm" : "_linux"; + Cli.Run("chmod", $"a+x {Path.Combine(targetDir, nativeLibsFolder, "tesseract")}"); + + Cli.Run("dpkg-deb", $"-Zxz --root-owner-group --build {workingDir} {debPath}"); + + if (!noSign) + { + Cli.Run("debsigs", $"--sign=origin {debPath}"); + } + + Output.OperationEnd($"Packaged deb: {debPath}"); + } +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Packaging/FlatpakPackager.cs b/NAPS2.Tools/Project/Packaging/FlatpakPackager.cs index 2cf58b34c4..2c9566e922 100644 --- a/NAPS2.Tools/Project/Packaging/FlatpakPackager.cs +++ b/NAPS2.Tools/Project/Packaging/FlatpakPackager.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using System.Text.RegularExpressions; using NAPS2.Tools.Project.Targets; namespace NAPS2.Tools.Project.Packaging; @@ -15,14 +14,6 @@ public static void Package(PackageInfo packageInfo, bool noPre) VerifyCanBuildArch(packageInfo.Platform); - // Update metainfo file with the current version/date - var metaInfo = File.ReadAllText(Path.Combine(Paths.SetupLinux, "com.naps2.Naps2.metainfo.xml")); - var version = ProjectHelper.GetDefaultProjectVersion(); - var date = DateTime.Now.ToString("yyyy-MM-dd"); - metaInfo = Regex.Replace(metaInfo, - @"]+/>", - $""); - // Update manifest file with the correct paths var manifest = File.ReadAllText(Path.Combine(Paths.SetupLinux, "com.naps2.Naps2.yml")); // TODO: Update this after we use a real repo path @@ -31,7 +22,8 @@ public static void Package(PackageInfo packageInfo, bool noPre) // Copy metainfo, manifest, icon, and desktop files to a temp folder var packageDir = Path.Combine(Paths.SetupObj, "flatpak"); Directory.CreateDirectory(packageDir); - File.WriteAllText(Path.Combine(packageDir, "com.naps2.Naps2.metainfo.xml"), metaInfo); + File.WriteAllText(Path.Combine(packageDir, "com.naps2.Naps2.metainfo.xml"), + ProjectHelper.GetLinuxMetaInfo(packageInfo)); File.WriteAllText(Path.Combine(packageDir, "com.naps2.Naps2.yml"), manifest); File.Copy( Path.Combine(Paths.SetupLinux, "com.naps2.Naps2.desktop"), @@ -39,6 +31,9 @@ public static void Package(PackageInfo packageInfo, bool noPre) File.Copy( Path.Combine(Paths.SolutionRoot, "NAPS2.Lib", "Icons", "scanner-128.png"), Path.Combine(packageDir, "com.naps2.Naps2.png"), true); + File.Copy( + Path.Combine(Paths.SetupLinux, "sane-streamdevices.patch"), + Path.Combine(packageDir, "sane-streamdevices.patch"), true); if (!noPre) { @@ -63,16 +58,21 @@ public static void Package(PackageInfo packageInfo, bool noPre) _ => "x86_64" }; var stateDir = Path.Combine(packageDir, "builder-state"); - Cli.Run("flatpak-builder", $"--arch {arch} --force-clean --state-dir {stateDir} {buildDir} {manifestPath}"); + Cli.Run("flatpak", $"run org.flatpak.Builder --arch {arch} --force-clean --state-dir {stateDir} {buildDir} {manifestPath}"); // Generate a temp repo with the package info Output.Verbose("Creating flatpak repo"); - var repoDir = Path.Combine(packageDir, "repo"); - Cli.Run("flatpak", $"build-export --arch {arch} {repoDir} {buildDir}"); + var repoDir = N2Config.FlatpakRepo; + repoDir = string.IsNullOrEmpty(repoDir) ? Path.Combine(packageDir, "repo") : repoDir; + var branch = packageInfo.VersionName.Contains('b') ? "beta" : "stable"; + var gpg = N2Config.FlatpakGpgKey; + var gpgArgs = string.IsNullOrEmpty(gpg) ? "" : $"--gpg-sign={gpg}"; + Cli.Run("flatpak", $"build-export {gpgArgs} --arch {arch} {repoDir} {buildDir} {branch}"); // Generate a single-file bundle from the temp repo Output.Verbose("Building flatpak bundle"); - Cli.Run("flatpak", $"build-bundle --arch {arch} {repoDir} {bundlePath} com.naps2.Naps2"); + Cli.Run("flatpak", + $"build-bundle {gpgArgs} --arch {arch} {repoDir} {bundlePath} com.naps2.Naps2 {branch} --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo"); Output.OperationEnd($"Packaged flatpak: {bundlePath}"); } @@ -88,4 +88,4 @@ private static void VerifyCanBuildArch(Platform platform) Cli.Run("qemu-aarch64-static", "--version"); } } -} \ No newline at end of file +} diff --git a/NAPS2.Tools/Project/Packaging/InnoSetupPackager.cs b/NAPS2.Tools/Project/Packaging/InnoSetupPackager.cs index 1e9e28a8aa..16a2042663 100644 --- a/NAPS2.Tools/Project/Packaging/InnoSetupPackager.cs +++ b/NAPS2.Tools/Project/Packaging/InnoSetupPackager.cs @@ -5,40 +5,64 @@ namespace NAPS2.Tools.Project.Packaging; public static class InnoSetupPackager { - public static void PackageExe(PackageInfo packageInfo) + public static void PackageExe(Func pkgInfoFunc, Platform platform, bool noSign) { - var exePath = packageInfo.GetPath("exe"); + string arch = platform == Platform.WinArm64 ? "arm64" : "x64"; + + Output.Verbose("Building binaries"); + if (platform != Platform.WinArm64) + { + Cli.Run("dotnet", "clean NAPS2.App.Worker -c Release"); + } + Cli.Run("dotnet", $"clean NAPS2.App.WinForms -r win-{arch} -c Release"); + Cli.Run("dotnet", $"clean NAPS2.App.Console -r win-{arch} -c Release"); + if (platform != Platform.WinArm64) + { + Cli.Run("dotnet", "publish NAPS2.App.Worker -c Release /p:DebugType=None /p:DebugSymbols=false"); + } + Cli.Run("dotnet", $"publish NAPS2.App.WinForms -r win-{arch} -c Release /p:DebugType=None /p:DebugSymbols=false"); + Cli.Run("dotnet", $"publish NAPS2.App.Console -r win-{arch} -c Release /p:DebugType=None /p:DebugSymbols=false"); + + var pkgInfo = pkgInfoFunc(); + if (!noSign) + { + Output.Verbose("Signing contents"); + WindowsSigning.SignContents(pkgInfo); + } + + var exePath = pkgInfo.GetPath("exe"); Output.Info($"Packaging exe installer: {exePath}"); - var innoDefPath = GenerateInnoDef(packageInfo); + var innoDefPath = GenerateInnoDef(pkgInfo, arch); // TODO: Use https://github.com/DomGries/InnoDependencyInstaller for .net dependency var iscc = Environment.ExpandEnvironmentVariables("%PROGRAMFILES(X86)%/Inno Setup 6/iscc.exe"); Cli.Run(iscc, $"\"{innoDefPath}\""); + if (!noSign) + { + Output.Verbose("Signing installer"); + WindowsSigning.SignFile(exePath); + } + Output.OperationEnd($"Packaged exe installer: {exePath}"); } - private static string GenerateInnoDef(PackageInfo packageInfo) + private static string GenerateInnoDef(PackageInfo packageInfo, string arch) { var template = File.ReadAllText(Path.Combine(Paths.SetupWindows, "setup.template.iss")); var defLines = new StringBuilder(); - defLines.AppendLine($"#define AppVersion \"{packageInfo.Version}\""); - defLines.AppendLine($"#define AppPlatform \"{packageInfo.Platform.PackageName()}\""); + defLines.AppendLine($"#define AppVersion \"{packageInfo.VersionNumber}\""); + defLines.AppendLine($"#define AppVersionName \"{packageInfo.VersionName}\""); + defLines.AppendLine($"#define AppPlatform \"{packageInfo.PackageName}\""); template = template.Replace("; !defs", defLines.ToString()); - var arch = new StringBuilder(); - if (packageInfo.Platform is Platform.Win64 or Platform.Win) - { - arch.AppendLine("ArchitecturesInstallIn64BitMode=x64"); - template = template.Replace("; !clean32", @"Type: filesandordirs; Name: ""{commonpf32}\NAPS2"""); - } - if (packageInfo.Platform == Platform.Win64) - { - arch.AppendLine("ArchitecturesAllowed=x64"); - } - template = template.Replace("; !arch", arch.ToString()); + var archLines = new StringBuilder(); + archLines.AppendLine($"ArchitecturesInstallIn64BitMode={arch}"); + template = template.Replace("; !clean32", @"Type: filesandordirs; Name: ""{commonpf32}\NAPS2"""); + archLines.AppendLine($"ArchitecturesAllowed={arch}"); + template = template.Replace("; !arch", archLines.ToString()); var fileLines = new StringBuilder(); foreach (var pkgFile in packageInfo.Files) diff --git a/NAPS2.Tools/Project/Packaging/MacPackager.cs b/NAPS2.Tools/Project/Packaging/MacPackager.cs index 6d3037900d..fae0fc38cb 100644 --- a/NAPS2.Tools/Project/Packaging/MacPackager.cs +++ b/NAPS2.Tools/Project/Packaging/MacPackager.cs @@ -14,7 +14,7 @@ public static void Package(PackageInfo packageInfo, bool noSign, bool noNotarize Output.Info($"Packaging installer: {pkgPath}"); Output.Verbose("Building bundle"); - var basePath = Path.Combine(Paths.SolutionRoot, "NAPS2.App.Mac", "bin", "Release", "net7-macos10.15"); + var basePath = Path.Combine(Paths.SolutionRoot, "NAPS2.App.Mac", "bin", "Release", "net9-macos"); string bundlePath = packageInfo.Platform switch { Platform.Mac => Path.Combine(basePath, "NAPS2.app"), @@ -26,12 +26,12 @@ public static void Package(PackageInfo packageInfo, bool noSign, bool noNotarize { Directory.Delete(bundlePath, true); } - if (packageInfo.Platform == Platform.Mac) - { - Cli.Run("dotnet", $"build NAPS2.App.Mac -c Release"); - } - else + Cli.Run("dotnet", $"build NAPS2.App.Mac -c Release"); + if (packageInfo.Platform != Platform.Mac) { + // By default resource files are only copied into the universal bundle. + // We also need to run this to copy into the arch-specific bundle. + // (And we still have to build above as weirdly this command on its own ONLY copies resources.) var runtimeId = packageInfo.Platform == Platform.MacArm ? "osx-arm64" : "osx-x64"; Cli.Run("dotnet", $"build NAPS2.App.Mac -c Release -r {runtimeId}"); } @@ -57,6 +57,18 @@ public static void Package(PackageInfo packageInfo, bool noSign, bool noNotarize Cli.Run("codesign", $"-s \"{applicationIdentity}\" \"{mainExe}\" -f --options runtime --entitlements \"{entitlements}\""); } + + var tesseractPath1 = Path.Combine(bundlePath, "Contents", "Resources", "_mac", "tesseract"); + if (Path.Exists(tesseractPath1)) + { + Cli.Run("chmod", $"+x \"{tesseractPath1}\""); + } + var tesseractPath2 = Path.Combine(bundlePath, "Contents", "Resources", "_macarm", "tesseract"); + if (Path.Exists(tesseractPath2)) + { + Cli.Run("chmod", $"+x \"{tesseractPath2}\""); + } + var signArgs = string.IsNullOrEmpty(installerIdentity) ? "" : $"--sign \"{installerIdentity}\""; Cli.Run("productbuild", $"--component \"{bundlePath}\" /Applications {signArgs} \"{pkgPath}\""); diff --git a/NAPS2.Tools/Project/Packaging/MsixPackager.cs b/NAPS2.Tools/Project/Packaging/MsixPackager.cs new file mode 100644 index 0000000000..ca4a9f87d4 --- /dev/null +++ b/NAPS2.Tools/Project/Packaging/MsixPackager.cs @@ -0,0 +1,110 @@ +using System.Text; + +namespace NAPS2.Tools.Project.Packaging; + +public static class MsixPackager +{ + public static void PackageMsix(Func pkgInfoFunc, bool noSign) + { + Output.Verbose("Building binaries"); + Cli.Run("dotnet", "clean NAPS2.App.Worker -c Release"); + Cli.Run("dotnet", "clean NAPS2.App.WinForms -r win-x64 -c Release"); + Cli.Run("dotnet", "clean NAPS2.App.Console -r win-x64 -c Release"); + Cli.Run("dotnet", + "publish NAPS2.App.Worker -c Release /p:DebugType=None /p:DebugSymbols=false /p:DefineConstants=MSI"); + Cli.Run("dotnet", + "publish NAPS2.App.WinForms -r win-x64 -c Release /p:DebugType=None /p:DebugSymbols=false /p:DefineConstants=MSI"); + Cli.Run("dotnet", + "publish NAPS2.App.Console -r win-x64 -c Release /p:DebugType=None /p:DebugSymbols=false /p:DefineConstants=MSI"); + + var pkgInfo = pkgInfoFunc(); + + var msixPath = pkgInfo.GetPath("msix"); + var msixStorePath = msixPath.Replace(".msix", "-store.msix"); + Output.Info($"Packaging msix installer: {msixPath}"); + + if (File.Exists(msixStorePath)) + { + File.Delete(msixStorePath); + } + if (File.Exists(msixPath)) + { + File.Delete(msixPath); + } + + var manifestPath = Path.Combine(Paths.SetupObj, "appxmanifest.xml"); + var resourcesPriPath = Path.Combine(Paths.SetupObj, "resources.pri"); + var msixConfig = Path.Combine(Paths.SetupWindows, "msix"); + File.Copy(Path.Combine(msixConfig, "appxmanifest.xml"), manifestPath, true); + var publishDir = Path.Combine(Paths.SolutionRoot, "NAPS2.App.WinForms", "bin", "Release", "net9", "win-x64", + "publish"); + var mappingFilePath = Path.Combine(Paths.SetupObj, "msixmapping.txt"); + + var mappingFile = new StreamWriter(new FileStream(mappingFilePath, FileMode.Create)); + mappingFile.WriteLine("[Files]"); + foreach (var file in pkgInfo.Files) + { + var fullSourcePath = Path.Combine(publishDir, file.SourcePath); + mappingFile.WriteLine($"\"{fullSourcePath}\" \"{file.DestPath}\""); + } + foreach (var assetFile in new DirectoryInfo(Path.Combine(msixConfig, "Assets")).EnumerateFiles()) + { + mappingFile.WriteLine($"\"{assetFile.FullName}\" \"Assets\\{assetFile.Name}\""); + } + mappingFile.WriteLine($"\"{manifestPath}\" \"AppxManifest.xml\""); + mappingFile.WriteLine($"\"{resourcesPriPath}\" \"resources.pri\""); + mappingFile.Close(); + + var makePri = @"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\makepri.exe"; + var makeAppx = @"C:\Program Files (x86)\Windows Kits\10\App Certification Kit\makeappx.exe"; + + if (File.Exists(resourcesPriPath)) File.Delete(resourcesPriPath); + Cli.Run(makePri, + $"new /pr \"{msixConfig}\" /cf \"{msixConfig}\\priconfig.xml\" /o /of \"{resourcesPriPath}\" /mn \"{manifestPath}\""); + + File.WriteAllText(manifestPath, File.ReadAllText(manifestPath) + .Replace( + "Version=\"1.0.0.0\"", + $"Version=\"{pkgInfo.VersionNumber}.0\"") + .Replace( + "", + GetSupportedLanguages(pkgInfo))); + + Cli.Run(makeAppx, $"pack /f \"{mappingFilePath}\" /p \"{msixStorePath}\""); + + File.WriteAllText(manifestPath, File.ReadAllText(manifestPath) + .Replace( + "CN=1D624E39-8523-4AAC-B3B6-1452E653A003", + N2Config.WindowsIdentity)); + + Cli.Run(makeAppx, $"pack /f \"{mappingFilePath}\" /p \"{msixPath}\""); + + if (!noSign) + { + Output.Verbose("Signing installer"); + WindowsSigning.SignFile(msixPath); + } + + Output.OperationEnd($"Packaged msix installer: {msixPath}"); + } + + private static string GetSupportedLanguages(PackageInfo pkgInfo) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + foreach (var language in pkgInfo.Files.Where(x => x.FileName.StartsWith("NAPS2.Lib.resources.dll")) + .Select(x => Path.GetFileName(x.DestDir))) + { + // MSIX expects language codes a bit different from NAPS2 + // TODO: Would it be better to use these codes internally? Would it match up more with Windows? + var correctedLanguage = language switch + { + "sr" => "sr-Cyrl", + "sr-CS" => "sr-Latn", + _ => language + }; + sb.AppendLine($""); + } + return sb.ToString(); + } +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Packaging/PackageCommand.cs b/NAPS2.Tools/Project/Packaging/PackageCommand.cs index 3607ea4cbf..714555cfe0 100644 --- a/NAPS2.Tools/Project/Packaging/PackageCommand.cs +++ b/NAPS2.Tools/Project/Packaging/PackageCommand.cs @@ -1,5 +1,8 @@ +using System.Text; using System.Text.RegularExpressions; using NAPS2.Tools.Project.Targets; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace NAPS2.Tools.Project.Packaging; @@ -7,46 +10,45 @@ public class PackageCommand : ICommand { public int Run(PackageOptions opts) { - // TODO: Fix windows targets to ensure that the project is built - // TODO: Allow customizing dotnet version - var constraints = new TargetConstraints + foreach (var target in TargetsHelper.EnumeratePackageTargets( + opts.PackageType, opts.Platform, true, opts.XCompile)) { - AllowMultiplePlatforms = true, - RequireBuildablePlatform = true - }; - foreach (var target in TargetsHelper.Enumerate(opts.BuildType, opts.Platform, constraints)) - { - switch (target.BuildType) + PackageInfo GetPackageInfoForConfig() => GetPackageInfo(target.Platform, opts.Name); + switch (target.Type) { - case BuildType.Exe: - // TODO: We might need configs designed for mac + linux - if (target.Platform.IsLinux()) - { - FlatpakPackager.Package(GetPackageInfo(target.Platform, "Release"), opts.NoPre); - } - else if (target.Platform.IsMac()) - { - MacPackager.Package(GetPackageInfo(target.Platform, "Release"), opts.NoSign, opts.NoNotarize); - } - else if (target.Platform.IsWindows()) - { - InnoSetupPackager.PackageExe(GetPackageInfo(target.Platform, "Release")); - } + case PackageType.Exe: + InnoSetupPackager.PackageExe(GetPackageInfoForConfig, target.Platform, opts.NoSign); break; - case BuildType.Msi: - WixToolsetPackager.PackageMsi(GetPackageInfo(target.Platform, "Release-Msi")); + case PackageType.Msi: + WixToolsetPackager.PackageMsi(GetPackageInfoForConfig, opts.NoSign); break; - case BuildType.Zip: - ZipArchivePackager.PackageZip(GetPackageInfo(target.Platform, "Release-Zip")); + case PackageType.Msix: + MsixPackager.PackageMsix(GetPackageInfoForConfig, opts.NoSign); + break; + case PackageType.Zip: + ZipArchivePackager.PackageZip(GetPackageInfoForConfig, target.Platform, opts.NoSign); + break; + case PackageType.Deb: + DebPackager.PackageDeb(GetPackageInfoForConfig(), opts.NoSign); + break; + case PackageType.Rpm: + RpmPackager.PackageRpm(GetPackageInfoForConfig(), opts.NoSign); + break; + case PackageType.Flatpak: + FlatpakPackager.Package(GetPackageInfoForConfig(), opts.NoPre); + break; + case PackageType.Pkg: + MacPackager.Package(GetPackageInfoForConfig(), opts.NoSign, opts.NoNotarize); break; } } return 0; } - private static PackageInfo GetPackageInfo(Platform platform, string preferredConfig) + private static PackageInfo GetPackageInfo(Platform platform, string? packageName) { - var pkgInfo = new PackageInfo(platform, ProjectHelper.GetProjectVersion("NAPS2.App.WinForms")); + var pkgInfo = new PackageInfo(platform, ProjectHelper.GetCurrentVersionName(), + ProjectHelper.GetCurrentVersion(), packageName); if (!platform.IsWindows()) { @@ -54,88 +56,202 @@ private static PackageInfo GetPackageInfo(Platform platform, string preferredCon return pkgInfo; } + string arch = platform == Platform.WinArm64 ? "win-arm64" : "win-x64"; + foreach (var project in new[] - { "NAPS2.Sdk", "NAPS2.Lib", "NAPS2.App.Worker", "NAPS2.App.Console", "NAPS2.App.WinForms" }) + { "NAPS2.App.WinForms", "NAPS2.App.Console" }) { - var buildPath = Path.Combine(Paths.SolutionRoot, project, "bin", preferredConfig, "net462"); + var buildPath = Path.Combine(Paths.SolutionRoot, project, "bin", "Release", "net9-windows", arch, + "publish"); if (!Directory.Exists(buildPath)) { - buildPath = Path.Combine(Paths.SolutionRoot, project, "bin", "Release", "net462"); - } - if (!Directory.Exists(buildPath)) - { - throw new Exception($"Could not find build path. Maybe run 'n2 build' first? {buildPath}"); + throw new Exception($"Could not find build path."); } PopulatePackageInfo(buildPath, platform, pkgInfo); } - var appBuildPath = Path.Combine(Paths.SolutionRoot, "NAPS2.App.WinForms", "bin", "Release", "net462"); - if (platform == Platform.Win) - { - AddPlatformFiles(pkgInfo, appBuildPath, "_win32"); - AddPlatformFiles(pkgInfo, appBuildPath, "_win64"); - } - else if (platform == Platform.Win32) - { - AddPlatformFiles(pkgInfo, appBuildPath, "_win32"); - } - else if (platform == Platform.Win64) + // Include the 32-bit worker (on both x64 and arm64) for TWAIN support + var workerPath = Path.Combine(Paths.SolutionRoot, "NAPS2.App.Worker", "bin", "Release", "net9-windows", + "win-x86", "publish"); + pkgInfo.AddFile(new PackageFile(workerPath, "lib", "NAPS2.Worker.exe")); + + var appBuildPath = Path.Combine(Paths.SolutionRoot, "NAPS2.App.WinForms", "bin", "Release", "net9-windows", + arch, "publish"); + if (platform == Platform.Win64) { AddPlatformFiles(pkgInfo, appBuildPath, "_win64"); // Special case as we have a 64 bit main app and a 32 bit worker - AddPlatformFile(pkgInfo, appBuildPath, "_win32", "NAPS2.Wia.Native.dll"); AddPlatformFile(pkgInfo, appBuildPath, "_win32", "twaindsm.dll"); - // TODO: We should run pdfium in a 64-bit worker - AddPlatformFile(pkgInfo, appBuildPath, "_win32", "pdfium.dll"); } - else + if (platform == Platform.WinArm64) { - throw new Exception("Unsupported platform"); + AddPlatformFiles(pkgInfo, appBuildPath, "_winarm"); } + pkgInfo.AddFile(new PackageFile(appBuildPath, "", "appsettings.xml")); pkgInfo.AddFile(new PackageFile(Paths.SolutionRoot, "", "LICENSE", "license.txt")); pkgInfo.AddFile(new PackageFile(Paths.SolutionRoot, "", "CONTRIBUTORS", "contributors.txt")); + return pkgInfo; } private static void PopulatePackageInfo(string buildPath, Platform platform, PackageInfo pkgInfo) { + string[] excludeDlls = + { + // DLLs that are unneeded but missed by the built-in trimming + "Microsoft.VisualBasic", + "System.Data", + "System.Private.DataContract", + "System.Windows.Forms.Design", + // For WPF + "D3D", + "Presentation", + "Reach", + "System.Windows.Controls.Ribbon", + "System.Windows.Input", + "System.Windows.Presentation", + "System.Xaml", + "UIAutomation", + "WindowsBase", + "wpfgfx", + // For debugging + "createdump", + "Microsoft.DiaSymReader", + "mscordaccore", + "mscordbi", + }; + var dir = new DirectoryInfo(buildPath); if (!dir.Exists) { throw new Exception($"Could not find path: {dir.FullName}"); } - foreach (var exeFile in dir.EnumerateFiles("*.exe")) + + // Parse the NAPS2.deps.json file to strip out dependencies we're "manually" trimming via "excludeDlls" + var depsFile = dir.EnumerateFiles("*.deps.json").First(); + JObject deps; + using (var stream = depsFile.OpenText()) + using (var reader = new JsonTextReader(stream)) + deps = (JObject) JToken.ReadFrom(reader); + string arch = platform == Platform.WinArm64 ? "win-arm64" : "win-x64"; + var targets = (JObject) deps["targets"]![$".NETCoreApp,Version=v9.0/{arch}"]!; + foreach (var pair in targets) { - var dest = exeFile.Name.ToLower() switch + var target = (JObject) pair.Value!; + if (target.TryGetValue("runtime", out var runtime)) + { + foreach (var runtimeDlls in new Dictionary((JObject) runtime)) + { + var parts = runtimeDlls.Key.Split("/"); + var dllName = parts.Last(); + if (excludeDlls.Any(exclude => dllName.StartsWith(exclude))) + { + ((JObject) runtime).Remove(runtimeDlls.Key); + } + } + } + if (target.TryGetValue("resources", out var resources)) + { + foreach (var runtimeDlls in new Dictionary((JObject) resources)) + { + var dllName = runtimeDlls.Key.Split("/").Last(); + if (excludeDlls.Any(exclude => dllName.StartsWith(exclude))) + { + ((JObject) resources).Remove(runtimeDlls.Key); + } + } + } + if (target.TryGetValue("native", out var native)) { - "naps2.worker.exe" => "lib", - _ => "" - }; - pkgInfo.AddFile(exeFile, dest); + foreach (var runtimeDlls in new Dictionary((JObject) native)) + { + var dllName = runtimeDlls.Key.Split("/").Last(); + if (excludeDlls.Any(exclude => dllName.StartsWith(exclude))) + { + ((JObject) native).Remove(runtimeDlls.Key); + } + } + } } - foreach (var configFile in dir.EnumerateFiles("*.exe.config")) + using (StreamWriter file = depsFile.CreateText()) + using (JsonTextWriter writer = new JsonTextWriter(file) { Formatting = Formatting.Indented }) + deps.WriteTo(writer); + + // Add each included file to the package contents + foreach (var exeFile in dir.EnumerateFiles("*.exe")) { - var dest = configFile.Name.ToLower() switch + if (excludeDlls.All(exclude => !exeFile.Name.StartsWith(exclude))) { - "naps2.worker.exe.config" => "lib", - _ => "" - }; - pkgInfo.AddFile(configFile, dest); + if (exeFile.Name == "NAPS2.Worker.exe") continue; + PatchExe(exeFile); + pkgInfo.AddFile(exeFile, ""); + } + } + foreach (var configFile in dir.EnumerateFiles("*.json")) + { + pkgInfo.AddFile(configFile, "lib"); } foreach (var dllFile in dir.EnumerateFiles("*.dll")) { - // TODO: Blacklist unneeded dlls - pkgInfo.AddFile(dllFile, "lib"); + if (excludeDlls.All(exclude => !dllFile.Name.StartsWith(exclude))) + { + pkgInfo.AddFile(dllFile, "lib"); + } } - foreach (var langFolder in dir.EnumerateDirectories().Where(x => Regex.IsMatch(x.Name, "[a-z]{2}(-[A-Za-z]+)?"))) + foreach (var langFolder in dir.EnumerateDirectories() + .Where(x => Regex.IsMatch(x.Name, "[a-z]{2}(-[A-Za-z]+)?"))) { foreach (var resourceDll in langFolder.EnumerateFiles("*.resources.dll")) { - pkgInfo.AddFile(resourceDll, Path.Combine("lib", langFolder.Name)); - pkgInfo.Languages.Add(langFolder.Name); + if (excludeDlls.All(exclude => !resourceDll.Name.StartsWith(exclude))) + { + pkgInfo.AddFile(resourceDll, Path.Combine("lib", langFolder.Name)); + pkgInfo.Languages.Add(langFolder.Name); + } + } + } + } + + private static void PatchExe(FileInfo exeFile) + { + // The dotnet base exes (e.g. NAPS2.exe) have a hard-coded path for the relevant dll (e.g. NAPS2.dll). + // This path is also the path at which all the dependencies are searched. By default, the path is in the current + // directory, but we can easily replace it with a subpath to the "lib" folder. (Note that the path is padded so + // we don't even need to offset the bytes afterward.) This means everything other than the exes can live in + // that "lib" subfolder once we do this patch. + var bytes = File.ReadAllBytes(exeFile.FullName); + var from = Path.ChangeExtension(exeFile.Name, ".dll"); + var to = @"lib\" + from; + var fromBytes = Encoding.UTF8.GetBytes(from); + var toBytes = Encoding.UTF8.GetBytes(to); + var index = SearchBytes(bytes, fromBytes); + if (bytes[(index - 4)..index] is [(byte) 'l', (byte) 'i', (byte) 'b', (byte) '\\']) + { + // Already patched + return; + } + for (int i = 0; i < toBytes.Length; i++) + { + bytes[index + i] = toBytes[i]; + } + File.WriteAllBytes(exeFile.FullName, bytes); + } + + private static int SearchBytes(byte[] haystack, byte[] needle) + { + var len = needle.Length; + var limit = haystack.Length - len; + for (var i = 0; i <= limit; i++) + { + var k = 0; + for (; k < len; k++) + { + if (needle[k] != haystack[i + k]) break; } + if (k == len) return i; } + return -1; } private static void AddPlatformFiles(PackageInfo pkgInfo, string buildPath, string platformPath) @@ -149,6 +265,7 @@ private static void AddPlatformFiles(PackageInfo pkgInfo, string buildPath, stri private static void AddPlatformFile(PackageInfo pkgInfo, string buildPath, string platformPath, string fileName) { - pkgInfo.AddFile(new PackageFile(Path.Combine(buildPath, platformPath), Path.Combine("lib", platformPath), fileName)); + pkgInfo.AddFile(new PackageFile(Path.Combine(buildPath, platformPath), Path.Combine("lib", platformPath), + fileName)); } } \ No newline at end of file diff --git a/NAPS2.Tools/Project/Packaging/PackageInfo.cs b/NAPS2.Tools/Project/Packaging/PackageInfo.cs index 160c2e91e5..80716e3117 100644 --- a/NAPS2.Tools/Project/Packaging/PackageInfo.cs +++ b/NAPS2.Tools/Project/Packaging/PackageInfo.cs @@ -4,27 +4,37 @@ namespace NAPS2.Tools.Project.Packaging; public class PackageInfo { - private readonly List _files = new(); - private readonly HashSet _destPaths = new(); + private readonly List _files = []; + private readonly HashSet _destPaths = []; - public PackageInfo(Platform platform, string version) + public PackageInfo(Platform platform, string versionName, string versionNumber, string? packageName) { Platform = platform; - Version = version; + VersionName = versionName; + VersionNumber = versionNumber; + PackageName = packageName == null ? platform.PackageName() : $"{platform.PackageName()}-{packageName}";; } public Platform Platform { get; } - public string Version { get; } + public string VersionName { get; } + + public string VersionNumber { get; } + + public string PackageName { get; } public string GetPath(string ext) { - return ProjectHelper.GetPackagePath(ext, Platform, Version); + return ProjectHelper.GetPackagePath(ext, Platform, VersionName, PackageName); } public IEnumerable Files => _files; - public HashSet Languages { get; } = new(); + ///

+ /// The set of languages that have resource files. This may be different from the set of NAPS2-supported languages + /// as WinForms etc. provide resources for a disjoint set of languages. + /// + public HashSet Languages { get; } = []; public void AddFile(FileInfo file, string destFolder, string? destFileName = null) { diff --git a/NAPS2.Tools/Project/Packaging/PackageOptions.cs b/NAPS2.Tools/Project/Packaging/PackageOptions.cs index 9f55e7d94d..4ab0b603a8 100644 --- a/NAPS2.Tools/Project/Packaging/PackageOptions.cs +++ b/NAPS2.Tools/Project/Packaging/PackageOptions.cs @@ -2,15 +2,18 @@ namespace NAPS2.Tools.Project.Packaging; -[Verb("pkg", HelpText = "Package the project, 'pkg {all|exe|msi|zip}'")] +[Verb("pkg", HelpText = "Package the project, 'pkg {all|exe|msi|zip|flatpak|pkg|deb|rpm}'")] public class PackageOptions : OptionsBase { - [Value(0, MetaName = "build type", Required = false, HelpText = "all|exe|msi|zip")] - public string? BuildType { get; set; } + [Value(0, MetaName = "package type", Required = false, HelpText = "all|exe|msi|zip|flatpak|pkg|deb|rpm")] + public string? PackageType { get; set; } [Option('p', "platform", Required = false, HelpText = "win|win32|win64|mac|macintel|macarm|linux")] public string? Platform { get; set; } + [Option("name", Required = false, HelpText = "Name to be appended to the package filename")] + public string? Name { get; set; } + [Option("nopre", Required = false, HelpText = "Skip pre-packaging steps")] public bool NoPre { get; set; } @@ -19,8 +22,7 @@ public class PackageOptions : OptionsBase [Option("nonotarize", Required = false, HelpText = "Skip notarization only")] public bool NoNotarize { get; set; } - - // TODO: Add net target (net462/net6/net6-windows etc.) - // TODO: Add an option to change the package name for building test packages + [Option("xcompile", Required = false, HelpText = "Cross-compile packages where possible (e.g. build linux-arm64 on linux-x64)")] + public bool XCompile { get; set; } } \ No newline at end of file diff --git a/NAPS2.Tools/Project/Packaging/RpmPackager.cs b/NAPS2.Tools/Project/Packaging/RpmPackager.cs new file mode 100644 index 0000000000..6b0fb43a8f --- /dev/null +++ b/NAPS2.Tools/Project/Packaging/RpmPackager.cs @@ -0,0 +1,91 @@ +using NAPS2.Tools.Project.Targets; + +namespace NAPS2.Tools.Project.Packaging; + +public static class RpmPackager +{ + public static void PackageRpm(PackageInfo pkgInfo, bool noSign) + { + var rpmPath = pkgInfo.GetPath("rpm"); + Output.Info($"Packaging rpm: {rpmPath}"); + + Output.Verbose("Building binaries"); + var runtimeId = pkgInfo.Platform == Platform.LinuxArm ? "linux-arm64" : "linux-x64"; + Cli.Run("dotnet", $"clean NAPS2.App.Gtk -c Release -r {runtimeId}"); + Cli.Run("dotnet", + $"publish NAPS2.App.Gtk -c Release -r {runtimeId} --self-contained /p:DebugType=None /p:DebugSymbols=false"); + + Output.Verbose("Creating package"); + + var workingDir = Path.Combine(Paths.SetupObj, "rpm"); + if (Directory.Exists(workingDir)) + { + Directory.Delete(workingDir, true); + } + + Directory.CreateDirectory(workingDir); + + foreach (var subdir in new[] { "RPMS", "SRPMS", "BUILD", "SOURCES", "SPECS", "tmp" }) + { + Directory.CreateDirectory(Path.Combine(workingDir, subdir)); + } + + var dirArg = $"-D \"_topdir {workingDir}\" -D \"_tmppath {workingDir}/tmp\""; + + // Create spec file + var template = File.ReadAllText(Path.Combine(Paths.SetupLinux, "rpm-spec")); + template = template.Replace("{!version}", pkgInfo.VersionNumber); + File.WriteAllText(Path.Combine(workingDir, "SPECS/naps2.spec"), template); + + // Copy binary files + var publishDir = Path.Combine(Paths.SolutionRoot, "NAPS2.App.Gtk", "bin", "Release", "net9", runtimeId, + "publish"); + var filesDir = Path.Combine(workingDir, $"naps2-{pkgInfo.VersionNumber}"); + var targetDir = Path.Combine(filesDir, "usr/lib/naps2"); + ProjectHelper.CopyDirectory(publishDir, targetDir); + + // Copy metadata files + var iconDir = Path.Combine(filesDir, "usr/share/icons/hicolor/128x128/apps"); + Directory.CreateDirectory(iconDir); + var appsDir = Path.Combine(filesDir, "usr/share/applications"); + Directory.CreateDirectory(appsDir); + var metainfoDir = Path.Combine(filesDir, "usr/share/metainfo"); + Directory.CreateDirectory(metainfoDir); + File.Copy( + Path.Combine(Paths.SolutionRoot, "NAPS2.Lib", "Icons", "scanner-128.png"), + Path.Combine(iconDir, "com.naps2.Naps2.png")); + File.Copy( + Path.Combine(Paths.SetupLinux, "com.naps2.Naps2.desktop"), + Path.Combine(appsDir, "naps2.desktop")); + File.WriteAllText(Path.Combine(metainfoDir, "com.naps2.Naps2.metainfo.xml"), + ProjectHelper.GetLinuxMetaInfo(pkgInfo)); + File.Copy( + Path.Combine(Paths.SolutionRoot, "LICENSE"), + Path.Combine(targetDir, "LICENSE.txt")); + + // Create symlinks + var binDir = Path.Combine(filesDir, "usr/bin"); + Directory.CreateDirectory(binDir); + Cli.Run("ln", $"-s /usr/lib/naps2/naps2 {Path.Combine(binDir, "naps2")}"); + + // Compress files + Cli.Run("tar", $"-zcvf {workingDir}/SOURCES/naps2-{pkgInfo.VersionNumber}.tar.gz {Path.GetFileName(filesDir)}", + workingDir: workingDir); + + // Build RPM + var arch = pkgInfo.Platform == Platform.LinuxArm ? "aarch64" : "x86_64"; + Cli.Run("rpmbuild", $"{dirArg} -ba --target {arch} {workingDir}/SPECS/naps2.spec"); + var sourceRpmPath = Path.Combine(workingDir, $"RPMS/{arch}/naps2-{pkgInfo.VersionNumber}-1.{arch}.rpm"); + + // Sign + if (!noSign) + { + Cli.Run("rpmsign", $"--addsign {sourceRpmPath}"); + } + + // Copy to output + File.Copy(sourceRpmPath, rpmPath, true); + + Output.OperationEnd($"Packaged rpm: {rpmPath}"); + } +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Packaging/WindowsSigning.cs b/NAPS2.Tools/Project/Packaging/WindowsSigning.cs new file mode 100644 index 0000000000..4e1b5e7bed --- /dev/null +++ b/NAPS2.Tools/Project/Packaging/WindowsSigning.cs @@ -0,0 +1,62 @@ +using System.Reflection; +using System.Security.Cryptography.X509Certificates; + +namespace NAPS2.Tools.Project.Packaging; + +public static class WindowsSigning +{ + public static void SignContents(PackageInfo packageInfo) + { + // Exclude resource DLLs from signing as that saves 40% time/space and doesn't really provide any value. + // TODO: Maybe reevaluate this + foreach (var batch in packageInfo.Files + .Where(file => Path.GetExtension(file.FileName) is ".exe" or ".dll") + .Where(file => !file.FileName.EndsWith(".resources.dll")) + .Where(NeedsSignature) + .Chunk(10)) + { + var files = string.Join(" ", batch.Select(file => $"\"{file.SourcePath}\"")); + if (files.Length > 0) + { + Cli.Run("signtool", + $"sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a /as {files}"); + } + } + } + + private static bool NeedsSignature(PackageFile file) + { + if (Path.GetExtension(file.FileName) == ".exe") + { + return true; + } + try + { + AssemblyName.GetAssemblyName(file.SourcePath); + } + catch (Exception) + { + // Not a .NET assembly + return false; + } + try + { +#pragma warning disable SYSLIB0057 // No replacement for this obsolete method yet + X509Certificate.CreateFromSignedFile(file.SourcePath); +#pragma warning restore SYSLIB0057 + // Already has a signature + return false; + } + catch (Exception) + { + // No signature + return true; + } + } + + public static void SignFile(string path) + { + Cli.Run("signtool", + $"sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a \"{path}\""); + } +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Packaging/WixToolsetPackager.cs b/NAPS2.Tools/Project/Packaging/WixToolsetPackager.cs index 0f21716cae..490131ad6e 100644 --- a/NAPS2.Tools/Project/Packaging/WixToolsetPackager.cs +++ b/NAPS2.Tools/Project/Packaging/WixToolsetPackager.cs @@ -6,51 +6,73 @@ namespace NAPS2.Tools.Project.Packaging; public static class WixToolsetPackager { - public static void PackageMsi(PackageInfo pkgInfo) + public static void PackageMsi(Func pkgInfoFunc, bool noSign) { + Output.Verbose("Building binaries"); + Cli.Run("dotnet", "clean NAPS2.App.Worker -c Release"); + Cli.Run("dotnet", "clean NAPS2.App.WinForms -r win-x64 -c Release"); + Cli.Run("dotnet", "clean NAPS2.App.Console -r win-x64 -c Release"); + Cli.Run("dotnet", "publish NAPS2.App.Worker -c Release /p:DebugType=None /p:DebugSymbols=false /p:DefineConstants=MSI"); + Cli.Run("dotnet", "publish NAPS2.App.WinForms -r win-x64 -c Release /p:DebugType=None /p:DebugSymbols=false /p:DefineConstants=MSI"); + Cli.Run("dotnet", "publish NAPS2.App.Console -r win-x64 -c Release /p:DebugType=None /p:DebugSymbols=false /p:DefineConstants=MSI"); + + var pkgInfo = pkgInfoFunc(); + if (!noSign) + { + Output.Verbose("Signing contents"); + WindowsSigning.SignContents(pkgInfo); + } + var msiPath = pkgInfo.GetPath("msi"); Output.Info($"Packaging msi installer: {msiPath}"); + var wxsPath = GenerateWxs(pkgInfo); - var candle = Environment.ExpandEnvironmentVariables("%PROGRAMFILES(X86)%/WiX Toolset v3.11/bin/candle.exe"); + var candle = Environment.ExpandEnvironmentVariables("%PROGRAMFILES(X86)%/WiX Toolset v3.14/bin/candle.exe"); var arch = pkgInfo.Platform == Platform.Win64 ? "x64" : "x86"; Cli.Run(candle, $"\"{wxsPath}\" -o \"{Paths.SetupObj}/\" -arch {arch}"); var wixobjPath = wxsPath.Replace(".wxs", ".wixobj"); - var light = Environment.ExpandEnvironmentVariables("%PROGRAMFILES(X86)%/WiX Toolset v3.11/bin/light.exe"); + var light = Environment.ExpandEnvironmentVariables("%PROGRAMFILES(X86)%/WiX Toolset v3.14/bin/light.exe"); Cli.Run(light, $"\"{wixobjPath}\" -spdb -ext WixUIExtension -o \"{msiPath}\""); + + if (!noSign) + { + Output.Verbose("Signing installer"); + WindowsSigning.SignFile(msiPath); + } + Output.OperationEnd($"Packaged msi installer: {msiPath}"); } private static string GenerateWxs(PackageInfo packageInfo) { - // TODO: Delete Setup.Msi project var template = File.ReadAllText(Path.Combine(Paths.SetupWindows, "setup.template.wxs")); - template = template.Replace("{{ !version }}", packageInfo.Version); - + template = template.Replace("{{ !version }}", packageInfo.VersionNumber); + var rootLines = new StringBuilder(); foreach (var rootFile in packageInfo.Files.Where(x => x.DestDir == "")) { DeclareFile(rootLines, rootFile); } template = template.Replace("", rootLines.ToString()); - + var libLines = new StringBuilder(); foreach (var libFile in packageInfo.Files.Where(x => x.DestDir == "lib")) { DeclareFile(libLines, libFile); } template = template.Replace("", libLines.ToString()); - + var win32Lines = new StringBuilder(); foreach (var win32File in packageInfo.Files.Where(x => x.DestDir == Path.Combine("lib", "_win32"))) { DeclareFile(win32Lines, win32File); } template = template.Replace("", win32Lines.ToString()); - + var win64Lines = new StringBuilder(); foreach (var win64File in packageInfo.Files.Where(x => x.DestDir == Path.Combine("lib", "_win64"))) { @@ -77,12 +99,6 @@ private static string GenerateWxs(PackageInfo packageInfo) template = template.Replace("", langRefsLines.ToString()); template = template.Replace("", langFilesLines.ToString()); - var replacementFor64 = packageInfo.Platform == Platform.Win32 ? "" : "$3"; - template = Regex.Replace(template, "(|}})(.*?)(|}})", replacementFor64, RegexOptions.Singleline); - - var replacementForUniv = packageInfo.Platform == Platform.Win ? "$3" : ""; - template = Regex.Replace(template, "(|}})(.*?)(|}})", replacementForUniv, RegexOptions.Singleline); - var wxsPath = Path.Combine(Paths.SetupObj, "setup.wxs"); File.WriteAllText(wxsPath, template); return wxsPath; diff --git a/NAPS2.Tools/Project/Packaging/ZipArchivePackager.cs b/NAPS2.Tools/Project/Packaging/ZipArchivePackager.cs index ccb5f68c0d..2a113afb6d 100644 --- a/NAPS2.Tools/Project/Packaging/ZipArchivePackager.cs +++ b/NAPS2.Tools/Project/Packaging/ZipArchivePackager.cs @@ -1,13 +1,40 @@ using System.IO.Compression; +using NAPS2.Tools.Project.Targets; namespace NAPS2.Tools.Project.Packaging; public static class ZipArchivePackager { - public static void PackageZip(PackageInfo pkgInfo) + public static void PackageZip(Func pkgInfoFunc, Platform platform, bool noSign) { + string arch = platform == Platform.WinArm64 ? "arm64" : "x64"; + + Output.Verbose("Building binaries"); + if (platform != Platform.WinArm64) + { + Cli.Run("dotnet", "clean NAPS2.App.Worker -c Release"); + } + Cli.Run("dotnet", $"clean NAPS2.App.WinForms -r win-{arch} -c Release"); + Cli.Run("dotnet", $"clean NAPS2.App.Console -r win-{arch} -c Release"); + if (platform != Platform.WinArm64) + { + Cli.Run("dotnet", + "publish NAPS2.App.Worker -c Release /p:DebugType=None /p:DebugSymbols=false /p:DefineConstants=ZIP"); + } + Cli.Run("dotnet", $"publish NAPS2.App.WinForms -r win-{arch} -c Release /p:DebugType=None /p:DebugSymbols=false /p:DefineConstants=ZIP"); + Cli.Run("dotnet", $"publish NAPS2.App.Console -r win-{arch} -c Release /p:DebugType=None /p:DebugSymbols=false /p:DefineConstants=ZIP"); + Cli.Run("dotnet", "build NAPS2.App.PortableLauncher -c Release"); + + var pkgInfo = pkgInfoFunc(); + if (!noSign) + { + Output.Verbose("Signing contents"); + WindowsSigning.SignContents(pkgInfo); + } + var zipPath = pkgInfo.GetPath("zip"); Output.Info($"Packaging zip archive: {zipPath}"); + if (File.Exists(zipPath)) { File.Delete(zipPath); @@ -19,6 +46,11 @@ public static void PackageZip(PackageInfo pkgInfo) { throw new Exception($"Could not find portable exe: {portableExe}"); } + if (!noSign) + { + Output.Verbose("Signing NAPS2.Portable.exe"); + WindowsSigning.SignFile(portableExe); + } using var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create); foreach (var file in pkgInfo.Files) diff --git a/NAPS2.Tools/Project/ProjectHelper.cs b/NAPS2.Tools/Project/ProjectHelper.cs index 9c7e1f7213..d7953eb8b7 100644 --- a/NAPS2.Tools/Project/ProjectHelper.cs +++ b/NAPS2.Tools/Project/ProjectHelper.cs @@ -1,43 +1,76 @@ using System.Text.RegularExpressions; using System.Threading; +using NAPS2.Tools.Project.Packaging; using NAPS2.Tools.Project.Targets; namespace NAPS2.Tools.Project; public static class ProjectHelper { - public static string GetProjectVersion(string projectName) + public static string GetCurrentVersion() { - var projectPath = Path.Combine(Paths.SolutionRoot, projectName, $"{projectName}.csproj"); - var projectFile = XDocument.Load(projectPath); - var version = projectFile.Descendants().SingleOrDefault(x => x.Name == "Version")?.Value; + var versionTargetsPath = Path.Combine(Paths.Setup, "targets", "VersionTargets.targets"); + var versionTargetsFile = XDocument.Load(versionTargetsPath); + var version = versionTargetsFile.Descendants().SingleOrDefault(x => x.Name.LocalName == "Version")?.Value; if (version == null) { - throw new Exception($"Could not read version from project: {projectPath}"); + throw new Exception($"Could not read version from project: {versionTargetsPath}"); } - if (!Regex.IsMatch(version, @"[0-9]+(\.[0-9]+){2}")) + return version; + } + + public static string GetCurrentVersionName() + { + var versionTargetsPath = Path.Combine(Paths.Setup, "targets", "VersionTargets.targets"); + var versionTargetsFile = XDocument.Load(versionTargetsPath); + var version = versionTargetsFile.Descendants().SingleOrDefault(x => x.Name.LocalName == "VersionName")?.Value; + if (version == null) { - throw new Exception($"Invalid project version: {version}"); + throw new Exception($"Could not read version from project: {versionTargetsPath}"); } return version; } - public static string GetDefaultProjectVersion() + public static string GetSdkVersion() { - return GetProjectVersion("NAPS2.App.WinForms"); + var sdkTargetsPath = Path.Combine(Paths.Setup, "targets", "SdkPackageTargets.targets"); + var sdkTargetsFile = XDocument.Load(sdkTargetsPath); + var version = sdkTargetsFile.Descendants().SingleOrDefault(x => x.Name.LocalName == "PackageVersion")?.Value; + if (version == null) + { + throw new Exception($"Could not read sdk version: {sdkTargetsPath}"); + } + return version; } - public static string GetPackagePath(string ext, Platform platform, string? version = null) + public static string[] GetSdkProjects() => new[] + { + "NAPS2.Sdk", + "NAPS2.Sdk.Worker.Win32", + "NAPS2.Escl", + "NAPS2.Escl.Server", + "NAPS2.Internals", + "NAPS2.Images", + "NAPS2.Images.Gdi", + "NAPS2.Images.Gtk", + "NAPS2.Images.Mac", + "NAPS2.Images.ImageSharp", + "NAPS2.Images.Wpf", + }; + + public static string GetPackagePath(string ext, Platform platform, string? version = null, + string? packageName = null) { - version ??= GetProjectVersion("NAPS2.App.WinForms"); - var path = Path.Combine(Paths.Publish, version, $"naps2-{version}-{platform.PackageName()}.{ext}"); + version ??= GetCurrentVersionName(); + packageName ??= platform.PackageName(); + var path = Path.Combine(Paths.Publish, version, $"naps2-{version}-{packageName}.{ext}"); Directory.CreateDirectory(Path.GetDirectoryName(path)!); return path; } public static string GetInstallationFolder(Platform platform) { - var pfVar = platform == Platform.Win32 ? "%PROGRAMFILES(X86)%" : "%PROGRAMFILES%"; + var pfVar = "%PROGRAMFILES%"; var pfPath = Environment.ExpandEnvironmentVariables(pfVar); return Path.Combine(pfPath, "NAPS2"); } @@ -76,4 +109,35 @@ public static void CloseMostRecentNaps2() Thread.Sleep(100); } } + + public static void CopyDirectory(string sourceDir, string destinationDir) + { + + var dir = new DirectoryInfo(sourceDir); + if (!dir.Exists) + throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); + DirectoryInfo[] dirs = dir.GetDirectories(); + Directory.CreateDirectory(destinationDir); + foreach (FileInfo file in dir.GetFiles()) + { + string targetFilePath = Path.Combine(destinationDir, file.Name); + file.CopyTo(targetFilePath); + } + foreach (DirectoryInfo subDir in dirs) + { + string newDestinationDir = Path.Combine(destinationDir, subDir.Name); + CopyDirectory(subDir.FullName, newDestinationDir); + } + } + + public static string GetLinuxMetaInfo(PackageInfo packageInfo) + { + // Update metainfo file with the current version/date + var metaInfo = File.ReadAllText(Path.Combine(Paths.SetupLinux, "com.naps2.Naps2.metainfo.xml")); + var date = DateTime.Now.ToString("yyyy-MM-dd"); + metaInfo = Regex.Replace(metaInfo, + @"]+/>", + $""); + return metaInfo; + } } \ No newline at end of file diff --git a/NAPS2.Tools/Project/Releasing/SetVersionCommand.cs b/NAPS2.Tools/Project/Releasing/SetVersionCommand.cs new file mode 100644 index 0000000000..8d959caaf4 --- /dev/null +++ b/NAPS2.Tools/Project/Releasing/SetVersionCommand.cs @@ -0,0 +1,48 @@ +using System.Text.RegularExpressions; + +namespace NAPS2.Tools.Project.Releasing; + +public class SetVersionCommand : ICommand +{ + public int Run(SetVersionOptions opts) + { + var versionName = opts.VersionName!; + if (!Regex.IsMatch(versionName, @"[1-9][0-9]*\.[0-9]+(\.[0-9]+|b[1-9][0-9]*)")) + { + throw new Exception("Invalid version format, expected X.Y.Z or X.YbZ"); + } + var versionNumber = versionName.Replace("b", "."); + + Output.Info($"Setting version to {versionName} ({versionNumber})"); + + var versionTargets = Path.Combine(Paths.Setup, "targets", "VersionTargets.targets"); + ReplaceInFile( + versionTargets, + @".*", + $"{versionNumber}"); + ReplaceInFile( + versionTargets, + @".*", + $"{versionName}"); + + var macProj = Path.Combine(Paths.SolutionRoot, "NAPS2.App.Mac", "Info.plist"); + ReplaceInFile( + macProj, + $@"CFBundleShortVersionString{Environment.NewLine} .*", + $"CFBundleShortVersionString{Environment.NewLine} {versionNumber}"); + + Output.OperationEnd("Version set."); + return 0; + } + + private void ReplaceInFile(string path, string pattern, string replacement) + { + var text = File.ReadAllText(path); + if (!Regex.IsMatch(text, pattern)) + { + throw new Exception($"Could not find match for '{pattern}' in '{Path.GetFileName(path)}'"); + } + text = Regex.Replace(text, pattern, replacement); + File.WriteAllText(path, text); + } +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Releasing/SetVersionOptions.cs b/NAPS2.Tools/Project/Releasing/SetVersionOptions.cs new file mode 100644 index 0000000000..bd53578fbb --- /dev/null +++ b/NAPS2.Tools/Project/Releasing/SetVersionOptions.cs @@ -0,0 +1,10 @@ +using CommandLine; + +namespace NAPS2.Tools.Project.Releasing; + +[Verb("setver", HelpText = "Set the version everywhere, e.g. 'setver 7.0b1' or 'setver 7.1.0'")] +public class SetVersionOptions : OptionsBase +{ + [Value(0, MetaName = "version name", Required = true)] + public string? VersionName { get; set; } +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Releasing/UploadCommand.cs b/NAPS2.Tools/Project/Releasing/UploadCommand.cs new file mode 100644 index 0000000000..1a2d017348 --- /dev/null +++ b/NAPS2.Tools/Project/Releasing/UploadCommand.cs @@ -0,0 +1,262 @@ +using System.Net.Http; +using System.Threading; +using NAPS2.Tools.Project.Targets; +using NuGet.Common; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using Octokit; +using Octokit.Internal; +using Renci.SshNet; +using FileMode = System.IO.FileMode; +using Repository = NuGet.Protocol.Core.Types.Repository; + +namespace NAPS2.Tools.Project.Releasing; + +public class UploadCommand : ICommand +{ + private const int GITHUB_REPO_ID = 39348622; + + public int Run(UploadOptions opts) + { + string version = opts.Version ?? ProjectHelper.GetCurrentVersionName(); + if (opts.Target != "sdk") + { + // Validate all package files + foreach (var target in TargetsHelper.EnumeratePackageTargets()) + { + var file = new FileInfo(target.PackagePath(version)); + if (!file.Exists) + { + throw new Exception($"Expected package to exist: {file.FullName}"); + } + if (opts.Version == null && !opts.AllowOld && file.LastWriteTime < DateTime.Now - TimeSpan.FromHours(2)) + { + throw new Exception($"Expected package to be recently modified: {file.FullName}"); + } + } + } + + bool didSomething = false; + if (opts.Target is "all" or "github") + { + Output.Info("Uploading binaries to Github"); + UploadToGithub(version).Wait(); + didSomething = true; + } + if (opts.Target is "all" or "sourceforge") + { + Output.Info("Uploading binaries to SourceForge"); + UploadToSourceForge(version).Wait(); + didSomething = true; + } + if (opts.Target is "all" or "static") + { + Output.Info("Uploading binaries to static site downloads.naps2.com"); + UploadToStaticSite(version, opts.PackageType); + didSomething = true; + } + if (opts.Target is "all" or "apt") + { + Output.Info("Updating Apt metadata on downloads.naps2.com"); + UpdateAptMetadata(version); + didSomething = true; + } + if (opts.Target is "sdk") + { + UploadToNuget().Wait(); + didSomething = true; + } + + if (didSomething) + { + Output.OperationEnd(opts.Target == "sdk" ? "Packages uploaded." : "Binaries uploaded."); + } + else + { + Output.Info("No upload target."); + } + return 0; + } + + private async Task UploadToNuget() + { + var repository = Repository.Factory.GetCoreV3("https://api.nuget.org/v3/index.json"); + var resource = await repository.GetResourceAsync(); + var v = ProjectHelper.GetSdkVersion(); + var packagePaths = ProjectHelper.GetSdkProjects() + .Select(x => $"{x}/bin/Release/{x}.{v}.nupkg").ToList(); + var key = await File.ReadAllTextAsync(Path.Combine(Paths.Naps2UserFolder, "nuget")); + await resource.Push( + packagePaths, + symbolSource: null, + timeoutInSecond: 5 * 60, + disableBuffering: false, + getApiKey: _ => key, + getSymbolApiKey: _ => null, + noServiceEndpoint: false, + skipDuplicate: false, + symbolPackageUpdateResource: null, + NullLogger.Instance); + } + + private async Task UploadToGithub(string version) + { + var token = await File.ReadAllTextAsync(Path.Combine(Paths.Naps2UserFolder, "github")); + var client = new GitHubClient(new ProductHeaderValue("cyanfish"), + new InMemoryCredentialStore(new Credentials(token))); + + var commits = await client.Repository.Commit.GetAll(GITHUB_REPO_ID, new CommitRequest + { + Since = DateTimeOffset.Now - TimeSpan.FromDays(1) + }); + var publishCommit = commits.SingleOrDefault(x => x.Commit.Message == $"PUBLISH ({version})") ?? + throw new Exception($"Could not find publish commit for {version}. Maybe push to github?"); + Output.Verbose($"Found publish commit {publishCommit.Sha}"); + + Output.Verbose($"Creating draft release"); + var release = await client.Repository.Release.Create(GITHUB_REPO_ID, + new NewRelease($"v{version}") + { + Name = version, + Body = GetChangelog(), + Draft = true, + Prerelease = version.Contains("b"), + TargetCommitish = publishCommit.Sha + }); + foreach (var package in TargetsHelper.EnumeratePackageTargets()) + { + var path = package.PackagePath(version); + if (File.Exists(path)) + { + Output.Verbose($"Uploading asset {path}"); + await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + await client.Repository.Release.UploadAsset(release, + new ReleaseAssetUpload(Path.GetFileName(path), "application/octet-stream", stream, + null)); + } + } + Output.Info($"Created draft Github release: {release.HtmlUrl}"); + } + + private async Task UploadToSourceForge(string version) + { + var config = await File.ReadAllTextAsync(Path.Combine(Paths.Naps2UserFolder, "sourceforge")); + var parts = config.Split("\n"); + var username = parts[0].Trim(); + var privateKeyFile = parts[1].Trim(); + var apiKey = parts[2].Trim(); + var connectionInfo = new ConnectionInfo("frs.sourceforge.net", username, + new PrivateKeyAuthenticationMethod(username, new PrivateKeyFile(privateKeyFile))); + using var client = new SftpClient(connectionInfo); + await client.ConnectAsync(CancellationToken.None); + Output.Verbose("Connected to SourceForge"); + try + { + client.CreateDirectory($"/home/frs/project/naps2/{version}"); + } + catch (Exception) + { + // Maybe already created + } + Output.Verbose("Updating readme with changelog"); + await using var changelogStream = File.OpenRead(Path.Combine(Paths.SolutionRoot, "CHANGELOG.md")); + client.UploadFile(changelogStream, "/home/frs/project/naps2/readme.txt"); + foreach (var package in TargetsHelper.EnumeratePackageTargets().Reverse()) + { + var path = package.PackagePath(version); + if (File.Exists(path)) + { + Output.Verbose($"Uploading asset {path}"); + await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + client.UploadFile(stream, $"/home/frs/project/naps2/{version}/{Path.GetFileName(path)}"); + } + } + if (version.Contains('b')) + { + Output.Verbose("Skipping default downloads for beta"); + return; + } + Output.Verbose($"Setting default downloads"); + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + await httpClient.PutAsync( + $"https://sourceforge.net/projects/naps2/files/{version}/naps2-{version}-win-x64.exe", + new FormUrlEncodedContent([ + new("default", "windows"), + new("default", "android"), + new("default", "bsd"), + new("default", "solaris"), + new("default", "others"), + new("api_key", apiKey) + ])); + await httpClient.PutAsync( + $"https://sourceforge.net/projects/naps2/files/{version}/naps2-{version}-mac-univ.pkg", + new FormUrlEncodedContent([ + new("default", "mac"), + new("api_key", apiKey) + ])); + await httpClient.PutAsync( + $"https://sourceforge.net/projects/naps2/files/{version}/naps2-{version}-linux-x64.deb", + new FormUrlEncodedContent([ + new("default", "linux"), + new("api_key", apiKey) + ])); + } + + private string GetChangelog() + { + var lines = File.ReadAllLines(Path.Combine(Paths.SolutionRoot, "CHANGELOG.md")); + var expected = $"Changes in {ProjectHelper.GetCurrentVersionName()}:"; + if (lines[0] == expected) + { + return string.Join('\n', lines.TakeWhile(x => x.Trim() != "")); + } + throw new Exception("Changelog needs updating (did not start with \"{expected}\")"); + } + + private void UploadToStaticSite(string version, string? packageType) + { + Cli.Run("ssh", $"user@downloads.naps2.com \"mkdir -p /var/www/html/{version}/\""); + // Only upload packages that are needed (e.g. for Microsoft store, Apt repo) + foreach (var package in TargetsHelper.EnumeratePackageTargets(packageType, null, false) + .Where(x => Path.GetExtension(x.PackagePath(version)) is ".msi" or ".deb")) + { + var path = package.PackagePath(version); + var fileName = Path.GetFileName(path); + Output.Verbose($"Uploading asset {path}"); + Cli.Run("scp", $"{path} user@downloads.naps2.com:/var/www/html/{version}/{fileName}"); + } + Output.Info($"Uploaded files."); + } + + private void UpdateAptMetadata(string version) + { + if (version.Contains('b')) + { + Output.Verbose("Skipping APT metadata for beta"); + return; + } + + var aptTemp = Path.Combine(Paths.SetupObj, "apt"); + if (Directory.Exists(aptTemp)) + { + Directory.Delete(aptTemp, true); + } + Directory.CreateDirectory(aptTemp); + + Cli.Run("ssh", "user@downloads.naps2.com \"mkdir -p /home/user/apt-temp-packages/\""); + Cli.Run("ssh", "user@downloads.naps2.com \"mkdir -p /home/user/apt-temp-release/\""); + Cli.Run("ssh", + "user@downloads.naps2.com \"cd /var/www/html/ ; apt-ftparchive packages . > /home/user/apt-temp-packages/Packages\" ; apt-ftparchive release /home/user/apt-temp-packages/ > /home/user/apt-temp-release/Release\""); + Cli.Run("scp", + $"user@downloads.naps2.com:/home/user/apt-temp-packages/Packages {Path.Combine(aptTemp, "Packages")}"); + Cli.Run("scp", + $"user@downloads.naps2.com:/home/user/apt-temp-release/Release {Path.Combine(aptTemp, "Release")}"); + Cli.Run("gpg", $"--output {Path.Combine(aptTemp, "Release.gpg")} --sign {Path.Combine(aptTemp, "Release")}"); + Cli.Run("gpg", $"--output {Path.Combine(aptTemp, "InRelease")} --clearsign {Path.Combine(aptTemp, "Release")}"); + Cli.Run("scp", $"{Path.Combine(aptTemp, "Packages")} user@downloads.naps2.com:/var/www/html/Packages"); + Cli.Run("scp", $"{Path.Combine(aptTemp, "Release")} user@downloads.naps2.com:/var/www/html/Release"); + Cli.Run("scp", $"{Path.Combine(aptTemp, "Release.gpg")} user@downloads.naps2.com:/var/www/html/Release.gpg"); + Cli.Run("scp", $"{Path.Combine(aptTemp, "InRelease")} user@downloads.naps2.com:/var/www/html/InRelease"); + } +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Releasing/UploadOptions.cs b/NAPS2.Tools/Project/Releasing/UploadOptions.cs new file mode 100644 index 0000000000..b916e57216 --- /dev/null +++ b/NAPS2.Tools/Project/Releasing/UploadOptions.cs @@ -0,0 +1,19 @@ +using CommandLine; + +namespace NAPS2.Tools.Project.Releasing; + +[Verb("upload", HelpText = "Upload the release binaries")] +public class UploadOptions : OptionsBase +{ + [Value(0, MetaName = "target", Required = true, HelpText = "github|sourceforge|static|sdk|all")] + public string? Target { get; set; } + + [Option("version", Required = false, HelpText = "Version to upload")] + public string? Version { get; set; } + + [Option("allow-old", Required = false, HelpText = "Allow old files")] + public bool AllowOld { get; set; } + + [Option('t', "package-type", Required = false, HelpText = "all|exe|msi|zip|flatpak|pkg|deb|rpm")] + public string? PackageType { get; set; } +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Releasing/WebsiteUpdateCommand.cs b/NAPS2.Tools/Project/Releasing/WebsiteUpdateCommand.cs new file mode 100644 index 0000000000..44545644cf --- /dev/null +++ b/NAPS2.Tools/Project/Releasing/WebsiteUpdateCommand.cs @@ -0,0 +1,95 @@ +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using NAPS2.Tools.Project.Targets; +using Newtonsoft.Json; + +namespace NAPS2.Tools.Project.Releasing; + +public class WebsiteUpdateCommand : ICommand +{ + private const string UPDATE_RELEASES_URL = "https://www.naps2.com/hooks/update-releases"; + private const string ADD_FILE_SIG_URL = "https://www.naps2.com/hooks/add-file-sig"; + + public int Run(WebsiteUpdateOptions opts) + { + var key = File.ReadAllText(Path.Combine(Paths.Naps2UserFolder, "website")); + + var client = new HttpClient + { + DefaultRequestHeaders = + { + { "Authorization", key } + } + }; + + if (!opts.NoSign) + { + var certPath = N2Config.AutoUpdateCert; + if (certPath == null) + { + Output.Info("Skipping file signatures as no certificate is configured"); + } + else + { + Output.Info($"Uploading file signatures"); + + Console.WriteLine("Password for auto update certificate:"); + var password = Console.ReadLine()?.Trim() ?? + throw new InvalidOperationException("Password not provided"); + + var cert = X509CertificateLoader.LoadPkcs12FromFile(certPath, password); + var sha1 = SHA1.Create(); + var sha256 = SHA256.Create(); + + // TODO: All files? These are the only ones we need for auto update + var exePath = ProjectHelper.GetPackagePath("exe", Platform.Win64, opts.Version); + var zipPath = ProjectHelper.GetPackagePath("zip", Platform.Win64, opts.Version); + + foreach (var path in new[] { exePath, zipPath }) + { + var bytes = File.ReadAllBytes(path); + var sha1Hash = sha1.ComputeHash(bytes); + var sha256Hash = sha256.ComputeHash(bytes); + var sig = cert.GetRSAPrivateKey()!.SignHash(sha1Hash, HashAlgorithmName.SHA1, + RSASignaturePadding.Pkcs1); + var sig256 = cert.GetRSAPrivateKey()!.SignHash(sha256Hash, HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + File.WriteAllBytes($"{path}.sig", sig); + var data = JsonConvert.SerializeObject(new + { + name = Path.GetFileName(path), + sha1 = Convert.ToBase64String(sha1Hash), + sha256 = Convert.ToBase64String(sha256Hash), + sig = Convert.ToBase64String(sig), + sig256 = Convert.ToBase64String(sig256) + }); + var response = client.PostAsync(ADD_FILE_SIG_URL, new StringContent(data, Encoding.UTF8, "application/json")).Result; + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception( + $"HTTP {response.StatusCode}: {response.Content.ReadAsStringAsync().Result}"); + } + } + } + } + + if (!opts.NoRelease) + { + Output.Info($"Updating releases"); + var response = client.PostAsync(UPDATE_RELEASES_URL, new StringContent("{}", new MediaTypeHeaderValue("application/json"))) + .Result; + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception( + $"HTTP {response.StatusCode}: {response.Content.ReadAsStringAsync().Result}"); + } + } + + Output.OperationEnd("Website updated."); + return 0; + } +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Releasing/WebsiteUpdateOptions.cs b/NAPS2.Tools/Project/Releasing/WebsiteUpdateOptions.cs new file mode 100644 index 0000000000..347259c0e1 --- /dev/null +++ b/NAPS2.Tools/Project/Releasing/WebsiteUpdateOptions.cs @@ -0,0 +1,16 @@ +using CommandLine; + +namespace NAPS2.Tools.Project.Releasing; + +[Verb("website", HelpText = "Update the website files")] +public class WebsiteUpdateOptions : OptionsBase +{ + [Option("version", Required = false, HelpText = "Version to sign")] + public string? Version { get; set; } + + [Option("nosign", Required = false, HelpText = "Skip file signatures")] + public bool NoSign { get; set; } + + [Option("norelease", Required = false, HelpText = "Skip release updates")] + public bool NoRelease { get; set; } +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/ShareCommand.cs b/NAPS2.Tools/Project/ShareCommand.cs index 3a041007dd..c61f20c317 100644 --- a/NAPS2.Tools/Project/ShareCommand.cs +++ b/NAPS2.Tools/Project/ShareCommand.cs @@ -7,7 +7,7 @@ public int Run(ShareOptions opts) bool doIn = opts.ShareType is "both" or "in"; bool doOut = opts.ShareType is "both" or "out"; - var version = ProjectHelper.GetDefaultProjectVersion(); + var version = ProjectHelper.GetCurrentVersionName(); var syncBaseFolder = N2Config.ShareDir; if (!Directory.Exists(syncBaseFolder)) @@ -81,6 +81,6 @@ private static void CopyFileIfNewer(FileInfo file, string targetFolder) private static IEnumerable GetFiles(string folderPath) { return new DirectoryInfo(folderPath).EnumerateFiles() - .Where(x => x.Extension is ".exe" or ".msi" or ".zip" or ".pkg" or ".flatpak"); + .Where(x => x.Extension is ".exe" or ".msi" or ".zip" or ".pkg" or ".flatpak" or ".deb" or ".rpm"); } } \ No newline at end of file diff --git a/NAPS2.Tools/Project/Targets/BuildType.cs b/NAPS2.Tools/Project/Targets/BuildType.cs index 280eb1624f..7460371bca 100644 --- a/NAPS2.Tools/Project/Targets/BuildType.cs +++ b/NAPS2.Tools/Project/Targets/BuildType.cs @@ -3,7 +3,8 @@ namespace NAPS2.Tools.Project.Targets; public enum BuildType { Debug, - Exe, + Release, Msi, - Zip + Zip, + Sdk } \ No newline at end of file diff --git a/NAPS2.Tools/Project/Targets/PackageType.cs b/NAPS2.Tools/Project/Targets/PackageType.cs new file mode 100644 index 0000000000..f21641987f --- /dev/null +++ b/NAPS2.Tools/Project/Targets/PackageType.cs @@ -0,0 +1,13 @@ +namespace NAPS2.Tools.Project.Targets; + +public enum PackageType +{ + Exe, + Msi, + Msix, + Zip, + Pkg, + Flatpak, + Deb, + Rpm +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Targets/Platform.cs b/NAPS2.Tools/Project/Targets/Platform.cs index ddd334c3dd..177660e045 100644 --- a/NAPS2.Tools/Project/Targets/Platform.cs +++ b/NAPS2.Tools/Project/Targets/Platform.cs @@ -2,9 +2,8 @@ namespace NAPS2.Tools.Project.Targets; public enum Platform { - Win, - Win32, Win64, + WinArm64, Mac, MacIntel, MacArm, diff --git a/NAPS2.Tools/Project/Targets/PlatformExtensions.cs b/NAPS2.Tools/Project/Targets/PlatformExtensions.cs index e063662309..d45dfdf986 100644 --- a/NAPS2.Tools/Project/Targets/PlatformExtensions.cs +++ b/NAPS2.Tools/Project/Targets/PlatformExtensions.cs @@ -2,7 +2,7 @@ namespace NAPS2.Tools.Project.Targets; public static class PlatformExtensions { - public static bool IsWindows(this Platform platform) => platform is Platform.Win or Platform.Win32 or Platform.Win64; + public static bool IsWindows(this Platform platform) => platform is Platform.Win64 or Platform.WinArm64; public static bool IsMac(this Platform platform) => platform is Platform.Mac or Platform.MacIntel or Platform.MacArm; diff --git a/NAPS2.Tools/Project/Targets/Target.cs b/NAPS2.Tools/Project/Targets/Target.cs index 4aa5dc8000..030a409b69 100644 --- a/NAPS2.Tools/Project/Targets/Target.cs +++ b/NAPS2.Tools/Project/Targets/Target.cs @@ -1,3 +1,6 @@ namespace NAPS2.Tools.Project.Targets; -public record Target(BuildType BuildType, Platform Platform); \ No newline at end of file +public record PackageTarget(PackageType Type, Platform Platform) +{ + public string PackagePath(string? version) => ProjectHelper.GetPackagePath(Type.ToString().ToLowerInvariant(), Platform, version); +} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Targets/TargetConstraints.cs b/NAPS2.Tools/Project/Targets/TargetConstraints.cs deleted file mode 100644 index 3c7dc941c3..0000000000 --- a/NAPS2.Tools/Project/Targets/TargetConstraints.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace NAPS2.Tools.Project.Targets; - -public record TargetConstraints -{ - public bool InstallersOnly { get; set; } - - public bool AllowDebug { get; set; } - - public bool AllowMultiplePlatforms { get; set; } - - public bool RequireBuildablePlatform { get; set; } -} \ No newline at end of file diff --git a/NAPS2.Tools/Project/Targets/TargetsHelper.cs b/NAPS2.Tools/Project/Targets/TargetsHelper.cs index e85ff7720c..667aa9dae9 100644 --- a/NAPS2.Tools/Project/Targets/TargetsHelper.cs +++ b/NAPS2.Tools/Project/Targets/TargetsHelper.cs @@ -6,9 +6,8 @@ public static class TargetsHelper { public static string PackageName(this Platform platform) => platform switch { - Platform.Win => "win", - Platform.Win32 => "win-x86", Platform.Win64 => "win-x64", + Platform.WinArm64 => "win-arm64", Platform.Mac => "mac-univ", Platform.MacIntel => "mac-x64", Platform.MacArm => "mac-arm64", @@ -17,113 +16,152 @@ public static class TargetsHelper _ => throw new ArgumentException() }; - public static IEnumerable Enumerate(string? buildTypeOpt, string? platformOpt, - TargetConstraints constraints) + public static IEnumerable EnumerateBuildTargets(string? buildType) { - if (string.IsNullOrEmpty(buildTypeOpt)) - { - buildTypeOpt = "all"; - } - if (string.IsNullOrEmpty(platformOpt)) - { - platformOpt = constraints.AllowMultiplePlatforms ? "all" : GetBuildablePlatforms()[0].ToString().ToLowerInvariant(); - } - - string[] allowedBuildTypes = GetAllowedBuildTypes(constraints); - string[] buildTypes = buildTypeOpt == "all" ? allowedBuildTypes : buildTypeOpt.Split("+"); - foreach (var buildType in buildTypes) - { - if (!allowedBuildTypes.Contains(buildType)) - { - throw new Exception( - $"Invalid build type '{buildType}', expected one of {string.Join(",", allowedBuildTypes)}"); - } - } - var buildTypesParsed = buildTypes.Select(ParseBuildType).ToList(); - - string[] allowedPlatforms = (constraints.RequireBuildablePlatform ? GetBuildablePlatforms() : GetAllPlatforms()) - .Select(x => x.ToString().ToLowerInvariant()).ToArray(); - string[] platforms = platformOpt == "all" ? allowedPlatforms : platformOpt.Split("+"); - foreach (var platform in platforms) - { - if (!allowedPlatforms.Contains(platform)) - { - throw new Exception($"Invalid platform '{platform}', expected one of {string.Join(",", allowedPlatforms)}"); - } - } - var platformsParsed = platforms.Select(ParsePlatform).ToList(); - - if (!constraints.AllowMultiplePlatforms && platformsParsed.Count > 1) - { - throw new Exception("Only one platform can be specified"); - } - - foreach (var buildType in buildTypesParsed) - { - foreach (var platform in platformsParsed) + return + buildType?.ToLowerInvariant() switch { - if (constraints.RequireBuildablePlatform && buildType == BuildType.Msi && - platform == Platform.Win) continue; - - yield return new Target(buildType, platform); - } - } + var x when string.IsNullOrEmpty(x) || x == "all" => + new[] { BuildType.Debug, BuildType.Release, BuildType.Msi, BuildType.Zip }, + "debug" => new[] { BuildType.Debug }, + "release" => new[] { BuildType.Release }, + "msi" => new[] { BuildType.Msi }, + "zip" => new[] { BuildType.Zip }, + "sdk" => new[] { BuildType.Sdk }, + _ => Array.Empty() + }; } - private static string[] GetAllowedBuildTypes(TargetConstraints constraints) + public static IEnumerable EnumeratePackageTargets() => EnumeratePackageTargets(null, null, false); + + public static IEnumerable EnumeratePackageTargets(string? packageTypeOpt, string? platformOpt, + bool requireCompatiblePlatform, bool xCompile = false) { - if (OperatingSystem.IsWindows()) - { - return constraints.InstallersOnly ? new[] { "exe", "msi" } : - constraints.AllowDebug ? new[] { "debug", "exe", "msi", "zip" } : - new[] { "exe", "msi", "zip" }; - } - if (OperatingSystem.IsMacOS()) - { - return new[] { "exe" }; - } - if (OperatingSystem.IsLinux()) + var targets = DoEnumeratePackageTargets(packageTypeOpt, platformOpt, requireCompatiblePlatform, xCompile).ToList(); + if (targets.Count == 0) { - return new[] { "exe" }; + throw new Exception($"Invalid package/platform combination: {packageTypeOpt}/{platformOpt}"); } - throw new InvalidOperationException("Unsupported OS"); + return targets; } - private static Platform[] GetAllPlatforms() => - new[] - { - Platform.Win, Platform.Win64, Platform.Win32, Platform.Mac, Platform.MacArm, Platform.MacIntel, Platform.Linux, Platform.LinuxArm - }; - - private static Platform[] GetBuildablePlatforms() + private static IEnumerable DoEnumeratePackageTargets(string? packageTypeOpt, string? platformOpt, + bool requireCompatiblePlatform, bool xCompile) { - if (OperatingSystem.IsWindows()) return new[] { Platform.Win, Platform.Win64, Platform.Win32 }; - if (OperatingSystem.IsMacOS()) - { - return RuntimeInformation.OSArchitecture == Architecture.Arm64 - ? new[] { Platform.Mac, Platform.MacArm, Platform.MacIntel } - : new[] { Platform.MacIntel }; - } - if (OperatingSystem.IsLinux()) + packageTypeOpt = packageTypeOpt?.ToLowerInvariant() ?? ""; + platformOpt = platformOpt?.ToLowerInvariant() ?? ""; + + foreach (var packageType in packageTypeOpt.Split("+")) { - return RuntimeInformation.OSArchitecture == Architecture.Arm64 - ? new[] { Platform.LinuxArm, Platform.Linux } - : new[] { Platform.Linux, Platform.LinuxArm }; + foreach (var platform in platformOpt.Split("+")) + { + bool allPkg = string.IsNullOrEmpty(packageType) || packageType == "all"; + bool allPlat = string.IsNullOrEmpty(platform) || platform == "all"; + if ((allPkg || packageType == "exe") && (!requireCompatiblePlatform || OperatingSystem.IsWindows())) + { + if (allPlat || platform == "win" || platform == "win64") + { + yield return new PackageTarget(PackageType.Exe, Platform.Win64); + } + if (allPlat || platform == "winarm") + { + yield return new PackageTarget(PackageType.Exe, Platform.WinArm64); + } + } + if ((allPkg || packageType == "msi") && (!requireCompatiblePlatform || OperatingSystem.IsWindows())) + { + if (allPlat || platform == "win" || platform == "win64") + { + yield return new PackageTarget(PackageType.Msi, Platform.Win64); + } + } + if ((allPkg || packageType == "msix") && (!requireCompatiblePlatform || OperatingSystem.IsWindows())) + { + if (allPlat || platform == "win" || platform == "win64") + { + yield return new PackageTarget(PackageType.Msix, Platform.Win64); + } + } + if ((allPkg || packageType == "zip") && (!requireCompatiblePlatform || OperatingSystem.IsWindows())) + { + if (allPlat || platform == "win64") + { + yield return new PackageTarget(PackageType.Zip, Platform.Win64); + } + } + if ((allPkg || packageType == "deb") && (!requireCompatiblePlatform || OperatingSystem.IsLinux())) + { + if ((allPlat || platform == "linux") && (!requireCompatiblePlatform || + xCompile || + RuntimeInformation.OSArchitecture == Architecture.X64)) + { + yield return new PackageTarget(PackageType.Deb, Platform.Linux); + } + if ((allPlat || platform == "linuxarm") && (!requireCompatiblePlatform || + xCompile || + RuntimeInformation.OSArchitecture == Architecture.Arm64)) + { + yield return new PackageTarget(PackageType.Deb, Platform.LinuxArm); + } + } + if ((allPkg || packageType == "rpm") && (!requireCompatiblePlatform || OperatingSystem.IsLinux())) + { + if ((allPlat || platform == "linux") && (!requireCompatiblePlatform || + xCompile || + RuntimeInformation.OSArchitecture == Architecture.X64)) + { + yield return new PackageTarget(PackageType.Rpm, Platform.Linux); + } + if ((allPlat || platform == "linuxarm") && (!requireCompatiblePlatform || + xCompile || + RuntimeInformation.OSArchitecture == Architecture.Arm64)) + { + yield return new PackageTarget(PackageType.Rpm, Platform.LinuxArm); + } + } + if ((allPkg || packageType == "flatpak") && (!requireCompatiblePlatform || OperatingSystem.IsLinux())) + { + if ((allPlat || platform == "linux") && (!requireCompatiblePlatform || + xCompile || + RuntimeInformation.OSArchitecture == Architecture.X64)) + { + yield return new PackageTarget(PackageType.Flatpak, Platform.Linux); + } + if ((allPlat || platform == "linuxarm") && (!requireCompatiblePlatform || + xCompile || + RuntimeInformation.OSArchitecture == Architecture.Arm64)) + { + yield return new PackageTarget(PackageType.Flatpak, Platform.LinuxArm); + } + } + if ((allPkg || packageType == "pkg") && (!requireCompatiblePlatform || OperatingSystem.IsMacOS())) + { + if ((allPlat || platform == "mac") && (!requireCompatiblePlatform || + RuntimeInformation.OSArchitecture == Architecture.Arm64)) + { + yield return new PackageTarget(PackageType.Pkg, Platform.Mac); + } + if (allPlat || platform == "macintel") + { + yield return new PackageTarget(PackageType.Pkg, Platform.MacIntel); + } + if ((allPlat || platform == "macarm") && (!requireCompatiblePlatform || + RuntimeInformation.OSArchitecture == Architecture.Arm64)) + { + yield return new PackageTarget(PackageType.Pkg, Platform.MacArm); + } + } + } } - throw new InvalidOperationException("Unsupported OS"); } - private static BuildType ParseBuildType(string value) + public static IEnumerable GetBuildTypesFromPackageType(string? packageType) { - return Enum.TryParse(typeof(BuildType), value, true, out var buildType) - ? (BuildType) buildType! - : throw new Exception("Invalid build type"); - } - - public static Platform ParsePlatform(string value) - { - return Enum.TryParse(typeof(Platform), value, true, out var platform) - ? (Platform) platform! - : throw new Exception("Invalid platform"); + return EnumeratePackageTargets(packageType, null, true).Select(x => (x.Type switch + { + PackageType.Msi => BuildType.Msi, + PackageType.Zip => BuildType.Zip, + _ => BuildType.Release + }).ToString().ToLowerInvariant()).Distinct(); } } \ No newline at end of file diff --git a/NAPS2.Tools/Project/TestCommand.cs b/NAPS2.Tools/Project/TestCommand.cs index 9e9345081b..ad9f5b003f 100644 --- a/NAPS2.Tools/Project/TestCommand.cs +++ b/NAPS2.Tools/Project/TestCommand.cs @@ -1,16 +1,56 @@ +using System.Runtime.InteropServices; + namespace NAPS2.Tools.Project; public class TestCommand : ICommand { public int Run(TestOptions opts) { - // TODO: Framework options (e.g. "-f net462") - Output.Info("Running tests"); - Cli.Run("dotnet", "test", new() + var arch = RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(); + var depsRootPath = OperatingSystem.IsMacOS() + ? $"NAPS2.App.Mac/bin/Debug/net9-macos/osx-{arch}" + : OperatingSystem.IsLinux() + ? $"NAPS2.App.Gtk/bin/Debug/net9/linux-{arch}" + : $"NAPS2.App.WinForms/bin/Debug/net9-windows/win-{arch}"; + var frameworkArg = OperatingSystem.IsWindows() ? "" : "-f net9"; + bool ranTests = false; + + void RunTests(string project, bool isRetry = false) + { + try + { + ranTests = true; + Cli.Run("dotnet", $"test -l \"console;verbosity=normal\" {frameworkArg} {project}", new() + { + { "NAPS2_TEST_DEPS", Path.Combine(Paths.SolutionRoot, depsRootPath) }, + { "NAPS2_TEST_NOGUI", opts.NoGui ? "1" : "0" }, + { "NAPS2_TEST_NONETWORK", opts.NoNetwork ? "1" : "0" }, + { "NAPS2_TEST_IMAGES", opts.Images ?? "" } + }); + } + catch (Exception) + { + if (isRetry) throw; + Output.Info("Tests failed, retrying once"); + RunTests(project, true); + } + } + + var scopes = string.IsNullOrEmpty(opts.Scope) ? ["sdk", "lib", "app"] : opts.Scope.Split('+'); + Output.Info($"Running tests ({string.Join(", ", scopes)})"); + if (scopes.Contains("sdk")) + { + RunTests("NAPS2.Sdk.Tests"); + } + if (scopes.Contains("lib")) + { + RunTests("NAPS2.Lib.Tests"); + } + if (scopes.Contains("app")) { - {"NAPS2_TEST_ROOT", Path.Combine(Paths.SolutionRoot, "NAPS2.App.Tests", "bin", "Debug", "net462")} - }); - Output.Info("Tests passed."); + RunTests("NAPS2.App.Tests"); + } + Output.Info(ranTests ? "Tests passed." : "Invalid scopes, no tests run."); return 0; } } \ No newline at end of file diff --git a/NAPS2.Tools/Project/TestOptions.cs b/NAPS2.Tools/Project/TestOptions.cs index 01c4c6ec11..90563af879 100644 --- a/NAPS2.Tools/Project/TestOptions.cs +++ b/NAPS2.Tools/Project/TestOptions.cs @@ -5,4 +5,15 @@ namespace NAPS2.Tools.Project; [Verb("test", HelpText = "Runs the project tests")] public class TestOptions : OptionsBase { + [Option("nogui", Required = false, HelpText = "Only run headless (no gui) tests")] + public bool NoGui { get; set; } + + [Option("nonetwork", Required = false, HelpText = "Skip ESCL tests that require network access")] + public bool NoNetwork { get; set; } + + [Option("images", Required = false, HelpText = "Run SDK tests with images for a particular platform (gdi/wpf/is/mac/gtk)")] + public string? Images { get; set; } + + [Option("scope", Required = false, HelpText = "Subset of tests to run (app/lib/sdk)")] + public string? Scope { get; set; } } \ No newline at end of file diff --git a/NAPS2.Tools/Project/Verification/ExeSetupVerifier.cs b/NAPS2.Tools/Project/Verification/ExeSetupVerifier.cs index fc855d72bf..063703cad2 100644 --- a/NAPS2.Tools/Project/Verification/ExeSetupVerifier.cs +++ b/NAPS2.Tools/Project/Verification/ExeSetupVerifier.cs @@ -28,7 +28,7 @@ private class UpgradeTest : IDisposable public UpgradeTest(Platform platform) { _platform = platform; - _install32 = ProjectHelper.GetInstallationFolder(Platform.Win32); + _install32 = Path.Combine(Environment.ExpandEnvironmentVariables("%PROGRAMFILES(X86)%"), "NAPS2"); _testFilePath = Path.Combine(_install32, "_verify_testfile.exe"); Directory.CreateDirectory(_install32); File.WriteAllText(_testFilePath, ""); @@ -40,7 +40,7 @@ public void Verify() { throw new Exception("Verification error: Exe installer did not delete old files"); } - if (_platform is Platform.Win or Platform.Win64 && Directory.Exists(_install32)) + if (Directory.Exists(_install32)) { throw new Exception("Verification error: Exe installer did not delete old install dir"); } diff --git a/NAPS2.Tools/Project/Verification/Verifier.cs b/NAPS2.Tools/Project/Verification/Verifier.cs index 7a70c60765..44f7704abd 100644 --- a/NAPS2.Tools/Project/Verification/Verifier.cs +++ b/NAPS2.Tools/Project/Verification/Verifier.cs @@ -5,8 +5,9 @@ public static class Verifier public static void RunVerificationTests(string testRoot) { Output.Info($"Running verification tests in: {testRoot}"); - Cli.Run("dotnet", "test NAPS2.App.Tests", new() + Cli.Run("dotnet", "test -l \"console;verbosity=normal\" NAPS2.App.Tests", new() { + { "NAPS2_TEST_DEPS", testRoot }, { "NAPS2_TEST_ROOT", testRoot }, { "NAPS2_TEST_VERIFY", "1" } }); diff --git a/NAPS2.Tools/Project/Verification/VerifyCommand.cs b/NAPS2.Tools/Project/Verification/VerifyCommand.cs index d679feba18..a5d664e590 100644 --- a/NAPS2.Tools/Project/Verification/VerifyCommand.cs +++ b/NAPS2.Tools/Project/Verification/VerifyCommand.cs @@ -6,27 +6,32 @@ public class VerifyCommand : ICommand { public int Run(VerifyOptions opts) { - var version = ProjectHelper.GetDefaultProjectVersion(); + if (!OperatingSystem.IsWindows()) + { + Output.Info("Verification tests are currently only supported on Windows."); + return 0; + } + + var version = ProjectHelper.GetCurrentVersionName(); using var appDriverRunner = AppDriverRunner.Start(); - var constraints = new TargetConstraints + foreach (var target in TargetsHelper.EnumeratePackageTargets(opts.PackageType, opts.Platform, true)) { - AllowMultiplePlatforms = true - }; - foreach (var target in TargetsHelper.Enumerate(opts.BuildType, opts.Platform, constraints)) - { - switch (target.BuildType) + switch (target.Type) { - case BuildType.Exe: + case PackageType.Exe: ExeSetupVerifier.Verify(target.Platform, version); break; - case BuildType.Msi: + case PackageType.Msi: MsiSetupVerifier.Verify(target.Platform, version); break; - case BuildType.Zip: + case PackageType.Zip: ZipArchiveVerifier.Verify(target.Platform, version, opts.NoCleanup); break; + default: + Output.Info($"Unsupported package type for verification: {target.Type}"); + break; } } return 0; diff --git a/NAPS2.Tools/Project/Verification/VerifyOptions.cs b/NAPS2.Tools/Project/Verification/VerifyOptions.cs index 84db935391..2f98a9c273 100644 --- a/NAPS2.Tools/Project/Verification/VerifyOptions.cs +++ b/NAPS2.Tools/Project/Verification/VerifyOptions.cs @@ -2,11 +2,11 @@ namespace NAPS2.Tools.Project.Verification; -[Verb("verify", HelpText = "Verify the packaged app, 'verify {all|exe|msi|zip}'")] +[Verb("verify", HelpText = "Verify the packaged app, 'verify {all|exe|msi|zip|flatpak|pkg|deb|rpm}'")] public class VerifyOptions : OptionsBase { - [Value(0, MetaName = "build type", Required = true, HelpText = "all|exe|msi|zip")] - public string? BuildType { get; set; } + [Value(0, MetaName = "package type", Required = true, HelpText = "all|exe|msi|zip|flatpak|pkg|deb|rpm")] + public string? PackageType { get; set; } [Option('p', "platform", Required = false, HelpText = "win|win32|win64|mac|macintel|macarm|linux")] public string? Platform { get; set; } diff --git a/NAPS2.Tools/Project/Verification/VirusScanCommand.cs b/NAPS2.Tools/Project/Verification/VirusScanCommand.cs index 4748835115..5cb732b24b 100644 --- a/NAPS2.Tools/Project/Verification/VirusScanCommand.cs +++ b/NAPS2.Tools/Project/Verification/VirusScanCommand.cs @@ -9,28 +9,13 @@ public class VirusScanCommand : ICommand public int Run(VirusScanOptions opts) { Output.Info("Checking for antivirus false positives"); - var version = ProjectHelper.GetDefaultProjectVersion(); + var version = ProjectHelper.GetCurrentVersionName(); - var constraints = new TargetConstraints - { - AllowMultiplePlatforms = true - }; var tasks = new List(); - foreach (var target in TargetsHelper.Enumerate(opts.BuildType, opts.Platform, constraints)) + foreach (var target in TargetsHelper.EnumeratePackageTargets(opts.PackageType, opts.Platform, false)) { - switch (target.BuildType) - { - case BuildType.Exe: - var ext = target.Platform.IsMac() ? "pkg" : target.Platform.IsLinux() ? "flatpak" : "exe"; - tasks.Add(StartVirusScan(ProjectHelper.GetPackagePath(ext, target.Platform, version))); - break; - case BuildType.Msi: - tasks.Add(StartVirusScan(ProjectHelper.GetPackagePath("msi", target.Platform, version))); - break; - case BuildType.Zip: - tasks.Add(StartVirusScan(ProjectHelper.GetPackagePath("zip", target.Platform, version))); - break; - } + var ext = target.Type.ToString().ToLowerInvariant(); + tasks.Add(StartVirusScan(ProjectHelper.GetPackagePath(ext, target.Platform, version))); } Task.WaitAll(tasks.ToArray()); Output.OperationEnd("No antivirus false positives."); diff --git a/NAPS2.Tools/Project/Verification/VirusScanOptions.cs b/NAPS2.Tools/Project/Verification/VirusScanOptions.cs index 89b845b928..294751696e 100644 --- a/NAPS2.Tools/Project/Verification/VirusScanOptions.cs +++ b/NAPS2.Tools/Project/Verification/VirusScanOptions.cs @@ -2,11 +2,11 @@ namespace NAPS2.Tools.Project.Verification; -[Verb("virus", HelpText = "Scan the packaged app for antivirus false positives, 'virus {all|exe|msi|zip}'")] +[Verb("virus", HelpText = "Scan the packaged app for antivirus false positives, 'virus {all|exe|msi|zip|flatpak|pkg|deb|rpm}'")] public class VirusScanOptions : OptionsBase { - [Value(0, MetaName = "build type", Required = true, HelpText = "all|exe|msi|zip")] - public string? BuildType { get; set; } + [Value(0, MetaName = "package type", Required = true, HelpText = "all|exe|msi|zip|flatpak|pkg|deb|rpm")] + public string? PackageType { get; set; } [Option('p', "platform", Required = false, HelpText = "win|win32|win64|mac|macintel|macarm|linux")] public string? Platform { get; set; } diff --git a/NAPS2.Tools/Project/Workflows/PublishCommand.cs b/NAPS2.Tools/Project/Workflows/PublishCommand.cs index 79f0b9cdf1..5fb6e255d0 100644 --- a/NAPS2.Tools/Project/Workflows/PublishCommand.cs +++ b/NAPS2.Tools/Project/Workflows/PublishCommand.cs @@ -1,4 +1,5 @@ using NAPS2.Tools.Project.Packaging; +using NAPS2.Tools.Project.Targets; using NAPS2.Tools.Project.Verification; namespace NAPS2.Tools.Project.Workflows; @@ -10,19 +11,33 @@ public int Run(PublishOptions opts) new CleanCommand().Run(new CleanOptions()); new BuildCommand().Run(new BuildOptions { - BuildType = opts.BuildType + BuildType = "debug" }); - new TestCommand().Run(new TestOptions()); - new PackageCommand().Run(new PackageOptions + foreach (var buildType in TargetsHelper.GetBuildTypesFromPackageType(opts.PackageType)) + { + new BuildCommand().Run(new BuildOptions + { + BuildType = buildType + }); + } + new TestCommand().Run(new TestOptions { - BuildType = opts.BuildType, - Platform = opts.Platform + NoGui = opts.NoGui }); - new VerifyCommand().Run(new VerifyOptions - { - BuildType = opts.BuildType, - Platform = opts.Platform + new PackageCommand().Run(new PackageOptions + { + PackageType = opts.PackageType, + Platform = opts.Platform, + XCompile = opts.XCompile }); + if (!opts.NoVerify) + { + new VerifyCommand().Run(new VerifyOptions + { + PackageType = opts.PackageType, + Platform = opts.Platform + }); + } return 0; } } \ No newline at end of file diff --git a/NAPS2.Tools/Project/Workflows/PublishOptions.cs b/NAPS2.Tools/Project/Workflows/PublishOptions.cs index 07430b589c..8f59c8c34e 100644 --- a/NAPS2.Tools/Project/Workflows/PublishOptions.cs +++ b/NAPS2.Tools/Project/Workflows/PublishOptions.cs @@ -5,12 +5,18 @@ namespace NAPS2.Tools.Project.Workflows; [Verb("publish", HelpText = "Build, test, package, and verify standard targets")] public class PublishOptions : OptionsBase { - [Value(0, MetaName = "build type", Required = false, HelpText = "all|exe|msi|zip")] - public string? BuildType { get; set; } + [Value(0, MetaName = "package type", Required = false, HelpText = "all|exe|msi|zip|flatpak|pkg|deb|rpm")] + public string? PackageType { get; set; } [Option('p', "platform", Required = false, HelpText = "all|win|win32|win64|mac|macintel|macarm|linux")] public string? Platform { get; set; } - [Option("nocleanup", Required = false, HelpText = "Skip cleaning up temp files")] - public bool NoCleanup { get; set; } + [Option("noverify", Required = false, HelpText = "Don't run verification tests")] + public bool NoVerify { get; set; } + + [Option("nogui", Required = false, HelpText = "Only run headless (no gui) tests")] + public bool NoGui { get; set; } + + [Option("xcompile", Required = false, HelpText = "Cross-compile packages where possible (e.g. build linux-arm64 on linux-x64)")] + public bool XCompile { get; set; } } \ No newline at end of file diff --git a/NAPS2.Tools/README.md b/NAPS2.Tools/README.md index 66a05b9f88..e5028f42b0 100644 --- a/NAPS2.Tools/README.md +++ b/NAPS2.Tools/README.md @@ -2,44 +2,18 @@ Tools for NAPS2 building, testing, packaging, verification, etc. -## How to run - -You will need the latest [.NET SDK](https://dotnet.microsoft.com/en-us/download). Open a terminal in your NAPS2 solution directory and run the following commands: +## Quickstart ### Powershell ``` function n2 { dotnet run --project NAPS2.Tools -- $args } -n2 build all ``` ### Bash ``` alias n2="dotnet run --project NAPS2.Tools --" -n2 build all ``` -## Commands - -Run `n2 help` for a full list of commands and `n2 help ` for all options for a particular command. Some examples: - -``` -n2 clean -n2 test -n2 build exe -n2 pkg exe -n2 verify exe -n2 publish exe -n2 virus exe -n2 share -``` -- `clean`: Clear out bin/obj subfolders -- `test`: Run solution tests -- `build`: Builds the solution with the specified configuration (debug/exe/msi/zip/all) -- `pkg`: Generates the specified package type, e.g. exe => naps2-{version}-win-x64.exe installer -- `verify`: Installs/extracts the packaged file and runs NAPS2.App.Tests against it - - Requires elevation for exe/msi as it does a real install on your local machine (and uninstalls the old NAPS2) -- `publish`: Runs a series of commands: clean, test, build, pkg, verify -- `virus`: Uploads a package to VirusTotal for a false positive check -- `share`: Syncs local packages with a cloud folder (to help with cross-platform packaging) \ No newline at end of file +For more documentation see the [wiki](https://github.com/cyanfish/naps2/wiki/2.-NAPS2.Tools). \ No newline at end of file diff --git a/NAPS2.Tools/Sdk/LocalSdkCommand.cs b/NAPS2.Tools/Sdk/LocalSdkCommand.cs new file mode 100644 index 0000000000..313ce078db --- /dev/null +++ b/NAPS2.Tools/Sdk/LocalSdkCommand.cs @@ -0,0 +1,28 @@ +using NAPS2.Tools.Project; + +namespace NAPS2.Tools.Sdk; + +public class LocalSdkCommand : ICommand +{ + public int Run(LocalSdkOptions opts) + { + Output.Info("Generating packages"); + if (!opts.NoBuild) + { + new BuildCommand().Run(new BuildOptions { BuildType = "sdk" }); + } + + var sdkVersion = ProjectHelper.GetSdkVersion(); + foreach (var project in ProjectHelper.GetSdkProjects()) + { + var projectFolder = project.StartsWith("NAPS2.Sdk.Worker") ? "NAPS2.Sdk.Worker" : project; + var nugetPath = Path.Combine(Paths.SolutionRoot, projectFolder, "bin", "Release", + $"{project}.{sdkVersion}.nupkg"); + Cli.Run("nuget", $"delete {project} {sdkVersion} -source \"{opts.LocalSource}\" -NonInteractive", ignoreErrorIfOutputContains: "Not Found"); + Cli.Run("nuget", $"add \"{nugetPath}\" -source \"{opts.LocalSource}\""); + } + + Output.OperationEnd("Packages added."); + return 0; + } +} \ No newline at end of file diff --git a/NAPS2.Tools/Sdk/LocalSdkOptions.cs b/NAPS2.Tools/Sdk/LocalSdkOptions.cs new file mode 100644 index 0000000000..3e033a1e12 --- /dev/null +++ b/NAPS2.Tools/Sdk/LocalSdkOptions.cs @@ -0,0 +1,16 @@ +using CommandLine; + +namespace NAPS2.Tools.Sdk; + +[Verb("localsdk", HelpText = "Builds SDK nuget packages and add to local nuget source'")] +public class LocalSdkOptions : OptionsBase +{ + [Value(0, MetaName = "local source", Required = true, HelpText = "Path to local nuget package source")] + public string? LocalSource { get; set; } + + [Option("nobuild", Required = false, HelpText = "Skip build")] + public bool NoBuild { get; set; } + + // TODO: Add a --purge option to delete userfolder/.nuget/{project} folders + // TODO: Add a --restore {csproj} option to run dotnet restore on the given csproj +} \ No newline at end of file diff --git a/NAPS2.sln b/NAPS2.sln index 035dd385ac..6fe5e782a4 100644 --- a/NAPS2.sln +++ b/NAPS2.sln @@ -1,434 +1,680 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28705.295 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.WinForms", "NAPS2.App.WinForms\NAPS2.App.WinForms.csproj", "{EAC4A133-93BE-4400-BFA9-CEB1615693C4}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{F6CC4F6D-EE76-43D9-977B-8802C9665CC4}" - ProjectSection(SolutionItems) = preProject - .nuget\NuGet.Config = .nuget\NuGet.Config - .nuget\NuGet.exe = .nuget\NuGet.exe - .nuget\NuGet.targets = .nuget\NuGet.targets - EndProjectSection +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Internals", "NAPS2.Internals\NAPS2.Internals.csproj", "{F8372BCA-7C34-8474-91E9-10CF9313D101}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Lib.Gtk", "NAPS2.Lib.Gtk\NAPS2.Lib.Gtk.csproj", "{72BD60EB-1EB1-F826-B98E-4D03E36DA624}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Lib.Mac", "NAPS2.Lib.Mac\NAPS2.Lib.Mac.csproj", "{F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Lib.Tests", "NAPS2.Lib.Tests\NAPS2.Lib.Tests.csproj", "{4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Lib.WinForms", "NAPS2.Lib.WinForms\NAPS2.Lib.WinForms.csproj", "{8B4407E8-8EB4-6A52-665C-F80ECB40F56D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Lib", "NAPS2.Lib\NAPS2.Lib.csproj", "{E97B675D-8519-6FDD-9418-D3785D23D7A0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Sdk.Samples", "NAPS2.Sdk.Samples\NAPS2.Sdk.Samples.csproj", "{A93D0028-A629-B669-0CF2-48EF7F6E82E7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Sdk.ScannerTests", "NAPS2.Sdk.ScannerTests\NAPS2.Sdk.ScannerTests.csproj", "{2DF61440-BF1F-98F9-F30B-118AE6758CC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Sdk.Tests", "NAPS2.Sdk.Tests\NAPS2.Sdk.Tests.csproj", "{FF5B01D1-8DEE-5B07-EE26-725DEF02895F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.Console", "NAPS2.App.Console\NAPS2.App.Console.csproj", "{2A321772-75B2-4601-A718-AF0A2370E6A8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Sdk.Worker.Build", "NAPS2.Sdk.Worker.Build\NAPS2.Sdk.Worker.Build.csproj", "{608A9BD7-2E5E-9BC6-CF82-6EACE281785C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Sdk", "NAPS2.Sdk\NAPS2.Sdk.csproj", "{968378FA-A649-4058-A928-1FCD97B23070}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Sdk.Worker.Win32", "NAPS2.Sdk.Worker.Win32\NAPS2.Sdk.Worker.Win32.csproj", "{D399C4A1-06D3-8FC2-F881-423D6729F6BF}" + ProjectSection(ProjectDependencies) = postProject + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C} = {608A9BD7-2E5E-9BC6-CF82-6EACE281785C} + EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.PortableLauncher", "NAPS2.App.PortableLauncher\NAPS2.App.PortableLauncher.csproj", "{A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Sdk", "NAPS2.Sdk\NAPS2.Sdk.csproj", "{DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Lib", "NAPS2.Lib\NAPS2.Lib.csproj", "{4D349529-149B-498B-8A55-373E6A67E1F0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Setup", "NAPS2.Setup\NAPS2.Setup.csproj", "{9D6A5555-68B1-13AA-27B4-36422715BEB8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.Worker", "NAPS2.App.Worker\NAPS2.App.Worker.csproj", "{BA9A65A0-00FE-4BF4-A023-7D804817453F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Tools", "NAPS2.Tools\NAPS2.Tools.csproj", "{F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Tools", "NAPS2.Tools\NAPS2.Tools.csproj", "{F6CD6C88-49EA-4443-BB45-69FB79788C61}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "app", "app", "{F95F473B-E98A-ACD6-1986-755F447BE36F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Sdk.Samples", "NAPS2.Sdk.Samples\NAPS2.Sdk.Samples.csproj", "{1165EE3A-5EE1-462B-8D9F-975D66710F1A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.Console", "NAPS2.App.Console\NAPS2.App.Console.csproj", "{66188578-A9BF-17A3-6F2F-F45E2CC35C9F}" + ProjectSection(ProjectDependencies) = postProject + {30C31689-1A7F-954A-A921-4A6707E6DCF5} = {30C31689-1A7F-954A-A921-4A6707E6DCF5} + EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Setup", "NAPS2.Setup\NAPS2.Setup.csproj", "{AC67D0DB-F980-456C-9E91-654CB4989736}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.Gtk", "NAPS2.App.Gtk\NAPS2.App.Gtk.csproj", "{B8BFC18F-B160-77BB-898D-8CD2969797B9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Sdk.Tests", "NAPS2.Sdk.Tests\NAPS2.Sdk.Tests.csproj", "{8F59DABD-EE59-471D-B3AE-9C2083FDB13E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.Mac", "NAPS2.App.Mac\NAPS2.App.Mac.csproj", "{4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Lib.WinForms", "NAPS2.Lib.WinForms\NAPS2.Lib.WinForms.csproj", "{95A03B74-A235-42A4-A433-E3884E180BE4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.PortableLauncher", "NAPS2.App.PortableLauncher\NAPS2.App.PortableLauncher.csproj", "{A5BF0DD2-4988-0E49-E917-8CF463E9E765}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Lib.Tests", "NAPS2.Lib.Tests\NAPS2.Lib.Tests.csproj", "{3906925A-2392-496C-8221-49A9687C38A8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.Tests", "NAPS2.App.Tests\NAPS2.App.Tests.csproj", "{C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Images", "NAPS2.Images\NAPS2.Images.csproj", "{BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.WinForms", "NAPS2.App.WinForms\NAPS2.App.WinForms.csproj", "{D524C6DA-EEE5-45EF-735A-B1F921D78577}" + ProjectSection(ProjectDependencies) = postProject + {30C31689-1A7F-954A-A921-4A6707E6DCF5} = {30C31689-1A7F-954A-A921-4A6707E6DCF5} + EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Images.Gdi", "NAPS2.Images.Gdi\NAPS2.Images.Gdi.csproj", "{4B0E200A-E9CF-465F-8CB2-6525AABEC94B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.Worker", "NAPS2.App.Worker\NAPS2.App.Worker.csproj", "{30C31689-1A7F-954A-A921-4A6707E6DCF5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.Tests", "NAPS2.App.Tests\NAPS2.App.Tests.csproj", "{3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "escl", "escl", "{AC40AF8E-5BE2-E02F-B15A-0A1E1DBBAED1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Sdk.ScannerTests", "NAPS2.Sdk.ScannerTests\NAPS2.Sdk.ScannerTests.csproj", "{D291C9E9-42D2-4601-9EE3-1CBCA200B897}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Escl.Server", "NAPS2.Escl.Server\NAPS2.Escl.Server.csproj", "{22306413-9041-748E-CF80-63770FBFA318}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Lib.Mac", "NAPS2.Lib.Mac\NAPS2.Lib.Mac.csproj", "{A598272C-CC3A-4670-A998-BB96B98570DB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Escl.Tests", "NAPS2.Escl.Tests\NAPS2.Escl.Tests.csproj", "{38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Images.Mac", "NAPS2.Images.Mac\NAPS2.Images.Mac.csproj", "{AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Escl.Usb", "NAPS2.Escl.Usb\NAPS2.Escl.Usb.csproj", "{C74DE521-931E-A4D4-2FA4-04C2C6D977FE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Escl", "NAPS2.Escl\NAPS2.Escl.csproj", "{7D99D58A-990F-4F64-A0C6-DF699F097655}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Escl", "NAPS2.Escl\NAPS2.Escl.csproj", "{88C01DE8-ED2E-0465-6A3B-7673A6DF314C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Escl.Server", "NAPS2.Escl.Server\NAPS2.Escl.Server.csproj", "{892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{011BDCA8-E5EE-9E14-FF24-A59B216152F3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Escl.Client", "NAPS2.Escl.Client\NAPS2.Escl.Client.csproj", "{B1A0D82E-898E-454F-9B9D-05E9447F3DE1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Images.Gdi", "NAPS2.Images.Gdi\NAPS2.Images.Gdi.csproj", "{251BDC21-838F-8006-6A99-48774229A593}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Escl.Tests", "NAPS2.Escl.Tests\NAPS2.Escl.Tests.csproj", "{ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Images.Gtk", "NAPS2.Images.Gtk\NAPS2.Images.Gtk.csproj", "{EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.Mac", "NAPS2.App.Mac\NAPS2.App.Mac.csproj", "{A280B315-3670-484D-B7A1-294E3DE56E7E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Images.ImageSharp", "NAPS2.Images.ImageSharp\NAPS2.Images.ImageSharp.csproj", "{31588109-3BFA-3D3F-AD09-074A0233812F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Lib.Gtk", "NAPS2.Lib.Gtk\NAPS2.Lib.Gtk.csproj", "{16BC4316-CEB6-40A1-BF82-BF58378B8CF9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Images.Mac", "NAPS2.Images.Mac\NAPS2.Images.Mac.csproj", "{A3673881-1312-3BA4-570E-3B21B75F203C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Images.Gtk", "NAPS2.Images.Gtk\NAPS2.Images.Gtk.csproj", "{0F0187B3-8633-46BB-A409-16D893E37B3D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Images.Wpf", "NAPS2.Images.Wpf\NAPS2.Images.Wpf.csproj", "{57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.Gtk", "NAPS2.App.Gtk\NAPS2.App.Gtk.csproj", "{DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Images", "NAPS2.Images\NAPS2.Images.csproj", "{B12B9C34-67D2-7AE8-7DDD-38F384DF3502}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug-Linux|Any CPU = Debug-Linux|Any CPU + Debug-Mac|Any CPU = Debug-Mac|Any CPU + Debug-Windows|Any CPU = Debug-Windows|Any CPU DebugLang|Any CPU = DebugLang|Any CPU - Release|Any CPU = Release|Any CPU + Release-Linux|Any CPU = Release-Linux|Any CPU + Release-Mac|Any CPU = Release-Mac|Any CPU Release-Msi|Any CPU = Release-Msi|Any CPU + Release-Windows|Any CPU = Release-Windows|Any CPU Release-Zip|Any CPU = Release-Zip|Any CPU + Sdk|Any CPU = Sdk|Any CPU Tools|Any CPU = Tools|Any CPU - Debug-Linux|Any CPU = Debug-Linux|Any CPU - Debug-Windows|Any CPU = Debug-Windows|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.Release|Any CPU.Build.0 = Release|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.Release-Msi|Any CPU.Build.0 = Release|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.Release-Zip|Any CPU.Build.0 = Release|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {EAC4A133-93BE-4400-BFA9-CEB1615693C4}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.Release|Any CPU.Build.0 = Release|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.Release-Msi|Any CPU.Build.0 = Release|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.Release-Zip|Any CPU.Build.0 = Release|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {2A321772-75B2-4601-A718-AF0A2370E6A8}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Debug|Any CPU.Build.0 = Debug|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Release|Any CPU.ActiveCfg = Release|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Release|Any CPU.Build.0 = Release|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Release-Msi|Any CPU.ActiveCfg = Release-Msi|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Release-Msi|Any CPU.Build.0 = Release-Msi|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Release-Zip|Any CPU.ActiveCfg = Release-Zip|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Release-Zip|Any CPU.Build.0 = Release-Zip|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.DebugLang|Any CPU.ActiveCfg = DebugLang|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.DebugLang|Any CPU.Build.0 = DebugLang|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {968378FA-A649-4058-A928-1FCD97B23070}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.Release-Zip|Any CPU.Build.0 = Release|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {A6BCC071-7667-4F8B-ACC2-6FC7F4A9CD32}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Release|Any CPU.Build.0 = Release|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Release-Msi|Any CPU.Build.0 = Release|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Release-Zip|Any CPU.Build.0 = Release|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.DebugLang|Any CPU.ActiveCfg = DebugLang|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.DebugLang|Any CPU.Build.0 = DebugLang|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {4D349529-149B-498B-8A55-373E6A67E1F0}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.Release|Any CPU.Build.0 = Release|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.Release-Msi|Any CPU.Build.0 = Release|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.Release-Zip|Any CPU.Build.0 = Release|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {BA9A65A0-00FE-4BF4-A023-7D804817453F}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {F6CD6C88-49EA-4443-BB45-69FB79788C61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F6CD6C88-49EA-4443-BB45-69FB79788C61}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {F6CD6C88-49EA-4443-BB45-69FB79788C61}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {F6CD6C88-49EA-4443-BB45-69FB79788C61}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {F6CD6C88-49EA-4443-BB45-69FB79788C61}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {F6CD6C88-49EA-4443-BB45-69FB79788C61}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {F6CD6C88-49EA-4443-BB45-69FB79788C61}.Tools|Any CPU.Build.0 = Debug|Any CPU - {F6CD6C88-49EA-4443-BB45-69FB79788C61}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {F6CD6C88-49EA-4443-BB45-69FB79788C61}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.Release|Any CPU.Build.0 = Release|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.Release-Msi|Any CPU.Build.0 = Release|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {1165EE3A-5EE1-462B-8D9F-975D66710F1A}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {AC67D0DB-F980-456C-9E91-654CB4989736}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AC67D0DB-F980-456C-9E91-654CB4989736}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {AC67D0DB-F980-456C-9E91-654CB4989736}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AC67D0DB-F980-456C-9E91-654CB4989736}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU - {AC67D0DB-F980-456C-9E91-654CB4989736}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU - {AC67D0DB-F980-456C-9E91-654CB4989736}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {AC67D0DB-F980-456C-9E91-654CB4989736}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {AC67D0DB-F980-456C-9E91-654CB4989736}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {8F59DABD-EE59-471D-B3AE-9C2083FDB13E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8F59DABD-EE59-471D-B3AE-9C2083FDB13E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8F59DABD-EE59-471D-B3AE-9C2083FDB13E}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {8F59DABD-EE59-471D-B3AE-9C2083FDB13E}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {8F59DABD-EE59-471D-B3AE-9C2083FDB13E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8F59DABD-EE59-471D-B3AE-9C2083FDB13E}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU - {8F59DABD-EE59-471D-B3AE-9C2083FDB13E}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU - {8F59DABD-EE59-471D-B3AE-9C2083FDB13E}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {8F59DABD-EE59-471D-B3AE-9C2083FDB13E}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {8F59DABD-EE59-471D-B3AE-9C2083FDB13E}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {8F59DABD-EE59-471D-B3AE-9C2083FDB13E}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {8F59DABD-EE59-471D-B3AE-9C2083FDB13E}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.Release|Any CPU.Build.0 = Release|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.Release-Msi|Any CPU.Build.0 = Release|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.Release-Zip|Any CPU.Build.0 = Release|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.DebugLang|Any CPU.ActiveCfg = DebugLang|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.DebugLang|Any CPU.Build.0 = DebugLang|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {95A03B74-A235-42A4-A433-E3884E180BE4}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {3906925A-2392-496C-8221-49A9687C38A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3906925A-2392-496C-8221-49A9687C38A8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3906925A-2392-496C-8221-49A9687C38A8}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {3906925A-2392-496C-8221-49A9687C38A8}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {3906925A-2392-496C-8221-49A9687C38A8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3906925A-2392-496C-8221-49A9687C38A8}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU - {3906925A-2392-496C-8221-49A9687C38A8}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU - {3906925A-2392-496C-8221-49A9687C38A8}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {3906925A-2392-496C-8221-49A9687C38A8}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {3906925A-2392-496C-8221-49A9687C38A8}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {3906925A-2392-496C-8221-49A9687C38A8}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {3906925A-2392-496C-8221-49A9687C38A8}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Release|Any CPU.Build.0 = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Release-Msi|Any CPU.Build.0 = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Release-Zip|Any CPU.Build.0 = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {BDFFACDD-968D-4060-ADBD-929E0F8CBCC3}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Release|Any CPU.Build.0 = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Release-Msi|Any CPU.Build.0 = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Release-Zip|Any CPU.Build.0 = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {4B0E200A-E9CF-465F-8CB2-6525AABEC94B}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}.DebugLang|Any CPU.ActiveCfg = DebugLang|Any CPU - {3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}.DebugLang|Any CPU.Build.0 = DebugLang|Any CPU - {3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {3CA45B6B-F055-4FFB-B9B6-0FF381A752F9}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {D291C9E9-42D2-4601-9EE3-1CBCA200B897}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D291C9E9-42D2-4601-9EE3-1CBCA200B897}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D291C9E9-42D2-4601-9EE3-1CBCA200B897}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {D291C9E9-42D2-4601-9EE3-1CBCA200B897}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {D291C9E9-42D2-4601-9EE3-1CBCA200B897}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {D291C9E9-42D2-4601-9EE3-1CBCA200B897}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {D291C9E9-42D2-4601-9EE3-1CBCA200B897}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {D291C9E9-42D2-4601-9EE3-1CBCA200B897}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {D291C9E9-42D2-4601-9EE3-1CBCA200B897}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {D291C9E9-42D2-4601-9EE3-1CBCA200B897}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {D291C9E9-42D2-4601-9EE3-1CBCA200B897}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {D291C9E9-42D2-4601-9EE3-1CBCA200B897}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.Release|Any CPU.Build.0 = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.Release-Msi|Any CPU.Build.0 = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.Release-Zip|Any CPU.Build.0 = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {A598272C-CC3A-4670-A998-BB96B98570DB}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.Release|Any CPU.Build.0 = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.Release-Msi|Any CPU.Build.0 = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.Release-Zip|Any CPU.Build.0 = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {AE3D98CE-0044-4FAB-AD7A-B1595FF7D83E}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Release|Any CPU.Build.0 = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Release-Msi|Any CPU.Build.0 = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Release-Zip|Any CPU.Build.0 = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {7D99D58A-990F-4F64-A0C6-DF699F097655}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Release|Any CPU.Build.0 = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Release-Msi|Any CPU.Build.0 = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Release-Zip|Any CPU.Build.0 = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {892CE6E0-CBB3-47F8-9BEA-815B28D6A8EB}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Release|Any CPU.Build.0 = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Release-Msi|Any CPU.Build.0 = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Release-Zip|Any CPU.Build.0 = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {B1A0D82E-898E-454F-9B9D-05E9447F3DE1}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.Release|Any CPU.Build.0 = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.Release-Msi|Any CPU.Build.0 = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.Release-Zip|Any CPU.Build.0 = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {A280B315-3670-484D-B7A1-294E3DE56E7E}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.Release|Any CPU.Build.0 = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.Release-Msi|Any CPU.Build.0 = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.Release-Zip|Any CPU.Build.0 = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {16BC4316-CEB6-40A1-BF82-BF58378B8CF9}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.Release|Any CPU.Build.0 = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.Release-Msi|Any CPU.Build.0 = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.Release-Zip|Any CPU.Build.0 = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {0F0187B3-8633-46BB-A409-16D893E37B3D}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.DebugLang|Any CPU.Build.0 = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.Release|Any CPU.Build.0 = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.Release-Msi|Any CPU.Build.0 = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.Release-Zip|Any CPU.Build.0 = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.Tools|Any CPU.ActiveCfg = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU - {DCAC4A2E-BF9A-4F90-9B16-08DC38038A71}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {D2414340-5F88-4C83-A829-CCA5E3867797} + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Release-Linux|Any CPU.Build.0 = Release|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Release-Mac|Any CPU.Build.0 = Release|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Sdk|Any CPU.Build.0 = Release|Any CPU + {F8372BCA-7C34-8474-91E9-10CF9313D101}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Release-Linux|Any CPU.Build.0 = Release|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {72BD60EB-1EB1-F826-B98E-4D03E36DA624}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Release-Mac|Any CPU.Build.0 = Release|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {F4FD67EE-DBEB-3E38-DBA6-DB55A76FC37C}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {4FC0E54F-CE94-011C-7B9D-C5AD212E0A76}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.DebugLang|Any CPU.ActiveCfg = DebugLang|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.DebugLang|Any CPU.Build.0 = DebugLang|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {8B4407E8-8EB4-6A52-665C-F80ECB40F56D}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.DebugLang|Any CPU.ActiveCfg = DebugLang|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.DebugLang|Any CPU.Build.0 = DebugLang|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Release-Linux|Any CPU.Build.0 = Release|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Release-Mac|Any CPU.Build.0 = Release|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Release-Msi|Any CPU.ActiveCfg = Release-Msi|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Release-Msi|Any CPU.Build.0 = Release-Msi|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Release-Zip|Any CPU.ActiveCfg = Release-Zip|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Release-Zip|Any CPU.Build.0 = Release-Zip|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {E97B675D-8519-6FDD-9418-D3785D23D7A0}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {A93D0028-A629-B669-0CF2-48EF7F6E82E7}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {2DF61440-BF1F-98F9-F30B-118AE6758CC5}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {FF5B01D1-8DEE-5B07-EE26-725DEF02895F}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {608A9BD7-2E5E-9BC6-CF82-6EACE281785C}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.Sdk|Any CPU.Build.0 = Release|Any CPU + {D399C4A1-06D3-8FC2-F881-423D6729F6BF}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Debug-Linux|Any CPU.ActiveCfg = DebugNoMac|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Debug-Linux|Any CPU.Build.0 = DebugNoMac|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Debug-Windows|Any CPU.ActiveCfg = DebugNoMac|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Debug-Windows|Any CPU.Build.0 = DebugNoMac|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.DebugLang|Any CPU.ActiveCfg = DebugLang|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.DebugLang|Any CPU.Build.0 = DebugLang|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Release-Linux|Any CPU.Build.0 = Release|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Release-Mac|Any CPU.Build.0 = Release|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Sdk|Any CPU.Build.0 = Release|Any CPU + {DC1A9AC4-9D9E-43FD-F90E-B9D2796D702E}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {9D6A5555-68B1-13AA-27B4-36422715BEB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D6A5555-68B1-13AA-27B4-36422715BEB8}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {9D6A5555-68B1-13AA-27B4-36422715BEB8}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {9D6A5555-68B1-13AA-27B4-36422715BEB8}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {9D6A5555-68B1-13AA-27B4-36422715BEB8}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {9D6A5555-68B1-13AA-27B4-36422715BEB8}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {9D6A5555-68B1-13AA-27B4-36422715BEB8}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {9D6A5555-68B1-13AA-27B4-36422715BEB8}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {9D6A5555-68B1-13AA-27B4-36422715BEB8}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {9D6A5555-68B1-13AA-27B4-36422715BEB8}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {9D6A5555-68B1-13AA-27B4-36422715BEB8}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {9D6A5555-68B1-13AA-27B4-36422715BEB8}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.Release-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.Release-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.Release-Msi|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.Release-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.Release-Zip|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.Sdk|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {F27D5DCF-E1A5-7B23-C688-6805C0D4D85B}.Tools|Any CPU.Build.0 = Debug|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Release-Linux|Any CPU.Build.0 = Release|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {B8BFC18F-B160-77BB-898D-8CD2969797B9}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Release-Mac|Any CPU.Build.0 = Release|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {A5BF0DD2-4988-0E49-E917-8CF463E9E765}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.DebugLang|Any CPU.ActiveCfg = DebugLang|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.DebugLang|Any CPU.Build.0 = DebugLang|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {D524C6DA-EEE5-45EF-735A-B1F921D78577}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {30C31689-1A7F-954A-A921-4A6707E6DCF5}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Release-Linux|Any CPU.Build.0 = Release|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Release-Mac|Any CPU.Build.0 = Release|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Sdk|Any CPU.Build.0 = Release|Any CPU + {22306413-9041-748E-CF80-63770FBFA318}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Sdk|Any CPU.Build.0 = Release|Any CPU + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Release-Linux|Any CPU.Build.0 = Release|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Release-Mac|Any CPU.Build.0 = Release|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Sdk|Any CPU.Build.0 = Release|Any CPU + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Debug|Any CPU.Build.0 = Debug|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Sdk|Any CPU.Build.0 = Release|Any CPU + {251BDC21-838F-8006-6A99-48774229A593}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Release-Linux|Any CPU.Build.0 = Release|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Sdk|Any CPU.Build.0 = Release|Any CPU + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Sdk|Any CPU.Build.0 = Release|Any CPU + {31588109-3BFA-3D3F-AD09-074A0233812F}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Release-Mac|Any CPU.Build.0 = Release|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Sdk|Any CPU.Build.0 = Release|Any CPU + {A3673881-1312-3BA4-570E-3B21B75F203C}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Sdk|Any CPU.Build.0 = Release|Any CPU + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Debug-Linux|Any CPU.ActiveCfg = Debug|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Debug-Linux|Any CPU.Build.0 = Debug|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Debug-Mac|Any CPU.ActiveCfg = Debug|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Debug-Mac|Any CPU.Build.0 = Debug|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.DebugLang|Any CPU.Build.0 = Debug|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Release-Linux|Any CPU.ActiveCfg = Release|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Release-Linux|Any CPU.Build.0 = Release|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Release-Mac|Any CPU.ActiveCfg = Release|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Release-Mac|Any CPU.Build.0 = Release|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Release-Msi|Any CPU.ActiveCfg = Release|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Release-Msi|Any CPU.Build.0 = Release|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Release-Zip|Any CPU.ActiveCfg = Release|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Release-Zip|Any CPU.Build.0 = Release|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Sdk|Any CPU.ActiveCfg = Release|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Sdk|Any CPU.Build.0 = Release|Any CPU + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502}.Tools|Any CPU.ActiveCfg = Debug|Any CPU EndGlobalSection - GlobalSection(SubversionScc) = preSolution - Svn-Managed = True - Manager = AnkhSVN - Subversion Support for Visual Studio + GlobalSection(NestedProjects) = preSolution + {66188578-A9BF-17A3-6F2F-F45E2CC35C9F} = {F95F473B-E98A-ACD6-1986-755F447BE36F} + {B8BFC18F-B160-77BB-898D-8CD2969797B9} = {F95F473B-E98A-ACD6-1986-755F447BE36F} + {4EBED44E-61AB-7A0C-4AF7-F42646E6FD3C} = {F95F473B-E98A-ACD6-1986-755F447BE36F} + {A5BF0DD2-4988-0E49-E917-8CF463E9E765} = {F95F473B-E98A-ACD6-1986-755F447BE36F} + {C613D909-0E52-4DC8-ED9D-A6ACCE7B50B3} = {F95F473B-E98A-ACD6-1986-755F447BE36F} + {D524C6DA-EEE5-45EF-735A-B1F921D78577} = {F95F473B-E98A-ACD6-1986-755F447BE36F} + {30C31689-1A7F-954A-A921-4A6707E6DCF5} = {F95F473B-E98A-ACD6-1986-755F447BE36F} + {22306413-9041-748E-CF80-63770FBFA318} = {AC40AF8E-5BE2-E02F-B15A-0A1E1DBBAED1} + {38F4A89A-2B48-54BE-FDA8-9BE22A6A0EE6} = {AC40AF8E-5BE2-E02F-B15A-0A1E1DBBAED1} + {C74DE521-931E-A4D4-2FA4-04C2C6D977FE} = {AC40AF8E-5BE2-E02F-B15A-0A1E1DBBAED1} + {88C01DE8-ED2E-0465-6A3B-7673A6DF314C} = {AC40AF8E-5BE2-E02F-B15A-0A1E1DBBAED1} + {251BDC21-838F-8006-6A99-48774229A593} = {011BDCA8-E5EE-9E14-FF24-A59B216152F3} + {EB7D92D6-BADC-3256-A4DC-EAAB48E9C652} = {011BDCA8-E5EE-9E14-FF24-A59B216152F3} + {31588109-3BFA-3D3F-AD09-074A0233812F} = {011BDCA8-E5EE-9E14-FF24-A59B216152F3} + {A3673881-1312-3BA4-570E-3B21B75F203C} = {011BDCA8-E5EE-9E14-FF24-A59B216152F3} + {57B2C6B4-1B23-3DE6-58B0-34D9DE942B70} = {011BDCA8-E5EE-9E14-FF24-A59B216152F3} + {B12B9C34-67D2-7AE8-7DDD-38F384DF3502} = {011BDCA8-E5EE-9E14-FF24-A59B216152F3} EndGlobalSection EndGlobal diff --git a/NAPS2.sln.DotSettings b/NAPS2.sln.DotSettings index 5d5b301fa6..f3c044ee33 100644 --- a/NAPS2.sln.DotSettings +++ b/NAPS2.sln.DotSettings @@ -1,14 +1,27 @@  + 0 + 0 + True MB DO_NOT_SHOW UI <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> True + True + True + True + True + True <data><IncludeFilters /><ExcludeFilters /></data> <data /> True + True + True True True True @@ -21,4 +34,5 @@ True True True - True \ No newline at end of file + True + 2.1.30 \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000000..03b91e9692 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 4403cbedc2..c4e178ecdf 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,42 @@ # NAPS2 - Not Another PDF Scanner -NAPS2 is a document scanning application with a focus on simplicity and ease of use. Scan your documents from WIA- and TWAIN-compatible scanners, organize the pages as you like, and save them as PDF, TIFF, JPEG, PNG, and other file formats. +

+NAPS2 on Windows NAPS2 on Mac NAPS2 on Linux +
+ NAPS2 on Windows, Mac, and Linux +

+ +NAPS2 is a document scanning application with a focus on simplicity and ease of use. Scan your documents from WIA, TWAIN, SANE, and ESCL scanners, organize the pages as you like, and save them as PDF, TIFF, JPEG, or PNG. Optical character recognition (OCR) is available using [Tesseract](https://github.com/tesseract-ocr/tesseract). System requirements: - Windows 7+ (x64, x86) - macOS 10.15+ (x64, arm64) -- Linux (x64, arm64) +- Linux (x64, arm64) (GTK 3.20+, glibc 2.27+, libsane) Visit the NAPS2 home page at [www.naps2.com](http://www.naps2.com). Other links: -- [Documentation](http://www.naps2.com/support.html) -- [Translations](http://translate.naps2.com/) - [Doc](http://www.naps2.com/doc-translations.html) -- [File a Ticket](https://sourceforge.net/p/naps2/tickets/) - For bug reports, feature requests, and general support inquiries. -- [Discussion Forums](https://sourceforge.net/p/naps2/discussion/general/) - For more open-ended discussion. -- [Donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=M77MFAP2ZV9RG) +- [Downloads](https://www.naps2.com/download) +- [Documentation](https://www.naps2.com/support) +- [Translations](https://translate.naps2.com/) +- [File a Ticket](https://sourceforge.net/p/naps2/tickets/) +- [Donate](https://www.naps2.com/donate?src=readme) ## NAPS2.Sdk (for developers) -> NAPS2.Sdk is a **work in progress**. Nuget packages will be made available once it is ready for public consumption. +[![NuGet](https://img.shields.io/nuget/v/NAPS2.Sdk)](https://www.nuget.org/packages/NAPS2.Sdk/) [NAPS2.Sdk](https://github.com/cyanfish/naps2/tree/master/NAPS2.Sdk) is a fully-featured scanning library, supporting WIA, TWAIN, SANE, and ESCL scanners on Windows, Mac, and Linux. [Read more.](https://github.com/cyanfish/naps2/tree/master/NAPS2.Sdk) - -Looking to contribute to NAPS2 or NAPS2.Sdk? Have a look at the [Developer Onboarding](https://www.naps2.com/doc-dev-onboarding.html) page. +## Build Instructions +Looking to contribute to NAPS2 or NAPS2.Sdk? Have a look at the [Github wiki](https://github.com/cyanfish/naps2/wiki/1.-Building-&-Development-Environment) for build instructions and more. ## License NAPS2 is licensed under the GNU GPL 2.0 (or later). Some projects have additional license options: +- NAPS2.Escl.* - GNU LGPL 2.1 (or later) - NAPS2.Images.* - GNU LGPL 2.1 (or later) +- NAPS2.Internals - GNU LGPL 2.1 (or later) - NAPS2.Sdk - GNU LGPL 2.1 (or later) - NAPS2.Sdk.Samples - MIT