202 Commits
Author SHA1 Message Date
Milan Nikolic 4e2f78026d Destroy current image 2026-06-25 11:26:03 +02:00
Milan Nikolic e1134cd902 Add page spin to preview other pages 2026-06-25 11:03:27 +02:00
Milan Nikolic f289c9cd06 Add profiles support to cli app 2026-06-25 10:59:12 +02:00
Milan Nikolic 0439a2edde Add NoUpscale option 2026-06-24 23:39:03 +02:00
Milan Nikolic 92c49290ae Add cover preview in FileDlg 2026-06-24 23:14:50 +02:00
Milan Nikolic 7f025aa9c2 Fix cli progressbar 2026-06-24 23:12:43 +02:00
Milan Nikolic a8b2f65ca5 Update README.md 2026-06-24 21:40:44 +02:00
Milan Nikolic 8f75ffc43b Split GUI files 2026-06-24 21:36:32 +02:00
Milan Nikolic aca062a3f2 Organize Input tab, add more tooltips 2026-06-24 21:18:19 +02:00
Milan Nikolic 9de98599a9 Add DPI option, issue #52 2026-06-24 20:44:29 +02:00
Milan Nikolic f65fc4bafa Turn Convert into Cancel button 2026-06-24 19:53:27 +02:00
Milan Nikolic 26a320ef9d Handle SORT_CB 2026-06-24 18:39:16 +02:00
Milan Nikolic c4f236bdf9 Add Command button, issue #33 2026-06-24 13:16:09 +02:00
Milan Nikolic 23ac71ee0c Add settings profiles, issue #34 and issue #58 2026-06-24 11:20:12 +02:00
Milan Nikolic 3f0ae41456 Add ZipLevel, issue #48 2026-06-24 07:55:16 +02:00
Milan Nikolic 04a047aa2e Add more tests 2026-06-24 07:10:02 +02:00
Milan Nikolic 1284b9ded7 Fix outdir and recursive, issue #41 2026-06-24 06:42:48 +02:00
Milan Nikolic b8d82e920c Fix comic chapters, issue #50 2026-06-24 04:50:10 +02:00
Milan Nikolic 3c81595e08 Add Combine option, issue #21 2026-06-24 04:37:38 +02:00
Milan Nikolic 0f6fbba1d7 Use Table instead of List 2026-06-23 19:42:06 +02:00
Milan Nikolic 47a10e9185 Update modules 2026-06-23 18:25:02 +02:00
Milan Nikolic a5817c3ba5 Add Lossless option 2026-06-23 18:16:11 +02:00
Milan Nikolic b7c422fe33 Add Effort option 2026-06-23 05:34:18 +02:00
Milan Nikolic 09630243fb Fix 4-bit 16 color palette 2026-06-23 04:39:03 +02:00
Milan Nikolic 59bc2b7a1e Remove dbus portal 2026-06-22 22:46:28 +02:00
Milan Nikolic 3a627d92e1 Use archives instead of go-unarr 2026-06-22 22:07:40 +02:00
Milan Nikolic f555f09e59 Update modules 2026-06-22 20:33:51 +02:00
Milan Nikolic ad4f522b1f Update README.md 2024-11-08 16:01:33 +01:00
Milan Nikolic 05f08f46ec Update modules 2024-11-06 20:58:21 +01:00
Milan Nikolic 4d845afa43 Update manifest 2024-11-06 20:24:21 +01:00
Milan Nikolic 0dee611bf1 Update dependencies 2024-11-06 19:14:14 +01:00
Milan Nikolic 4bd35fe570 Update screenshots 2024-11-06 18:47:53 +01:00
Milan Nikolic f85fe990ff Ignore macOS files, issue #37 2024-11-06 18:31:22 +01:00
Milan Nikolic 31e1c94f70 Set MULTILINE option, issue #32 2024-11-06 17:55:10 +01:00
Milan Nikolic 8e14a22a9b Fix recursive, issue #27 2024-11-06 17:43:01 +01:00
Milan Nikolic 5d20f5c81e Handle error 2024-11-06 17:07:41 +01:00
Milan Nikolic a2738b4c08 Update dependencies 2024-11-06 17:05:18 +01:00
Milan Nikolic a0b1f65940 Update README.md 2024-11-06 17:04:56 +01:00
Milan Nikolic 1e0f5b5956 Update README.md 2024-11-06 06:10:24 +01:00
Milan Nikolic 544d824afb Update modules 2024-11-06 06:10:11 +01:00
Milan Nikolic 90ed56d4ad Add space 2024-11-06 05:13:20 +01:00
Milan Nikolic eff4730014 Split files 2024-11-06 05:10:12 +01:00
Milan Nikolic 85059d7df3 Update modules 2024-11-04 19:30:31 +01:00
Milan Nikolic 9e944142d6 Update modules 2024-11-04 16:36:21 +01:00
Milan Nikolic ee2cf2e14d Update README.md 2024-11-04 16:18:31 +01:00
Milan Nikolic bf95d62242 Split files 2024-11-04 16:01:38 +01:00
Milan Nikolic be50bf1e16 Update README.md 2024-11-04 13:54:08 +01:00
Milan Nikolic 1a5520c050 Save jpeg files as .jpg, issue #35 2024-11-04 13:53:58 +01:00
Milan Nikolic 4f23f228a8 Add version flag for gui, issue #39 2024-11-04 12:31:08 +01:00
Milan Nikolic 5b2ed5233b Remove pflag dependency 2024-11-04 11:19:59 +01:00
Milan Nikolic e2c41b3193 Show logo on error 2024-11-04 11:01:39 +01:00
Milan Nikolic 4c51c152f3 Remove icon 2024-11-04 10:28:01 +01:00
Milan Nikolic c5cbc9fa9b Rename file 2024-11-04 10:22:24 +01:00
Milan Nikolic a668ae5e3d Update metadata 2024-11-04 10:21:25 +01:00
Milan Nikolic 3bd7e672eb Update metadata 2024-11-04 10:19:02 +01:00
Milan Nikolic 6b21c36902 Update metadata 2024-11-03 22:56:43 +01:00
Milan Nikolic d54cec94a2 Update id 2024-11-03 22:45:13 +01:00
Milan Nikolic 0df2f7d39d Rename xdg files 2024-11-03 22:42:58 +01:00
Milan Nikolic e7b9fd8c2c Add .gitignore 2024-11-03 22:30:26 +01:00
Milan Nikolic f795c70c33 Add office documents 2024-11-03 22:00:51 +01:00
Milan Nikolic 3bb40a87cd Update README.md 2024-11-03 21:59:04 +01:00
Milan Nikolic e38b5b3fb4 Update modules 2024-11-03 19:52:24 +01:00
Milan Nikolic f57b2598f1 Update actions 2024-11-03 19:22:31 +01:00
Milan Nikolic fca9f31fcf Update actions 2024-11-03 19:18:49 +01:00
Milan Nikolic 5f5fa2bb80 Use jpegli for smaller files 2024-11-03 19:05:02 +01:00
Milan Nikolic 622c82a9df Fix remove last 2024-11-03 19:03:44 +01:00
Milan Nikolic f4c149ac31 Update logo 2024-11-03 19:02:08 +01:00
Milan Nikolic de2f8a2035 Update palette 2024-11-03 09:20:26 +01:00
Milan Nikolic 749ff0b943 Add back 4bit BMP 2024-11-03 00:37:52 +01:00
Milan Nikolic 1f33bec28c Use bild library 2024-11-02 23:01:32 +01:00
Milan Nikolic 6a4054455a Add logo 2024-11-02 21:32:09 +01:00
Milan Nikolic 962a2e6dee Enable Wayland 2024-11-02 20:50:14 +01:00
Milan Nikolic 63013998c7 Add docx and pptx 2024-11-02 20:16:23 +01:00
Milan Nikolic c0a077fcd1 Remove ImageMagick dependency 2024-11-02 20:08:02 +01:00
Milan Nikolic 71d8f9dbb6 Remove levels 2024-11-02 19:26:09 +01:00
Milan Nikolic c794ea6b9d Use pngstructure instead of IM 2024-11-02 18:47:18 +01:00
Milan Nikolic 74c5de6994 Update dependencies 2024-10-22 20:31:01 +02:00
Milan Nikolic 1a36ec17b2 Update dependencies 2024-10-22 19:42:31 +02:00
Milan Nikolic a9e3281a5f Add modules 2024-02-08 16:05:56 +01:00
Milan Nikolic 962fde8c8d Update modules 2024-02-08 15:50:40 +01:00
Milan Nikolic a8084ed897 Update metadata 2024-02-08 15:47:35 +01:00
Milan Nikolic 183c42613f Change webp library 2024-02-08 15:38:41 +01:00
Milan Nikolic cad75231b1 Update modules 2024-02-08 15:12:10 +01:00
Milan NikolicandGitHub c32019615d Merge pull request #22 from kianmeng/fix-typo
Fix typo, convertor -> converter
2023-11-16 07:43:27 +01:00
Kian-Meng Ang dd9000cfea Fix typo, convertor -> converter
Found via `codespell`.
2023-11-16 13:01:43 +08:00
Milan Nikolic 4fc3209b4e Update README.md 2023-09-29 09:53:27 +02:00
Milan Nikolic 000bd20f71 Update README.md 2023-09-16 14:17:10 +02:00
Milan Nikolic f558b109e5 Update manifest 2023-09-14 16:44:12 +02:00
Milan Nikolic 5922f102be Update manifest 2023-09-14 16:42:34 +02:00
Milan Nikolic f84d2d4333 Update modules 2023-09-14 15:05:49 +02:00
Milan Nikolic 551ab9e137 Change flatpak exec/command name to cbconvert 2023-09-14 15:02:27 +02:00
Milan Nikolic 046cc2c1fd Update modules 2023-09-14 11:51:48 +02:00
Milan Nikolic a5b1f41ba6 Update modules 2023-09-14 04:33:48 +02:00
Milan Nikolic 9f628741e0 Update metainfo 2023-09-14 04:28:53 +02:00
Milan Nikolic ccbb27ff6d Update modules 2023-09-14 04:04:36 +02:00
Milan Nikolic 04473e95bf Update url 2023-09-14 03:48:06 +02:00
Milan Nikolic c061297ad0 Update modules 2023-09-14 03:43:17 +02:00
Milan Nikolic 544344d5c2 Update make.bash 2023-09-14 03:39:19 +02:00
Milan Nikolic e14aa6baab Add GUI 2023-09-14 03:27:07 +02:00
Milan Nikolic 6400ae14d8 Update modules 2023-09-13 13:39:13 +02:00
Milan Nikolic 788a9d3bd5 Add Cancel, split file 2023-09-13 13:14:09 +02:00
Milan Nikolic bff17636f6 Update modules 2023-09-11 16:03:04 +02:00
Milan Nikolic bd19faf18e Add mobi test file 2023-09-11 15:42:48 +02:00
Milan Nikolic cf1e8a1fec Add support for JXL 2023-09-11 15:34:11 +02:00
Milan Nikolic 63ad96ac3a Add support for XPS 2023-09-07 21:43:31 +02:00
Milan Nikolic 9bd7193124 Remove hotei/bmp 2023-09-07 21:38:15 +02:00
Milan Nikolic 09a27ac9a4 Remove musl build 2023-09-02 18:42:27 +02:00
Milan Nikolic 4109ae154f Update README.md 2023-09-02 18:41:33 +02:00
Milan Nikolic f01e858a24 Update build 2023-09-02 18:20:46 +02:00
Milan Nikolic 8c21b430a9 Update actions 2023-09-02 17:19:36 +02:00
Milan Nikolic 5fbd462a78 Update actions 2023-09-02 16:17:46 +02:00
Milan Nikolic 0797c8456f Add actions 2023-09-02 16:14:32 +02:00
Milan Nikolic c3f6c5a499 Update README.md 2023-09-02 12:14:06 +02:00
Milan Nikolic fa162972b2 Update README.md 2023-09-02 12:11:24 +02:00
Milan Nikolic aa65bfa4f4 Add tests 2023-09-02 12:11:09 +02:00
Milan Nikolic 6c293adc1f Update modules 2023-09-02 10:11:03 +02:00
Milan Nikolic 6936f697b4 Remove flip 2023-09-02 10:02:08 +02:00
Milan Nikolic 83d7a1ea6e Update README.md 2023-09-02 09:23:02 +02:00
Milan Nikolic 3d56765415 Update modules 2023-09-02 09:20:33 +02:00
Milan Nikolic b1fcf530da Add version and preview 2023-09-02 09:18:47 +02:00
Milan Nikolic 7dc21fc0b4 Update modules 2023-08-29 20:15:22 +02:00
Milan Nikolic fdfa80875e Add File type 2023-08-29 20:07:35 +02:00
Milan Nikolic 50b0911586 Close archive 2023-08-27 11:43:19 +02:00
Milan Nikolic 8db1690d3c Update thumbnail 2023-08-27 11:38:41 +02:00
Milan Nikolic 91e336f772 Update README.md 2023-08-24 15:04:51 +02:00
Milan Nikolic 903a8bbcfc Update modules 2023-08-24 11:46:48 +02:00
Milan Nikolic e938b140f0 Update README.md 2023-08-24 11:43:14 +02:00
Milan Nikolic 0a3f4d6992 Add support for MOBI 2023-08-24 11:42:19 +02:00
Milan Nikolic c5902fb131 Update modules 2023-08-24 09:05:37 +02:00
Milan Nikolic d6ece90041 Add support for adding/removing files 2023-08-24 09:00:52 +02:00
Milan Nikolic c34b4d0a98 Allow recursive for plain directories 2023-08-23 13:16:30 +02:00
Milan Nikolic f0e2232e51 Update modules 2023-08-23 12:43:05 +02:00
Milan Nikolic 0ba0337ef8 Preserve output directory structure when recursive is enabled 2023-08-23 12:17:32 +02:00
Milan Nikolic 6e97d977ae Update modules 2023-08-22 14:39:07 +02:00
Milan Nikolic 685b6bf153 Update modules 2023-08-22 14:37:05 +02:00
Milan Nikolic 14f2e87525 Lint 2023-08-22 14:36:03 +02:00
Milan Nikolic cd2f7501e0 Update modules 2023-08-22 11:30:50 +02:00
Milan Nikolic 69b0b51010 Allow for piped input 2023-08-22 11:29:33 +02:00
Milan Nikolic c5494af60b Add support for CBT 2023-08-22 10:27:48 +02:00
Milan Nikolic f447c618de Update README.md 2023-08-21 20:12:03 +02:00
Milan Nikolic 54261d8528 Set comment 2023-08-21 20:04:05 +02:00
Milan Nikolic f92125643f Update modules 2023-08-21 17:54:10 +02:00
Milan Nikolic 7b6c766dcc Refactor 2023-08-21 17:52:57 +02:00
Milan Nikolic dfea3dc6a3 Update dependencies 2023-06-02 10:55:02 +02:00
Milan Nikolic 16d01e292e Update dependencies 2023-06-02 10:52:08 +02:00
Milan Nikolic 6fe10f5bdb Update README.md 2023-06-02 10:46:38 +02:00
Milan Nikolic 547a3937a8 Update dependencies 2023-06-02 10:43:36 +02:00
Milan Nikolic d47c1c1464 Use ToSlash for cover, issue #19 2023-06-02 10:32:09 +02:00
Milan Nikolic 5befff07ae Handle all errors 2023-03-08 06:21:57 +01:00
Milan Nikolic d4abc99239 Add meta command 2023-03-08 05:42:49 +01:00
Milan Nikolic f1804ce13c Refactor 2023-03-04 11:30:11 +01:00
Milan Nikolic a74034d669 Update modules 2023-03-04 10:35:41 +01:00
Milan Nikolic 64fb152c36 Cover format, issue #18 2023-03-04 10:27:01 +01:00
Milan Nikolic 7082e3db90 Initialize IM 2023-01-14 10:52:04 +01:00
Milan Nikolic 943cd9bb95 Close file 2023-01-14 10:39:20 +01:00
Milan Nikolic ec2c321ff3 Expose IM functions 2023-01-14 10:37:17 +01:00
Milan Nikolic a0ed277971 Update README.md 2023-01-13 18:37:40 +01:00
Milan Nikolic b5b4457088 Update build 2023-01-13 18:33:57 +01:00
Milan Nikolic c24d7b7e18 Update README.md 2023-01-12 13:36:20 +01:00
Milan Nikolic 8ba065e68b Use pkgconfig 2023-01-12 13:18:42 +01:00
Milan Nikolic 9cd5616b48 Build linux/arm64 binary 2023-01-12 13:08:21 +01:00
Milan Nikolic 13a3b6e23e Update README.md 2023-01-12 10:06:09 +01:00
Milan Nikolic 9a047d1977 Fallback to IM decoding for broken images, issue #15 2023-01-12 10:03:45 +01:00
Milan Nikolic d6e7248112 Case-insensitive for coverName, issue #13 2023-01-12 09:10:24 +01:00
Milan Nikolic 9eb20db167 Handle errors 2023-01-12 08:45:32 +01:00
Milan Nikolic 04d2dd79d5 Handle no-convert for directories, issue #9 2023-01-12 08:44:00 +01:00
Milan Nikolic 2b2788ed6e Update modules 2022-12-08 12:38:38 +01:00
Milan Nikolic 164e7bfd75 Update modules 2022-12-08 12:35:53 +01:00
Milan Nikolic 27da60ed76 Update modules 2022-12-08 12:28:08 +01:00
Milan Nikolic 0963cb1762 Update README.md 2022-09-09 10:01:14 +02:00
Milan Nikolic 443c4e6aaa Update README.md 2022-09-08 23:44:56 +02:00
Milan Nikolic 9da5b97f15 Add support for AVIF 2022-09-08 23:38:58 +02:00
Milan Nikolic adbb86f9f7 Add more architectures 2022-09-08 23:36:47 +02:00
Milan Nikolic efb8b776fb Fixes 2022-09-07 19:55:59 +02:00
Milan Nikolic fa954fce1a Fixes 2022-09-07 12:16:50 +02:00
Milan Nikolic 7d80f5238e Update README.md 2022-09-05 18:45:43 +02:00
Milan Nikolic 56e07aae23 Update version 2022-09-05 18:45:30 +02:00
Milan Nikolic 3c3123a50d Update README.md 2022-09-05 18:32:46 +02:00
Milan Nikolic 2e03350437 Update README.md 2022-09-05 18:16:27 +02:00
Milan Nikolic 1fdc1610f5 Update README.md 2022-09-05 18:13:03 +02:00
Milan Nikolic a5e796d017 Remove actions 2022-09-05 18:12:48 +02:00
Milan Nikolic cffd119eba Add actions 2022-09-05 18:04:18 +02:00
Milan Nikolic cda03e0122 Big update 2022-09-05 18:03:58 +02:00
Milan Nikolic 6a73f6f20e Remove defunct GUI 2022-09-05 18:01:45 +02:00
Milan NikolicandGitHub 5df10a58ee Update README.md 2017-01-24 15:30:08 +01:00
Milan Nikolic 081736ee04 new release 2016-01-22 17:28:02 +01:00
Milan Nikolic 50f1edb843 Merge branch 'master' of github.com:gen2brain/cbconvert 2016-01-22 17:17:59 +01:00
Milan Nikolic 2c5bc9e21d support thumbnails in file managers 2016-01-22 17:17:43 +01:00
Milan Nikolic d9cdf2e197 Update README.md 2015-12-02 23:57:10 +01:00
Milan Nikolic 68e868944b Update README.md 2015-12-02 21:48:51 +01:00
Milan Nikolic 3a84012005 Update README.md 2015-12-02 21:24:21 +01:00
Milan Nikolic 23802d2d00 Update README.md 2015-12-02 20:53:26 +01:00
Milan Nikolic 17e598bb3b Update README.md 2015-12-02 20:53:01 +01:00
Milan Nikolic 2fd32c30e3 Update README.md 2015-12-02 20:52:30 +01:00
Milan Nikolic b930b06a7d add GUI 2015-12-02 20:46:42 +01:00
Milan Nikolic c8d472e576 Update README.md 2015-11-05 10:22:30 +01:00
Milan Nikolic 6701bfcfb0 rename cmd 2015-11-05 10:17:39 +01:00
Milan Nikolic 6cea1aa441 rename cmd 2015-11-05 10:14:41 +01:00
Milan Nikolic a05b46e6b8 move main to cmd dir 2015-11-05 10:09:44 +01:00
Milan Nikolic d77d2c0ce7 convertor struct 2015-11-05 09:54:21 +01:00
Milan Nikolic bc8520d825 Update README.md 2015-11-05 08:03:56 +01:00
Milan Nikolic 3619ed6c0d Update README.md 2015-11-05 07:59:07 +01:00
60 changed files with 6920 additions and 1025 deletions
+23
View File
@@ -0,0 +1,23 @@
on: [push, pull_request]
name: Test
jobs:
test:
strategy:
fail-fast: false
matrix:
go-version: [1.23.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
CGO_ENABLED: 0
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Test
run: go test
env:
CGO_ENABLED: 1
+5
View File
@@ -0,0 +1,5 @@
cmd/cbconvert-gui/cbconvert-gui
cmd/cbconvert/cbconvert
.idea
.vscode
+188 -113
View File
@@ -1,156 +1,231 @@
CBconvert
=========
## CBconvert
Introduction
------------
### Introduction
CBconvert is a [Comic Book](http://en.wikipedia.org/wiki/Comic_Book_Archive_file) converter.
It allows you to convert individual comics or bulk convert comics to different formats to fit your various devices.
Features
--------
It can convert comics to different formats to fit your various devices.
- reads RAR, ZIP, 7Z, GZ, BZ2, CBR, CBZ, CB7, CBT, PDF, EPUB, XPS and plain directory
- always saves processed comic in CBZ (ZIP) archive format
- images can be converted to JPEG, PNG, GIF, TIFF or 4-Bit BMP (16 colors) file format
- choose resize algorithm (NearestNeighbor, Box, Linear, MitchellNetravali, CatmullRom, Gaussian, Lanczos)
- rotate, flip, adjust brightness/contrast or grayscale images
- export covers from comics
- create thumbnails from covers by [freedesktop](http://www.freedesktop.org/wiki/) specification
<img src="cmd/cbconvert-gui/screenshots/linux-01.jpg" width="700" alt="screenshot" />
Download
--------
See more [screenshots](https://github.com/gen2brain/cbconvert/blob/master/cmd/cbconvert-gui/screenshots/).
- [Windows binary](https://github.com/gen2brain/cbconvert/releases/download/0.4.0/cbconvert-0.4.0.zip)
### Features
- [Linux 64bit binary](https://github.com/gen2brain/cbconvert/releases/download/0.4.0/cbconvert-0.4.0.tar.gz)
- [Linux 64bit static binary](https://github.com/gen2brain/cbconvert/releases/download/0.4.0/cbconvert-0.4.0-static.tar.gz)
* reads CBR (RAR), CBZ (ZIP), CB7 (7Z), CBT (TAR), PDF, XPS, EPUB, MOBI, DOCX, PPTX and plain directory
* saves processed files in ZIP archive format or TAR
* images can be converted to JPEG, PNG, TIFF, WEBP, AVIF, JXL, or 4-Bit BMP (16 colors) image format
* rotate, adjust brightness/contrast or grayscale images
* resize filters (NearestNeighbor, Box, Linear, MitchellNetravali, CatmullRom, Gaussian, Lanczos)
* export covers from comics
* create thumbnails from covers by [FreeDesktop](http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html) specification
Using
-----
### Download
usage: cbconvert [<flags>] <command> [<args> ...]
Download the latest binaries from the [releases](https://github.com/gen2brain/cbconvert/releases).
Comic Book convert tool.
Linux Flatpak is available at [Flathub](https://flathub.org/apps/io.github.gen2brain.cbconvert).
Flags:
--help Show context-sensitive help (also try --help-long and --help-man).
--version Show application version.
--outdir="." Output directory
--size=0 Process only files larger then size (in MB)
--recursive Process subdirectories recursively
--quiet Hide console output
### Compile
Args:
<args> filename or directory
You must have `CGO_ENABLED=1`. Note that `Go` will disable cgo when cross-compiling.
Commands:
help [<command>...]
Show help.
Install to `GOBIN` (you can point `GOBIN` to e.g. `/usr/local/bin` or `~/.local/bin`):
`go install github.com/gen2brain/cbconvert/cmd/cbconvert@latest`
For GUI app, check [IUP](https://github.com/gen2brain/iup-go) requirements, and then install:
`go install github.com/gen2brain/cbconvert/cmd/cbconvert-gui@latest`
### Build tags
* `extlib` - use external `libmupdf` and `libunarr` libraries
* `pkgconfig` - enable pkg-config (used with `extlib`)
### Using cbconvert in file managers to generate FreeDesktop thumbnails
Copy/install `cbconvert` cli binary to your `PATH`, create file `~/.local/share/thumbnailers/cbconvert.thumbnailer`
and paste contents from [thumbnailer](https://github.com/gen2brain/cbconvert/tree/master/cmd/cbconvert-gui/dist/linux/io.github.gen2brain.cbconvert.thumbnailer).
This is what it looks like in the `PCManFM` file manager:
<img src="cmd/cbconvert-gui/screenshots/thumbnails.jpg" width="700" alt="thumbnails" />
convert [<flags>] <args>...
Convert archive or document (default command)
### Using command line app
--width=0 Image width
--height=0 Image height
--fit Best fit for required width and height
--quality=75 JPEG image quality
--filter=2 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos
--png Encode images to PNG instead of JPEG
--bmp Encode images to 4-Bit BMP (16 colors) instead of JPEG
--gif Encode images to GIF instead of JPEG
--tiff Encode images to TIFF instead of JPEG
--rgb Convert images that have RGB colorspace (use --no-rgb if you only want to convert grayscale images)
--nonimage Leave non image files in archive (use --no-nonimage to remove non image files from archive)
--grayscale Convert images to grayscale (monochromatic)
--rotate=0 Rotate images, valid values are 0, 90, 180, 270
--flip="none" Flip images, valid values are none, horizontal, vertical
--brightness=0 Adjust brightness of the images, must be in range (-100, 100)
--contrast=0 Adjust contrast of the images, must be in range (-100, 100)
--suffix=SUFFIX Add suffix to file basename
```
Usage: cbconvert <command> [<flags>] [file1 dir1 ... fileOrDirN]
cover [<flags>] <args>...
Extract cover
--width=0 Image width
--height=0 Image height
--fit Best fit for required width and height
--quality=75 JPEG image quality
--filter=2 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos
Commands:
thumbnail [<flags>] <args>...
Extract cover thumbnail (freedesktop spec.)
convert
Convert archive or document
--width=0 Image width
--height=0 Image height
--fit Best fit for required width and height
--filter=2 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos
--width
Image width (default "0")
--height
Image height (default "0")
--fit
Best fit for required width and height (default "false")
--no-upscale
Do not upscale images already smaller than the requested width/height (default "false")
--dpi
Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
--format
Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl (default "jpeg")
--archive
Archive format, valid values are zip, tar (default "zip")
--zip-level
ZIP compression level, 0 disables compression, 1-9 sets deflate level (1 fastest, 9 smallest), -1 uses the default (default "-1")
--quality
Image quality (default "75")
--effort
Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default (default "-1")
--lossless
Lossless compression (webp, avif, jxl), ignores quality (default "false")
--combine
Combine all inputs into a single archive (default "false")
--outfile
Output file name for --combine (default: first input + -combined) (default "")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
--no-cover
Do not convert the cover image (default "false")
--no-rgb
Do not convert images that have RGB colorspace (default "false")
--no-nonimage
Remove non-image files from the archive (default "false")
--no-convert
Do not transform or convert images (default "false")
--grayscale
Convert images to grayscale (monochromatic) (default "false")
--rotate
Rotate images, valid values are 0, 90, 180, 270 (default "0")
--brightness
Adjust the brightness of the images, must be in the range (-100, 100) (default "0")
--contrast
Adjust the contrast of the images, must be in the range (-100, 100) (default "0")
--suffix
Add suffix to file basename (default "")
--outdir
Output directory (default ".")
--size
Process only files larger than size (in MB) (default "0")
--recursive
Process subdirectories recursively (default "false")
--quiet
Hide console output (default "false")
[man page](https://en.wikipedia.org/wiki/Man_page) is also available:
cbconvert --help-man | man /dev/stdin
cover
Extract cover
Examples
--------
--width
Image width (default "0")
--height
Image height (default "0")
--fit
Best fit for required width and height (default "false")
--no-upscale
Do not upscale images already smaller than the requested width/height (default "false")
--dpi
Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
--format
Image format, valid values are jpeg, png, tiff, bmp, webp, avif (default "jpeg")
--quality
Image quality (default "75")
--effort
Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default (default "-1")
--lossless
Lossless compression (webp, avif, jxl), ignores quality (default "false")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
--outdir
Output directory (default ".")
--size
Process only files larger than size (in MB) (default "0")
--recursive
Process subdirectories recursively (default "false")
--quiet
Hide console output (default "false")
Rescale images to 1200px for all supported files found in directory with size larger then 60MB:
thumbnail
Extract cover thumbnail (freedesktop spec.)
cbconvert --recursive --width 1200 --size 60 /media/comics/Thorgal/
--width
Image width (default "0")
--height
Image height (default "0")
--fit
Best fit for required width and height (default "false")
--no-upscale
Do not upscale images already smaller than the requested width/height (default "false")
--dpi
Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
--outdir
Output directory (default ".")
--outfile
Output file (default "")
--size
Process only files larger than size (in MB) (default "0")
--recursive
Process subdirectories recursively (default "false")
--quiet
Hide console output (default "false")
Convert all images in pdf to 4bit BMP image and save result in ~/comics directory:
meta
CBZ metadata
cbconvert --bmp --outdir ~/comics /media/comics/Garfield/Garfield_01.pdf
--cover
Print cover name (default "false")
--comment
Print zip comment (default "false")
--comment-body
Set zip comment (default "")
--file-add
Add file to archive (default "")
--file-remove
Remove file from archive (glob pattern, i.e. *.xml) (default "")
[BMP](http://en.wikipedia.org/wiki/BMP_file_format) format is very good choice for black&white pages. Archive size can be smaller 2-3x and file will be readable by comic readers.
version
Print version
```
Generate thumbnails by freedesktop specification in ~/.thumbnails/normal directory with width 512:
### Examples
cbconvert thumbnail --width 512 --outdir ~/.thumbnails/normal /media/comics/GrooTheWanderer/
* Rescale images to 1200px for all supported files found in a directory with a size larger than 60MB:
Extract covers to ~/covers dir for all supported files found in directory, Lanczos algorithm is used for resizing:
`cbconvert --recursive --width 1200 --size 60 /media/comics/Thorgal/`
cbconvert cover --outdir ~/covers --filter=7 /media/comics/GrooTheWanderer/
* Convert all images in pdf to 4bit BMP images and save the result in ~/comics directory:
Compile
-------
`cbconvert --format bmp --outdir ~/comics /media/comics/Garfield/Garfield_01.pdf`
Install imagemagick dev packages:
[BMP](http://en.wikipedia.org/wiki/BMP_file_format) format is a very good choice for black&white pages. Archive size can be smaller 2-3x and the file will be readable by comic readers.
apt-get install libmagickcore-dev libmagickwand-dev
* Extract covers to ~/covers dir for all supported files found in the directory, Lanczos algorithm is used for resizing:
Compile latest MuPDF:
`cbconvert cover --outdir ~/covers --filter=7 /media/comics/GrooTheWanderer/`
git clone git://git.ghostscript.com/mupdf.git && cd mupdf
git submodule update --init --recursive
curl -L https://gist.githubusercontent.com/gen2brain/7869ac4c6db5933f670f/raw/1619394dc957ae10bcd73c713760993466b4bfea/mupdf-openssl-curl.patch | patch -p1
sed -e "1iHAVE_X11 = no" -e "1iWANT_OPENSSL = no" -e "1iWANT_CURL = no" -i Makerules
HAVE_X11=no HAVE_GLFW=no HAVE_GLUT=no WANT_OPENSSL=no WANT_CURL=no HAVE_MUJS=yes HAVE_JSCORE=no HAVE_V8=no make && make install
* Convert all images to AVIF format:
Compile unarr library:
`cbconvert --format avif --quality 50 --width 1280 --outdir ~/comics /media/comics/Misc/`
git clone https://github.com/zeniko/unarr && cd unarr
mkdir lzma920 && cd lzma920 && curl -L http://www.7-zip.org/a/lzma920.tar.bz2 | tar -xjvp && cd ..
curl -L http://zlib.net/zlib-1.2.8.tar.gz | tar -xzvp
curl -L http://www.bzip.org/1.0.6/bzip2-1.0.6.tar.gz | tar -xzvp
curl -L https://gist.githubusercontent.com/gen2brain/89fe506863be3fb139e8/raw/8783a7d81e22ad84944d146c5e33beab6dffc641/unarr-makefile.patch | patch -p1
CFLAGS="-DHAVE_7Z -DHAVE_ZLIB -DHAVE_BZIP2 -I./lzma920/C -I./zlib-1.2.8 -I./bzip2-1.0.6" make
cp build/debug/libunarr.a /usr/lib64/ && cp unarr.h /usr/include
* Combine several issues into a single archive:
Install dependencies:
`cbconvert convert --combine --outfile Series.cbz --outdir ~/comics issue1.cbz issue2.cbr issue3.cb7`
go get github.com/cheggaaa/pb
go get github.com/disintegration/imaging
go get github.com/gen2brain/go-fitz
go get github.com/gen2brain/go-unarr
go get github.com/gographics/imagick/imagick
go get github.com/hotei/bmp
go get github.com/skarademir/naturalsort
go get golang.org/x/image/tiff
go get golang.org/x/image/webp
go get gopkg.in/alecthomas/kingpin.v2
### Quality settings
Install go package:
This table maps quality settings for JPEG to the respective AVIF and WEBP quality settings:
| | | | | |
|--------------|----|----|----|----|
| JPEG quality | 50 | 60 | 70 | 80 |
| AVIF quality | 48 | 51 | 56 | 64 |
| WEBP quality | 55 | 64 | 72 | 82 |
go get github.com/gen2brain/cbconvert
go install github.com/gen2brain/cbconvert
+560 -891
View File
File diff suppressed because it is too large Load Diff
+526
View File
@@ -0,0 +1,526 @@
package cbconvert
import (
"archive/tar"
"archive/zip"
"compress/flate"
"context"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/mholt/archives"
)
// archiveSave saves workdir to CBZ archive.
func (c *Converter) archiveSave(fileName string) error {
if c.Opts.Archive == "zip" {
return c.archiveSaveZip(fileName)
} else if c.Opts.Archive == "tar" {
return c.archiveSaveTar(fileName)
}
return nil
}
// archiveSaveZip saves workdir to CBZ archive.
func (c *Converter) archiveSaveZip(fileName string) error {
if c.OnCompress != nil {
c.OnCompress()
}
var zipName string
if c.Opts.Recursive {
outDir := c.recursiveDir(fileName)
if err := os.MkdirAll(outDir, 0755); err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
zipName = filepath.Join(outDir, fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix))
} else {
zipName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix))
}
zipFile, err := os.Create(zipName)
if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
z := zip.NewWriter(zipFile)
if c.Opts.ZipLevel >= 1 {
level := c.Opts.ZipLevel
z.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
return flate.NewWriter(out, level)
})
}
files, err := os.ReadDir(c.Workdir)
if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
for _, file := range files {
r, err := os.ReadFile(filepath.Join(c.Workdir, file.Name()))
if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
info, err := file.Info()
if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
zipInfo, err := zip.FileInfoHeader(info)
if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
zipInfo.Method = zip.Deflate
if c.Opts.ZipLevel == 0 {
zipInfo.Method = zip.Store
}
w, err := z.CreateHeader(zipInfo)
if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
_, err = w.Write(r)
if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
}
if err = z.Close(); err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
if err = zipFile.Close(); err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
err = os.RemoveAll(c.Workdir)
if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
return nil
}
// archiveSaveTar saves workdir to CBT archive.
func (c *Converter) archiveSaveTar(fileName string) error {
if c.OnCompress != nil {
c.OnCompress()
}
var tarName string
if c.Opts.Recursive {
outDir := c.recursiveDir(fileName)
if err := os.MkdirAll(outDir, 0755); err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
tarName = filepath.Join(outDir, fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix))
} else {
tarName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix))
}
tarFile, err := os.Create(tarName)
if err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
tw := tar.NewWriter(tarFile)
files, err := os.ReadDir(c.Workdir)
if err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
for _, file := range files {
r, err := os.ReadFile(filepath.Join(c.Workdir, file.Name()))
if err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
info, err := file.Info()
if err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
err = tw.WriteHeader(header)
if err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
_, err = tw.Write(r)
if err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
}
if err = tw.Close(); err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
if err = tarFile.Close(); err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
err = os.RemoveAll(c.Workdir)
if err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
return nil
}
// archiveOpen identifies the archive and returns its extractor and a reader positioned at the start.
func archiveOpen(ctx context.Context, fileName string) (io.ReadCloser, archives.Extractor, io.Reader, error) {
file, err := os.Open(fileName)
if err != nil {
return nil, nil, nil, err
}
format, input, err := archives.Identify(ctx, fileName, file)
if err != nil {
file.Close()
return nil, nil, nil, err
}
ex, ok := format.(archives.Extractor)
if !ok {
file.Close()
return nil, nil, nil, fmt.Errorf("%s: unsupported archive format", fileName)
}
return file, ex, input, nil
}
// FileType returns the detected archive container, document extension, or "DIR".
func FileType(path string) string {
if isArchive(path) {
file, err := os.Open(path)
if err == nil {
defer file.Close()
format, _, err := archives.Identify(context.Background(), path, file)
if err == nil {
return strings.ToUpper(strings.TrimPrefix(format.Extension(), "."))
}
}
}
if info, err := os.Stat(path); err == nil && info.IsDir() {
return "DIR"
}
return strings.ToUpper(strings.TrimPrefix(filepath.Ext(path), "."))
}
// archiveList lists contents of archive.
func (c *Converter) archiveList(fileName string) ([]string, error) {
var contents []string
ctx := context.Background()
file, ex, input, err := archiveOpen(ctx, fileName)
if err != nil {
return contents, fmt.Errorf("archiveList: %w", err)
}
defer file.Close()
err = ex.Extract(ctx, input, func(ctx context.Context, f archives.FileInfo) error {
if f.IsDir() {
return nil
}
contents = append(contents, f.NameInArchive)
return nil
})
if err != nil {
return contents, fmt.Errorf("archiveList: %w", err)
}
return contents, nil
}
// archiveFile returns the contents of a single named file from the archive.
func (c *Converter) archiveFile(fileName, name string) ([]byte, error) {
var data []byte
ctx := context.Background()
file, ex, input, err := archiveOpen(ctx, fileName)
if err != nil {
return nil, fmt.Errorf("archiveFile: %w", err)
}
defer file.Close()
err = ex.Extract(ctx, input, func(ctx context.Context, f archives.FileInfo) error {
if f.NameInArchive != name {
return nil
}
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
data, err = io.ReadAll(rc)
if err != nil {
return err
}
return fs.SkipAll
})
if err != nil {
return nil, fmt.Errorf("archiveFile: %w", err)
}
return data, nil
}
// archiveComment returns ZIP comment.
func (c *Converter) archiveComment(fileName string) (string, error) {
zr, err := zip.OpenReader(fileName)
if err != nil {
return "", fmt.Errorf("archiveComment: %w", err)
}
defer zr.Close()
return zr.Comment, nil
}
// archiveSetComment sets ZIP comment.
func (c *Converter) archiveSetComment(fileName, commentBody string) error {
zr, err := zip.OpenReader(fileName)
if err != nil {
return fmt.Errorf("archiveSetComment: %w", err)
}
defer zr.Close()
zf, err := os.CreateTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("archiveSetComment: %w", err)
}
tmpName := zf.Name()
defer os.Remove(tmpName)
zw := zip.NewWriter(zf)
err = zw.SetComment(commentBody)
if err != nil {
return fmt.Errorf("archiveSetComment: %w", err)
}
for _, item := range zr.File {
ir, err := item.OpenRaw()
if err != nil {
return fmt.Errorf("archiveSetComment: %w", err)
}
item := item
it, err := zw.CreateRaw(&item.FileHeader)
if err != nil {
return fmt.Errorf("archiveSetComment: %w", err)
}
_, err = io.Copy(it, ir)
if err != nil {
return fmt.Errorf("archiveSetComment: %w", err)
}
}
err = zw.Close()
if err != nil {
return fmt.Errorf("archiveSetComment: %w", err)
}
err = zf.Close()
if err != nil {
return fmt.Errorf("archiveSetComment: %w", err)
}
data, err := os.ReadFile(tmpName)
if err != nil {
return fmt.Errorf("archiveSetComment: %w", err)
}
err = os.WriteFile(fileName, data, 0644)
if err != nil {
return fmt.Errorf("archiveSetComment: %w", err)
}
return nil
}
// archiveFileAdd adds a file to the archive.
func (c *Converter) archiveFileAdd(fileName, newFileName string) error {
zr, err := zip.OpenReader(fileName)
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
defer zr.Close()
zf, err := os.CreateTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
tmpName := zf.Name()
defer os.Remove(tmpName)
zw := zip.NewWriter(zf)
for _, item := range zr.File {
if item.Name == newFileName {
continue
}
ir, err := item.OpenRaw()
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
item := item
it, err := zw.CreateRaw(&item.FileHeader)
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
_, err = io.Copy(it, ir)
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
}
info, err := os.Stat(newFileName)
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
newData, err := os.ReadFile(newFileName)
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
zipInfo, err := zip.FileInfoHeader(info)
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
zipInfo.Method = zip.Deflate
w, err := zw.CreateHeader(zipInfo)
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
_, err = w.Write(newData)
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
err = zw.Close()
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
err = zf.Close()
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
data, err := os.ReadFile(tmpName)
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
err = os.WriteFile(fileName, data, 0644)
if err != nil {
return fmt.Errorf("archiveFileAdd: %w", err)
}
return nil
}
// archiveFileRemove removes files from archive.
func (c *Converter) archiveFileRemove(fileName, pattern string) error {
zr, err := zip.OpenReader(fileName)
if err != nil {
return fmt.Errorf("archiveFileRemove: %w", err)
}
defer zr.Close()
zf, err := os.CreateTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("archiveFileRemove: %w", err)
}
tmpName := zf.Name()
defer os.Remove(tmpName)
zw := zip.NewWriter(zf)
for _, item := range zr.File {
matched, err := filepath.Match(pattern, item.Name)
if err != nil {
return fmt.Errorf("archiveFileRemove: %w", err)
}
if matched {
continue
}
ir, err := item.OpenRaw()
if err != nil {
return fmt.Errorf("archiveFileRemove: %w", err)
}
item := item
it, err := zw.CreateRaw(&item.FileHeader)
if err != nil {
return fmt.Errorf("archiveFileRemove: %w", err)
}
_, err = io.Copy(it, ir)
if err != nil {
return fmt.Errorf("archiveFileRemove: %w", err)
}
}
err = zw.Close()
if err != nil {
return fmt.Errorf("archiveFileRemove: %w", err)
}
err = zf.Close()
if err != nil {
return fmt.Errorf("archiveFileRemove: %w", err)
}
data, err := os.ReadFile(tmpName)
if err != nil {
return fmt.Errorf("archiveFileRemove: %w", err)
}
err = os.WriteFile(fileName, data, 0644)
if err != nil {
return fmt.Errorf("archiveFileRemove: %w", err)
}
return nil
}
+414
View File
@@ -0,0 +1,414 @@
package cbconvert
import (
"bytes"
"context"
"fmt"
"image"
"image/png"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"github.com/gen2brain/avif"
"github.com/gen2brain/go-fitz"
"github.com/gen2brain/jpegli"
"github.com/gen2brain/jpegxl"
"github.com/gen2brain/webp"
"github.com/jsummers/gobmp"
"github.com/mholt/archives"
"golang.org/x/image/tiff"
"golang.org/x/sync/errgroup"
)
// convertDocument converts PDF/EPUB document to CBZ.
func (c *Converter) convertDocument(ctx context.Context, fileName string) error {
doc, err := fitz.New(fileName)
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
defer doc.Close()
c.Ncontents = doc.NumPage()
c.CurrContent = 0
if c.OnStart != nil {
c.OnStart()
}
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(runtime.NumCPU() + 1)
for n := 0; n < c.Ncontents; n++ {
if ctx.Err() != nil {
return fmt.Errorf("convertDocument: %w", ctx.Err())
}
img, err := doc.ImageDPI(n, c.renderDPI())
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
if img != nil {
eg.Go(func() error {
return c.imageConvert(ctx, img, n, "")
})
}
}
err = eg.Wait()
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
return nil
}
// convertArchive converts archive to CBZ.
func (c *Converter) convertArchive(ctx context.Context, fileName string) error {
contents, err := c.archiveList(fileName)
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
images := imagesFromSlice(contents)
c.Ncontents = len(images)
c.CurrContent = 0
if c.OnStart != nil {
c.OnStart()
}
cover := c.coverName(images)
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(runtime.NumCPU() + 1)
file, ex, input, err := archiveOpen(ctx, fileName)
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
defer file.Close()
err = ex.Extract(ctx, input, func(ctx context.Context, f archives.FileInfo) error {
if err := ctx.Err(); err != nil {
return err
}
if f.IsDir() {
return nil
}
pathName := f.NameInArchive
rc, err := f.Open()
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
data, err := io.ReadAll(rc)
if err != nil {
rc.Close()
return fmt.Errorf("convertArchive: %w", err)
}
if err = rc.Close(); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
if isImage(pathName) {
if c.Opts.NoConvert {
if err = copyFile(bytes.NewReader(data), c.workPath(flatName(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
return nil
}
if cover == pathName && c.Opts.NoCover {
if err = copyFile(bytes.NewReader(data), c.workPath(flatName(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
return nil
}
var img image.Image
img, err = c.imageDecode(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
if c.Opts.NoRGB && !isGrayScale(img) {
if err = copyFile(bytes.NewReader(data), c.workPath(flatName(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
return nil
}
if img != nil {
eg.Go(func() error {
return c.imageConvert(ctx, img, 0, pathName)
})
}
} else {
if filepath.Ext(pathName) == ".DS_Store" || strings.Contains(pathName, "__MACOSX") {
return nil
}
if c.prefix == "" && !c.Opts.NoNonImage {
if err = copyFile(bytes.NewReader(data), c.workPath(flatName(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
}
}
return nil
})
if werr := eg.Wait(); werr != nil {
return fmt.Errorf("convertArchive: %w", werr)
}
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
return nil
}
// convertDirectory converts directory to CBZ.
func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error {
contents, err := imagesFromPath(dirPath)
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
images := imagesFromSlice(contents)
c.Ncontents = len(images)
c.CurrContent = 0
if c.OnStart != nil {
c.OnStart()
}
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(runtime.NumCPU() + 1)
for index, img := range contents {
if ctx.Err() != nil {
return fmt.Errorf("convertDirectory: %w", ctx.Err())
}
rel, rerr := filepath.Rel(dirPath, img)
if rerr != nil {
rel = filepath.Base(img)
}
file, err := os.Open(img)
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if isNonImage(img) && c.prefix == "" && !c.Opts.NoNonImage {
if err = copyFile(file, c.workPath(flatName(rel))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if err = file.Close(); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
continue
} else if isImage(img) {
if c.Opts.NoConvert {
if err = copyFile(file, c.workPath(flatName(rel))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if err = file.Close(); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
continue
}
var i image.Image
i, err = c.imageDecode(file)
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if c.Opts.NoRGB && !isGrayScale(i) {
if err = copyFile(file, c.workPath(flatName(rel))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if err = file.Close(); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
continue
}
if err = file.Close(); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if i != nil {
eg.Go(func() error {
return c.imageConvert(ctx, i, index, rel)
})
}
}
}
err = eg.Wait()
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
return nil
}
// workPath returns the path of name inside the workdir, with the combine prefix applied.
func (c *Converter) workPath(name string) string {
return filepath.Join(c.Workdir, c.prefix+name)
}
// imageConvert converts image.Image.
func (c *Converter) imageConvert(ctx context.Context, img image.Image, index int, pathName string) error {
err := ctx.Err()
if err != nil {
return fmt.Errorf("imageConvert: %w", err)
}
atomic.AddInt32(&c.CurrContent, 1)
if c.OnProgress != nil {
c.OnProgress()
}
ext := c.Opts.Format
if ext == "jpeg" {
ext = "jpg"
}
var fileName string
if pathName != "" {
fileName = c.workPath(fmt.Sprintf("%s.%s", flatName(strings.TrimSuffix(pathName, filepath.Ext(pathName))), ext))
} else {
fileName = c.workPath(fmt.Sprintf("%03d.%s", index, ext))
}
img = c.imageTransform(img)
w, err := os.Create(fileName)
if err != nil {
return fmt.Errorf("imageConvert: %w", err)
}
defer w.Close()
if err := c.imageEncode(img, w); err != nil {
return fmt.Errorf("imageConvert: %w", err)
}
return nil
}
// imageTransform transforms image (resize, rotate, brightness, contrast).
func (c *Converter) imageTransform(img image.Image) image.Image {
var i = img
if c.Opts.Width > 0 || c.Opts.Height > 0 {
i = c.resizeFit(i)
}
if c.Opts.Rotate > 0 {
switch c.Opts.Rotate {
case 90:
i = rotate(i, 90)
case 180:
i = rotate(i, 180)
case 270:
i = rotate(i, 270)
}
}
if c.Opts.Brightness != 0 {
i = brightness(i, float64(c.Opts.Brightness))
}
if c.Opts.Contrast != 0 {
i = contrast(i, float64(c.Opts.Contrast))
}
if c.Opts.Grayscale {
i = imageToGray(i)
}
return i
}
// imageDecode decodes image from reader.
func (c *Converter) imageDecode(reader io.Reader) (image.Image, error) {
img, _, err := image.Decode(reader)
if err != nil {
return img, fmt.Errorf("imageDecode: %w", err)
}
return img, nil
}
// imageEncode encodes image to file.
func (c *Converter) imageEncode(img image.Image, w io.Writer) error {
var err error
switch c.Opts.Format {
case "png":
err = png.Encode(w, img)
case "tiff":
err = tiff.Encode(w, img, &tiff.Options{Compression: tiff.Uncompressed})
case "jpeg":
opts := &jpegli.EncodingOptions{}
opts.Quality = c.Opts.Quality
opts.ChromaSubsampling = image.YCbCrSubsampleRatio420
opts.ProgressiveLevel = 2
opts.AdaptiveQuantization = true
opts.DCTMethod = jpegli.DefaultDCTMethod
err = jpegli.Encode(w, img, opts)
case "webp":
method := webp.DefaultMethod
if c.Opts.Effort >= 0 {
method = min(max(c.Opts.Effort, 0), 6)
}
err = webp.Encode(w, img, webp.Options{Quality: c.Opts.Quality, Method: method, Lossless: c.Opts.Lossless})
case "avif":
speed := avif.DefaultSpeed
if c.Opts.Effort >= 0 {
speed = min(max(c.Opts.Effort, 0), 10)
}
err = avif.Encode(w, img, avif.Options{Quality: c.Opts.Quality, Speed: speed, Lossless: c.Opts.Lossless})
case "jxl":
effort := jpegxl.DefaultEffort
if c.Opts.Effort >= 0 {
effort = min(max(c.Opts.Effort, 1), 10)
}
err = jpegxl.Encode(w, img, jpegxl.Options{Quality: c.Opts.Quality, Effort: effort, Lossless: c.Opts.Lossless})
case "bmp":
opts := &gobmp.EncoderOptions{}
opts.SupportTransparency(false)
err = gobmp.EncodeWithOptions(w, imageToPaletted(img), opts)
}
if err != nil {
return fmt.Errorf("imageEncode: %w", err)
}
return nil
}
+268
View File
@@ -0,0 +1,268 @@
package cbconvert
import (
"bytes"
"fmt"
"image"
"os"
"path/filepath"
"sort"
"strings"
"github.com/fvbommel/sortorder"
"github.com/gen2brain/go-fitz"
)
// coverArchive extracts cover from archive.
func (c *Converter) coverArchive(fileName string) (image.Image, error) {
var images []string
contents, err := c.archiveList(fileName)
if err != nil {
return nil, fmt.Errorf("coverArchive: %w", err)
}
for _, ct := range contents {
if isImage(ct) {
images = append(images, ct)
}
}
cover := c.coverName(images)
data, err := c.archiveFile(fileName, cover)
if err != nil {
return nil, fmt.Errorf("coverArchive: %w", err)
}
var img image.Image
img, err = c.imageDecode(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("coverArchive: %w", err)
}
return img, nil
}
// coverDocument extracts cover from document.
func (c *Converter) coverDocument(fileName string) (image.Image, error) {
doc, err := fitz.New(fileName)
if err != nil {
return nil, fmt.Errorf("coverDocument: %w", err)
}
defer doc.Close()
img, err := doc.ImageDPI(0, c.renderDPI())
if err != nil {
return nil, fmt.Errorf("coverDocument: %w", err)
}
return img, nil
}
// coverDirectory extracts cover from directory.
func (c *Converter) coverDirectory(dir string) (image.Image, error) {
contents, err := imagesFromPath(dir)
if err != nil {
return nil, fmt.Errorf("coverDirectory: %w", err)
}
images := imagesFromSlice(contents)
cover := c.coverName(images)
file, err := os.Open(cover)
if err != nil {
return nil, fmt.Errorf("coverDirectory: %w", err)
}
defer file.Close()
var img image.Image
img, err = c.imageDecode(file)
if err != nil {
return nil, fmt.Errorf("coverDirectory: %w", err)
}
return img, nil
}
// pageArchive extracts the page-th image (natural reading order) from an archive.
func (c *Converter) pageArchive(fileName string, page int) (image.Image, error) {
contents, err := c.archiveList(fileName)
if err != nil {
return nil, fmt.Errorf("pageArchive: %w", err)
}
images := imagesFromSlice(contents)
sort.Sort(sortorder.Natural(images))
if page < 0 || page >= len(images) {
return nil, fmt.Errorf("pageArchive: page %d out of range (%d pages)", page+1, len(images))
}
data, err := c.archiveFile(fileName, images[page])
if err != nil {
return nil, fmt.Errorf("pageArchive: %w", err)
}
img, err := c.imageDecode(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("pageArchive: %w", err)
}
return img, nil
}
// pageDocument extracts the page-th rendered page from a document.
func (c *Converter) pageDocument(fileName string, page int) (image.Image, error) {
doc, err := fitz.New(fileName)
if err != nil {
return nil, fmt.Errorf("pageDocument: %w", err)
}
defer doc.Close()
if page < 0 || page >= doc.NumPage() {
return nil, fmt.Errorf("pageDocument: page %d out of range (%d pages)", page+1, doc.NumPage())
}
img, err := doc.ImageDPI(page, c.renderDPI())
if err != nil {
return nil, fmt.Errorf("pageDocument: %w", err)
}
return img, nil
}
// pageDirectory extracts the page-th image (natural reading order) from a directory.
func (c *Converter) pageDirectory(dir string, page int) (image.Image, error) {
contents, err := imagesFromPath(dir)
if err != nil {
return nil, fmt.Errorf("pageDirectory: %w", err)
}
images := imagesFromSlice(contents)
sort.Sort(sortorder.Natural(images))
if page < 0 || page >= len(images) {
return nil, fmt.Errorf("pageDirectory: page %d out of range (%d pages)", page+1, len(images))
}
file, err := os.Open(images[page])
if err != nil {
return nil, fmt.Errorf("pageDirectory: %w", err)
}
defer file.Close()
img, err := c.imageDecode(file)
if err != nil {
return nil, fmt.Errorf("pageDirectory: %w", err)
}
return img, nil
}
// pageImage returns the page-th image of a comic file, document or directory.
func (c *Converter) pageImage(fileName string, fileInfo os.FileInfo, page int) (image.Image, error) {
var err error
var img image.Image
switch {
case fileInfo.IsDir():
img, err = c.pageDirectory(fileName, page)
case isDocument(fileName):
img, err = c.pageDocument(fileName, page)
case isArchive(fileName):
img, err = c.pageArchive(fileName, page)
}
if err != nil {
return nil, fmt.Errorf("pageImage: %w", err)
}
return img, nil
}
// PageCount returns the number of pages (images) in a comic file, document or directory.
func (c *Converter) PageCount(fileName string, fileInfo os.FileInfo) (int, error) {
switch {
case fileInfo.IsDir():
contents, err := imagesFromPath(fileName)
if err != nil {
return 0, fmt.Errorf("PageCount: %w", err)
}
return len(imagesFromSlice(contents)), nil
case isDocument(fileName):
doc, err := fitz.New(fileName)
if err != nil {
return 0, fmt.Errorf("PageCount: %w", err)
}
defer doc.Close()
return doc.NumPage(), nil
case isArchive(fileName):
contents, err := c.archiveList(fileName)
if err != nil {
return 0, fmt.Errorf("PageCount: %w", err)
}
return len(imagesFromSlice(contents)), nil
}
return 0, nil
}
// coverName returns the filename that is the most likely to be the cover.
func (c *Converter) coverName(images []string) string {
if len(images) == 0 {
return ""
}
lower := make([]string, 0)
for idx, img := range images {
img = strings.ToLower(img)
lower = append(lower, img)
ext := baseNoExt(img)
if strings.HasPrefix(img, "cover") || strings.HasPrefix(img, "front") ||
strings.HasSuffix(ext, "cover") || strings.HasSuffix(ext, "front") {
return filepath.ToSlash(images[idx])
}
}
sort.Sort(sortorder.Natural(lower))
cover := lower[0]
for idx, img := range images {
img = strings.ToLower(img)
if img == cover {
return filepath.ToSlash(images[idx])
}
}
return ""
}
// coverImage returns cover as image.Image.
func (c *Converter) coverImage(fileName string, fileInfo os.FileInfo) (image.Image, error) {
var err error
var cover image.Image
switch {
case fileInfo.IsDir():
cover, err = c.coverDirectory(fileName)
case isDocument(fileName):
cover, err = c.coverDocument(fileName)
case isArchive(fileName):
cover, err = c.coverArchive(fileName)
}
if c.OnProgress != nil {
c.OnProgress()
}
if err != nil {
return nil, fmt.Errorf("coverImage: %w", err)
}
return cover, nil
}
+151
View File
@@ -0,0 +1,151 @@
package cbconvert
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// imagesFromPath returns list of found image files for given directory.
func imagesFromPath(path string) ([]string, error) {
var images []string
walkFiles := func(fp string, f os.FileInfo, err error) error {
if !f.IsDir() && f.Mode()&os.ModeType == 0 {
if f.Size() > 0 && (isImage(fp)) {
images = append(images, fp)
}
}
return nil
}
f, err := filepath.Abs(path)
if err != nil {
return images, fmt.Errorf("imagesFromPath: %w", err)
}
stat, err := os.Stat(f)
if err != nil {
return images, fmt.Errorf("imagesFromPath: %w", err)
}
if !stat.IsDir() && stat.Mode()&os.ModeType == 0 {
if isImage(f) {
images = append(images, f)
}
} else {
err = filepath.Walk(f, walkFiles)
if err != nil {
return images, fmt.Errorf("imagesFromPath: %w", err)
}
}
return images, nil
}
// imagesFromSlice returns list of found image files for given slice of files.
func imagesFromSlice(files []string) []string {
var images []string
for _, f := range files {
if isImage(f) {
images = append(images, f)
}
}
return images
}
// isArchive checks if a file is archive.
func isArchive(f string) bool {
var types = []string{".rar", ".zip", ".7z", ".tar", ".cbr", ".cbz", ".cb7", ".cbt"}
for _, t := range types {
if strings.ToLower(filepath.Ext(f)) == t {
return true
}
}
return false
}
// isDocument checks if file is document.
func isDocument(f string) bool {
var types = []string{".pdf", ".xps", ".epub", ".mobi", ".docx", ".pptx", ".xlsx"}
for _, t := range types {
if strings.ToLower(filepath.Ext(f)) == t {
return true
}
}
return false
}
// isImage checks if file is image.
func isImage(f string) bool {
var types = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".webp", ".avif", ".jxl"}
for _, t := range types {
if strings.ToLower(filepath.Ext(f)) == t {
return true
}
}
return false
}
// isNonImage checks for allowed files in archive.
func isNonImage(f string) bool {
var types = []string{".nfo", ".xml", ".txt"}
for _, t := range types {
if strings.ToLower(filepath.Ext(f)) == t {
return true
}
}
return false
}
// isSize checks size of file.
func isSize(a, b int64) bool {
if a > 0 {
if b < int64(a)*(1024*1024) {
return false
}
}
return true
}
// baseNoExt returns base name without extension.
func baseNoExt(filename string) string {
return strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
}
// flatName flattens a path into a single collision-free name by replacing separators.
func flatName(name string) string {
name = strings.ReplaceAll(name, "\\", "/")
return strings.ReplaceAll(name, "/", "_")
}
// copyFile copies reader to file.
func copyFile(reader io.Reader, filename string) error {
err := os.MkdirAll(filepath.Dir(filename), 0755)
if err != nil {
return fmt.Errorf("copyFile: %w", err)
}
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("copyFile: %w", err)
}
defer file.Close()
_, err = io.Copy(file, reader)
if err != nil {
return fmt.Errorf("copyFile: %w", err)
}
return nil
}
+192
View File
@@ -0,0 +1,192 @@
package cbconvert
import (
"image"
"image/color"
"image/draw"
"math"
"github.com/anthonynsimon/bild/adjust"
"github.com/anthonynsimon/bild/transform"
)
// Resample filters.
const (
// NearestNeighbor is the fastest resampling filter, no antialiasing.
nearestNeighbor int = iota
// Box filter (averaging pixels).
box
// Linear is the bilinear filter, smooth and reasonably fast.
linear
// MitchellNetravali is a smooth bicubic filter.
mitchellNetravali
// CatmullRom is a sharp bicubic filter.
catmullRom
// Gaussian is a blurring filter, which uses gaussian function, useful for noise removal.
gaussian
// Lanczos is a high-quality resampling filter, it's slower than cubic filters.
lanczos
)
var filters = map[int]transform.ResampleFilter{
nearestNeighbor: transform.NearestNeighbor,
box: transform.Box,
linear: transform.Linear,
mitchellNetravali: transform.MitchellNetravali,
catmullRom: transform.CatmullRom,
gaussian: transform.Gaussian,
lanczos: transform.Lanczos,
}
// resampleFilter returns the resample filter for index i, falling back to Linear for an unknown index.
func resampleFilter(i int) transform.ResampleFilter {
if f, ok := filters[i]; ok {
return f
}
return filters[linear]
}
func resize(img image.Image, width, height int, filter transform.ResampleFilter) *image.RGBA {
dstW, dstH := width, height
srcW := img.Bounds().Dx()
srcH := img.Bounds().Dy()
if dstW == 0 {
tmpW := float64(dstH) * float64(srcW) / float64(srcH)
dstW = int(math.Max(1.0, math.Floor(tmpW+0.5)))
}
if dstH == 0 {
tmpH := float64(dstW) * float64(srcH) / float64(srcW)
dstH = int(math.Max(1.0, math.Floor(tmpH+0.5)))
}
if srcW == dstW && srcH == dstH {
return imageToRGBA(img)
}
return transform.Resize(img, dstW, dstH, filter)
}
func fit(img image.Image, width, height int, filter transform.ResampleFilter) *image.RGBA {
maxW, maxH := width, height
b := img.Bounds()
srcW := b.Dx()
srcH := b.Dy()
if srcW <= maxW && srcH <= maxH {
return imageToRGBA(img)
}
srcAspectRatio := float64(srcW) / float64(srcH)
maxAspectRatio := float64(maxW) / float64(maxH)
var dstW, dstH int
if srcAspectRatio > maxAspectRatio {
dstW = maxW
dstH = int(float64(dstW) / srcAspectRatio)
} else {
dstH = maxH
dstW = int(float64(dstH) * srcAspectRatio)
}
return resize(img, dstW, dstH, filter)
}
// withinBounds reports whether img already fits within width by height; a zero dimension is unbounded.
func withinBounds(img image.Image, width, height int) bool {
b := img.Bounds()
return (width == 0 || b.Dx() <= width) && (height == 0 || b.Dy() <= height)
}
// resizeFit resizes img to the configured width/height, honoring Fit and NoUpscale.
func (c *Converter) resizeFit(img image.Image) image.Image {
if c.Opts.Fit {
return fit(img, c.Opts.Width, c.Opts.Height, resampleFilter(c.Opts.Filter))
}
if c.Opts.NoUpscale && withinBounds(img, c.Opts.Width, c.Opts.Height) {
return img
}
return resize(img, c.Opts.Width, c.Opts.Height, resampleFilter(c.Opts.Filter))
}
func rotate(img image.Image, angle float64) *image.RGBA {
return transform.Rotate(img, angle, &transform.RotationOptions{ResizeBounds: true, Pivot: &image.Point{}})
}
func brightness(img image.Image, change float64) *image.RGBA {
return adjust.Brightness(img, change/100)
}
func contrast(img image.Image, change float64) *image.RGBA {
return adjust.Contrast(img, change/100)
}
// imageToRGBA converts an image.Image to *image.RGBA.
func imageToRGBA(src image.Image) *image.RGBA {
if dst, ok := src.(*image.RGBA); ok {
return dst
}
b := src.Bounds()
dst := image.NewRGBA(b)
draw.Draw(dst, dst.Bounds(), src, b.Min, draw.Src)
return dst
}
// imageToGray converts an image.Image to *image.Gray.
func imageToGray(src image.Image) *image.Gray {
if dst, ok := src.(*image.Gray); ok {
return dst
}
b := src.Bounds()
dst := image.NewGray(b)
draw.Draw(dst, dst.Bounds(), src, b.Min, draw.Src)
return dst
}
// isGrayScale checks if the image is grayscale.
func isGrayScale(img image.Image) bool {
model := img.ColorModel()
if model == color.GrayModel || model == color.Gray16Model {
return true
}
return false
}
var colors16 = []color.Color{
color.RGBA{A: 255},
color.RGBA{R: 17, G: 17, B: 17, A: 255},
color.RGBA{R: 34, G: 34, B: 34, A: 255},
color.RGBA{R: 51, G: 51, B: 51, A: 255},
color.RGBA{R: 68, G: 68, B: 68, A: 255},
color.RGBA{R: 85, G: 85, B: 85, A: 255},
color.RGBA{R: 102, G: 102, B: 102, A: 255},
color.RGBA{R: 119, G: 119, B: 119, A: 255},
color.RGBA{R: 136, G: 136, B: 136, A: 255},
color.RGBA{R: 153, G: 153, B: 153, A: 255},
color.RGBA{R: 170, G: 170, B: 170, A: 255},
color.RGBA{R: 187, G: 187, B: 187, A: 255},
color.RGBA{R: 204, G: 204, B: 204, A: 255},
color.RGBA{R: 221, G: 221, B: 221, A: 255},
color.RGBA{R: 238, G: 238, B: 238, A: 255},
color.RGBA{R: 255, G: 255, B: 255, A: 255},
}
// imageToPaletted converts an image.Image to *image.Paletted using 16-color palette.
func imageToPaletted(src image.Image) *image.Paletted {
b := src.Bounds()
dst := image.NewPaletted(b, colors16)
draw.FloydSteinberg.Draw(dst, b, imageToGray(src), b.Min)
return dst
}
+814
View File
@@ -0,0 +1,814 @@
package cbconvert
import (
"archive/zip"
"fmt"
"image"
"os"
"path/filepath"
"strings"
"testing"
)
func TestConvert(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Error(err)
}
opts := NewOptions()
opts.OutDir = tmpDir
conv := New(opts)
files, err := conv.Files([]string{"testdata/test", "testdata"})
if err != nil {
t.Error(err)
}
for _, format := range []string{"jpeg", "png", "tiff", "bmp", "webp", "avif", "jxl"} {
conv.Opts.Format = format
for _, file := range files {
conv.Opts.Suffix = fmt.Sprintf("_%s%s", format, filepath.Ext(file.Path))
err = conv.Convert(file)
if err != nil {
t.Errorf("format %s: file %s: %v", format, file.Name, err)
}
}
}
err = os.RemoveAll(tmpDir)
if err != nil {
t.Error(err)
}
}
func TestCover(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Error(err)
}
opts := NewOptions()
opts.OutDir = tmpDir
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.cbt"})
if err != nil {
t.Error(err)
}
for _, file := range files {
err = conv.Cover(file)
if err != nil {
t.Error(err)
}
}
err = os.RemoveAll(tmpDir)
if err != nil {
t.Error(err)
}
}
func TestThumbnail(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Error(err)
}
opts := NewOptions()
opts.OutDir = tmpDir
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.pdf"})
if err != nil {
t.Error(err)
}
for _, file := range files {
err = conv.Thumbnail(file)
if err != nil {
t.Error(err)
}
}
err = os.RemoveAll(tmpDir)
if err != nil {
t.Error(err)
}
}
func TestArgs(t *testing.T) {
opts := NewOptions()
if got := opts.Args(); len(got) != 0 {
t.Errorf("defaults should emit no flags, got %v", got)
}
opts.Format = "webp"
opts.Quality = 90
opts.Effort = 4
opts.Lossless = true
opts.Width = 1200
opts.NoUpscale = true
opts.DPI = 150
opts.Grayscale = true
opts.OutDir = "/out"
got := strings.Join(opts.Args(), " ")
want := "--width 1200 --no-upscale --dpi 150 --format webp --quality 90 --effort 4 --lossless --grayscale --outdir /out"
if got != want {
t.Errorf("Args() = %q, want %q", got, want)
}
}
func TestNoUpscale(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
width := func(o Options) int {
conv := New(o)
files, err := conv.Files([]string{"testdata/test.cbz"})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
return firstPage(t, conv, filepath.Join(tmpDir, "test.cbz")).Bounds().Dx()
}
base := NewOptions()
base.OutDir = tmpDir
orig := width(base)
up := NewOptions()
up.OutDir = tmpDir
up.Width = orig * 2
up.NoUpscale = true
if got := width(up); got != orig {
t.Errorf("NoUpscale should keep original width %d, got %d", orig, got)
}
no := NewOptions()
no.OutDir = tmpDir
no.Width = orig * 2
if got := width(no); got != orig*2 {
t.Errorf("without NoUpscale should upscale to %d, got %d", orig*2, got)
}
down := NewOptions()
down.OutDir = tmpDir
down.Width = orig / 2
down.NoUpscale = true
if got := width(down); got != orig/2 {
t.Errorf("NoUpscale should still downscale to %d, got %d", orig/2, got)
}
}
func TestConvertDPI(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
dims := func(dpi int) int {
opts := NewOptions()
opts.OutDir = tmpDir
opts.DPI = dpi
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.pdf"})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
return firstPage(t, conv, filepath.Join(tmpDir, "test.cbz")).Bounds().Dx()
}
low := dims(150)
high := dims(600)
if low >= high {
t.Errorf("higher DPI should render larger pages: 150dpi=%d, 600dpi=%d", low, high)
}
}
func TestPreviewPage(t *testing.T) {
for _, name := range []string{"testdata/test.cbz", "testdata/test.pdf"} {
conv := New(NewOptions())
files, err := conv.Files([]string{name})
if err != nil {
t.Fatal(err)
}
if len(files) != 1 {
t.Fatalf("%s: expected 1 file, got %d", name, len(files))
}
file := files[0]
n, err := conv.PageCount(file.Path, file.Stat)
if err != nil {
t.Fatal(err)
}
if n < 2 {
t.Fatalf("%s: expected >= 2 pages, got %d", name, n)
}
for _, page := range []int{0, 1, n - 1} {
img, err := conv.PreviewPage(file.Path, file.Stat, page, 0, 0)
if err != nil || img.Image == nil {
t.Fatalf("%s: page %d: img=%v err=%v", name, page, img.Image, err)
}
}
if _, err := conv.PreviewPage(file.Path, file.Stat, n, 0, 0); err == nil {
t.Errorf("%s: page %d (out of range) should error", name, n)
}
}
}
func TestConvertResize(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
opts := NewOptions()
opts.OutDir = tmpDir
opts.Width = 100
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.cbz"})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
img := firstPage(t, conv, filepath.Join(tmpDir, "test.cbz"))
if got := img.Bounds().Dx(); got != 100 {
t.Errorf("resized width: got %d, want 100", got)
}
}
func TestConvertFit(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
opts := NewOptions()
opts.OutDir = tmpDir
opts.Width = 120
opts.Height = 120
opts.Fit = true
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.cbz"})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
img := firstPage(t, conv, filepath.Join(tmpDir, "test.cbz"))
w, h := img.Bounds().Dx(), img.Bounds().Dy()
if w > 120 || h > 120 {
t.Errorf("fit exceeded bounds: got %dx%d, want <= 120x120", w, h)
}
if w != 120 && h != 120 {
t.Errorf("fit did not touch a bound: got %dx%d, want one side == 120", w, h)
}
}
func TestConvertTar(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
opts := NewOptions()
opts.OutDir = tmpDir
opts.Archive = "tar"
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.cbz"})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
out := filepath.Join(tmpDir, "test.cbt")
list, err := conv.archiveList(out)
if err != nil {
t.Fatalf("read tar output: %v", err)
}
if len(list) != 2 {
t.Errorf("expected 2 pages in tar output, got %d: %v", len(list), list)
}
}
func TestZipLevel(t *testing.T) {
convertWith := func(level int) *zip.ReadCloser {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.RemoveAll(tmpDir) })
opts := NewOptions()
opts.OutDir = tmpDir
opts.ZipLevel = level
opts.NoConvert = true
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.cbz"})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
zr, err := zip.OpenReader(filepath.Join(tmpDir, "test.cbz"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { zr.Close() })
return zr
}
store := convertWith(0)
for _, f := range store.File {
if f.Method != zip.Store {
t.Errorf("level 0: %s stored with method %d, want Store", f.Name, f.Method)
}
if f.CompressedSize64 != f.UncompressedSize64 {
t.Errorf("level 0: %s is compressed (%d < %d)", f.Name, f.CompressedSize64, f.UncompressedSize64)
}
}
deflate := convertWith(9)
for _, f := range deflate.File {
if f.Method != zip.Deflate {
t.Errorf("level 9: %s method %d, want Deflate", f.Name, f.Method)
}
}
}
func TestImageTransforms(t *testing.T) {
conv := New(NewOptions())
f, err := os.Open("testdata/test/00.jpg")
if err != nil {
t.Fatal(err)
}
defer f.Close()
src, err := conv.imageDecode(f)
if err != nil {
t.Fatal(err)
}
srcW, srcH := src.Bounds().Dx(), src.Bounds().Dy()
conv.Opts.Rotate = 90
rotated := conv.imageTransform(src)
if rotated.Bounds().Dx() != srcH || rotated.Bounds().Dy() != srcW {
t.Errorf("rotate 90: got %dx%d, want %dx%d", rotated.Bounds().Dx(), rotated.Bounds().Dy(), srcH, srcW)
}
conv.Opts = NewOptions()
conv.Opts.Grayscale = true
gray := conv.imageTransform(src)
if !isGrayScale(gray) {
t.Errorf("grayscale: result is not grayscale")
}
conv.Opts = NewOptions()
conv.Opts.Brightness = 20
conv.Opts.Contrast = 20
adjusted := conv.imageTransform(src)
if adjusted.Bounds().Dx() != srcW || adjusted.Bounds().Dy() != srcH {
t.Errorf("brightness/contrast changed dimensions: got %dx%d, want %dx%d",
adjusted.Bounds().Dx(), adjusted.Bounds().Dy(), srcW, srcH)
}
}
func TestCoverName(t *testing.T) {
conv := New(NewOptions())
tests := []struct {
name string
images []string
want string
}{
{"empty", nil, ""},
{"natural sort", []string{"10.jpg", "2.jpg", "1.jpg"}, "1.jpg"},
{"cover prefix wins", []string{"01.jpg", "cover.jpg", "02.jpg"}, "cover.jpg"},
{"front prefix wins", []string{"01.jpg", "front.png", "00.jpg"}, "front.png"},
{"cover suffix wins", []string{"01.jpg", "page_cover.jpg"}, "page_cover.jpg"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := conv.coverName(tt.images); got != tt.want {
t.Errorf("coverName(%v) = %q, want %q", tt.images, got, tt.want)
}
})
}
}
func TestCoverDirectory(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
opts := NewOptions()
opts.OutDir = tmpDir
conv := New(opts)
files, err := conv.Files([]string{"testdata/test"})
if err != nil {
t.Fatal(err)
}
if len(files) != 1 {
t.Fatalf("expected 1 directory file, got %d", len(files))
}
for _, file := range files {
if err := conv.Cover(file); err != nil {
t.Fatal(err)
}
}
if _, err := os.Stat(filepath.Join(tmpDir, "test.jpg")); err != nil {
t.Errorf("directory cover not written: %v", err)
}
}
func TestFileType(t *testing.T) {
tests := []struct {
path string
want string
}{
{"testdata/test.cbz", "ZIP"},
{"testdata/test.cbr", "RAR"},
{"testdata/test.cb7", "7Z"},
{"testdata/test.cbt", "TAR"},
{"testdata/test.pdf", "PDF"},
{"testdata/test", "DIR"},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
if got := FileType(tt.path); got != tt.want {
t.Errorf("FileType(%q) = %q, want %q", tt.path, got, tt.want)
}
})
}
}
func TestCombine(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
opts := NewOptions()
opts.OutDir = tmpDir
opts.OutFile = "merged"
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.cbz", "testdata/test.cbt"})
if err != nil {
t.Fatal(err)
}
if len(files) != 2 {
t.Fatalf("expected 2 input files, got %d", len(files))
}
if err := conv.Combine(files); err != nil {
t.Fatal(err)
}
zr, err := zip.OpenReader(filepath.Join(tmpDir, "merged.cbz"))
if err != nil {
t.Fatalf("open combined archive: %v", err)
}
defer zr.Close()
var names []string
for _, f := range zr.File {
names = append(names, f.Name)
}
if len(names) != 4 {
t.Fatalf("expected 4 pages in combined archive, got %d: %v", len(names), names)
}
// each input is prefixed so identically named pages do not collide
var first, second int
for _, n := range names {
switch {
case strings.HasPrefix(n, "0001_"):
first++
case strings.HasPrefix(n, "0002_"):
second++
}
}
if first != 2 || second != 2 {
t.Errorf("expected 2 pages from each input, got 0001_=%d 0002_=%d: %v", first, second, names)
}
}
func TestSubfolders(t *testing.T) {
page0, err := os.ReadFile("testdata/test/00.jpg")
if err != nil {
t.Fatal(err)
}
page1, err := os.ReadFile("testdata/test/01.jpg")
if err != nil {
t.Fatal(err)
}
inDir, err := os.MkdirTemp(os.TempDir(), "cbc-in")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(inDir)
src := filepath.Join(inDir, "chapters.cbz")
buildZip(t, src, []zipEntry{
{"chapter1/00.jpg", page0},
{"chapter1/01.jpg", page1},
{"chapter2/00.jpg", page0},
{"chapter2/01.jpg", page1},
})
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc-out")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
opts := NewOptions()
opts.OutDir = tmpDir
conv := New(opts)
files, err := conv.Files([]string{src})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
zr, err := zip.OpenReader(filepath.Join(tmpDir, "chapters.cbz"))
if err != nil {
t.Fatalf("open output archive: %v", err)
}
defer zr.Close()
// without subfolder preservation chapter2/00 overwrites chapter1/00 and only 2 pages survive
if len(zr.File) != 4 {
var names []string
for _, f := range zr.File {
names = append(names, f.Name)
}
t.Fatalf("expected 4 pages from numbered subfolders, got %d: %v", len(zr.File), names)
}
}
func TestMeta(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// operate on a copy so the fixture stays intact
data, err := os.ReadFile("testdata/test.cbz")
if err != nil {
t.Fatal(err)
}
archive := filepath.Join(tmpDir, "meta.cbz")
if err := os.WriteFile(archive, data, 0644); err != nil {
t.Fatal(err)
}
conv := New(NewOptions())
conv.Opts = NewOptions()
conv.Opts.CommentBody = "hello world"
if _, err := conv.Meta(archive); err != nil {
t.Fatalf("set comment: %v", err)
}
conv.Opts = NewOptions()
conv.Opts.Comment = true
got, err := conv.Meta(archive)
if err != nil {
t.Fatalf("get comment: %v", err)
}
if got != "hello world" {
t.Errorf("comment roundtrip: got %q, want %q", got, "hello world")
}
extra := filepath.Join(tmpDir, "ComicInfo.xml")
if err := os.WriteFile(extra, []byte("<ComicInfo/>"), 0644); err != nil {
t.Fatal(err)
}
conv.Opts = NewOptions()
conv.Opts.FileAdd = extra
if _, err := conv.Meta(archive); err != nil {
t.Fatalf("add file: %v", err)
}
if !archiveHas(t, conv, archive, "ComicInfo.xml") {
t.Errorf("added file not found in archive")
}
conv.Opts = NewOptions()
conv.Opts.FileRemove = "ComicInfo.xml"
if _, err := conv.Meta(archive); err != nil {
t.Fatalf("remove file: %v", err)
}
if archiveHas(t, conv, archive, "ComicInfo.xml") {
t.Errorf("removed file still present in archive")
}
}
func TestRecursive(t *testing.T) {
inDir, err := os.MkdirTemp(os.TempDir(), "cbc-in")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(inDir)
sub := filepath.Join(inDir, "chapter1")
if err := os.MkdirAll(sub, 0755); err != nil {
t.Fatal(err)
}
src, err := os.ReadFile("testdata/test.cbz")
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sub, "test.cbz"), src, 0644); err != nil {
t.Fatal(err)
}
outDir, err := os.MkdirTemp(os.TempDir(), "cbc-out")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(outDir)
opts := NewOptions()
opts.OutDir = outDir
opts.Recursive = true
conv := New(opts)
files, err := conv.Files([]string{inDir})
if err != nil {
t.Fatal(err)
}
if len(files) != 1 {
t.Fatalf("expected 1 file, got %d", len(files))
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Error(err)
}
}
// output must mirror the input subtree relative to the input root, not the absolute path
want := filepath.Join(outDir, "chapter1", "test.cbz")
if _, err := os.Stat(want); err != nil {
t.Errorf("expected output relative to input root at %s: %v", want, err)
}
}
type zipEntry struct {
name string
data []byte
}
func buildZip(t *testing.T, path string, entries []zipEntry) {
t.Helper()
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
zw := zip.NewWriter(f)
for _, e := range entries {
w, err := zw.Create(e.name)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(e.data); err != nil {
t.Fatal(err)
}
}
if err := zw.Close(); err != nil {
t.Fatal(err)
}
}
func firstPage(t *testing.T, conv *Converter, archive string) image.Image {
t.Helper()
zr, err := zip.OpenReader(archive)
if err != nil {
t.Fatal(err)
}
defer zr.Close()
if len(zr.File) == 0 {
t.Fatalf("archive %s has no entries", archive)
}
rc, err := zr.File[0].Open()
if err != nil {
t.Fatal(err)
}
defer rc.Close()
img, err := conv.imageDecode(rc)
if err != nil {
t.Fatal(err)
}
return img
}
func archiveHas(t *testing.T, conv *Converter, archive, name string) bool {
t.Helper()
list, err := conv.archiveList(archive)
if err != nil {
t.Fatal(err)
}
for _, n := range list {
if n == name {
return true
}
}
return false
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

+172
View File
@@ -0,0 +1,172 @@
package main
import (
"os"
"path/filepath"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/iup-go/iup"
)
func fileDlg(title string, multiple, directory bool, dirKey string) ([]string, error) {
ret := make([]string, 0)
dlg := iup.FileDlg()
defer dlg.Destroy()
if !directory {
mf := "YES"
if !multiple {
mf = "NO"
}
dlg.SetAttributes(map[string]string{
"DIALOGTYPE": "OPEN",
"MULTIPLEFILES": mf,
"MULTIVALUEPATH": "YES",
"EXTFILTER": "Comic Files|*.rar;*.zip;*.7z;*.tar;*.cbr;*.cbz;*.cb7;*.cbt;*.pdf;*.epub;*.mobi;*.docx;*.pptx|",
"FILTER": "*.cb*", // for Motif
"TITLE": title,
"SHOWPREVIEW": "YES",
"PREVIEWWIDTH": "240",
"PREVIEWHEIGHT": "320",
})
dlg.SetCallback("FILE_CB", iup.FileFunc(previewCover()))
} else {
dlg.SetAttributes(map[string]string{
"DIALOGTYPE": "DIR",
"TITLE": title,
})
}
setStartDir(dlg, dirKey)
iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT)
if dlg.GetInt("STATUS") == 0 {
switch {
case multiple:
// MULTIVALUEPATH makes each MULTIVALUE a full path (id 0 is the path), so a folder-spanning selection works.
count := dlg.GetInt("MULTIVALUECOUNT")
if count > 1 {
for i := 1; i < count; i++ {
ret = append(ret, iup.GetAttributeId(dlg, "MULTIVALUE", i))
}
} else if value := dlg.GetAttribute("VALUE"); value != "" {
ret = append(ret, value)
}
default:
ret = append(ret, dlg.GetAttribute("VALUE"))
}
rememberDir(dlg, dirKey)
}
return ret, nil
}
const dlgPreviewName = "_FILEDLGPREVIEW_"
// previewCover returns a FILE_CB handler that draws the highlighted comic's cover in the dialog preview pane.
func previewCover() iup.FileFunc {
var image iup.Ihandle
var lastFile string
return func(ih iup.Ihandle, filename, status string) int {
switch status {
case "PAINT":
iup.DrawBegin(ih)
cw, ch := iup.DrawGetSize(ih)
iup.DrawParentBackground(ih)
if filename != lastFile {
lastFile = filename
if image != 0 {
image.Destroy()
image = 0
}
if img := loadCover(filename, cw, ch); img != 0 {
image = img
iup.SetHandle(dlgPreviewName, image)
}
}
if image != 0 {
iw, iih, _ := iup.DrawGetImageInfo(dlgPreviewName)
iup.DrawImage(ih, dlgPreviewName, (cw-iw)/2, (ch-iih)/2, iw, iih)
} else {
ih.SetAttribute("DRAWCOLOR", "128 128 128")
tw, th := iup.DrawGetTextSize(ih, "No preview")
iup.DrawText(ih, "No preview", (cw-tw)/2, (ch-th)/2, 0, 0)
}
iup.DrawEnd(ih)
case "FINISH":
if image != 0 {
image.Destroy()
image = 0
}
lastFile = ""
}
return iup.DEFAULT
}
}
// loadCover extracts the cover of a comic file and returns it as an IUP image fitted to w by h, or 0.
func loadCover(path string, w, h int) iup.Ihandle {
if w <= 0 || h <= 0 || !isComic(path) {
return 0
}
fi, err := os.Stat(path)
if err != nil || fi.IsDir() {
return 0
}
opts := cbconvert.NewOptions()
opts.DPI = 96
img, err := cbconvert.New(opts).Preview(path, fi, w, h)
if err != nil || img.Image == nil {
return 0
}
return iup.ImageFromImage(img.Image)
}
func isComic(path string) bool {
switch strings.ToLower(filepath.Ext(path)) {
case ".rar", ".zip", ".7z", ".tar", ".cbr", ".cbz", ".cb7", ".cbt",
".pdf", ".xps", ".epub", ".mobi", ".docx", ".pptx", ".xlsx":
return true
}
return false
}
func saveDlg(title, dirKey string) string {
dlg := iup.FileDlg()
defer dlg.Destroy()
dlg.SetAttributes(map[string]string{
"DIALOGTYPE": "SAVE",
"EXTFILTER": "Comic Files|*.cbz;*.cbt|",
"FILTER": "*.cb*", // for Motif
"TITLE": title,
})
setStartDir(dlg, dirKey)
iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT)
if dlg.GetInt("STATUS") == -1 {
return ""
}
rememberDir(dlg, dirKey)
return dlg.GetAttribute("VALUE")
}
@@ -0,0 +1,10 @@
[Desktop Entry]
Name=CBconvert
GenericName=A Comic Book converter
Comment=A comic converter with support for .cb*, .pdf, .xps, .epub, .mobi and directories.
Exec=cbconvert-gui
Icon=io.github.gen2brain.cbconvert
Terminal=false
Type=Application
StartupNotify=true
Categories=Graphics;Utility;
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>io.github.gen2brain.cbconvert</id>
<name>CBconvert</name>
<summary>A Comic Book converter</summary>
<metadata_license>MIT</metadata_license>
<project_license>GPL-3</project_license>
<description>
<p>
A comic book converter with support for .cb*, .pdf, .xps, .epub, .mobi and directories.
Images can be converted to JPEG, PNG, TIFF, WEBP, AVIF, JXL or 4-Bit BMP (16 colors) file format.
</p>
</description>
<developer id="com.github.gen2brain">
<name>Milan Nikolic</name>
</developer>
<launchable type="desktop-id">io.github.gen2brain.cbconvert.desktop</launchable>
<screenshots>
<screenshot type="default">
<caption>Options</caption>
<image>https://raw.githubusercontent.com/gen2brain/cbconvert/master/cmd/cbconvert-gui/screenshots/linux-01.jpg</image>
</screenshot>
<screenshot>
<caption>Converting</caption>
<image>https://raw.githubusercontent.com/gen2brain/cbconvert/master/cmd/cbconvert-gui/screenshots/linux-02.jpg</image>
</screenshot>
</screenshots>
<url type="homepage">https://github.com/gen2brain/cbconvert</url>
<url type="bugtracker">https://github.com/gen2brain/cbconvert/issues</url>
<content_rating type="oars-1.1"/>
<releases>
<release version="1.1.0" date="2024-11-06" type="stable">
<description>
<ul>
<li>Use jpegli for smaller files</li>
<li>Remove ImageMagick dependency</li>
<li>Wayland support</li>
<li>Support for DOCX and PPTX</li>
</ul>
</description>
<url type="details">https://github.com/gen2brain/cbconvert/releases/tag/v1.1.0</url>
</release>
<release version="1.0.4" date="2024-02-08" type="stable">
<description>
<ul>
<li>Update modules and dependencies</li>
</ul>
</description>
<url type="details">https://github.com/gen2brain/cbconvert/releases/tag/v1.0.4</url>
</release>
<release version="1.0.3" date="2023-09-14" type="stable">
<description>
<ul>
<li>Add content rating</li>
</ul>
</description>
<url type="details">https://github.com/gen2brain/cbconvert/releases/tag/v1.0.3</url>
</release>
<release version="1.0.2" date="2023-09-14" type="stable">
<description>
<ul>
<li>Change flatpak exec/command name to cbconvert</li>
</ul>
</description>
<url type="details">https://github.com/gen2brain/cbconvert/releases/tag/v1.0.2</url>
</release>
<release version="1.0.1" date="2023-09-14" type="stable">
<description>
<ul>
<li>Add flatpak manifest</li>
</ul>
</description>
<url type="details">https://github.com/gen2brain/cbconvert/releases/tag/v1.0.1</url>
</release>
<release version="1.0.0" date="2023-09-14" type="stable">
<description>
<ul>
<li>Add GUI</li>
<li>Add support for JPEG XL</li>
</ul>
</description>
<url type="details">https://github.com/gen2brain/cbconvert/releases/tag/v1.0.0</url>
</release>
</releases>
</component>
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,4 @@
[Thumbnailer Entry]
TryExec=cbconvert
Exec=cbconvert thumbnail --quiet --width %s --outfile %o %i
MimeType=application/pdf;application/x-cb7;application/x-cbt;application/epub+zip;application/vnd.comicbook-rar;application/vnd.comicbook+zip;application/x-mobipocket-ebook;application/vnd.ms-xpsdocument;application/vnd.openxmlformats-officedocument.wordprocessingml.document;application/vnd.openxmlformats-officedocument.presentationml.presentation;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;
+156
View File
@@ -0,0 +1,156 @@
# github.com/anthonynsimon/bild v0.14.0
## explicit; go 1.21
github.com/anthonynsimon/bild/adjust
github.com/anthonynsimon/bild/clone
github.com/anthonynsimon/bild/math/f64
github.com/anthonynsimon/bild/parallel
github.com/anthonynsimon/bild/transform
github.com/anthonynsimon/bild/util
# github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d
## explicit; go 1.13
github.com/dsoprea/go-exif/v2
github.com/dsoprea/go-exif/v2/common
github.com/dsoprea/go-exif/v2/undefined
# github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd
## explicit; go 1.13
github.com/dsoprea/go-logging
# github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d
## explicit; go 1.13
github.com/dsoprea/go-png-image-structure
# github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349
## explicit; go 1.12
github.com/dsoprea/go-utility/image
# github.com/dustin/go-humanize v1.0.1
## explicit; go 1.16
github.com/dustin/go-humanize
# github.com/ebitengine/purego v0.8.1
## explicit; go 1.18
github.com/ebitengine/purego
github.com/ebitengine/purego/internal/cgo
github.com/ebitengine/purego/internal/fakecgo
github.com/ebitengine/purego/internal/strings
# github.com/fvbommel/sortorder v1.1.0
## explicit; go 1.13
github.com/fvbommel/sortorder
# github.com/gen2brain/avif v0.4.1
## explicit; go 1.21
github.com/gen2brain/avif
# github.com/gen2brain/cbconvert v1.0.5-0.20241106181414-0dee611bf1de
## explicit; go 1.23
github.com/gen2brain/cbconvert
# github.com/gen2brain/go-fitz v1.24.14
## explicit; go 1.22
github.com/gen2brain/go-fitz
github.com/gen2brain/go-fitz/include/mupdf
github.com/gen2brain/go-fitz/include/mupdf/fitz
github.com/gen2brain/go-fitz/libs
# github.com/gen2brain/go-unarr v0.2.4
## explicit; go 1.21
github.com/gen2brain/go-unarr
github.com/gen2brain/go-unarr/unarrc
github.com/gen2brain/go-unarr/unarrc/external
github.com/gen2brain/go-unarr/unarrc/external/bzip2
github.com/gen2brain/go-unarr/unarrc/external/unarr
github.com/gen2brain/go-unarr/unarrc/external/unarr/_7z
github.com/gen2brain/go-unarr/unarrc/external/unarr/common
github.com/gen2brain/go-unarr/unarrc/external/unarr/lzmasdk
github.com/gen2brain/go-unarr/unarrc/external/unarr/rar
github.com/gen2brain/go-unarr/unarrc/external/unarr/tar
github.com/gen2brain/go-unarr/unarrc/external/unarr/zip
github.com/gen2brain/go-unarr/unarrc/external/zlib
# github.com/gen2brain/iup-go/iup v0.0.0-20241106050025-0f971ac33ed4
## explicit; go 1.22
github.com/gen2brain/iup-go/iup
github.com/gen2brain/iup-go/iup/external
github.com/gen2brain/iup-go/iup/external/include
github.com/gen2brain/iup-go/iup/external/src
github.com/gen2brain/iup-go/iup/external/src/cocoa
github.com/gen2brain/iup-go/iup/external/src/gtk
github.com/gen2brain/iup-go/iup/external/src/mot
github.com/gen2brain/iup-go/iup/external/src/win
github.com/gen2brain/iup-go/iup/external/src/win/wdl
github.com/gen2brain/iup-go/iup/external/src/win/wdl/dummy
github.com/gen2brain/iup-go/iup/external/srcgl
github.com/gen2brain/iup-go/iup/manifest
# github.com/gen2brain/jpegli v0.3.3
## explicit; go 1.22
github.com/gen2brain/jpegli
# github.com/gen2brain/jpegxl v0.4.2
## explicit; go 1.22
github.com/gen2brain/jpegxl
# github.com/gen2brain/webp v0.5.1
## explicit; go 1.22
github.com/gen2brain/webp
# github.com/go-errors/errors v1.5.1
## explicit; go 1.14
github.com/go-errors/errors
# github.com/godbus/dbus/v5 v5.1.0
## explicit; go 1.12
github.com/godbus/dbus/v5
# github.com/golang/geo v0.0.0-20230421003525-6adc56603217
## explicit; go 1.18
github.com/golang/geo/r1
github.com/golang/geo/r2
github.com/golang/geo/r3
github.com/golang/geo/s1
github.com/golang/geo/s2
# github.com/google/uuid v1.6.0
## explicit
github.com/google/uuid
# github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25
## explicit
github.com/jsummers/gobmp
# github.com/jupiterrider/ffi v0.2.1
## explicit; go 1.18
github.com/jupiterrider/ffi
# github.com/tetratelabs/wazero v1.8.1
## explicit; go 1.21
github.com/tetratelabs/wazero
github.com/tetratelabs/wazero/api
github.com/tetratelabs/wazero/experimental
github.com/tetratelabs/wazero/experimental/sys
github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1
github.com/tetratelabs/wazero/internal/descriptor
github.com/tetratelabs/wazero/internal/engine/interpreter
github.com/tetratelabs/wazero/internal/engine/wazevo
github.com/tetratelabs/wazero/internal/engine/wazevo/backend
github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64
github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64
github.com/tetratelabs/wazero/internal/engine/wazevo/backend/regalloc
github.com/tetratelabs/wazero/internal/engine/wazevo/frontend
github.com/tetratelabs/wazero/internal/engine/wazevo/ssa
github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi
github.com/tetratelabs/wazero/internal/expctxkeys
github.com/tetratelabs/wazero/internal/filecache
github.com/tetratelabs/wazero/internal/fsapi
github.com/tetratelabs/wazero/internal/ieee754
github.com/tetratelabs/wazero/internal/internalapi
github.com/tetratelabs/wazero/internal/leb128
github.com/tetratelabs/wazero/internal/moremath
github.com/tetratelabs/wazero/internal/platform
github.com/tetratelabs/wazero/internal/sock
github.com/tetratelabs/wazero/internal/sys
github.com/tetratelabs/wazero/internal/sysfs
github.com/tetratelabs/wazero/internal/u32
github.com/tetratelabs/wazero/internal/u64
github.com/tetratelabs/wazero/internal/version
github.com/tetratelabs/wazero/internal/wasip1
github.com/tetratelabs/wazero/internal/wasm
github.com/tetratelabs/wazero/internal/wasm/binary
github.com/tetratelabs/wazero/internal/wasmdebug
github.com/tetratelabs/wazero/internal/wasmruntime
github.com/tetratelabs/wazero/sys
# golang.org/x/image v0.21.0
## explicit; go 1.18
golang.org/x/image/ccitt
golang.org/x/image/tiff
golang.org/x/image/tiff/lzw
# golang.org/x/net v0.30.0
## explicit; go 1.18
golang.org/x/net/context
# golang.org/x/sync v0.8.0
## explicit; go 1.18
golang.org/x/sync/errgroup
# gopkg.in/yaml.v2 v2.4.0
## explicit; go 1.15
gopkg.in/yaml.v2
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>cbconvert-gui</string>
<key>CFBundleIconFile</key>
<string>icon</string>
<key>CFBundleIdentifier</key>
<string>io.github.gen2brain.cbconvert</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>CBconvert</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSHighResolutionCapable</key>
<string>True</string>
</dict>
</plist>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>19H2</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>iup</string>
<key>CFBundleIdentifier</key>
<string>br.puc-rio.tecgraf.iup</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>iup</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>12D4e</string>
<key>DTPlatformName</key>
<string>macosx</string>
<key>DTPlatformVersion</key>
<string>11.1</string>
<key>DTSDKBuild</key>
<string>20C63</string>
<key>DTSDKName</key>
<string>macosx11.1</string>
<key>DTXcode</key>
<string>1240</string>
<key>DTXcodeBuild</key>
<string>12D4e</string>
<key>LSMinimumSystemVersion</key>
<string>10.14</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2015 Tecgraf, PUC-Rio, Brazil. All rights reserved.</string>
</dict>
</plist>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+56
View File
@@ -0,0 +1,56 @@
module github.com/gen2brain/cbconvert/cmd/cbconvert-gui
go 1.26
require (
github.com/gen2brain/cbconvert v1.0.5-0.20260623161611-a5817c3ba5de
github.com/gen2brain/iup-go/iup v0.32.1-0.20260604112206-a19da6654449
)
require (
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/anthonynsimon/bild v0.15.0 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.4 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.1 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/gen2brain/avif v0.5.1 // indirect
github.com/gen2brain/go-fitz v1.24.15 // indirect
github.com/gen2brain/jpegli v0.4.0 // indirect
github.com/gen2brain/jpegxl v0.5.1 // indirect
github.com/gen2brain/webp v0.6.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/jupiterrider/ffi v0.7.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mholt/archives v0.1.5 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.1.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.5 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/stangelandcl/ppmd v0.1.1 // indirect
github.com/tetratelabs/wazero v1.12.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/image v0.43.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace github.com/gen2brain/cbconvert => ../..
+143
View File
@@ -0,0 +1,143 @@
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/anthonynsimon/bild v0.15.0 h1:FzvaNLuNlAPKw1Xz7V2WYOcGIEBMj8Y6ZyAk7CI+HzA=
github.com/anthonynsimon/bild v0.15.0/go.mod h1:qIgJ9FldkCn0iy5Ad24fzUkz5R+iJ0WfhiV+6FeCB5A=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.4 h1:iHiVJfxbrB6RF4X+snI2MpVgNBKmVfGaTqZGNlMQIU0=
github.com/bodgit/sevenzip v1.6.4/go.mod h1:ZtNi5KNgHXeXg1G7WiF0IWSuFE2eG6lt/cTGlvuirO0=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs=
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc=
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d h1:8+qI8ant/vZkNSsbwSjIR6XJfWcDVTg/qx/3pRUUZNA=
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw=
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 h1:/py11NlxDaOxkT9OKN+gXgT+QOH5xj1ZRoyusfRIlo4=
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo=
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gen2brain/avif v0.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk=
github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/gen2brain/iup-go/iup v0.32.1-0.20260604112206-a19da6654449 h1:HPqLUcspf4aPnyQIgeFKlLmphFVpFP34fzr1ZdnNyfc=
github.com/gen2brain/iup-go/iup v0.32.1-0.20260604112206-a19da6654449/go.mod h1:V4f7tHOJAeHtjQ+ju795QKv6DGdLEb4L5cmWB1sjSzU=
github.com/gen2brain/jpegli v0.4.0 h1:TnmAO8EcekS/XCboyLPjP6cqz025tkhdaFkiYNd3wH8=
github.com/gen2brain/jpegli v0.4.0/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegxl v0.5.1 h1:UuBUIkZ35DErImU3bTA6rltfV5zSgVNOA/K5a6JibfE=
github.com/gen2brain/jpegxl v0.5.1/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64=
github.com/gen2brain/webp v0.6.1/go.mod h1:iGWMaCSw7t3I/Cv9llzEKmpnR36S8lS8VL/ZVjxU0JE=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 h1:q100v18oYIaE4p5myLFqDyfXTOxivzwGyLjGCEfv8x4=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jupiterrider/ffi v0.7.0 h1:RKsl6Ascal+3kyAqR5Qcbp83LceQMLc1VZbPfHWoNzs=
github.com/jupiterrider/ffi v0.7.0/go.mod h1:9dauhpOfNqrqk28fxuu0kkdeFtT9Qr4vbfigiuIXN7c=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM=
github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/nwaples/rardecode/v2 v2.2.5 h1:L5doqgGfQwI7qADJMqnkrSB86rpPsqQDrHeO0HWa5JY=
github.com/nwaples/rardecode/v2 v2.2.5/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stangelandcl/ppmd v0.1.1 h1:c25QazhlWUn5nmR1QOzafKhQxBicAr7GGCKER2aJ8H8=
github.com/stangelandcl/ppmd v0.1.1/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+511
View File
@@ -0,0 +1,511 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strconv"
"github.com/fvbommel/sortorder"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/iup-go/iup"
)
// selectRow focuses and selects the given 0-based row in the table.
func selectRow(i int) {
if i < 0 || i >= len(files) {
return
}
index = i
iup.GetHandle("Table").SetAttribute("FOCUSCELL", fmt.Sprintf("%d:1", i+1))
}
// onSort re-syncs the files slice to the table's displayed order after a sort.
func onSort(ih iup.Ihandle, col int) int {
n := len(files)
if n < 2 {
return iup.DEFAULT
}
rowKey := func(name, size string) string {
return name + "\x00" + size
}
buckets := make(map[string][]int, n)
for i, f := range files {
size := strconv.FormatFloat(float64(f.Stat.Size())/(1024*1024), 'f', 2, 64)
k := rowKey(f.Name, size)
buckets[k] = append(buckets[k], i)
}
var selPath string
if index >= 0 && index < len(files) {
selPath = files[index].Path
}
reordered := make([]cbconvert.File, 0, n)
for lin := 1; lin <= n; lin++ {
k := rowKey(iup.GetAttributeId2(ih, "", lin, 1), iup.GetAttributeId2(ih, "", lin, 3))
idxs := buckets[k]
if len(idxs) == 0 {
return iup.DEFAULT
}
reordered = append(reordered, files[idxs[0]])
buckets[k] = idxs[1:]
}
files = reordered
index = -1
if selPath != "" {
for i, f := range files {
if f.Path == selPath {
selectRow(i)
break
}
}
}
return iup.DEFAULT
}
// addFiles appends files, natural-sorts the list, and rebuilds the table.
func addFiles(fs []cbconvert.File) {
if len(fs) == 0 {
return
}
wasEmpty := len(files) == 0
var selPath string
if index >= 0 && index < len(files) {
selPath = files[index].Path
}
files = append(files, fs...)
sort.Slice(files, func(i, j int) bool {
return sortorder.NaturalLess(files[i].Name, files[j].Name)
})
t := iup.GetHandle("Table")
t.SetAttribute("NUMLIN", strconv.Itoa(len(files)))
for i, f := range files {
lin := i + 1
iup.SetAttributeId2(t, "", lin, 1, f.Name)
iup.SetAttributeId2(t, "", lin, 2, cbconvert.FileType(f.Path))
iup.SetAttributeId2(t, "", lin, 3, strconv.FormatFloat(float64(f.Stat.Size())/(1024*1024), 'f', 2, 64))
}
if wasEmpty {
selectRow(0)
setActive()
previewPost()
return
}
index = -1
for i, f := range files {
if f.Path == selPath {
selectRow(i)
break
}
}
setActive()
}
func previewPost() {
if index == -1 || len(files) == 0 {
return
}
file := files[index]
// On a new file, fetch the count first; the Page POSTMESSAGE handler clamps previewPage and renders.
if file.Path != previewPath {
previewPath = file.Path
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
go pageCountPost(file)
return
}
previewRender()
}
// previewRenderSize is the size the cover is rendered at.
const previewRenderSize = 1200
// previewRender renders the current file at previewPage off the UI thread.
func previewRender() {
if index == -1 || len(files) == 0 {
return
}
file := files[index]
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
opts := options()
page := previewPage
go func(opts cbconvert.Options) {
conv := cbconvert.New(opts)
var s string
img, err := conv.PreviewPage(file.Path, file.Stat, page, previewRenderSize, previewRenderSize)
if err != nil {
s = err.Error()
fmt.Println(err)
}
iup.PostMessage(iup.GetHandle("Preview"), s, page, img)
}(opts)
}
// pageCountPost computes the file's page count off the UI thread and posts it, tagged with the path, to the Page spin.
func pageCountPost(file cbconvert.File) {
n, err := cbconvert.New(cbconvert.NewOptions()).PageCount(file.Path, file.Stat)
if err != nil || n < 1 {
n = 1
}
iup.PostMessage(iup.GetHandle("Page"), file.Path, n, nil)
}
// onPageChanged re-renders the preview for the spin's page; dedupes so spin and typing don't both fire.
func onPageChanged() int {
page := iup.GetHandle("Page").GetInt("VALUE") - 1
if page < 0 {
page = 0
}
if page == previewPage {
return iup.DEFAULT
}
previewPage = page
previewRender()
return iup.DEFAULT
}
func onAddFiles(ih iup.Ihandle) int {
args, err := fileDlg("Add Files", true, false, inputDirKey)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
if len(args) > 0 {
conv := cbconvert.New(options())
fs, err := conv.Files(args)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
addFiles(fs)
}
return iup.DEFAULT
}
func onAddDir(ih iup.Ihandle) int {
args, err := fileDlg("Add Directory", false, true, inputDirKey)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
if len(args) > 0 {
conv := cbconvert.New(options())
fs, err := conv.Files(args)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
addFiles(fs)
}
return iup.DEFAULT
}
func onRemove(ih iup.Ihandle) int {
if index < 0 || index >= len(files) {
return iup.IGNORE
}
iup.GetHandle("Table").SetAttribute("DELLIN", strconv.Itoa(index+1))
files = slices.Delete(files, index, index+1)
if index >= len(files) {
index = len(files) - 1
}
setActive()
if len(files) == 0 {
clearPreview()
} else {
previewPost()
}
return iup.DEFAULT
}
// clearPreview resets the preview state and repaints the canvas to its empty state.
func clearPreview() {
iup.Destroy(iup.GetHandle("cover"))
hasCover = false
previewPath = ""
previewPage = 0
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
iup.Update(iup.GetHandle("Preview"))
}
func onRemoveAll(ih iup.Ihandle) int {
index = -1
files = make([]cbconvert.File, 0)
iup.GetHandle("Table").SetAttribute("NUMLIN", "0")
clearPreview()
setActive()
return iup.DEFAULT
}
func onThumbnail(ih iup.Ihandle) int {
conv := cbconvert.New(options())
conv.Nfiles = len(files)
activeConv = conv
setBusy(true)
conv.OnProgress = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv)
}
var canceled = false
conv.OnCancel = func() {
canceled = true
}
iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
if c == iup.K_ESC {
conv.Cancel()
}
return iup.DEFAULT
}))
iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv)
go func(c *cbconvert.Converter) {
for _, file := range files {
if canceled {
break
}
if err := c.Thumbnail(file); err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
continue
}
}
iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
}(conv)
return iup.DEFAULT
}
func onCover(ih iup.Ihandle) int {
conv := cbconvert.New(options())
conv.Nfiles = len(files)
activeConv = conv
setBusy(true)
conv.OnProgress = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv)
}
var canceled = false
conv.OnCancel = func() {
canceled = true
}
iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
if c == iup.K_ESC {
conv.Cancel()
}
return iup.DEFAULT
}))
iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv)
go func(c *cbconvert.Converter) {
for _, file := range files {
if canceled {
break
}
if err := c.Cover(file); err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
continue
}
}
iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
}(conv)
return iup.DEFAULT
}
func onConvert(ih iup.Ihandle) int {
if busy {
if activeConv != nil {
activeConv.Cancel()
}
return iup.DEFAULT
}
conv := cbconvert.New(options())
conv.Nfiles = len(files)
activeConv = conv
setBusy(true)
conv.OnStart = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "convert", 0, conv)
}
conv.OnProgress = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "progress", 0, conv)
}
iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
if c == iup.K_ESC {
conv.Cancel()
}
return iup.DEFAULT
})).SetCallback("CLOSE_CB", iup.CloseFunc(func(ih iup.Ihandle) int {
if err := os.RemoveAll(conv.Workdir); err != nil {
fmt.Println(err)
}
return iup.DEFAULT
}))
convertErr := func(err error) {
if errors.Is(err, context.Canceled) {
if err := os.RemoveAll(conv.Workdir); err != nil {
fmt.Println(err)
}
return
}
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
if err := os.RemoveAll(conv.Workdir); err != nil {
fmt.Println(err)
}
}
go func(c *cbconvert.Converter) {
if c.Opts.Combine {
if err := c.Combine(files); err != nil {
convertErr(err)
}
} else {
for _, file := range files {
if err := c.Convert(file); err != nil {
convertErr(err)
if errors.Is(err, context.Canceled) {
break
}
continue
}
}
}
iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
}(conv)
return iup.DEFAULT
}
func onOutputDirectory(ih iup.Ihandle) int {
args, err := fileDlg("Output Directory", false, true, outputDirKey)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
if len(args) == 1 {
iup.GetHandle("OutDir").SetAttribute("VALUE", args[0])
}
setActive()
return iup.DEFAULT
}
func onOutputFile(ih iup.Ihandle) int {
name := saveDlg("Output File", outputDirKey)
if name != "" {
iup.GetHandle("OutFile").SetAttribute("VALUE", filepath.Base(name))
iup.GetHandle("OutDir").SetAttribute("VALUE", filepath.Dir(name))
setActive()
}
return iup.DEFAULT
}
func onFilterChanged(ih iup.Ihandle) int {
switch ih.GetInt("VALUE") {
case 1:
ih.SetAttribute("TIP", "NearestNeighbor is the fastest resampling filter, no antialiasing")
case 2:
ih.SetAttribute("TIP", "Box filter (averaging pixels)")
case 3:
ih.SetAttribute("TIP", "Linear is the bilinear filter, smooth and reasonably fast")
case 4:
ih.SetAttribute("TIP", "MitchellNetravali is a smooth bicubic filter")
case 5:
ih.SetAttribute("TIP", "CatmullRom is a sharp bicubic filter")
case 6:
ih.SetAttribute("TIP", "Gaussian is a blurring filter that uses gaussian function, useful for noise removal")
case 7:
ih.SetAttribute("TIP", "Lanczos is a high-quality resampling filter, it's slower than cubic filters")
}
previewPost()
return iup.DEFAULT
}
+148
View File
@@ -0,0 +1,148 @@
package main
import (
"bytes"
_ "embed"
"flag"
"fmt"
"image/png"
"os"
"path/filepath"
"runtime/debug"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/iup-go/iup"
)
//go:generate rsrc --ico dist/windows/icon.ico --arch amd64 -o main_windows_amd64.syso
//go:embed assets/logo.png
var appLogo []byte
//go:embed assets/loading.gif
var appLoading []byte
var appVersion string
var (
index = -1
files []cbconvert.File
config iup.Ihandle
activeConv *cbconvert.Converter
busy bool
previewPage int // 0-based page shown in the preview
previewPath string // path of the file whose page range is loaded
hasCover bool // whether a cover image is loaded for the preview canvas
)
func init() {
if appVersion != "" {
return
}
info, ok := debug.ReadBuildInfo()
if !ok {
return
}
if info.Main.Version != "" {
appVersion = info.Main.Version
}
for _, kv := range info.Settings {
if kv.Value == "" {
continue
}
if kv.Key == "vcs.revision" {
appVersion = kv.Value
if len(appVersion) > 7 {
appVersion = kv.Value[:7]
}
}
}
}
func main() {
parseFlags()
iup.Open()
defer iup.Close()
iup.SetGlobal("APPNAME", "cbconvert")
iup.SetGlobal("APPID", "io.github.gen2brain.cbconvert")
iup.SetGlobal("AUTODARKMODE", "YES")
config = iup.Config()
iup.ConfigLoad(config)
img, _ := png.Decode(bytes.NewReader(appLogo))
iup.ImageFromImage(img).SetHandle("logo")
dlg := iup.Dialog(layout()).SetAttributes(fmt.Sprintf(`TITLE="CBconvert %s", ICON=logo`, appVersion)).SetHandle("dlg")
dlg.SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
sp := strings.Split(s, ": ")
if len(sp) > 1 {
iup.MessageError(ih, fmt.Sprintf("%s\n\n%s", sp[0], strings.Join(sp[1:], ": ")))
}
return iup.DEFAULT
}))
dlg.SetCallback("THEMECHANGED_CB", iup.ThemeChangedFunc(func(ih iup.Ihandle, darkMode int) int {
t := iup.GetHandle("Table")
tableRowColors(t, darkMode == 1)
t.SetAttribute("REDRAW", "YES")
return iup.DEFAULT
}))
iup.Map(dlg)
profilesInit()
iup.ShowXY(dlg, iup.CENTER, iup.CENTER)
iup.MainLoop()
}
func parseFlags() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>]\n\n", filepath.Base(os.Args[0]))
fmt.Fprintf(os.Stderr, "\nCommands:\n")
fmt.Fprintf(os.Stderr, "\n version\n \tPrint version\n\n")
}
flag.NewFlagSet("version", flag.ExitOnError)
flag.Parse()
if flag.NArg() >= 1 {
if flag.Arg(0) == "version" {
fmt.Println(filepath.Base(os.Args[0]), appVersion)
os.Exit(0)
} else {
flag.Usage()
os.Exit(1)
}
}
}
func layout() iup.Ihandle {
return iup.Vbox(
iup.Hbox(
preview(),
iup.Hbox(
iup.Vbox(
list(),
tabs(),
).SetAttributes("NGAP=5"),
).SetAttributes("NGAP=5"),
buttons(),
).SetAttributes("NGAP=5, NMARGIN=5x5"),
iup.Label("").SetAttributes("SEPARATOR=HORIZONTAL"),
status(),
)
}
Binary file not shown.
+102
View File
@@ -0,0 +1,102 @@
package main
import (
"strconv"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/iup-go/iup"
)
func options() cbconvert.Options {
var opts cbconvert.Options
opts.Recursive = iup.GetHandle("Recursive").GetAttribute("VALUE") == "ON"
opts.NoRGB = iup.GetHandle("NoRGB").GetAttribute("VALUE") == "ON"
opts.NoCover = iup.GetHandle("NoCover").GetAttribute("VALUE") == "ON"
opts.Size = iup.GetHandle("Size").GetInt("VALUE")
opts.OutDir = iup.GetHandle("OutDir").GetAttribute("VALUE")
opts.Suffix = iup.GetHandle("Suffix").GetAttribute("VALUE")
opts.NoConvert = iup.GetHandle("NoConvert").GetAttribute("VALUE") == "ON"
opts.NoNonImage = iup.GetHandle("NoNonImage").GetAttribute("VALUE") == "ON"
opts.Archive = strings.ToLower(iup.GetHandle("Archive").GetAttribute("VALUESTRING"))
opts.ZipLevel = zipLevel(iup.GetHandle("ZipLevel").GetAttribute("VALUESTRING"))
opts.Format = strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING"))
opts.Width = iup.GetHandle("Width").GetInt("VALUE")
opts.Height = iup.GetHandle("Height").GetInt("VALUE")
opts.DPI = dpiValue(iup.GetHandle("DPI").GetAttribute("VALUE"))
opts.Fit = iup.GetHandle("Fit").GetAttribute("VALUE") == "ON"
opts.NoUpscale = iup.GetHandle("NoUpscale").GetAttribute("VALUE") == "ON"
opts.Filter = iup.GetHandle("Filter").GetInt("VALUE") - 1
opts.Quality = iup.GetHandle("Quality").GetInt("VALUE")
switch opts.Format {
case "webp", "avif", "jxl":
opts.Effort = iup.GetHandle("Effort").GetInt("VALUE")
default:
opts.Effort = -1
}
opts.Lossless = iup.GetHandle("Lossless").GetAttribute("VALUE") == "ON"
opts.Combine = iup.GetHandle("Combine").GetAttribute("VALUE") == "ON"
if opts.Combine {
opts.OutFile = iup.GetHandle("OutFile").GetAttribute("VALUE")
}
opts.Grayscale = iup.GetHandle("Grayscale").GetAttribute("VALUE") == "ON"
opts.Brightness = iup.GetHandle("Brightness").GetInt("VALUE")
opts.Contrast = iup.GetHandle("Contrast").GetInt("VALUE")
opts.Rotate = iup.GetHandle("Rotate").GetInt("VALUESTRING")
return opts
}
// shellArg quotes a command-line argument that contains whitespace.
func shellArg(s string) string {
if strings.ContainsAny(s, " \t") {
return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"`
}
return s
}
func commandLine() string {
parts := append([]string{"cbconvert", "convert"}, options().Args()...)
for _, file := range files {
parts = append(parts, file.Path)
}
for i, p := range parts {
parts[i] = shellArg(p)
}
return strings.Join(parts, " ")
}
func onCommand(iup.Ihandle) int {
iup.GetText("Command Line", commandLine(), -1)
return iup.DEFAULT
}
// zipLevel maps the compression dropdown selection to Options.ZipLevel.
func zipLevel(value string) int {
switch value {
case "Default":
return -1
case "Store (none)":
return 0
default:
level, err := strconv.Atoi(value)
if err != nil {
return -1
}
return level
}
}
func dpiValue(value string) int {
dpi, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil {
return 0
}
return dpi
}
+10
View File
@@ -0,0 +1,10 @@
## Screenshots
Windows
<img src="windows-01.jpg" width="700" title="Windows" alt="Windows" />
Linux
<img src="linux-01.jpg" width="700" title="Linux" alt="Linux" />
macOS
<img src="macos-01.jpg" width="700" title="macOS" alt="macOS" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

+249
View File
@@ -0,0 +1,249 @@
package main
import (
"fmt"
"slices"
"strconv"
"strings"
"github.com/gen2brain/iup-go/iup"
)
const (
pathsGroup = "Paths"
profilesGroup = "Profiles"
inputDirKey = "InputDir"
outputDirKey = "OutputDir"
)
type settingKind int
const (
kindBool settingKind = iota
kindInt
kindStr
)
type setting struct {
handle string
kind settingKind
def string
}
var settings = []setting{
{"Recursive", kindBool, "OFF"},
{"NoRGB", kindBool, "OFF"},
{"NoCover", kindBool, "OFF"},
{"NoConvert", kindBool, "OFF"},
{"NoNonImage", kindBool, "OFF"},
{"Combine", kindBool, "OFF"},
{"Fit", kindBool, "OFF"},
{"NoUpscale", kindBool, "OFF"},
{"Lossless", kindBool, "OFF"},
{"Grayscale", kindBool, "OFF"},
{"OutDir", kindStr, ""},
{"Suffix", kindStr, ""},
{"Width", kindStr, ""},
{"Height", kindStr, ""},
{"DPI", kindStr, "Default"},
{"Size", kindInt, "0"},
{"Quality", kindInt, "75"},
{"Effort", kindInt, "0"},
{"Brightness", kindInt, "0"},
{"Contrast", kindInt, "0"},
{"Format", kindInt, "1"},
{"Archive", kindInt, "1"},
{"ZipLevel", kindInt, "1"},
{"Filter", kindInt, "3"},
{"Rotate", kindInt, "1"},
}
func profileGroup(name string) string {
return "Profile:" + name
}
func profileNames() []string {
s := iup.ConfigGetVariableStr(config, profilesGroup, "Names")
if s == "" {
return nil
}
return strings.Split(s, ";")
}
func currentProfile() string {
return iup.ConfigGetVariableStrDef(config, profilesGroup, "Current", "Default")
}
func setStartDir(dlg iup.Ihandle, key string) {
if dir := iup.ConfigGetVariableStr(config, pathsGroup, key); dir != "" {
dlg.SetAttribute("DIRECTORY", dir)
}
}
func rememberDir(dlg iup.Ihandle, key string) {
dir := dlg.GetAttribute("DIRECTORY")
if dir == "" {
return
}
iup.ConfigSetVariableStr(config, pathsGroup, key, dir)
iup.ConfigSave(config)
}
func settingsSave(group string) {
for _, s := range settings {
h := iup.GetHandle(s.handle)
switch s.kind {
case kindBool:
v := 0
if h.GetAttribute("VALUE") == "ON" {
v = 1
}
iup.ConfigSetVariableInt(config, group, s.handle, v)
case kindInt:
iup.ConfigSetVariableInt(config, group, s.handle, h.GetInt("VALUE"))
case kindStr:
iup.ConfigSetVariableStr(config, group, s.handle, h.GetAttribute("VALUE"))
}
}
iup.ConfigSave(config)
}
// settingsApply sets every control from the given profile group, or from defaults when a group is empty.
func settingsApply(group string) {
for _, s := range settings {
h := iup.GetHandle(s.handle)
switch s.kind {
case kindBool:
def := 0
if s.def == "ON" {
def = 1
}
v := def
if group != "" {
v = iup.ConfigGetVariableIntDef(config, group, s.handle, def)
}
if v != 0 {
h.SetAttribute("VALUE", "ON")
} else {
h.SetAttribute("VALUE", "OFF")
}
case kindInt:
def, _ := strconv.Atoi(s.def)
v := def
if group != "" {
v = iup.ConfigGetVariableIntDef(config, group, s.handle, def)
}
h.SetAttribute("VALUE", strconv.Itoa(v))
case kindStr:
v := s.def
if group != "" {
v = iup.ConfigGetVariableStrDef(config, group, s.handle, s.def)
}
h.SetAttribute("VALUE", v)
}
}
syncLabels()
setActive()
previewPost()
}
// syncLabels mirrors slider values into their value labels and retunes the effort slider for the current format.
func syncLabels() {
iup.GetHandle("LabelQuality").SetAttribute("TITLE", iup.GetHandle("Quality").GetInt("VALUE"))
iup.GetHandle("LabelBrightness").SetAttribute("TITLE", iup.GetHandle("Brightness").GetInt("VALUE"))
iup.GetHandle("LabelContrast").SetAttribute("TITLE", iup.GetHandle("Contrast").GetInt("VALUE"))
format := strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING"))
eff := iup.GetHandle("Effort").GetInt("VALUE")
setEffort(format)
switch format {
case "webp", "avif", "jxl":
val := iup.GetHandle("Effort")
val.SetAttribute("VALUE", strconv.Itoa(eff))
iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", val.GetAttribute("EFFORTNAME"), eff))
}
iup.Refresh(iup.GetHandle("Tabs"))
}
func fillProfileList() {
list := iup.GetHandle("Profile")
list.SetAttribute("REMOVEITEM", "ALL")
cur := currentProfile()
sel := 1
for i, n := range profileNames() {
list.SetAttribute(strconv.Itoa(i+1), n)
if n == cur {
sel = i + 1
}
}
list.SetAttribute("VALUE", strconv.Itoa(sel))
}
// profilesInit loads the current profile on startup, creating a default one on the first run.
func profilesInit() {
if len(profileNames()) == 0 {
iup.ConfigSetVariableStr(config, profilesGroup, "Names", "Default")
iup.ConfigSetVariableStr(config, profilesGroup, "Current", "Default")
settingsSave(profileGroup("Default"))
}
fillProfileList()
settingsApply(profileGroup(currentProfile()))
}
func onProfileSelect(ih iup.Ihandle) int {
name := ih.GetAttribute("VALUESTRING")
if name == "" {
return iup.DEFAULT
}
iup.ConfigSetVariableStr(config, profilesGroup, "Current", name)
iup.ConfigSave(config)
settingsApply(profileGroup(name))
return iup.DEFAULT
}
func onSave(iup.Ihandle) int {
name := currentProfile()
if iup.GetParam("Save Profile", nil, "Name: %s\n", &name) != 1 {
return iup.DEFAULT
}
name = strings.TrimSpace(name)
if name == "" || strings.ContainsAny(name, ".;") {
iup.Message("Invalid Name", "Profile name must not be empty or contain '.' or ';'.")
return iup.DEFAULT
}
settingsSave(profileGroup(name))
names := profileNames()
if !slices.Contains(names, name) {
names = append(names, name)
iup.ConfigSetVariableStr(config, profilesGroup, "Names", strings.Join(names, ";"))
}
iup.ConfigSetVariableStr(config, profilesGroup, "Current", name)
iup.ConfigSave(config)
fillProfileList()
return iup.DEFAULT
}
func onReset(iup.Ihandle) int {
settingsApply("")
return iup.DEFAULT
}
+130
View File
@@ -0,0 +1,130 @@
package main
import "github.com/gen2brain/iup-go/iup"
func setActive() {
if busy {
return
}
opts := options()
count := iup.GetHandle("Table").GetInt("NUMLIN")
if count > 0 && index != -1 {
iup.GetHandle("PageBox").SetAttribute("VISIBLE", "YES")
} else {
iup.GetHandle("PageBox").SetAttribute("VISIBLE", "NO")
}
if count == 0 {
iup.GetHandle("Remove").SetAttribute("ACTIVE", "NO")
iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "NO")
iup.GetHandle("Preview").SetAttribute("IMAGE", "logo")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
} else {
if index != -1 {
iup.GetHandle("Remove").SetAttribute("ACTIVE", "YES")
}
iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "YES")
}
active := "YES"
var tip string
switch {
case count == 0 && opts.OutDir == "":
active, tip = "NO", "Add files and set output directory"
case count == 0:
active, tip = "NO", "Add files"
case opts.OutDir == "":
active, tip = "NO", "Set output directory"
}
enabledTip := map[string]string{
"Thumbnail": "Extract cover thumbnails",
"Cover": "Extract covers",
"Convert": "Convert files to the selected format",
}
for _, h := range []string{"Thumbnail", "Cover", "Convert"} {
b := iup.GetHandle(h)
b.SetAttribute("ACTIVE", active)
if active == "YES" {
b.SetAttribute("TIP", enabledTip[h])
} else {
b.SetAttribute("TIP", tip)
}
}
if opts.NoConvert {
iup.GetHandle("VboxImage").SetAttribute("ACTIVE", "NO")
iup.GetHandle("VboxTransform").SetAttribute("ACTIVE", "NO")
} else {
iup.GetHandle("VboxImage").SetAttribute("ACTIVE", "YES")
iup.GetHandle("VboxTransform").SetAttribute("ACTIVE", "YES")
}
canLossless := opts.Format == "webp" || opts.Format == "avif" || opts.Format == "jxl"
losslessOn := canLossless && opts.Lossless
if (opts.Format == "jpeg" || canLossless) && !opts.NoConvert && !losslessOn {
iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "NO")
}
if canLossless && !opts.NoConvert {
iup.GetHandle("VboxEffort").SetAttribute("ACTIVE", "YES")
iup.GetHandle("Lossless").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxEffort").SetAttribute("ACTIVE", "NO")
iup.GetHandle("Lossless").SetAttribute("ACTIVE", "NO")
}
if opts.Width != 0 && opts.Height != 0 && !opts.NoConvert {
iup.GetHandle("Fit").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("Fit").SetAttribute("ACTIVE", "NO")
}
if opts.Combine {
iup.GetHandle("VboxOutFile").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxOutFile").SetAttribute("ACTIVE", "NO")
}
if opts.Archive == "zip" {
iup.GetHandle("VboxZipLevel").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxZipLevel").SetAttribute("ACTIVE", "NO")
}
}
// setBusy locks the UI while an operation runs and turns Convert into a Cancel button.
func setBusy(on bool) {
busy = on
// Controls not governed by setActive; setActive owns the rest.
always := "YES"
if on {
always = "NO"
}
for _, h := range []string{"AddFiles", "AddDir", "Profile", "Reset", "Save", "Command", "Tabs", "Table"} {
iup.GetHandle(h).SetAttribute("ACTIVE", always)
}
convert := iup.GetHandle("Convert")
if on {
for _, h := range []string{"Remove", "RemoveAll", "Thumbnail", "Cover"} {
iup.GetHandle(h).SetAttribute("ACTIVE", "NO")
}
convert.SetAttribute("ACTIVE", "YES")
convert.SetAttribute("TITLE", "Cancel")
convert.SetAttribute("TIP", "Cancel the running operation (or press Esc)")
} else {
activeConv = nil
convert.SetAttribute("TITLE", "&Convert")
convert.SetAttribute("TIP", "Convert files to the selected format")
setActive() // restores the conditional buttons and option boxes
}
}
+710
View File
@@ -0,0 +1,710 @@
package main
import (
"bytes"
"fmt"
"image/gif"
"math"
"net/url"
"strconv"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/iup-go/iup"
)
func setEffort(format string) {
val := iup.GetHandle("Effort")
var name string
switch format {
case "webp":
val.SetAttributes("MIN=0, MAX=6, SHOWTICKS=7, VALUE=4")
val.SetAttribute("TIP", "WEBP method, higher is better/slower (0-6, default 4)")
name = "Method"
case "avif":
val.SetAttributes("MIN=0, MAX=10, SHOWTICKS=11, VALUE=10")
val.SetAttribute("TIP", "AVIF speed, higher is faster/worse (0-10, default 10)")
name = "Speed"
case "jxl":
val.SetAttributes("MIN=1, MAX=10, SHOWTICKS=10, VALUE=7")
val.SetAttribute("TIP", "JXL effort, higher is better/slower (1-10, default 7)")
name = "Effort"
default:
return
}
val.SetAttribute("EFFORTNAME", name)
iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", name, val.GetInt("VALUE")))
iup.Refresh(iup.GetHandle("LabelEffort"))
}
// tableRowColors sets the alternating row colors for dark or light mode.
func tableRowColors(t iup.Ihandle, dark bool) {
even, odd := "#F0F0F0", "#FFFFFF"
if dark {
even, odd = "#3A3A3A", "#2D2D2D"
}
t.SetAttribute("EVENROWCOLOR", even)
t.SetAttribute("ODDROWCOLOR", odd)
}
func list() iup.Ihandle {
t := iup.Table().SetHandle("Table")
t.SetAttributes(map[string]string{
"EXPAND": "YES",
"NUMCOL": "3",
"NUMLIN": "0",
"TITLE1": "Title",
"TITLE2": "Type",
"TITLE3": "Size (MiB)",
"WIDTH1": "300",
"WIDTH2": "50",
"WIDTH3": "100",
"ALIGNMENT2": "ACENTER",
"ALIGNMENT3": "ARIGHT",
"SELECTIONMODE": "SINGLE",
"USERRESIZE": "YES",
"STRETCHLAST": "NO",
"FOCUSRECT": "NO",
"SORTABLE": "YES",
"ALTERNATECOLOR": "YES",
})
tableRowColors(t, iup.GetGlobal("DARKMODE") == "YES" && iup.GetGlobal("AUTODARKMODE") == "YES")
t.SetCallback("ENTERITEM_CB", iup.EnterItemFunc(func(ih iup.Ihandle, lin, col int) int {
index = lin - 1
setActive()
previewPost()
return iup.DEFAULT
}))
t.SetCallback("SORT_CB", iup.TableSortFunc(onSort))
t.SetCallback("DROPFILES_CB", iup.DropFilesFunc(func(ih iup.Ihandle, fileName string, num, x, y int) int {
dec, err := url.QueryUnescape(fileName)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
conv := cbconvert.New(options())
fs, err := conv.Files([]string{dec})
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
addFiles(fs)
return iup.DEFAULT
}))
return iup.Vbox(t)
}
func preview() iup.Ihandle {
return iup.Frame(
iup.Vbox(
iup.Canvas().SetAttributes("EXPAND=YES, MINSIZE=400x, BORDER=NO").SetHandle("Preview").
SetCallback("ACTION", iup.ActionFunc(drawPreview)).
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(previewMessage)),
iup.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"),
),
)
}
// drawPreview draws the cover scaled to fit, else the logo centered.
func drawPreview(ih iup.Ihandle) int {
iup.DrawBegin(ih)
defer iup.DrawEnd(ih)
cw, ch := iup.DrawGetSize(ih)
iup.DrawParentBackground(ih)
name := "logo"
if hasCover {
name = "cover"
}
iw, ihh, _ := iup.DrawGetImageInfo(name)
if iw <= 0 || ihh <= 0 {
return iup.DEFAULT
}
dw, dh := iw, ihh
if hasCover {
s := math.Min(float64(cw)/float64(iw), float64(ch)/float64(ihh))
dw = int(float64(iw) * s)
dh = int(float64(ihh) * s)
}
iup.DrawImage(ih, name, (cw-dw)/2, (ch-dh)/2, dw, dh)
return iup.DEFAULT
}
// previewMessage receives a rendered cover from previewRender and triggers a canvas redraw.
func previewMessage(ih iup.Ihandle, s string, i int, p any) int {
if i != previewPage {
return iup.DEFAULT
}
img := p.(cbconvert.Image)
iup.GetHandle("Loading").SetAttributes("VISIBLE=NO, STOP=YES")
if img.Image != nil && len(s) == 0 {
iup.Destroy(iup.GetHandle("cover"))
iup.ImageFromImage(img.Image).SetHandle("cover")
hasCover = true
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height))
} else {
iup.Destroy(iup.GetHandle("cover"))
hasCover = false
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
sp := strings.Split(s, ": ")
if len(sp) > 1 {
iup.MessageError(ih, fmt.Sprintf("%s\n\n%s", sp[0], strings.Join(sp[1:], ": ")))
}
}
iup.Update(ih)
return iup.DEFAULT
}
// pageBox is the page-navigation spin shown in the status bar; hidden until a comic is selected.
func pageBox() iup.Ihandle {
return iup.Hbox(
iup.Space().SetAttribute("SIZE", "5"),
iup.Label("Page:"),
iup.Space().SetAttribute("SIZE", "3"),
iup.Text().SetAttributes(`SPIN=YES, SPINMIN=1, SPINMAX=1, VALUE=1, VISIBLECOLUMNS=3, MASK="/d*"`).SetHandle("Page").
SetAttribute("TIP", "Preview a different page of the selected comic").
SetCallback("SPIN_CB", iup.SpinFunc(func(ih iup.Ihandle, pos int) int {
return onPageChanged()
})).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
return onPageChanged()
})).
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
if s != previewPath {
return iup.DEFAULT
}
ih.SetAttribute("SPINMAX", strconv.Itoa(i))
iup.GetHandle("PageCount").SetAttribute("TITLE", fmt.Sprintf("/ %d", i))
if previewPage > i-1 {
previewPage = i - 1
}
if previewPage < 0 {
previewPage = 0
}
ih.SetAttribute("VALUE", strconv.Itoa(previewPage+1))
previewRender()
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "3"),
iup.Label("").SetHandle("PageCount"),
).SetAttributes("ALIGNMENT=ACENTER, VISIBLE=NO").SetHandle("PageBox")
}
func tabInput() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Toggle(" Recurse SubDirectories").SetHandle("Recursive").
SetAttributes(`TIP="Process subdirectories recursively"`),
iup.Toggle(" Only Grayscale Images").SetHandle("NoRGB").
SetAttributes(`TIP="Do not convert images that have RGB colorspace"`),
iup.Toggle(" Exclude Cover").SetHandle("NoCover").
SetAttributes(`TIP="Do not convert the cover image"`),
iup.Toggle(" Remove Non-Image Files from the Archive").SetHandle("NoNonImage").
SetAttribute("TIP", "Remove .nfo, .xml, .txt files from the archive"),
iup.Toggle(" Do not Transform or Convert Images").SetHandle("NoConvert").
SetAttributes(`TIP="Copy images from archive or directory without modifications"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
).SetAttributes("NGAP=10"),
iup.Space().SetAttribute("SIZE", "15"),
iup.Vbox(
iup.Vbox(
iup.Label("Minimum Size (MiB):"),
iup.Text().SetAttributes(`SPIN=YES, SPINMAX=2048, VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Size").
SetAttributes(`TIP="Process only files larger than minimum size"`),
),
iup.Vbox(
iup.Label("Document DPI:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"EDITBOX": "YES",
"VISIBLECOLUMNS": "6",
"VALUE": "Default",
"1": "Default",
"2": "150",
"3": "300",
"4": "600",
"5": "1200",
}).SetHandle("DPI").
SetAttribute("TIP", "Resolution for rendering documents (PDF, EPUB, etc.); Default is 300"),
),
).SetAttributes("NGAP=10"),
).SetHandle("VboxInput")
}
func tabOutput() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Vbox(
iup.Label("Output Directory:"),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutDir").
SetAttribute("TIP", "Directory where converted files are written (required)").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.Button("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onOutputDirectory)),
),
iup.Vbox(
iup.Label("Add Suffix to Output File:"),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("Suffix").
SetAttribute("TIP", "Add suffix to filename, i.e. filename_suffix.cbz"),
),
iup.Vbox(
iup.Label("Archive Format:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "ZIP",
"2": "TAR",
}).SetHandle("Archive").
SetAttribute("TIP", "Output container: ZIP (.cbz) or uncompressed TAR (.cbt)").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Compression:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "Default",
"2": "Store (none)",
"3": "1",
"4": "2",
"5": "3",
"6": "4",
"7": "5",
"8": "6",
"9": "7",
"10": "8",
"11": "9",
}).SetHandle("ZipLevel").
SetAttribute("TIP", "ZIP compression: Store disables it, 1 is fastest, 9 is smallest"),
).SetHandle("VboxZipLevel"),
).SetAttributes("NGAP=10"),
iup.Space().SetAttribute("SIZE", "15"),
iup.Vbox(
iup.Vbox(
iup.Toggle(" Combine into single file").SetHandle("Combine").
SetAttributes(`TIP="Merge all listed files into one archive"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Output File:"),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutFile").
SetAttribute("TIP", "Combined file name (default: first input + -combined)"),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.Button("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onOutputFile)),
).SetHandle("VboxOutFile"),
).SetAttributes("NGAP=10"),
).SetHandle("VboxOutput")
}
func tabImage() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Vbox(
iup.Label("Format:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "JPEG",
"2": "PNG",
"3": "TIFF",
"4": "BMP",
"5": "WEBP",
"6": "AVIF",
"7": "JXL",
}).SetHandle("Format").
SetAttribute("TIP", "Output image format for the converted pages").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setEffort(strings.ToLower(ih.GetAttribute("VALUESTRING")))
setActive()
previewPost()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Size:"),
iup.Hbox(
iup.Text().SetAttributes(`CUEBANNER="width", VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Width").
SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "2"),
iup.Label("x"),
iup.Space().SetAttribute("SIZE", "2"),
iup.Text().SetAttributes(`CUEBANNER="height", VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Height").
SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"),
),
iup.Vbox(
iup.Toggle(" Best Fit").SetHandle("Fit").
SetAttributes(`TIP="Best fit for required width and height"`),
iup.Toggle(" No Upscale").SetHandle("NoUpscale").
SetAttribute("TIP", "Do not enlarge images already smaller than the requested size"),
),
iup.Vbox(
iup.Label("Resize Filter:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "3",
"TIP": "Linear is the bilinear filter, smooth and reasonably fast",
"1": "NearestNeighbor",
"2": "Box",
"3": "Linear",
"4": "MitchellNetravali",
"5": "CatmullRom",
"6": "Gaussian",
"7": "Lanczos",
}).SetHandle("Filter").SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onFilterChanged)),
),
).SetAttributes("NGAP=10"),
iup.Space().SetAttribute("SIZE", "15"),
iup.Vbox(
iup.Vbox(
iup.Hbox(
iup.Label("Quality: "),
iup.Label("75").SetHandle("LabelQuality"),
).SetAttributes("NMARGIN=0"),
iup.Val("").SetAttributes(`MIN=0, MAX=100, VALUE=75, SHOWTICKS=10`).SetHandle("Quality").
SetAttribute("TIP", "Quality affects JPEG, WEBP, AVIF and JXL").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelQuality").SetAttribute("TITLE", ih.GetInt("VALUE"))
iup.Refresh(iup.GetHandle("LabelQuality"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
).SetHandle("VboxQuality"),
iup.Vbox(
iup.Label("Effort:").SetHandle("LabelEffort"),
iup.Val("").SetAttributes(`MIN=0, MAX=10, VALUE=0, SHOWTICKS=11`).SetHandle("Effort").
SetAttribute("TIP", "Encoder speed/effort (WEBP, AVIF, JXL)").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", ih.GetAttribute("EFFORTNAME"), ih.GetInt("VALUE")))
iup.Refresh(iup.GetHandle("LabelEffort"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
).SetHandle("VboxEffort"),
iup.Vbox(
iup.Toggle(" Lossless").SetHandle("Lossless").
SetAttributes(`TIP="Lossless compression (WEBP, AVIF, JXL), ignores quality"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
previewPost()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Toggle(" Grayscale").SetHandle("Grayscale").
SetAttributes(`TIP="Convert images to grayscale (monochromatic)"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
previewPost()
return iup.DEFAULT
})),
),
).SetAttributes("NGAP=10"),
).SetHandle("VboxImage")
}
func tabTransform() iup.Ihandle {
return iup.Vbox(
iup.Vbox(
iup.Hbox(
iup.Label("Brightness: "),
iup.Label("0").SetHandle("LabelBrightness"),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"),
iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Brightness").
SetAttributes(`TIP="Adjust the brightness of the images"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelBrightness").SetAttribute("TITLE", iup.GetHandle("Brightness").GetInt("VALUE"))
iup.Refresh(iup.GetHandle("LabelBrightness"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Hbox(
iup.Label("Contrast: "),
iup.Label("0").SetHandle("LabelContrast"),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"),
iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Contrast").
SetAttributes(`TIP="Adjust the contrast of the images"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelContrast").SetAttribute("TITLE", iup.GetHandle("Contrast").GetInt("VALUE"))
iup.Refresh(iup.GetHandle("LabelContrast"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Rotate:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "0",
"2": "90",
"3": "180",
"4": "270",
}).SetHandle("Rotate").
SetAttribute("TIP", "Rotate every page clockwise by the given angle in degrees").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
previewPost()
return iup.DEFAULT
})),
),
).SetHandle("VboxTransform").SetAttributes("NGAP=10")
}
func tabs() iup.Ihandle {
return iup.Tabs(
tabInput().SetAttributes("TABTITLE=Input, NMARGIN=10x10"),
tabOutput().SetAttributes("TABTITLE=Output, NMARGIN=10x10"),
tabImage().SetAttributes("TABTITLE=Image, NMARGIN=10x10"),
tabTransform().SetAttributes("TABTITLE=Transform, NMARGIN=10x10"),
).SetHandle("Tabs")
}
func buttons() iup.Ihandle {
addFiles := iup.Button("Add &Files...").SetHandle("AddFiles").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddFiles))
addDir := iup.Button("Add &Dir...").SetHandle("AddDir").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddDir))
remove := iup.Button("Remove").SetHandle("Remove").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemove))
removeAll := iup.Button("Remove All").SetHandle("RemoveAll").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemoveAll))
thumbnail := iup.Button("Thumbnail").SetHandle("Thumbnail").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onThumbnail))
cover := iup.Button("Cover").SetHandle("Cover").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onCover))
convert := iup.Button("&Convert").SetHandle("Convert").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onConvert))
reset := iup.Button("Reset").SetHandle("Reset").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", "Restore all settings to their defaults").
SetCallback("ACTION", iup.ActionFunc(onReset))
save := iup.Button("Save").SetHandle("Save").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", "Save current settings to a profile").
SetCallback("ACTION", iup.ActionFunc(onSave))
command := iup.Button("Command").SetHandle("Command").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", "Show the equivalent command line").
SetCallback("ACTION", iup.ActionFunc(onCommand))
profile := iup.List().SetAttributes("DROPDOWN=YES").SetHandle("Profile").
SetAttribute("TIP", "Select a settings profile").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onProfileSelect))
iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command).SetAttribute("NORMALIZE", "BOTH")
iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command, profile).SetAttribute("NORMALIZE", "HORIZONTAL")
return iup.Vbox(
iup.Vbox(
addFiles,
addDir,
remove,
removeAll,
).SetAttribute("NGAP", "2"),
iup.Space().SetAttribute("SIZE", "x5"),
iup.Vbox(
thumbnail,
cover,
).SetAttribute("NGAP", "2"),
iup.Space().SetAttribute("SIZE", "x5"),
iup.Vbox(
convert,
),
iup.Fill(),
iup.Vbox(
iup.Label("Profile:"),
profile,
reset,
save,
command,
).SetAttribute("NGAP", "2"),
).SetHandle("Buttons").SetAttributes("ALIGNMENT=ACENTER")
}
func status() iup.Ihandle {
return iup.Hbox(
loading(),
pageBox(),
iup.Fill(),
iup.Label("File 1 of 1").SetHandle("LabelStatus1").SetAttributes("VISIBLE=NO"),
iup.Space().SetAttribute("SIZE", "5"),
iup.Label("(000/000)").SetHandle("LabelStatus2").SetAttributes("VISIBLE=NO"),
iup.Space().SetAttribute("SIZE", "5"),
iup.ProgressBar().SetAttributes("VISIBLE=NO").SetHandle("ProgressBar").
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
switch s {
case "convert":
conv := p.(*cbconvert.Converter)
ih.SetAttributes("VALUE=0, VISIBLE=YES")
ih.SetAttribute("MAX", conv.Ncontents)
iup.GetHandle("LabelStatus1").SetAttribute("TITLE", fmt.Sprintf("File %d of %d", conv.CurrFile, conv.Nfiles))
iup.GetHandle("LabelStatus1").SetAttributes("VISIBLE=YES")
iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES")
iup.Refresh(iup.GetHandle("StatusBar"))
case "start":
conv := p.(*cbconvert.Converter)
ih.SetAttributes("VALUE=0, VISIBLE=YES")
ih.SetAttribute("MAX", conv.Nfiles)
iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES")
case "progress":
conv := p.(*cbconvert.Converter)
ih.SetAttribute("VALUE", conv.CurrContent)
iup.GetHandle("LabelStatus2").SetAttribute("TITLE", fmt.Sprintf("(%03d/%03d)", conv.CurrContent, conv.Ncontents))
iup.Refresh(iup.GetHandle("StatusBar"))
case "progress2":
conv := p.(*cbconvert.Converter)
ih.SetAttribute("VALUE", conv.CurrFile)
iup.GetHandle("LabelStatus2").SetAttribute("TITLE", fmt.Sprintf("(%03d/%03d)", conv.CurrFile, conv.Nfiles))
iup.Refresh(iup.GetHandle("StatusBar"))
case "finish":
setBusy(false)
iup.GetHandle("LabelStatus1").SetAttributes(`TITLE="", VISIBLE=NO`)
iup.GetHandle("LabelStatus2").SetAttributes(`TITLE="", VISIBLE=NO`)
ih.SetAttributes("VALUE=0, VISIBLE=NO")
iup.Refresh(iup.GetHandle("StatusBar"))
iup.GetHandle("dlg").SetCallback("K_ANY", nil)
iup.GetHandle("dlg").SetCallback("CLOSE_CB", nil)
}
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "5x0"),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=5x5").SetHandle("StatusBar")
}
func loading() iup.Ihandle {
img, _ := gif.DecodeAll(bytes.NewReader(appLoading))
animation := iup.User()
for idx, i := range img.Image {
name := fmt.Sprintf("Loading%d", idx)
iup.ImageFromImage(i).SetHandle(name)
iup.Append(animation, iup.GetHandle(name))
}
return iup.AnimatedLabel(animation).SetAttributes("VISIBLE=NO").SetHandle("Loading")
}
+60
View File
@@ -0,0 +1,60 @@
module github.com/gen2brain/cbconvert/cmd/cbconvert
go 1.26
require (
github.com/gen2brain/cbconvert v1.0.5-0.20260623161611-a5817c3ba5de
github.com/schollz/progressbar/v3 v3.19.0
golang.org/x/term v0.44.0
)
require (
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/anthonynsimon/bild v0.15.0 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.4 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.1 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/gen2brain/avif v0.5.1 // indirect
github.com/gen2brain/go-fitz v1.24.15 // indirect
github.com/gen2brain/jpegli v0.4.0 // indirect
github.com/gen2brain/jpegxl v0.5.1 // indirect
github.com/gen2brain/webp v0.6.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/jupiterrider/ffi v0.7.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mholt/archives v0.1.5 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.1.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/nwaples/rardecode/v2 v2.2.5 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/stangelandcl/ppmd v0.1.1 // indirect
github.com/tetratelabs/wazero v1.12.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/image v0.43.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace github.com/gen2brain/cbconvert => ../..
+155
View File
@@ -0,0 +1,155 @@
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/anthonynsimon/bild v0.15.0 h1:FzvaNLuNlAPKw1Xz7V2WYOcGIEBMj8Y6ZyAk7CI+HzA=
github.com/anthonynsimon/bild v0.15.0/go.mod h1:qIgJ9FldkCn0iy5Ad24fzUkz5R+iJ0WfhiV+6FeCB5A=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.4 h1:iHiVJfxbrB6RF4X+snI2MpVgNBKmVfGaTqZGNlMQIU0=
github.com/bodgit/sevenzip v1.6.4/go.mod h1:ZtNi5KNgHXeXg1G7WiF0IWSuFE2eG6lt/cTGlvuirO0=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs=
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc=
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d h1:8+qI8ant/vZkNSsbwSjIR6XJfWcDVTg/qx/3pRUUZNA=
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw=
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 h1:/py11NlxDaOxkT9OKN+gXgT+QOH5xj1ZRoyusfRIlo4=
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo=
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gen2brain/avif v0.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk=
github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/gen2brain/jpegli v0.4.0 h1:TnmAO8EcekS/XCboyLPjP6cqz025tkhdaFkiYNd3wH8=
github.com/gen2brain/jpegli v0.4.0/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegxl v0.5.1 h1:UuBUIkZ35DErImU3bTA6rltfV5zSgVNOA/K5a6JibfE=
github.com/gen2brain/jpegxl v0.5.1/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64=
github.com/gen2brain/webp v0.6.1/go.mod h1:iGWMaCSw7t3I/Cv9llzEKmpnR36S8lS8VL/ZVjxU0JE=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 h1:q100v18oYIaE4p5myLFqDyfXTOxivzwGyLjGCEfv8x4=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jupiterrider/ffi v0.7.0 h1:RKsl6Ascal+3kyAqR5Qcbp83LceQMLc1VZbPfHWoNzs=
github.com/jupiterrider/ffi v0.7.0/go.mod h1:9dauhpOfNqrqk28fxuu0kkdeFtT9Qr4vbfigiuIXN7c=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM=
github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/nwaples/rardecode/v2 v2.2.5 h1:L5doqgGfQwI7qADJMqnkrSB86rpPsqQDrHeO0HWa5JY=
github.com/nwaples/rardecode/v2 v2.2.5/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stangelandcl/ppmd v0.1.1 h1:c25QazhlWUn5nmR1QOzafKhQxBicAr7GGCKER2aJ8H8=
github.com/stangelandcl/ppmd v0.1.1/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+630
View File
@@ -0,0 +1,630 @@
package main
import (
"bufio"
"errors"
"flag"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"strings"
"syscall"
"github.com/gen2brain/cbconvert"
pb "github.com/schollz/progressbar/v3"
"golang.org/x/term"
)
var appVersion string
func init() {
if appVersion != "" {
return
}
info, ok := debug.ReadBuildInfo()
if !ok {
return
}
if info.Main.Version != "" {
appVersion = info.Main.Version
}
for _, kv := range info.Settings {
if kv.Value == "" {
continue
}
if kv.Key == "vcs.revision" {
appVersion = kv.Value
if len(appVersion) > 10 {
appVersion = kv.Value[:10]
}
}
}
}
func main() {
opts, args := parseFlags()
if opts.Version {
fmt.Println(filepath.Base(os.Args[0]), appVersion)
os.Exit(0)
}
conv := cbconvert.New(opts)
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
for range c {
if err := os.RemoveAll(conv.Workdir); err != nil {
fmt.Println(err)
}
os.Exit(1)
}
}()
if _, err := os.Stat(opts.OutDir); err != nil {
if err := os.MkdirAll(opts.OutDir, 0775); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
files, err := conv.Files(args)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
interactive := !opts.Quiet && term.IsTerminal(int(os.Stderr.Fd()))
var bar *pb.ProgressBar
newBar := func(max int, description string) {
bar = pb.NewOptions(max,
pb.OptionShowCount(),
pb.OptionClearOnFinish(),
pb.OptionUseANSICodes(true),
pb.OptionSetPredictTime(false),
pb.OptionSetDescription(description),
)
}
clearLine := func() {
fmt.Fprint(os.Stderr, "\033[2K\r")
}
cleanup := func() {
if interactive {
if bar != nil {
_ = bar.Finish()
}
clearLine()
}
}
if interactive && (opts.Cover || opts.Thumbnail || opts.Meta) {
newBar(conv.Nfiles, "")
}
conv.OnStart = func() {
if interactive {
clearLine()
newBar(conv.Ncontents, fmt.Sprintf("Converting %d of %d:", conv.CurrFile, conv.Nfiles))
}
}
conv.OnProgress = func() {
if bar != nil {
_ = bar.Add(1)
}
}
conv.OnCompress = func() {
if interactive {
if bar != nil {
_ = bar.Finish()
}
fmt.Fprintf(os.Stderr, "Compressing %d of %d...", conv.CurrFile, conv.Nfiles)
}
}
if opts.Combine {
if err := conv.Combine(files); err != nil {
fmt.Println(err)
os.Exit(1)
}
cleanup()
return
}
for _, file := range files {
switch {
case opts.Meta:
ret, err := conv.Meta(file.Path)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if opts.Cover {
fmt.Println(ret)
} else if opts.Comment {
fmt.Println(ret)
}
continue
case opts.Cover:
if err := conv.Cover(file); err != nil {
fmt.Println(err)
os.Exit(1)
}
continue
case opts.Thumbnail:
if err = conv.Thumbnail(file); err != nil {
fmt.Println(err)
os.Exit(1)
}
continue
}
if err := conv.Convert(file); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
cleanup()
}
// parseFlags parses command line flags.
func parseFlags() (cbconvert.Options, []string) {
opts := cbconvert.Options{}
var args []string
base := defaultOptions()
if len(os.Args) >= 2 {
switch os.Args[1] {
case "convert", "cover", "thumbnail":
if name := profileArg(os.Args[2:]); name != "" {
o, err := loadProfile(name)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
base = o
}
}
}
var profile string
const profileUsage = "Load a saved GUI profile as defaults; explicit flags still override"
convert := flag.NewFlagSet("convert", flag.ExitOnError)
convert.StringVar(&profile, "profile", "", profileUsage)
convert.IntVar(&opts.Width, "width", base.Width, "Image width")
convert.IntVar(&opts.Height, "height", base.Height, "Image height")
convert.BoolVar(&opts.Fit, "fit", base.Fit, "Best fit for required width and height")
convert.BoolVar(&opts.NoUpscale, "no-upscale", base.NoUpscale, "Do not upscale images already smaller than the requested width/height")
convert.IntVar(&opts.DPI, "dpi", base.DPI, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)")
convert.StringVar(&opts.Format, "format", base.Format, "Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl")
convert.StringVar(&opts.Archive, "archive", base.Archive, "Archive format, valid values are zip, tar")
convert.IntVar(&opts.ZipLevel, "zip-level", base.ZipLevel, "ZIP compression level, 0 disables compression, 1-9 sets deflate level (1 fastest, 9 smallest), -1 uses the default")
convert.IntVar(&opts.Quality, "quality", base.Quality, "Image quality")
convert.IntVar(&opts.Effort, "effort", base.Effort, "Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default")
convert.BoolVar(&opts.Lossless, "lossless", base.Lossless, "Lossless compression (webp, avif, jxl), ignores quality")
convert.BoolVar(&opts.Combine, "combine", base.Combine, "Combine all inputs into a single archive")
convert.StringVar(&opts.OutFile, "outfile", base.OutFile, "Output file name for --combine (default: first input + -combined)")
convert.IntVar(&opts.Filter, "filter", base.Filter, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos")
convert.BoolVar(&opts.NoCover, "no-cover", base.NoCover, "Do not convert the cover image")
convert.BoolVar(&opts.NoRGB, "no-rgb", base.NoRGB, "Do not convert images that have RGB colorspace")
convert.BoolVar(&opts.NoNonImage, "no-nonimage", base.NoNonImage, "Remove non-image files from the archive")
convert.BoolVar(&opts.NoConvert, "no-convert", base.NoConvert, "Do not transform or convert images")
convert.BoolVar(&opts.Grayscale, "grayscale", base.Grayscale, "Convert images to grayscale (monochromatic)")
convert.IntVar(&opts.Rotate, "rotate", base.Rotate, "Rotate images, valid values are 0, 90, 180, 270")
convert.IntVar(&opts.Brightness, "brightness", base.Brightness, "Adjust the brightness of the images, must be in the range (-100, 100)")
convert.IntVar(&opts.Contrast, "contrast", base.Contrast, "Adjust the contrast of the images, must be in the range (-100, 100)")
convert.StringVar(&opts.Suffix, "suffix", base.Suffix, "Add suffix to file basename")
convert.StringVar(&opts.OutDir, "outdir", base.OutDir, "Output directory")
convert.IntVar(&opts.Size, "size", base.Size, "Process only files larger than size (in MB)")
convert.BoolVar(&opts.Recursive, "recursive", base.Recursive, "Process subdirectories recursively")
convert.BoolVar(&opts.Quiet, "quiet", base.Quiet, "Hide console output")
cover := flag.NewFlagSet("cover", flag.ExitOnError)
cover.StringVar(&profile, "profile", "", profileUsage)
cover.IntVar(&opts.Width, "width", base.Width, "Image width")
cover.IntVar(&opts.Height, "height", base.Height, "Image height")
cover.BoolVar(&opts.Fit, "fit", base.Fit, "Best fit for required width and height")
cover.BoolVar(&opts.NoUpscale, "no-upscale", base.NoUpscale, "Do not upscale images already smaller than the requested width/height")
cover.IntVar(&opts.DPI, "dpi", base.DPI, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)")
cover.StringVar(&opts.Format, "format", base.Format, "Image format, valid values are jpeg, png, tiff, bmp, webp, avif")
cover.IntVar(&opts.Quality, "quality", base.Quality, "Image quality")
cover.IntVar(&opts.Effort, "effort", base.Effort, "Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default")
cover.BoolVar(&opts.Lossless, "lossless", base.Lossless, "Lossless compression (webp, avif, jxl), ignores quality")
cover.IntVar(&opts.Filter, "filter", base.Filter, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos")
cover.StringVar(&opts.OutDir, "outdir", base.OutDir, "Output directory")
cover.IntVar(&opts.Size, "size", base.Size, "Process only files larger than size (in MB)")
cover.BoolVar(&opts.Recursive, "recursive", base.Recursive, "Process subdirectories recursively")
cover.BoolVar(&opts.Quiet, "quiet", base.Quiet, "Hide console output")
thumbnail := flag.NewFlagSet("thumbnail", flag.ExitOnError)
thumbnail.StringVar(&profile, "profile", "", profileUsage)
thumbnail.IntVar(&opts.Width, "width", base.Width, "Image width")
thumbnail.IntVar(&opts.Height, "height", base.Height, "Image height")
thumbnail.BoolVar(&opts.Fit, "fit", base.Fit, "Best fit for required width and height")
thumbnail.BoolVar(&opts.NoUpscale, "no-upscale", base.NoUpscale, "Do not upscale images already smaller than the requested width/height")
thumbnail.IntVar(&opts.DPI, "dpi", base.DPI, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)")
thumbnail.IntVar(&opts.Filter, "filter", base.Filter, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos")
thumbnail.StringVar(&opts.OutDir, "outdir", base.OutDir, "Output directory")
thumbnail.StringVar(&opts.OutFile, "outfile", base.OutFile, "Output file")
thumbnail.IntVar(&opts.Size, "size", base.Size, "Process only files larger than size (in MB)")
thumbnail.BoolVar(&opts.Recursive, "recursive", base.Recursive, "Process subdirectories recursively")
thumbnail.BoolVar(&opts.Quiet, "quiet", base.Quiet, "Hide console output")
meta := flag.NewFlagSet("meta", flag.ExitOnError)
meta.BoolVar(&opts.Cover, "cover", false, "Print cover name")
meta.BoolVar(&opts.Comment, "comment", false, "Print zip comment")
meta.StringVar(&opts.CommentBody, "comment-body", "", "Set zip comment")
meta.StringVar(&opts.FileAdd, "file-add", "", "Add file to archive")
meta.StringVar(&opts.FileRemove, "file-remove", "", "Remove file from archive (glob pattern, i.e. *.xml)")
flag.NewFlagSet("version", flag.ExitOnError)
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0]))
fmt.Fprintf(os.Stderr, "\nCommands:\n")
fmt.Fprintf(os.Stderr, "\n convert\n \tConvert archive or document\n\n")
order := []string{"profile", "width", "height", "fit", "no-upscale", "dpi", "format", "archive", "zip-level", "quality", "effort", "lossless", "combine", "outfile", "filter", "no-cover", "no-rgb",
"no-nonimage", "no-convert", "grayscale", "rotate", "brightness", "contrast", "suffix", "outdir", "size", "recursive", "quiet"}
for _, name := range order {
f := convert.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}
fmt.Fprintf(os.Stderr, "\n cover\n \tExtract cover\n\n")
order = []string{"profile", "width", "height", "fit", "no-upscale", "dpi", "format", "quality", "effort", "lossless", "filter", "outdir", "size", "recursive", "quiet"}
for _, name := range order {
f := cover.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}
fmt.Fprintf(os.Stderr, "\n thumbnail\n \tExtract cover thumbnail (freedesktop spec.)\n\n")
order = []string{"profile", "width", "height", "fit", "no-upscale", "dpi", "filter", "outdir", "outfile", "size", "recursive", "quiet"}
for _, name := range order {
f := thumbnail.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}
fmt.Fprintf(os.Stderr, "\n meta\n \tCBZ metadata\n\n")
order = []string{"cover", "comment", "comment-body", "file-add", "file-remove"}
for _, name := range order {
f := meta.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}
fmt.Fprintf(os.Stderr, "\n version\n \tPrint version\n\n")
}
if len(os.Args) < 2 {
flag.Usage()
fmt.Fprintf(os.Stderr, "no command\n")
os.Exit(1)
}
pipe := piped()
if pipe {
args = lines(os.Stdin)
}
switch os.Args[1] {
case "convert":
operands := parseArgs(convert, os.Args[2:])
if !pipe {
args = operands
}
case "cover":
opts.Cover = true
operands := parseArgs(cover, os.Args[2:])
if !pipe {
args = operands
}
case "thumbnail":
opts.Thumbnail = true
operands := parseArgs(thumbnail, os.Args[2:])
if !pipe {
args = operands
}
case "meta":
opts.Meta = true
operands := parseArgs(meta, os.Args[2:])
if !pipe {
args = operands
}
case "version":
opts.Version = true
}
if len(args) == 0 && !opts.Version {
flag.Usage()
_, _ = fmt.Fprintf(os.Stderr, "no arguments\n")
os.Exit(1)
}
return opts, args
}
// parseArgs parses flags interspersed with file/dir operands.
func parseArgs(fs *flag.FlagSet, args []string) []string {
var operands []string
_ = fs.Parse(args)
for fs.NArg() > 0 {
operands = append(operands, fs.Arg(0))
_ = fs.Parse(fs.Args()[1:])
}
return operands
}
// piped checks if we have piped stdin.
func piped() bool {
f, err := os.Stdin.Stat()
if err != nil {
return false
}
if f.Mode()&os.ModeNamedPipe == 0 {
return false
}
return true
}
// lines returns slice of lines from the reader.
func lines(r io.Reader) []string {
data := make([]string, 0)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
text := scanner.Text()
data = append(data, text)
}
return data
}
// configPath returns the IupConfig file the GUI writes, matching IUP's per-platform location for APPNAME "cbconvert".
func configPath() (string, error) {
if runtime.GOOS == "windows" {
dir := os.Getenv("LocalAppData")
if dir == "" {
return "", errors.New("configPath: LocalAppData is not set")
}
return filepath.Join(dir, "cbconvert", "config.cfg"), nil
}
dir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("configPath: %w", err)
}
return filepath.Join(dir, "cbconvert", "config"), nil
}
// parseINI reads a simple INI file into section -> key -> value.
func parseINI(path string) (map[string]map[string]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
sections := make(map[string]map[string]string)
var cur map[string]string
sc := bufio.NewScanner(f)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
cur = make(map[string]string)
sections[line[1:len(line)-1]] = cur
continue
}
if cur == nil {
continue
}
if k, v, ok := strings.Cut(line, "="); ok {
cur[strings.TrimSpace(k)] = strings.TrimSpace(v)
}
}
if err := sc.Err(); err != nil {
return nil, err
}
return sections, nil
}
// defaultOptions returns the convert defaults used when no profile is loaded.
func defaultOptions() cbconvert.Options {
o := cbconvert.NewOptions()
o.OutDir = "."
return o
}
// loadProfile reads the named GUI profile and translates its control values into Options.
func loadProfile(name string) (cbconvert.Options, error) {
o := defaultOptions()
path, err := configPath()
if err != nil {
return o, fmt.Errorf("loadProfile: %w", err)
}
ini, err := parseINI(path)
if err != nil {
return o, fmt.Errorf("loadProfile: %w", err)
}
sec, ok := ini["Profile:"+name]
if !ok {
return o, fmt.Errorf("loadProfile: profile %q not found in %s%s", name, path, knownProfiles(ini))
}
str := func(key string, set func(string)) {
if v, ok := sec[key]; ok {
set(v)
}
}
boolean := func(key string, set func(bool)) {
if v, ok := sec[key]; ok {
set(v == "1")
}
}
integer := func(key string, set func(int)) {
if v, ok := sec[key]; ok {
if n, err := strconv.Atoi(v); err == nil {
set(n)
}
}
}
integer("Width", func(n int) { o.Width = n })
integer("Height", func(n int) { o.Height = n })
boolean("Fit", func(b bool) { o.Fit = b })
boolean("NoUpscale", func(b bool) { o.NoUpscale = b })
str("DPI", func(v string) { o.DPI = dpiFromString(v) })
str("Format", func(v string) { o.Format = formatFromIndex(v) })
str("Archive", func(v string) { o.Archive = archiveFromIndex(v) })
str("ZipLevel", func(v string) { o.ZipLevel = zipLevelFromIndex(v) })
integer("Quality", func(n int) { o.Quality = n })
integer("Effort", func(n int) { o.Effort = n })
boolean("Lossless", func(b bool) { o.Lossless = b })
boolean("Combine", func(b bool) { o.Combine = b })
integer("Filter", func(n int) { o.Filter = n - 1 })
boolean("NoCover", func(b bool) { o.NoCover = b })
boolean("NoRGB", func(b bool) { o.NoRGB = b })
boolean("NoNonImage", func(b bool) { o.NoNonImage = b })
boolean("NoConvert", func(b bool) { o.NoConvert = b })
boolean("Grayscale", func(b bool) { o.Grayscale = b })
str("Rotate", func(v string) { o.Rotate = rotateFromIndex(v) })
integer("Brightness", func(n int) { o.Brightness = n })
integer("Contrast", func(n int) { o.Contrast = n })
str("Suffix", func(v string) { o.Suffix = v })
str("OutDir", func(v string) { o.OutDir = v })
integer("Size", func(n int) { o.Size = n })
boolean("Recursive", func(b bool) { o.Recursive = b })
// Effort is format-specific in the GUI: only webp/avif/jxl use the slider, others fall back to the format default.
switch o.Format {
case "webp", "avif", "jxl":
default:
o.Effort = -1
}
return o, nil
}
// knownProfiles lists the profile names from the config, for a helpful "not found" message.
func knownProfiles(ini map[string]map[string]string) string {
if p, ok := ini["Profiles"]; ok {
if names := p["Names"]; names != "" {
return "\navailable profiles: " + strings.ReplaceAll(names, ";", ", ")
}
}
return ""
}
// profileArg extracts the --profile value from args, since it must be known before flag defaults are built.
func profileArg(args []string) string {
for i := 0; i < len(args); i++ {
if args[i] == "--profile" || args[i] == "-profile" {
if i+1 < len(args) {
return args[i+1]
}
return ""
}
for _, pfx := range []string{"--profile=", "-profile="} {
if v, ok := strings.CutPrefix(args[i], pfx); ok {
return v
}
}
}
return ""
}
// The index translations below mirror the GUI dropdown encodings stored in the profile.
func dpiFromString(s string) int {
n, err := strconv.Atoi(strings.TrimSpace(s))
if err != nil {
return 0
}
return n
}
var profileFormats = []string{"jpeg", "png", "tiff", "bmp", "webp", "avif", "jxl"}
func formatFromIndex(s string) string {
if i, _ := strconv.Atoi(s); i >= 1 && i <= len(profileFormats) {
return profileFormats[i-1]
}
return "jpeg"
}
func archiveFromIndex(s string) string {
if s == "2" {
return "tar"
}
return "zip"
}
func zipLevelFromIndex(s string) int {
switch i, _ := strconv.Atoi(s); i {
case 1:
return -1
case 2:
return 0
default:
return i - 2
}
}
func rotateFromIndex(s string) int {
switch s {
case "2":
return 90
case "3":
return 180
case "4":
return 270
default:
return 0
}
}
+160
View File
@@ -0,0 +1,160 @@
package main
import (
"os"
"path/filepath"
"runtime"
"testing"
)
func TestProfileArg(t *testing.T) {
cases := []struct {
args []string
want string
}{
{[]string{"--profile", "webp", "a.cbz"}, "webp"},
{[]string{"--profile=webp", "a.cbz"}, "webp"},
{[]string{"-profile", "webp"}, "webp"},
{[]string{"-profile=webp"}, "webp"},
{[]string{"--width", "800", "--profile", "x", "a.cbz"}, "x"},
{[]string{"--width", "800", "a.cbz"}, ""},
{[]string{"--profile"}, ""},
}
for _, c := range cases {
if got := profileArg(c.args); got != c.want {
t.Errorf("profileArg(%v) = %q, want %q", c.args, got, c.want)
}
}
}
func TestIndexTranslations(t *testing.T) {
if got := formatFromIndex("5"); got != "webp" {
t.Errorf("formatFromIndex(5) = %q, want webp", got)
}
if got := formatFromIndex("1"); got != "jpeg" {
t.Errorf("formatFromIndex(1) = %q, want jpeg", got)
}
if got := formatFromIndex("99"); got != "jpeg" {
t.Errorf("formatFromIndex(99) = %q, want jpeg fallback", got)
}
if got := archiveFromIndex("2"); got != "tar" {
t.Errorf("archiveFromIndex(2) = %q, want tar", got)
}
if got := archiveFromIndex("1"); got != "zip" {
t.Errorf("archiveFromIndex(1) = %q, want zip", got)
}
zip := map[string]int{"1": -1, "2": 0, "3": 1, "11": 9}
for in, want := range zip {
if got := zipLevelFromIndex(in); got != want {
t.Errorf("zipLevelFromIndex(%s) = %d, want %d", in, got, want)
}
}
rot := map[string]int{"1": 0, "2": 90, "3": 180, "4": 270}
for in, want := range rot {
if got := rotateFromIndex(in); got != want {
t.Errorf("rotateFromIndex(%s) = %d, want %d", in, got, want)
}
}
if got := dpiFromString("Default"); got != 0 {
t.Errorf("dpiFromString(Default) = %d, want 0", got)
}
if got := dpiFromString("150"); got != 150 {
t.Errorf("dpiFromString(150) = %d, want 150", got)
}
}
func TestParseINI(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config")
data := "[Profiles]\nNames=Default;webp\n\n[Profile:webp]\nFormat=5\nQuality=90\n"
if err := os.WriteFile(path, []byte(data), 0644); err != nil {
t.Fatal(err)
}
ini, err := parseINI(path)
if err != nil {
t.Fatal(err)
}
if ini["Profile:webp"]["Format"] != "5" {
t.Errorf("Format = %q, want 5", ini["Profile:webp"]["Format"])
}
if ini["Profiles"]["Names"] != "Default;webp" {
t.Errorf("Names = %q", ini["Profiles"]["Names"])
}
}
func TestLoadProfile(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("config path override via XDG_CONFIG_HOME is Linux-specific")
}
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "cbconvert"), 0755); err != nil {
t.Fatal(err)
}
data := "[Profile:webp]\nFormat=5\nQuality=90\nEffort=4\nArchive=2\nWidth=800\nFit=1\nFilter=7\nRotate=2\n"
if err := os.WriteFile(filepath.Join(dir, "cbconvert", "config"), []byte(data), 0644); err != nil {
t.Fatal(err)
}
t.Setenv("XDG_CONFIG_HOME", dir)
o, err := loadProfile("webp")
if err != nil {
t.Fatal(err)
}
if o.Format != "webp" {
t.Errorf("Format = %q, want webp", o.Format)
}
if o.Quality != 90 {
t.Errorf("Quality = %d, want 90", o.Quality)
}
if o.Effort != 4 {
t.Errorf("Effort = %d, want 4 (webp keeps the slider value)", o.Effort)
}
if o.Archive != "tar" {
t.Errorf("Archive = %q, want tar", o.Archive)
}
if o.Width != 800 || !o.Fit {
t.Errorf("Width/Fit = %d/%v, want 800/true", o.Width, o.Fit)
}
if o.Filter != 6 {
t.Errorf("Filter = %d, want 6 (GUI index 7 - 1)", o.Filter)
}
if o.Rotate != 90 {
t.Errorf("Rotate = %d, want 90", o.Rotate)
}
if _, err := loadProfile("missing"); err == nil {
t.Error("loadProfile(missing) should error")
}
}
func TestLoadProfileEffortGate(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("config path override via XDG_CONFIG_HOME is Linux-specific")
}
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "cbconvert"), 0755); err != nil {
t.Fatal(err)
}
// Format=1 (jpeg) with a stored Effort must collapse to -1, mirroring the GUI.
data := "[Profile:jpeg]\nFormat=1\nEffort=4\n"
if err := os.WriteFile(filepath.Join(dir, "cbconvert", "config"), []byte(data), 0644); err != nil {
t.Fatal(err)
}
t.Setenv("XDG_CONFIG_HOME", dir)
o, err := loadProfile("jpeg")
if err != nil {
t.Fatal(err)
}
if o.Effort != -1 {
t.Errorf("Effort = %d, want -1 for non-effort format", o.Effort)
}
}
+52
View File
@@ -0,0 +1,52 @@
module github.com/gen2brain/cbconvert
go 1.26
require (
github.com/anthonynsimon/bild v0.15.0
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d
github.com/dustin/go-humanize v1.0.1
github.com/fvbommel/sortorder v1.1.0
github.com/gen2brain/avif v0.5.1
github.com/gen2brain/go-fitz v1.24.15
github.com/gen2brain/jpegli v0.4.0
github.com/gen2brain/jpegxl v0.5.1
github.com/gen2brain/webp v0.6.1
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25
github.com/mholt/archives v0.1.5
golang.org/x/image v0.43.0
golang.org/x/sync v0.21.0
)
require (
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.4 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
github.com/ebitengine/purego v0.10.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jupiterrider/ffi v0.7.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.1.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.5 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/stangelandcl/ppmd v0.1.1 // indirect
github.com/tetratelabs/wazero v1.12.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
+141
View File
@@ -0,0 +1,141 @@
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/anthonynsimon/bild v0.15.0 h1:FzvaNLuNlAPKw1Xz7V2WYOcGIEBMj8Y6ZyAk7CI+HzA=
github.com/anthonynsimon/bild v0.15.0/go.mod h1:qIgJ9FldkCn0iy5Ad24fzUkz5R+iJ0WfhiV+6FeCB5A=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.4 h1:iHiVJfxbrB6RF4X+snI2MpVgNBKmVfGaTqZGNlMQIU0=
github.com/bodgit/sevenzip v1.6.4/go.mod h1:ZtNi5KNgHXeXg1G7WiF0IWSuFE2eG6lt/cTGlvuirO0=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs=
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc=
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d h1:8+qI8ant/vZkNSsbwSjIR6XJfWcDVTg/qx/3pRUUZNA=
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw=
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 h1:/py11NlxDaOxkT9OKN+gXgT+QOH5xj1ZRoyusfRIlo4=
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo=
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gen2brain/avif v0.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk=
github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/gen2brain/jpegli v0.4.0 h1:TnmAO8EcekS/XCboyLPjP6cqz025tkhdaFkiYNd3wH8=
github.com/gen2brain/jpegli v0.4.0/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegxl v0.5.1 h1:UuBUIkZ35DErImU3bTA6rltfV5zSgVNOA/K5a6JibfE=
github.com/gen2brain/jpegxl v0.5.1/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64=
github.com/gen2brain/webp v0.6.1/go.mod h1:iGWMaCSw7t3I/Cv9llzEKmpnR36S8lS8VL/ZVjxU0JE=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 h1:q100v18oYIaE4p5myLFqDyfXTOxivzwGyLjGCEfv8x4=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jupiterrider/ffi v0.7.0 h1:RKsl6Ascal+3kyAqR5Qcbp83LceQMLc1VZbPfHWoNzs=
github.com/jupiterrider/ffi v0.7.0/go.mod h1:9dauhpOfNqrqk28fxuu0kkdeFtT9Qr4vbfigiuIXN7c=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM=
github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/nwaples/rardecode/v2 v2.2.5 h1:L5doqgGfQwI7qADJMqnkrSB86rpPsqQDrHeO0HWa5JY=
github.com/nwaples/rardecode/v2 v2.2.5/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stangelandcl/ppmd v0.1.1 h1:c25QazhlWUn5nmR1QOzafKhQxBicAr7GGCKER2aJ8H8=
github.com/stangelandcl/ppmd v0.1.1/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-21
View File
@@ -1,21 +0,0 @@
#!/usr/bin/env bash
mkdir -p build
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/cbconvert
strip build/cbconvert
CGO_LDFLAGS="-ldl -lltdl -lfreetype -lm -lz -static-libgcc" CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/cbconvert-static --ldflags '-extldflags "-static"'
strip build/cbconvert-static
CGO_LDFLAGS="-L/usr/i686-pc-mingw32/usr/lib" \
CGO_CFLAGS="-I/usr/i686-pc-mingw32/usr/include -Wno-poison-system-directories" \
CGO_CXXFLAGS="-I/usr/i686-pc-mingw32/usr/include -Wno-poison-system-directories" \
CGO_CPPFLAGS="-I/usr/i686-pc-mingw32/usr/include -Wno-poison-system-directories" \
PKG_CONFIG=/usr/bin/i686-pc-mingw32-pkg-config \
PKG_CONFIG_PATH=/usr/i686-pc-mingw32/usr/lib/pkgconfig \
PKG_CONFIG_LIBDIR=/usr/i686-pc-mingw32/usr/lib/pkgconfig \
CC="i686-pc-mingw32-gcc" CXX="i686-pc-mingw32-g++" \
CC_FOR_TARGET=i686-pc-mingw32-gcc CXX_FOR_TARGET=i686-pc-mingw32-g++ \
CGO_ENABLED=1 GOOS=windows GOARCH=386 go build -o build/cbconvert.exe -ldflags "-linkmode external -extldflags -static"
i686-pc-mingw32-strip build/cbconvert.exe
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB