159 Commits

Author SHA1 Message Date
Milan Nikolic 1e4ec17382 Update modules 2024-11-06 22:49:36 +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 Nikolic 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 Nikolic 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
62 changed files with 4165 additions and 2874 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
+149 -135
View File
@@ -1,184 +1,198 @@
CBconvert ## CBconvert
=========
Introduction ### Introduction
------------
CBconvert is a [Comic Book](http://en.wikipedia.org/wiki/Comic_Book_Archive_file) converter written in [Go language](https://golang.org/). CBconvert is a [Comic Book](http://en.wikipedia.org/wiki/Comic_Book_Archive_file) converter.
It can convert one comic at a time or bulk convert comics to different formats to fit your various devices. It can convert comics to different formats to fit your various devices.
![screenshot](http://cbconvert.com/screenshot.png) <img src="cmd/cbconvert-gui/screenshots/linux-01.jpg" width="700" alt="screenshot" />
Features See more [screenshots](https://github.com/gen2brain/cbconvert/blob/master/cmd/cbconvert-gui/screenshots/).
--------
- reads RAR, ZIP, 7Z, GZ, BZ2, CBR, CBZ, CB7, CBT, PDF, EPUB, XPS and plain directory ### Features
- 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
- rotate, flip, adjust brightness/contrast, adjust levels (Photoshop like) or grayscale images
- choose resize algorithm (NearestNeighbor, Box, Linear, MitchellNetravali, CatmullRom, Gaussian, Lanczos)
- export covers from comics
- create thumbnails from covers by [freedesktop](http://www.freedesktop.org/wiki/) specification
Download * 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
- [Windows GUI](https://github.com/gen2brain/cbconvert/releases/download/0.5.0/cbconvert-0.5.zip) ### Download
- [Windows CMD](https://github.com/gen2brain/cbconvert/releases/download/0.5.0/cbconvert-cmd-0.5.zip)
- [Linux 64bit GUI](https://github.com/gen2brain/cbconvert/releases/download/0.5.0/cbconvert-0.5.tar.gz) Download the latest binaries from the [releases](https://github.com/gen2brain/cbconvert/releases).
- [Linux 64bit CMD](https://github.com/gen2brain/cbconvert/releases/download/0.5.0/cbconvert-cmd-0.5.tar.gz)
Using cbconvert in file managers to generate freedesktop thumbnails Linux Flatpak is available at [Flathub](https://flathub.org/apps/io.github.gen2brain.cbconvert).
-------------------------------------------------------------------
Just copy cbconvert cmd binary to your PATH and create file ~/.local/share/thumbnailers/cbconvert.thumbnailer : ### Using cbconvert in file managers to generate FreeDesktop thumbnails
[Thumbnailer Entry] Copy/install `cbconvert` cli binary to your `PATH`, create file `~/.local/share/thumbnailers/cbconvert.thumbnailer`
TryExec=cbconvert and paste contents from [thumbnailer](https://github.com/gen2brain/cbconvert/tree/master/cmd/cbconvert-gui/dist/linux/io.github.gen2brain.cbconvert.thumbnailer).
Exec=cbconvert thumbnail --quiet --width %s --outfile %o %i
MimeType=application/pdf;application/x-pdf;image/pdf;application/x-cbz;application/x-cbr;application/x-cb7;application/x-cbt;application/oxps;application/vnd.ms-xpsdocument;application/epub+zip;
This is how it looks like in PCManFM file manager: This is what it looks like in the `PCManFM` file manager:
![thumbnails](http://cbconvert.com/thumbnails.png) <img src="cmd/cbconvert-gui/screenshots/thumbnails.jpg" width="700" alt="thumbnails" />
Using command line app ### Using command line app
----------------------
usage: cbconvert [<flags>] <command> [<args> ...] ```
    Usage: cbconvert <command> [<flags>] [file1 dir1 ... fileOrDirN]
Comic Book convert tool.
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
Args:
<args> filename or directory
Commands:
help [<command>...]
Show help.
convert [<flags>] <args>...     Commands:
Convert archive or document (default command)
--width=0 Image width       convert
--height=0 Image height             Convert archive or document
--fit Best fit for required width and height
--format="jpeg" Image format, valid values are jpeg, png, gif, tiff, bmp
--quality=75 JPEG image quality
--filter=2 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos
--cover Convert cover image (use --no-cover if you want to exclude cover)
--rgb Convert images that have RGB colorspace (use --no-rgb if you only want to convert grayscaled 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
--levels-inmin=0 Shadow input value
--levels-inmax=255 Highlight input value
--levels-gamma=1 Midpoint/Gamma
--levels-outmin=0 Shadow output value
--levels-outmax=255 Highlight output value
cover [<flags>] <args>...         --width
Extract cover             Image width (default "0")
        --height
            Image height (default "0")
        --fit
            Best fit for required width and height (default "false")
        --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")
        --quality
            Image quality (default "75")
        --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")
--width=0 Image width       cover
--height=0 Image height             Extract cover
--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
thumbnail [<flags>] <args>...         --width
Extract cover thumbnail (freedesktop spec.)             Image width (default "0")
        --height
            Image height (default "0")
        --fit
            Best fit for required width and height (default "false")
        --format
            Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl (default "jpeg")
        --quality
            Image quality (default "75")
        --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")
--width=0 Image width       thumbnail
--height=0 Image height             Extract cover thumbnail (freedesktop spec.)
--fit Best fit for required width and height
--filter=2 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos
[man page](https://en.wikipedia.org/wiki/Man_page) is also available:         --width
            Image width (default "0")
        --height
            Image height (default "0")
        --fit
            Best fit for required width and height (default "false")
        --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")
cbconvert --help-man | man /dev/stdin       meta
            CBZ metadata
Examples         --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(s) from archive (glob pattern, i.e. *.xml) (default "")
Rescale images to 1200px for all supported files found in directory with size larger then 60MB: ```
cbconvert --recursive --width 1200 --size 60 /media/comics/Thorgal/ ### Examples
Convert all images in pdf to 4bit BMP image and save result in ~/comics directory: * Rescale images to 1200px for all supported files found in a directory with a size larger than 60MB:
cbconvert --bmp --outdir ~/comics /media/comics/Garfield/Garfield_01.pdf `cbconvert --recursive --width 1200 --size 60 /media/comics/Thorgal/`
[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. * Convert all images in pdf to 4bit BMP images and save the result in ~/comics directory:
Generate thumbnails by [freedesktop specification](http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html) in ~/.thumbnails/normal directory with width 512: `cbconvert --format bmp --outdir ~/comics /media/comics/Garfield/Garfield_01.pdf`
cbconvert thumbnail --width 512 --outdir ~/.thumbnails/normal /media/comics/GrooTheWanderer/ [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.
Extract covers to ~/covers dir for all supported files found in directory, Lanczos algorithm is used for resizing: * Extract covers to ~/covers dir for all supported files found in the directory, Lanczos algorithm is used for resizing:
cbconvert cover --outdir ~/covers --filter=7 /media/comics/GrooTheWanderer/ `cbconvert cover --outdir ~/covers --filter=7 /media/comics/GrooTheWanderer/`
Compile * Convert all images to AVIF format:
-------
Install imagemagick dev packages: `cbconvert --format avif --quality 50 --width 1280 --outdir ~/comics /media/comics/Misc/`
apt-get install libmagickcore-dev libmagickwand-dev ### Quality settings
Compile latest MuPDF: This table maps quality settings for JPEG to the respective AVIF and WEBP quality settings:
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 | JPEG quality | 50 | 60 | 70 | 80 |
sed -e "1iHAVE_X11 = no" -e "1iWANT_OPENSSL = no" -e "1iWANT_CURL = no" -i Makerules | AVIF quality | 48 | 51 | 56 | 64 |
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 | WEBP quality | 55 | 64 | 72 | 82 |
Compile unarr library: ### Compile
git clone https://github.com/zeniko/unarr && cd unarr You must have `CGO_ENABLED=1`. Note that `Go` will disable cgo when cross-compiling.
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
Install dependencies: Install to `GOBIN` (you can point `GOBIN` to e.g. `/usr/local/bin` or `~/.local/bin`):
go get github.com/cheggaaa/pb `go install github.com/gen2brain/cbconvert/cmd/cbconvert@latest`
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
For command line app: For GUI app, check [IUP](https://github.com/gen2brain/iup-go) requirements, and then install:
go get github.com/gen2brain/cbconvert `go install github.com/gen2brain/cbconvert/cmd/cbconvert-gui@latest`
go build -o $GOPATH/bin/cbconvert github.com/gen2brain/cbconvert/cmd
For GUI app: ### Build tags
go get gopkg.in/qml.v1 * `extlib` - use external `libmupdf` and `libunarr` libraries
go get github.com/gen2brain/cbconvert * `pkgconfig` - enable pkg-config (used with `extlib`)
go build -o $GOPATH/bin/cbconvert github.com/gen2brain/cbconvert/gui
+429 -947
View File
File diff suppressed because it is too large Load Diff
+425
View File
@@ -0,0 +1,425 @@
package cbconvert
import (
"archive/tar"
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/gen2brain/go-unarr"
)
// 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 {
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:]
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755)
if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
zipName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), 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)
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
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 {
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:]
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755)
if err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
tarName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), 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
}
// archiveList lists contents of archive.
func (c *Converter) archiveList(fileName string) ([]string, error) {
var contents []string
archive, err := unarr.NewArchive(fileName)
if err != nil {
return contents, fmt.Errorf("archiveList: %w", err)
}
defer archive.Close()
contents, err = archive.List()
if err != nil {
return contents, fmt.Errorf("archiveList: %w", err)
}
return contents, 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 file to 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
}
+408
View File
@@ -0,0 +1,408 @@
package cbconvert
import (
"bytes"
"context"
"errors"
"fmt"
"image"
"image/png"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"github.com/gen2brain/avif"
"github.com/gen2brain/go-fitz"
"github.com/gen2brain/go-unarr"
"github.com/gen2brain/jpegli"
"github.com/gen2brain/jpegxl"
"github.com/gen2brain/webp"
"github.com/jsummers/gobmp"
"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 {
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
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.Image(n)
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 {
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
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)
archive, err := unarr.NewArchive(fileName)
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
defer archive.Close()
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(runtime.NumCPU() + 1)
for {
if ctx.Err() != nil {
return fmt.Errorf("convertArchive: %w", ctx.Err())
}
err := archive.Entry()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return fmt.Errorf("convertArchive: %w", err)
}
data, err := archive.ReadAll()
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
pathName := archive.Name()
if isImage(pathName) {
if c.Opts.NoConvert {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
continue
}
if cover == pathName && c.Opts.NoCover {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
continue
}
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), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
continue
}
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") {
continue
}
if !c.Opts.NoNonImage {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
}
}
}
err = eg.Wait()
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 {
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
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())
}
file, err := os.Open(img)
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if isNonImage(img) && !c.Opts.NoNonImage {
if err = copyFile(file, filepath.Join(c.Workdir, filepath.Base(img))); 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, filepath.Join(c.Workdir, filepath.Base(img))); 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, filepath.Join(c.Workdir, filepath.Base(img))); 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, img)
})
}
}
}
err = eg.Wait()
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
return nil
}
// 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 = filepath.Join(c.Workdir, fmt.Sprintf("%s.%s", baseNoExt(pathName), ext))
} else {
fileName = filepath.Join(c.Workdir, 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 {
if c.Opts.Fit {
i = fit(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
} else {
i = resize(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
}
}
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":
err = webp.Encode(w, img, webp.Options{Quality: c.Opts.Quality, Method: webp.DefaultMethod})
case "avif":
err = avif.Encode(w, img, avif.Options{Quality: c.Opts.Quality, Speed: avif.DefaultSpeed})
case "jxl":
err = jpegxl.Encode(w, img, jpegxl.Options{Quality: c.Opts.Quality, Effort: jpegxl.DefaultEffort})
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
}
+153
View File
@@ -0,0 +1,153 @@
package cbconvert
import (
"bytes"
"fmt"
"image"
"os"
"path/filepath"
"sort"
"strings"
"github.com/fvbommel/sortorder"
"github.com/gen2brain/go-fitz"
"github.com/gen2brain/go-unarr"
)
// 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)
archive, err := unarr.NewArchive(fileName)
if err != nil {
return nil, fmt.Errorf("coverArchive: %w", err)
}
defer archive.Close()
if err = archive.EntryFor(cover); err != nil {
return nil, fmt.Errorf("coverArchive: %w", err)
}
data, err := archive.ReadAll()
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.Image(0)
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
}
// 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
}
+145
View File
@@ -0,0 +1,145 @@
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 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))
}
// 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
}
+163
View File
@@ -0,0 +1,163 @@
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 that 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,
}
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)
}
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 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{0, 0, 0, 255},
color.RGBA{17, 17, 17, 255},
color.RGBA{34, 34, 34, 255},
color.RGBA{51, 51, 51, 255},
color.RGBA{68, 68, 68, 255},
color.RGBA{85, 85, 85, 255},
color.RGBA{102, 102, 102, 255},
color.RGBA{119, 119, 119, 255},
color.RGBA{136, 136, 136, 255},
color.RGBA{153, 153, 153, 255},
color.RGBA{170, 170, 170, 255},
color.RGBA{187, 187, 187, 255},
color.RGBA{204, 204, 204, 255},
color.RGBA{221, 221, 221, 255},
color.RGBA{238, 238, 238, 255},
color.RGBA{255, 255, 255, 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.Draw(dst, dst.Bounds(), src, b.Min, draw.Src)
return dst
}
+101
View File
@@ -0,0 +1,101 @@
package cbconvert
import (
"fmt"
"os"
"path/filepath"
"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.Path, file.Stat)
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.Path, file.Stat)
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.Path, file.Stat)
if err != nil {
t.Error(err)
}
}
err = os.RemoveAll(tmpDir)
if err != nil {
t.Error(err)
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -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.20241106192421-4d845afa43ca
## 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>

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

+36
View File
@@ -0,0 +1,36 @@
module github.com/gen2brain/cbconvert/cmd/cbconvert-gui
go 1.23
require (
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca
github.com/gen2brain/iup-go/iup v0.0.0-20241106050025-0f971ac33ed4
github.com/godbus/dbus/v5 v5.1.0
)
require (
github.com/anthonynsimon/bild v0.14.0 // 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.8.1 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/gen2brain/avif v0.4.1 // indirect
github.com/gen2brain/go-fitz v1.24.14 // indirect
github.com/gen2brain/go-unarr v0.2.4 // indirect
github.com/gen2brain/jpegli v0.3.3 // indirect
github.com/gen2brain/jpegxl v0.4.2 // indirect
github.com/gen2brain/webp v0.5.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/jupiterrider/ffi v0.2.1 // indirect
github.com/tetratelabs/wazero v1.8.1 // indirect
golang.org/x/image v0.21.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
+81
View File
@@ -0,0 +1,81 @@
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
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.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.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.4.1 h1:fjwv5SDNYHdI1gbW6MJn3Yaxs1ldUEfAIAH8Ahee538=
github.com/gen2brain/avif v0.4.1/go.mod h1:oePci7KPleKZ8X/2rjZ3FlVm2JFYjPwXiQpNgq9wrzs=
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca h1:qYA+cXWUG/Dx+wG2yQ4iuw/s5ROGdXAdo5aIUHrOq4U=
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca/go.mod h1:pQ9kQXsCZQqy7LraruUkBl7CpKNHFpWayxqQ+qliE8Y=
github.com/gen2brain/go-fitz v1.24.14 h1:09weRkjVtLYNGo7l0J7DyOwBExbwi8SJ9h8YPhw9WEo=
github.com/gen2brain/go-fitz v1.24.14/go.mod h1:0KaZeQgASc20Yp5R/pFzyy7SmP01XcoHKNF842U2/S4=
github.com/gen2brain/go-unarr v0.2.4 h1:Iu2kqtGfkLBSQoTFwMkSCmp0g3GrEM/XMVWzo9TQr/Y=
github.com/gen2brain/go-unarr v0.2.4/go.mod h1:0kdy3HtjKBcEaewifXZguHCvt4qD9V8iJCx4FPEOWT8=
github.com/gen2brain/iup-go/iup v0.0.0-20241106050025-0f971ac33ed4 h1:ElkeiufJ5qqFbhxLH0TP7FhLOm37mXezIOk5iJgV27Q=
github.com/gen2brain/iup-go/iup v0.0.0-20241106050025-0f971ac33ed4/go.mod h1:F026AOq3tZrMCqTMa3K32xLh7fyTdc9+KMTMUX6EtX0=
github.com/gen2brain/jpegli v0.3.3 h1:ryCOQpmGuVk6FA+QBe9st6cW48jsRdVOPiNrAJ50m+k=
github.com/gen2brain/jpegli v0.3.3/go.mod h1:6Dbgr+ni1IUBqGVOKHn8lY+6DvwSGfAfC7pPQiSK6uA=
github.com/gen2brain/jpegxl v0.4.2 h1:Ff0jAWtCRdc9yjPc9jkyak6Ji/A89Jg0KI+D7qOEtRI=
github.com/gen2brain/jpegxl v0.4.2/go.mod h1:zIIDnzh7WqG+z66zyzLWQ0M4AS5xi//pyJLgu32GB1o=
github.com/gen2brain/webp v0.5.1 h1:ly9olTGveZEpq3soJuCmex9fxLJ0ipHcQRRSRit5EUE=
github.com/gen2brain/webp v0.5.1/go.mod h1:Nb3xO5sy6MeUAHhru9H3GT7nlOQO5dKRNNlE92CZrJw=
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/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.2.1 h1:08GJVDqz4eoQq7cKT1T0kwb9MB58XEAGjgxDvz80yBs=
github.com/jupiterrider/ffi v0.2.1/go.mod h1:tJ7Q8p/3blFjdWt5qJU4W5oDE0xloImvrViE+0td0Rk=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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=
+959
View File
@@ -0,0 +1,959 @@
package main
import (
"bytes"
"context"
_ "embed"
"errors"
"flag"
"fmt"
"image/gif"
"image/png"
"net/url"
"os"
"path/filepath"
"runtime/debug"
"slices"
"strconv"
"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
)
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("UTF8MODE", "YES")
iup.SetGlobal("UTF8MODE_FILE", "YES")
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("RESIZE_CB", iup.ResizeFunc(func(ih iup.Ihandle, width, height int) int {
iup.GetHandle("Preview").SetAttribute("IMAGE", "logo")
iup.Refresh(ih)
previewPost()
return iup.DEFAULT
}))
iup.Map(dlg)
setActive()
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 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.Format = strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING"))
opts.Width = iup.GetHandle("Width").GetInt("VALUE")
opts.Height = iup.GetHandle("Height").GetInt("VALUE")
opts.Fit = iup.GetHandle("Fit").GetAttribute("VALUE") == "ON"
opts.Filter = iup.GetHandle("Filter").GetInt("VALUE") - 1
opts.Quality = iup.GetHandle("Quality").GetInt("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
}
func setActive() {
opts := options()
count := iup.GetHandle("List").GetInt("COUNT")
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")
}
if opts.OutDir == "" {
iup.GetHandle("Thumbnail").SetAttributes(`ACTIVE=NO`)
iup.GetHandle("Cover").SetAttributes(`ACTIVE=NO`)
iup.GetHandle("Convert").SetAttributes(`ACTIVE=NO`)
if count > 0 {
iup.GetHandle("Thumbnail").SetAttributes(`ACTIVE=NO, TIP="Set Output Directory"`)
iup.GetHandle("Cover").SetAttributes(`ACTIVE=NO, TIP="Set Output Directory"`)
iup.GetHandle("Convert").SetAttributes(`ACTIVE=NO, TIP="Set Output Directory"`)
}
} else {
if count > 0 {
iup.GetHandle("Thumbnail").SetAttributes(`ACTIVE=YES, TIP=""`)
iup.GetHandle("Cover").SetAttributes(`ACTIVE=YES, TIP=""`)
iup.GetHandle("Convert").SetAttributes(`ACTIVE=YES, TIP=""`)
} else {
iup.GetHandle("Thumbnail").SetAttributes(`ACTIVE=NO`)
iup.GetHandle("Cover").SetAttributes(`ACTIVE=NO`)
iup.GetHandle("Convert").SetAttributes(`ACTIVE=NO`)
}
}
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")
}
if (opts.Format == "jpeg" || opts.Format == "webp" || opts.Format == "avif" || opts.Format == "jxl") && !opts.NoConvert {
iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxQuality").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")
}
}
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(),
)
}
func list() iup.Ihandle {
return iup.Vbox(
iup.List().SetAttributes("EXPAND=YES, VISIBLECOLUMNS=16, VISIBLELINES=5").SetHandle("List").
SetCallback("ACTION", iup.ListActionFunc(func(ih iup.Ihandle, text string, item int, state int) int {
if state == 1 {
index = item - 1
setActive()
previewPost()
}
return iup.DEFAULT
})).
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
}
for _, file := range fs {
iup.SetAttribute(iup.GetHandle("List"), "APPENDITEM", fmt.Sprintf("%s (%s)", file.Name, file.SizeHuman))
files = append(files, file)
}
setActive()
return iup.DEFAULT
})),
)
}
func previewPost() {
if index == -1 || len(files) == 0 {
return
}
width, height := previewSize()
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
if strings.ToLower(iup.GetGlobal("DRIVER")) == "motif" {
iup.GetHandle("Preview").SetAttribute("IMAGE", "")
}
opts := options()
go func(opts cbconvert.Options) {
conv := cbconvert.New(opts)
var s string
file := files[index]
img, err := conv.Preview(file.Path, file.Stat, width, height)
if err != nil {
s = err.Error()
fmt.Println(err)
}
iup.PostMessage(iup.GetHandle("Preview"), s, 0, img)
}(opts)
}
func previewSize() (int, int) {
var width, height int
sp := strings.Split(iup.GetHandle("Preview").GetAttribute("RASTERSIZE"), "x")
if len(sp) == 2 {
width, _ = strconv.Atoi(sp[0])
height, _ = strconv.Atoi(sp[1])
}
return width, height
}
func preview() iup.Ihandle {
return iup.Frame(
iup.Vbox(
iup.Label("").SetAttributes("EXPAND=YES, ALIGNMENT=ACENTER, MINSIZE=400x, IMAGE=cover").SetHandle("Preview").
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
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")
ih.SetAttribute("IMAGE", "cover")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height))
} else {
ih.SetAttribute("IMAGE", "logo")
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:], ": ")))
}
}
return iup.DEFAULT
})),
iup.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"),
),
)
}
func tabs() iup.Ihandle {
vboxInput := 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
})),
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.Space().SetAttributes("EXPAND=HORIZONTAL"),
).SetHandle("VboxInput").SetAttributes("MARGIN=5x5, GAP=5")
vboxOutput := iup.Vbox(
iup.Vbox(
iup.Label("Output Directory:"),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutDir").
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"),
),
).SetHandle("VboxOutput").SetAttributes("MARGIN=5x5, GAP=5")
vboxImage := 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").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
previewPost()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Size:"),
iup.Hbox(
iup.Text().SetAttributes(`CUEBANNER=" width", VISIBLECOLUMNS=4, 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.Label("x"),
iup.Text().SetAttributes(`CUEBANNER=" height", VISIBLECOLUMNS=4, 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, MARGIN=0"),
),
iup.Vbox(
iup.Toggle(" Best Fit").SetHandle("Fit").
SetAttributes(`TIP="Best fit for required width and height"`),
),
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)),
),
iup.Vbox(
iup.Hbox(
iup.Label("Quality: "),
iup.Label("75").SetHandle("LabelQuality"),
).SetAttributes("MARGIN=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"))
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.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
})),
),
).SetHandle("VboxImage").SetAttributes("MARGIN=5x5, GAP=5")
vboxTransform := iup.Vbox(
iup.Vbox(
iup.Hbox(
iup.Label("Brightness: "),
iup.Label("0").SetHandle("LabelBrightness"),
).SetAttributes("ALIGNMENT=ACENTER, MARGIN=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"))
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, MARGIN=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"))
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").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
previewPost()
return iup.DEFAULT
})),
),
).SetHandle("VboxTransform").SetAttributes("MARGIN=5x5, GAP=5")
return iup.Tabs(
vboxInput.SetAttributes("TABTITLE=Input"),
vboxOutput.SetAttributes("TABTITLE=Output"),
vboxImage.SetAttributes("TABTITLE=Image"),
vboxTransform.SetAttributes("TABTITLE=Transform"),
).SetHandle("Tabs").SetAttributes("MINSIZE=320x400, EXPAND=HORIZONTAL, MULTILINE=YES")
}
func buttons() iup.Ihandle {
return iup.Vbox(
iup.Frame(
iup.Vbox(
iup.Button("Add &Files...").SetHandle("AddFiles").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddFiles)),
iup.Button("Add &Dir...").SetHandle("AddDir").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddDir)),
iup.Button("Remove").SetHandle("Remove").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemove)),
iup.Button("Remove All").SetHandle("RemoveAll").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemoveAll)),
).SetAttributes("NGAP=5"),
),
iup.Frame(
iup.Vbox(
iup.Button("Thumbnail").SetHandle("Thumbnail").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onThumbnail)),
iup.Button("Cover").SetHandle("Cover").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onCover)),
).SetAttributes("NGAP=5"),
),
iup.Frame(
iup.Vbox(
iup.Button("&Convert").SetHandle("Convert").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onConvert)),
),
),
).SetHandle("Buttons").SetAttributes("ALIGNMENT=ACENTER, NGAP=10")
}
func status() iup.Ihandle {
return iup.Hbox(
loading(),
iup.Fill(),
iup.Label("File 1 of 1").SetHandle("LabelStatus1").SetAttributes("VISIBLE=NO"),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.Label("(000/000)").SetHandle("LabelStatus2").SetAttributes("VISIBLE=NO"),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.ProgressBar().SetAttributes("RASTERSIZE=200x15, 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("List").SetAttributes("ACTIVE=NO")
iup.GetHandle("Tabs").SetAttributes("ACTIVE=NO")
iup.GetHandle("Buttons").SetAttributes("ACTIVE=NO")
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("List").SetAttributes("ACTIVE=NO")
iup.GetHandle("Tabs").SetAttributes("ACTIVE=NO")
iup.GetHandle("Buttons").SetAttributes("ACTIVE=NO")
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":
iup.GetHandle("List").SetAttributes("ACTIVE=YES")
iup.GetHandle("Tabs").SetAttributes("ACTIVE=YES")
iup.GetHandle("Buttons").SetAttributes("ACTIVE=YES")
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")
}
func onAddFiles(ih iup.Ihandle) int {
args, err := fileDlg("Add Files", true, false)
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
}
for _, file := range fs {
iup.SetAttribute(iup.GetHandle("List"), "APPENDITEM", fmt.Sprintf("%s (%s)", file.Name, file.SizeHuman))
files = append(files, file)
}
setActive()
}
return iup.DEFAULT
}
func onAddDir(ih iup.Ihandle) int {
args, err := fileDlg("Add Directory", false, true)
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
}
for _, file := range fs {
iup.SetAttribute(iup.GetHandle("List"), "APPENDITEM", fmt.Sprintf("%s (%s)", file.Name, file.SizeHuman))
files = append(files, file)
}
setActive()
}
return iup.DEFAULT
}
func onRemove(ih iup.Ihandle) int {
if index == -1 || len(files) == 0 {
return iup.IGNORE
}
if len(files) == 1 {
files = make([]cbconvert.File, 0)
} else {
files = slices.Delete(files, index, index)
}
iup.GetHandle("List").SetAttribute("REMOVEITEM", iup.GetHandle("List").GetAttribute("VALUE"))
setActive()
return iup.DEFAULT
}
func onRemoveAll(ih iup.Ihandle) int {
index = -1
files = make([]cbconvert.File, 0)
iup.GetHandle("List").SetAttribute("REMOVEITEM", "ALL")
setActive()
return iup.DEFAULT
}
func onThumbnail(ih iup.Ihandle) int {
conv := cbconvert.New(options())
conv.Nfiles = len(files)
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.Path, file.Stat); 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)
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.Path, file.Stat); 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 {
conv := cbconvert.New(options())
conv.Nfiles = len(files)
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
}))
go func(c *cbconvert.Converter) {
for _, file := range files {
if err := c.Convert(file.Path, file.Stat); err != nil {
if errors.Is(err, context.Canceled) {
if err := os.RemoveAll(c.Workdir); err != nil {
fmt.Println(err)
}
break
}
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
if err := os.RemoveAll(c.Workdir); err != nil {
fmt.Println(err)
}
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)
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 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
}
+65
View File
@@ -0,0 +1,65 @@
//go:build !portal
package main
import (
"path/filepath"
"strings"
"github.com/gen2brain/iup-go/iup"
)
func fileDlg(title string, multiple, directory bool) ([]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,
"EXTFILTER": "Comic Files|*.rar;*.zip;*.7z;*.tar;*.cbr;*.cbz;*.cb7;*.cbt;*.pdf;*.epub;*.mobi;*.docx;*.pptx|",
"FILTER": "*.cb*", // for Motif
"TITLE": title,
})
} else {
dlg.SetAttributes(map[string]string{
"DIALOGTYPE": "DIR",
"TITLE": title,
})
}
iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT)
if dlg.GetInt("STATUS") == 0 {
if !directory {
value := dlg.GetAttribute("VALUE")
sp := strings.Split(value, "|")
if strings.ToLower(iup.GetGlobal("DRIVER")) == "cocoa" {
for _, file := range sp {
ret = append(ret, file)
}
} else {
if len(sp) > 1 {
for _, file := range sp[1 : len(sp)-1] {
ret = append(ret, filepath.Join(sp[0], file))
}
} else {
ret = append(ret, value)
}
}
} else {
value := dlg.GetAttribute("VALUE")
ret = append(ret, value)
}
}
return ret, nil
}
+106
View File
@@ -0,0 +1,106 @@
//go:build portal
package main
import (
"net/url"
"github.com/godbus/dbus/v5"
)
func fileDlg(title string, multiple, directory bool) ([]string, error) {
ret := make([]string, 0)
conn, err := dbus.ConnectSessionBus()
if err != nil {
return ret, err
}
defer conn.Close()
dest := "org.freedesktop.portal.Desktop"
path := "/org/freedesktop/portal/desktop"
resp := "org.freedesktop.portal.Request.Response"
if err = conn.AddMatchSignal(
dbus.WithMatchInterface(dest),
dbus.WithMatchObjectPath(dbus.ObjectPath(path)),
dbus.WithMatchSender(conn.Names()[0]),
); err != nil {
return ret, err
}
c := make(chan *dbus.Signal, 10)
conn.Signal(c)
type Item struct {
Index uint32
Filter string
}
type Filter struct {
Title string
Filters []Item
}
filters := []Filter{
{
"Comic Files",
[]Item{
Item{0, "*.rar"},
Item{0, "*.zip"},
Item{0, "*.7z"},
Item{0, "*.tar"},
Item{0, "*.cbr"},
Item{0, "*.cbz"},
Item{0, "*.cb7"},
Item{0, "*.cbt"},
Item{0, "*.pdf"},
Item{0, "*.epub"},
Item{0, "*.mobi"},
Item{0, "*.docx"},
Item{0, "*.pptx"},
},
},
}
opts := map[string]any{
"multiple": multiple,
"directory": directory,
}
if !directory {
opts["filters"] = filters
}
obj := conn.Object(dest, dbus.ObjectPath(path))
call := obj.Call("org.freedesktop.portal.FileChooser.OpenFile", 0, "", title, opts)
if call.Err != nil {
return ret, call.Err
}
for v := range c {
if v.Name != resp {
continue
}
status := v.Body[0].(uint32)
if status == 0 {
m := v.Body[1].(map[string]dbus.Variant)
uris := m["uris"].Value().([]string)
for _, uri := range uris {
u, err := url.ParseRequestURI(uri)
if err != nil {
return ret, err
}
ret = append(ret, u.Path)
}
}
break
}
return ret, nil
}
Binary file not shown.
+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

+39
View File
@@ -0,0 +1,39 @@
module github.com/gen2brain/cbconvert/cmd/cbconvert
go 1.23
require (
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca
github.com/schollz/progressbar/v3 v3.13.1
)
require (
github.com/anthonynsimon/bild v0.14.0 // 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.8.1 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/gen2brain/avif v0.4.1 // indirect
github.com/gen2brain/go-fitz v1.24.14 // indirect
github.com/gen2brain/go-unarr v0.2.4 // indirect
github.com/gen2brain/jpegli v0.3.3 // indirect
github.com/gen2brain/jpegxl v0.4.2 // indirect
github.com/gen2brain/webp v0.5.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/jupiterrider/ffi v0.2.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/rivo/uniseg v0.3.4 // indirect
github.com/tetratelabs/wazero v1.8.1 // indirect
golang.org/x/image v0.21.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/term v0.25.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
+102
View File
@@ -0,0 +1,102 @@
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
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/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.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.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.4.1 h1:fjwv5SDNYHdI1gbW6MJn3Yaxs1ldUEfAIAH8Ahee538=
github.com/gen2brain/avif v0.4.1/go.mod h1:oePci7KPleKZ8X/2rjZ3FlVm2JFYjPwXiQpNgq9wrzs=
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca h1:qYA+cXWUG/Dx+wG2yQ4iuw/s5ROGdXAdo5aIUHrOq4U=
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca/go.mod h1:pQ9kQXsCZQqy7LraruUkBl7CpKNHFpWayxqQ+qliE8Y=
github.com/gen2brain/go-fitz v1.24.14 h1:09weRkjVtLYNGo7l0J7DyOwBExbwi8SJ9h8YPhw9WEo=
github.com/gen2brain/go-fitz v1.24.14/go.mod h1:0KaZeQgASc20Yp5R/pFzyy7SmP01XcoHKNF842U2/S4=
github.com/gen2brain/go-unarr v0.2.4 h1:Iu2kqtGfkLBSQoTFwMkSCmp0g3GrEM/XMVWzo9TQr/Y=
github.com/gen2brain/go-unarr v0.2.4/go.mod h1:0kdy3HtjKBcEaewifXZguHCvt4qD9V8iJCx4FPEOWT8=
github.com/gen2brain/jpegli v0.3.3 h1:ryCOQpmGuVk6FA+QBe9st6cW48jsRdVOPiNrAJ50m+k=
github.com/gen2brain/jpegli v0.3.3/go.mod h1:6Dbgr+ni1IUBqGVOKHn8lY+6DvwSGfAfC7pPQiSK6uA=
github.com/gen2brain/jpegxl v0.4.2 h1:Ff0jAWtCRdc9yjPc9jkyak6Ji/A89Jg0KI+D7qOEtRI=
github.com/gen2brain/jpegxl v0.4.2/go.mod h1:zIIDnzh7WqG+z66zyzLWQ0M4AS5xi//pyJLgu32GB1o=
github.com/gen2brain/webp v0.5.1 h1:ly9olTGveZEpq3soJuCmex9fxLJ0ipHcQRRSRit5EUE=
github.com/gen2brain/webp v0.5.1/go.mod h1:Nb3xO5sy6MeUAHhru9H3GT7nlOQO5dKRNNlE92CZrJw=
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-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
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.2.1 h1:08GJVDqz4eoQq7cKT1T0kwb9MB58XEAGjgxDvz80yBs=
github.com/jupiterrider/ffi v0.2.1/go.mod h1:tJ7Q8p/3blFjdWt5qJU4W5oDE0xloImvrViE+0td0Rk=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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/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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw=
github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE=
github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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=
+325
View File
@@ -0,0 +1,325 @@
package main
import (
"bufio"
"flag"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"runtime/debug"
"syscall"
"github.com/gen2brain/cbconvert"
pb "github.com/schollz/progressbar/v3"
)
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)
}
var bar *pb.ProgressBar
if opts.Cover || opts.Thumbnail || opts.Meta {
if !opts.Quiet {
bar = pb.NewOptions(conv.Nfiles,
pb.OptionShowCount(),
pb.OptionClearOnFinish(),
pb.OptionUseANSICodes(true),
pb.OptionSetPredictTime(false),
)
}
}
conv.OnStart = func() {
if !opts.Quiet {
bar = pb.NewOptions(conv.Ncontents,
pb.OptionShowCount(),
pb.OptionClearOnFinish(),
pb.OptionUseANSICodes(true),
pb.OptionSetDescription(fmt.Sprintf("Converting %d of %d:", conv.CurrFile, conv.Nfiles)),
pb.OptionSetPredictTime(false),
)
}
}
conv.OnProgress = func() {
if !opts.Quiet {
_ = bar.Add(1)
}
}
conv.OnCompress = func() {
if !opts.Quiet {
fmt.Fprintf(os.Stderr, "Compressing %d of %d...\r", conv.CurrFile, conv.Nfiles)
}
}
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.Path, file.Stat); err != nil {
fmt.Println(err)
os.Exit(1)
}
continue
case opts.Thumbnail:
if err = conv.Thumbnail(file.Path, file.Stat); err != nil {
fmt.Println(err)
os.Exit(1)
}
continue
}
if err := conv.Convert(file.Path, file.Stat); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
fmt.Fprintf(os.Stderr, "\r")
}
// parseFlags parses command line flags.
func parseFlags() (cbconvert.Options, []string) {
opts := cbconvert.Options{}
var args []string
convert := flag.NewFlagSet("convert", flag.ExitOnError)
convert.IntVar(&opts.Width, "width", 0, "Image width")
convert.IntVar(&opts.Height, "height", 0, "Image height")
convert.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
convert.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl")
convert.StringVar(&opts.Archive, "archive", "zip", "Archive format, valid values are zip, tar")
convert.IntVar(&opts.Quality, "quality", 75, "Image quality")
convert.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos")
convert.BoolVar(&opts.NoCover, "no-cover", false, "Do not convert the cover image")
convert.BoolVar(&opts.NoRGB, "no-rgb", false, "Do not convert images that have RGB colorspace")
convert.BoolVar(&opts.NoNonImage, "no-nonimage", false, "Remove non-image files from the archive")
convert.BoolVar(&opts.NoConvert, "no-convert", false, "Do not transform or convert images")
convert.BoolVar(&opts.Grayscale, "grayscale", false, "Convert images to grayscale (monochromatic)")
convert.IntVar(&opts.Rotate, "rotate", 0, "Rotate images, valid values are 0, 90, 180, 270")
convert.IntVar(&opts.Brightness, "brightness", 0, "Adjust the brightness of the images, must be in the range (-100, 100)")
convert.IntVar(&opts.Contrast, "contrast", 0, "Adjust the contrast of the images, must be in the range (-100, 100)")
convert.StringVar(&opts.Suffix, "suffix", "", "Add suffix to file basename")
convert.StringVar(&opts.OutDir, "outdir", ".", "Output directory")
convert.IntVar(&opts.Size, "size", 0, "Process only files larger than size (in MB)")
convert.BoolVar(&opts.Recursive, "recursive", false, "Process subdirectories recursively")
convert.BoolVar(&opts.Quiet, "quiet", false, "Hide console output")
cover := flag.NewFlagSet("cover", flag.ExitOnError)
cover.IntVar(&opts.Width, "width", 0, "Image width")
cover.IntVar(&opts.Height, "height", 0, "Image height")
cover.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
cover.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif")
cover.IntVar(&opts.Quality, "quality", 75, "Image quality")
cover.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos")
cover.StringVar(&opts.OutDir, "outdir", ".", "Output directory")
cover.IntVar(&opts.Size, "size", 0, "Process only files larger than size (in MB)")
cover.BoolVar(&opts.Recursive, "recursive", false, "Process subdirectories recursively")
cover.BoolVar(&opts.Quiet, "quiet", false, "Hide console output")
thumbnail := flag.NewFlagSet("thumbnail", flag.ExitOnError)
thumbnail.IntVar(&opts.Width, "width", 0, "Image width")
thumbnail.IntVar(&opts.Height, "height", 0, "Image height")
thumbnail.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
thumbnail.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos")
thumbnail.StringVar(&opts.OutDir, "outdir", ".", "Output directory")
thumbnail.StringVar(&opts.OutFile, "outfile", "", "Output file")
thumbnail.IntVar(&opts.Size, "size", 0, "Process only files larger than size (in MB)")
thumbnail.BoolVar(&opts.Recursive, "recursive", false, "Process subdirectories recursively")
thumbnail.BoolVar(&opts.Quiet, "quiet", false, "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{"width", "height", "fit", "format", "archive", "quality", "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{"width", "height", "fit", "format", "quality", "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{"width", "height", "fit", "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":
_ = convert.Parse(os.Args[2:])
if !pipe {
args = convert.Args()
}
case "cover":
opts.Cover = true
_ = cover.Parse(os.Args[2:])
if !pipe {
args = cover.Args()
}
case "thumbnail":
opts.Thumbnail = true
_ = thumbnail.Parse(os.Args[2:])
if !pipe {
args = thumbnail.Args()
}
case "meta":
opts.Meta = true
_ = meta.Parse(os.Args[2:])
if !pipe {
args = meta.Args()
}
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
}
// piped checks if we have a 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 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
}
-149
View File
@@ -1,149 +0,0 @@
// Author: Milan Nikolic <gen2brain@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
//go:generate goversioninfo
import (
"fmt"
"image/jpeg"
"os"
"os/signal"
"strconv"
"syscall"
"github.com/cheggaaa/pb"
"github.com/gen2brain/cbconvert"
"gopkg.in/alecthomas/kingpin.v2"
)
// Parses command line flags
func parseFlags() (cbconvert.Options, []string) {
opts := cbconvert.Options{}
var args []string
kingpin.Version("CBconvert 0.5.0")
kingpin.CommandLine.Help = "Comic Book convert tool."
kingpin.UsageTemplate(kingpin.CompactUsageTemplate)
kingpin.Flag("outdir", "Output directory").Default(".").StringVar(&opts.Outdir)
kingpin.Flag("size", "Process only files larger then size (in MB)").Default(strconv.Itoa(0)).Int64Var(&opts.Size)
kingpin.Flag("recursive", "Process subdirectories recursively").BoolVar(&opts.Recursive)
kingpin.Flag("quiet", "Hide console output").BoolVar(&opts.Quiet)
convert := kingpin.Command("convert", "Convert archive or document (default command)").Default()
convert.Arg("args", "filename or directory").Required().ExistingFilesOrDirsVar(&args)
convert.Flag("width", "Image width").Default(strconv.Itoa(0)).IntVar(&opts.Width)
convert.Flag("height", "Image height").Default(strconv.Itoa(0)).IntVar(&opts.Height)
convert.Flag("fit", "Best fit for required width and height").BoolVar(&opts.Fit)
convert.Flag("format", "Image format, valid values are jpeg, png, gif, tiff, bmp").Default("jpeg").StringVar(&opts.Format)
convert.Flag("quality", "JPEG image quality").Default(strconv.Itoa(jpeg.DefaultQuality)).IntVar(&opts.Quality)
convert.Flag("filter", "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos").Default(strconv.Itoa(cbconvert.Linear)).IntVar(&opts.Filter)
convert.Flag("cover", "Convert cover image (use --no-cover if you want to exclude cover)").Default("true").BoolVar(&opts.ConvertCover)
convert.Flag("rgb", "Convert images that have RGB colorspace (use --no-rgb if you only want to convert grayscaled images)").Default("true").BoolVar(&opts.RGB)
convert.Flag("nonimage", "Leave non image files in archive (use --no-nonimage to remove non image files from archive)").Default("true").BoolVar(&opts.NonImage)
convert.Flag("grayscale", "Convert images to grayscale (monochromatic)").BoolVar(&opts.Grayscale)
convert.Flag("rotate", "Rotate images, valid values are 0, 90, 180, 270").Default(strconv.Itoa(0)).IntVar(&opts.Rotate)
convert.Flag("flip", "Flip images, valid values are none, horizontal, vertical").Default("none").StringVar(&opts.Flip)
convert.Flag("brightness", "Adjust brightness of the images, must be in range (-100, 100)").Default(strconv.Itoa(0)).Float64Var(&opts.Brightness)
convert.Flag("contrast", "Adjust contrast of the images, must be in range (-100, 100)").Default(strconv.Itoa(0)).Float64Var(&opts.Contrast)
convert.Flag("suffix", "Add suffix to file basename").StringVar(&opts.Suffix)
convert.Flag("levels-inmin", "Shadow input value").Default(strconv.Itoa(0)).Float64Var(&opts.LevelsInMin)
convert.Flag("levels-gamma", "Midpoint/Gamma").Default(strconv.Itoa(1.00)).Float64Var(&opts.LevelsGamma)
convert.Flag("levels-inmax", "Highlight input value").Default(strconv.Itoa(255)).Float64Var(&opts.LevelsInMax)
convert.Flag("levels-outmin", "Shadow output value").Default(strconv.Itoa(0)).Float64Var(&opts.LevelsOutMin)
convert.Flag("levels-outmax", "Highlight output value").Default(strconv.Itoa(255)).Float64Var(&opts.LevelsOutMax)
cover := kingpin.Command("cover", "Extract cover")
cover.Arg("args", "filename or directory").Required().ExistingFilesOrDirsVar(&args)
cover.Flag("width", "Image width").Default(strconv.Itoa(0)).IntVar(&opts.Width)
cover.Flag("height", "Image height").Default(strconv.Itoa(0)).IntVar(&opts.Height)
cover.Flag("fit", "Best fit for required width and height").BoolVar(&opts.Fit)
cover.Flag("quality", "JPEG image quality").Default(strconv.Itoa(jpeg.DefaultQuality)).IntVar(&opts.Quality)
cover.Flag("filter", "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos").Default(strconv.Itoa(cbconvert.Linear)).IntVar(&opts.Filter)
thumbnail := kingpin.Command("thumbnail", "Extract cover thumbnail (freedesktop spec.)")
thumbnail.Arg("args", "filename or directory").Required().ExistingFilesOrDirsVar(&args)
thumbnail.Flag("outfile", "Output file").Default("").StringVar(&opts.Outfile)
thumbnail.Flag("width", "Image width").Default(strconv.Itoa(0)).IntVar(&opts.Width)
thumbnail.Flag("height", "Image height").Default(strconv.Itoa(0)).IntVar(&opts.Height)
thumbnail.Flag("fit", "Best fit for required width and height").BoolVar(&opts.Fit)
thumbnail.Flag("filter", "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos").Default(strconv.Itoa(cbconvert.Linear)).IntVar(&opts.Filter)
switch kingpin.Parse() {
case "cover":
opts.Cover = true
case "thumbnail":
opts.Thumbnail = true
}
return opts, args
}
func main() {
opts, args := parseFlags()
conv := cbconvert.NewConvertor(opts)
var bar *pb.ProgressBar
c := make(chan os.Signal, 3)
signal.Notify(c, os.Interrupt, syscall.SIGHUP, syscall.SIGTERM)
go func() {
for _ = range c {
fmt.Fprintf(os.Stderr, "Aborting\n")
os.RemoveAll(conv.Workdir)
os.Exit(1)
}
}()
if _, err := os.Stat(opts.Outdir); err != nil {
os.MkdirAll(opts.Outdir, 0777)
}
files := conv.GetFiles(args)
if opts.Cover || opts.Thumbnail {
if !opts.Quiet {
bar = pb.New(conv.Nfiles)
bar.ShowTimeLeft = false
bar.Start()
}
}
for _, file := range files {
stat, err := os.Stat(file)
if err != nil {
fmt.Fprintf(os.Stderr, "Error Stat: %v\n", err.Error())
continue
}
if opts.Cover {
conv.ExtractCover(file, stat)
if !opts.Quiet {
bar.Increment()
}
continue
} else if opts.Thumbnail {
conv.ExtractThumbnail(file, stat)
if !opts.Quiet {
bar.Increment()
}
continue
}
conv.ConvertComic(file, stat)
}
}
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env bash
CHROOT="/home/milann/chroot"
MINGW="/usr/i686-w64-mingw32"
mkdir -p build
rm -f resource.syso
LIBRARY_PATH="$CHROOT/usr/lib:$CHROOT/lib" \
PKG_CONFIG_PATH="$CHROOT/usr/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$CHROOT/usr/lib/pkgconfig" \
CGO_LDFLAGS="-L$CHROOT/usr/lib -L$CHROOT/lib" \
CC_FOR_TARGET="x86_64-pc-linux-gnu-gcc" CXX_FOR_TARGET="x86_64-pc-linux-gnu-g++" \
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -v -x -o build/cbconvert
strip build/cbconvert
go generate
PKG_CONFIG="/usr/bin/i686-w64-mingw32-pkg-config" \
PKG_CONFIG_PATH="$MINGW/usr/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$MINGW/usr/lib/pkgconfig" \
CGO_LDFLAGS="-L$MINGW/usr/lib" \
CGO_CFLAGS="-I$MINGW/usr/include -Wno-poison-system-directories" \
CGO_CXXFLAGS="-I$MINGW/usr/include -Wno-poison-system-directories" \
CGO_CPPFLAGS="-I$MINGW/usr/include -Wno-poison-system-directories" \
CC="i686-w64-mingw32-gcc" CXX="i686-w64-mingw32-g++" \
CC_FOR_TARGET="i686-w64-mingw32-gcc" CXX_FOR_TARGET="i686-w64-mingw32-g++" \
CGO_ENABLED=1 GOOS=windows GOARCH=386 go build -v -x -o build/cbconvert.exe -ldflags "-linkmode external '-extldflags=-static -Wl,--allow-multiple-definition'"
i686-w64-mingw32-strip build/cbconvert.exe
-44
View File
@@ -1,44 +0,0 @@
{
"FixedFileInfo":
{
"FileVersion": {
"Major": 0,
"Minor": 6,
"Patch": 0,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 6,
"Patch": 0,
"Build": 0
},
"FileFlagsMask": "3f",
"FileFlags ": "00",
"FileOS": "040004",
"FileType": "01",
"FileSubType": "00"
},
"StringFileInfo":
{
"Comments": "Comic Book converter",
"CompanyName": "",
"FileDescription": "CBconvert CLI",
"FileVersion": "0.6.0",
"InternalName": "",
"LegalCopyright": "",
"LegalTrademarks": "",
"OriginalFilename": "cbconvert.exe",
"PrivateBuild": "",
"ProductName": "CBconvert",
"ProductVersion": "0.6.0",
"SpecialBuild": ""
},
"VarFileInfo":
{
"Translation": {
"LangID": "0409",
"CharsetID": "04B0"
}
}
}
+32
View File
@@ -0,0 +1,32 @@
module github.com/gen2brain/cbconvert
go 1.23
require (
github.com/anthonynsimon/bild v0.14.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.4.1
github.com/gen2brain/go-fitz v1.24.14
github.com/gen2brain/go-unarr v0.2.3
github.com/gen2brain/jpegli v0.3.3
github.com/gen2brain/jpegxl v0.4.2
github.com/gen2brain/webp v0.5.1
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25
golang.org/x/image v0.21.0
golang.org/x/sync v0.8.0
)
require (
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.8.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/jupiterrider/ffi v0.2.1 // indirect
github.com/tetratelabs/wazero v1.8.1 // indirect
golang.org/x/net v0.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
+79
View File
@@ -0,0 +1,79 @@
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
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.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.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.4.0 h1:JuwAX2rVrkAzQrZx9lpIKx/ovCO35gCUquarfJ6uhHc=
github.com/gen2brain/avif v0.4.0/go.mod h1:oePci7KPleKZ8X/2rjZ3FlVm2JFYjPwXiQpNgq9wrzs=
github.com/gen2brain/avif v0.4.1 h1:fjwv5SDNYHdI1gbW6MJn3Yaxs1ldUEfAIAH8Ahee538=
github.com/gen2brain/avif v0.4.1/go.mod h1:oePci7KPleKZ8X/2rjZ3FlVm2JFYjPwXiQpNgq9wrzs=
github.com/gen2brain/go-fitz v1.24.14 h1:09weRkjVtLYNGo7l0J7DyOwBExbwi8SJ9h8YPhw9WEo=
github.com/gen2brain/go-fitz v1.24.14/go.mod h1:0KaZeQgASc20Yp5R/pFzyy7SmP01XcoHKNF842U2/S4=
github.com/gen2brain/go-unarr v0.2.3 h1:VwZg0P6Dc/8Uh51McjVhzUMg4wHwwbiyqjEFsFELc0c=
github.com/gen2brain/go-unarr v0.2.3/go.mod h1:hoHheVuf0KT8/hfvkEL7GMwj2h7fq0lF72NdyySdr3c=
github.com/gen2brain/jpegli v0.3.3 h1:ryCOQpmGuVk6FA+QBe9st6cW48jsRdVOPiNrAJ50m+k=
github.com/gen2brain/jpegli v0.3.3/go.mod h1:6Dbgr+ni1IUBqGVOKHn8lY+6DvwSGfAfC7pPQiSK6uA=
github.com/gen2brain/jpegxl v0.4.1 h1:jWaVp5GkXLJXjRvC+g0R9+uo+xoWiS7DYKxJ3n5gHL4=
github.com/gen2brain/jpegxl v0.4.1/go.mod h1:zIIDnzh7WqG+z66zyzLWQ0M4AS5xi//pyJLgu32GB1o=
github.com/gen2brain/jpegxl v0.4.2 h1:Ff0jAWtCRdc9yjPc9jkyak6Ji/A89Jg0KI+D7qOEtRI=
github.com/gen2brain/jpegxl v0.4.2/go.mod h1:zIIDnzh7WqG+z66zyzLWQ0M4AS5xi//pyJLgu32GB1o=
github.com/gen2brain/webp v0.5.0 h1:nn3o0BtKltoFKX9rlDZG/Y/aWqNzUZVyXdB815yVNfU=
github.com/gen2brain/webp v0.5.0/go.mod h1:Nb3xO5sy6MeUAHhru9H3GT7nlOQO5dKRNNlE92CZrJw=
github.com/gen2brain/webp v0.5.1 h1:ly9olTGveZEpq3soJuCmex9fxLJ0ipHcQRRSRit5EUE=
github.com/gen2brain/webp v0.5.1/go.mod h1:Nb3xO5sy6MeUAHhru9H3GT7nlOQO5dKRNNlE92CZrJw=
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-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
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.2.1 h1:08GJVDqz4eoQq7cKT1T0kwb9MB58XEAGjgxDvz80yBs=
github.com/jupiterrider/ffi v0.2.1/go.mod h1:tJ7Q8p/3blFjdWt5qJU4W5oDE0xloImvrViE+0td0Rk=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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=
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

-965
View File
@@ -1,965 +0,0 @@
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
import Qt.labs.settings 1.0
ApplicationWindow {
id: applicationWindow
visible: true
width: 800
height: 600
title: "CBconvert"
property int margin: 15
property int screenWidth: Screen.width
property int screenHeight: Screen.height
function updateImage() {
imagePreview.source = ""
sizePreview.text = ""
if(groupBoxPreview.checked) {
if(c.len > 0) {
imagePreview.source = "image://cover/" + c.get(listView.currentIndex).path
}
}
}
ColumnLayout {
id: splitView1
anchors.rightMargin: margin
anchors.leftMargin: margin
anchors.bottomMargin: margin
anchors.topMargin: margin
anchors.fill: parent
RowLayout {
id: rowLayout1
anchors.bottom: rowLayout2.top
anchors.right: parent.right
anchors.left: parent.left
anchors.top: parent.top
anchors.bottomMargin: 15
Layout.minimumHeight: 250
ScrollView {
id: scrollview
anchors.right: columnButtons.left
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
anchors.rightMargin: margin
frameVisible: true
highlightOnFocus: false
verticalScrollBarPolicy: 2
flickableItem.interactive: true
focus: true
ListView {
id: listView
anchors.topMargin: 0
anchors.fill: parent
spacing: 1
focus: true
model: c.len
header: Rectangle {
height: 20
width: ListView.view.width
color: "#DCDCDC"
Text {
id: nameHeader
text: 'Name'
anchors.left: parent.left
anchors.leftMargin: 5
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
onClicked: {
c.byName()
}
}
}
Text {
id: typeHeader
text: 'Type'
width: 40
anchors.right: sizeHeader.left
anchors.rightMargin: 40
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
onClicked: {
c.byType()
}
}
}
Text {
id: sizeHeader
text: 'Size'
width: 40
anchors.right: parent.right
anchors.rightMargin: 15
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
onClicked: {
c.bySize()
}
}
}
}
delegate: Item {
id: item1
width: ListView.view.width
height: 20
Text {
id: nameItem
text: c.get(index).name
anchors.left: parent.left
anchors.leftMargin: 5
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 170
elide: Text.ElideRight
}
Text {
id: typeItem
width: 40
text: c.get(index).type
anchors.right: sizeItem.left
anchors.rightMargin: 40
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
}
Text {
id: sizeItem
width: 40
text: c.get(index).sizeHuman
anchors.right: parent.right
anchors.rightMargin: 15
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
anchors.fill: parent
onClicked: {
listView.currentIndex = index
listView.forceActiveFocus()
}
}
}
highlight: Rectangle {
width: ListView.view ? ListView.view.width : undefined
color: "#326686"
opacity: 0.2
y: listView.currentItem.y
Behavior on y {
SpringAnimation {
spring: 3
damping: 0.2
}
}
}
onCurrentItemChanged: updateImage()
}
}
ColumnLayout {
id: columnButtons
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
anchors.top: parent.top
anchors.topMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
ColumnLayout {
id: columnButtonsFiles
anchors.right: parent.right
anchors.rightMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.top: parent.top
anchors.topMargin: 0
spacing: 5
Button {
id: buttonAddFile
objectName: "buttonAddFile"
text: "Add &Files"
onClicked: {
fileDialogFile.open()
}
}
Button {
id: buttonAddDir
objectName: "buttonAddDir"
text: "Add &Dir"
onClicked: {
fileDialogDir.open()
}
}
Button {
id: buttonRemove
objectName: "buttonRemove"
text: "Remove"
enabled: (c.len !== 0) ? true : false
onClicked: {
c.remove(listView.currentIndex)
updateImage()
}
}
Button {
id: buttonRemoveAll
objectName: "buttonRemoveAll"
text: "Remove All"
enabled: (c.len !== 0) ? true : false
onClicked: {
c.removeAll()
updateImage()
}
}
}
ColumnLayout {
id: columnButtonsActions
anchors.left: parent.left
anchors.leftMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
spacing: 5
Button {
id: buttonThumbnail
objectName: "buttonThumbnail"
text: "Thumbnail"
anchors.bottom: buttonCover.top
anchors.bottomMargin: 5
tooltip: "Extract Thumbnail (freedesktop spec.)"
enabled: (textFieldOutDir.text != "" && c.len !== 0) ? true : false
}
Button {
id: buttonCover
objectName: "buttonCover"
text: "Cover"
anchors.bottom: buttonConvert.top
anchors.bottomMargin: 15
tooltip: "Extract Cover"
enabled: (textFieldOutDir.text != "" && c.len !== 0) ? true : false
}
Button {
id: buttonConvert
objectName: "buttonConvert"
text: "&Convert"
tooltip: "Convert archives and documents"
enabled: (textFieldOutDir.text != "" && c.len !== 0) ? true : false
}
}
}
}
RowLayout {
id: rowLayout2
spacing: 0
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.left: parent.left
ColumnLayout {
id: columnLeft
anchors.right: columnMiddle.left
anchors.rightMargin: margin
anchors.left: parent.left
anchors.leftMargin: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
anchors.top: parent.top
anchors.topMargin: 0
Layout.minimumWidth: 200
GroupBox {
id: groupBoxPreview
checkable: true
flat: true
anchors.fill: parent
title: "Preview"
Image {
id: imagePreview
anchors.fill: parent
fillMode: Image.PreserveAspectFit
asynchronous: true
cache: false
}
BusyIndicator {
running: imagePreview.status === Image.Loading
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
}
onCheckedChanged: updateImage()
}
Text {
id: sizePreview
objectName: "sizePreview"
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: groupBoxPreview.bottom
anchors.topMargin: -5
}
}
ColumnLayout {
id: columnMiddle
anchors.right: columnRight.left
anchors.rightMargin: margin
anchors.top: parent.top
anchors.topMargin: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
Layout.fillHeight: true
GroupBox {
id: groupBoxInput
flat: true
anchors.right: parent.right
anchors.rightMargin: 0
title: "Input"
anchors.top: parent.top
anchors.topMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
ColumnLayout {
id: columnInput
anchors.bottomMargin: 0
anchors.fill: parent
spacing: 5
CheckBox {
id: checkBoxRecursive
objectName: "checkBoxRecursive"
text: "Recurse SubDirectories"
}
CheckBox {
id: checkBoxNoRGB
objectName: "checkBoxNoRGB"
text: "Only Grayscaled Images"
}
CheckBox {
id: checkBoxConvertCover
objectName: "checkBoxConvertCover"
text: "Exclude Cover"
}
RowLayout {
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
spacing: 5
SpinBox {
id: spinboxSize
objectName: "spinboxSize"
stepSize: 10
prefix: ""
maximumValue: 1000
suffix: " MiB"
}
Text {
text: "Minimum Size"
}
}
}
}
GroupBox {
id: groupBoxTransform
flat: true
anchors.right: parent.right
anchors.rightMargin: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
title: "Transform"
ColumnLayout {
id: columnTransform
anchors.fill: parent
spacing: 5
Button {
id: buttonLevels
text: "Levels..."
enabled: (c.len > 0) ? true : false
onClicked: {
levelsDialog.open()
}
}
Text {
text: "Brightness: " + sliderBrightness.value
}
Slider {
id: sliderBrightness
objectName: "sliderBrightness"
value: 0
stepSize: 1
minimumValue: -100
maximumValue: 100
activeFocusOnPress: true
updateValueWhileDragging: false
enabled: (c.len > 0) ? true : false
onValueChanged: updateImage()
}
Text {
text: "Contrast: " + sliderContrast.value
}
Slider {
id: sliderContrast
objectName: "sliderContrast"
value: 0
maximumValue: 100
stepSize: 1
minimumValue: -100
activeFocusOnPress: true
updateValueWhileDragging: false
enabled: (c.len > 0) ? true : false
onValueChanged: updateImage()
}
Text {
text: "Flip:"
}
ComboBox {
id: comboBoxFlip
objectName: "comboBoxFlip"
enabled: (c.len > 0) ? true : false
model: ListModel {
ListElement {
text: "None"
}
ListElement {
text: "Horizontal"
}
ListElement {
text: "Vertical"
}
}
onActivated: updateImage()
}
Text {
text: "Rotate:"
}
ComboBox {
id: comboBoxRotate
objectName: "comboBoxRotate"
enabled: (c.len > 0) ? true : false
model: ListModel {
ListElement {
text: "0"
}
ListElement {
text: "90"
}
ListElement {
text: "180"
}
ListElement {
text: "270"
}
}
onActivated: updateImage()
}
}
}
}
ColumnLayout {
id: columnRight
anchors.top: parent.top
anchors.topMargin: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
Layout.fillHeight: true
GroupBox {
id: groupBoxOutput
flat: true
title: "Output"
anchors.top: parent.top
anchors.topMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
ColumnLayout {
id: columnOutput
anchors.bottomMargin: 0
anchors.fill: parent
spacing: 5
RowLayout {
id: rowLayoutOutput
anchors.top: parent.top
anchors.topMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
spacing: 5
TextField {
id: textFieldOutDir
objectName: "textFieldOutDir"
anchors.right: buttonBrowse.left
anchors.rightMargin: 5
anchors.left: parent.left
anchors.leftMargin: 0
placeholderText: "Output Directory"
Settings {
id: settingsOutDir
property alias text: textFieldOutDir.text
}
}
Button {
id: buttonBrowse
text: "..."
anchors.right: parent.right
anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter
onClicked: {
fileDialogOutput.open()
}
}
}
TextField {
id: textFieldSuffix
objectName: "textFieldSuffix"
anchors.left: parent.left
anchors.leftMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
placeholderText: "Add Suffix to Output File"
}
CheckBox {
id: checkBoxNonImage
objectName: "checkBoxNonImage"
text: "Remove Non-Image Files"
}
}
}
GroupBox {
id: groupBoxImage
flat: true
anchors.left: parent.left
anchors.leftMargin: 0
title: "Image"
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
ColumnLayout {
id: columnFormat
anchors.fill: parent
spacing: 5
Text {
text: "Format:"
}
ComboBox {
id: comboBoxFormat
objectName: "comboBoxFormat"
enabled: (c.len > 0) ? true : false
model: ListModel {
ListElement {
text: "JPEG"
}
ListElement {
text: "PNG"
}
ListElement {
text: "GIF"
}
ListElement {
text: "BMP"
}
ListElement {
text: "TIFF"
}
}
onActivated: updateImage()
}
Text {
text: "Size:"
}
RowLayout {
spacing: 5
TextField {
id: width
objectName: "width"
placeholderText: "width"
maximumLength: 4
implicitWidth: 50
onAccepted: updateImage()
}
Text {
id: x
text: "x"
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
}
TextField {
id: height
objectName: "height"
placeholderText: "height"
maximumLength: 4
implicitWidth: 50
onAccepted: updateImage()
}
}
CheckBox {
id: checkBoxFit
objectName: "checkBoxFit"
text: "Best Fit"
enabled: (c.len > 0) ? true : false
onClicked: updateImage()
}
Text {
text: "Resize Algorithm:"
}
ComboBox {
id: comboBoxFilter
objectName: "comboBoxFilter"
currentIndex: 2
enabled: (c.len > 0) ? true : false
model: ListModel {
ListElement {
text: "NearestNeighbor"
}
ListElement {
text: "Box"
}
ListElement {
text: "Linear"
}
ListElement {
text: "MitchellNetravali"
}
ListElement {
text: "CatmullRom"
}
ListElement {
text: "Gaussian"
}
ListElement {
text: "Lanczos"
}
}
onActivated: updateImage()
Settings {
id: settingsFilter
property alias currentIndex: comboBoxFilter.currentIndex
}
}
Text {
text: "Quality: " + sliderQuality.value
}
Slider {
id: sliderQuality
objectName: "sliderQuality"
stepSize: 1
value: 75
maximumValue: 100
activeFocusOnPress: true
updateValueWhileDragging: false
enabled: (c.len > 0 && comboBoxFormat.currentText == "JPEG") ? true : false
onValueChanged: updateImage()
}
CheckBox {
id: checkBoxGrayscale
objectName: "checkBoxGrayscale"
text: "Convert to Grayscale"
enabled: (c.len > 0) ? true : false
onClicked: updateImage()
}
Settings {
id: settingsQuality
property alias value: sliderQuality.value
}
}
}
}
}
}
FileDialog {
id: fileDialogFile
modality: Qt.WindowModal
title: "Add Files"
selectFolder: false
selectMultiple: true
selectExisting: true
sidebarVisible: true
nameFilters: [ "Comic files (*.rar *.zip *.7z *.gz *.bz2 *.cbr *.cbz *.cb7 *.cbt *.pdf *.epub *.xps)" ]
onAccepted: {
c.addUrls(decodeURIComponent(fileUrls.join("_CBSEP_")))
}
}
FileDialog {
id: fileDialogDir
modality: Qt.WindowModal
title: "Add Directory"
selectFolder: true
sidebarVisible: true
onAccepted: {
c.addUrls(decodeURIComponent(fileUrl.toString()))
}
}
FileDialog {
id: fileDialogOutput
modality: Qt.WindowModal
title: "Output Directory"
selectFolder: true
sidebarVisible: true
onAccepted: {
textFieldOutDir.text = decodeURIComponent(fileUrl.toString().replace("file://", ""))
}
}
Dialog {
id: levelsDialog
objectName: "levelsDialog"
title: "Levels"
standardButtons: StandardButton.Close
width: 230
height: 150
ColumnLayout {
anchors.fill: parent
Text {
text: "Input Levels:"
}
RowLayout {
anchors.left: parent.left
anchors.leftMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
spacing: 5
SpinBox {
id: spinboxLevelsInMin
objectName: "spinboxLevelsInMin"
anchors.left: parent.left
anchors.leftMargin: 0
stepSize: 1
maximumValue: 255
value: 0
onEditingFinished: updateImage()
Keys.onReturnPressed: {
event.accepted = true
}
}
SpinBox {
id: spinboxLevelsGamma
objectName: "spinboxLevelsGamma"
anchors.horizontalCenter: parent.horizontalCenter
decimals: 2
stepSize: 0.01
maximumValue: 10.00
value: 1.00
onEditingFinished: updateImage()
}
SpinBox {
id: spinboxLevelsInMax
objectName: "spinboxLevelsInMax"
anchors.right: parent.right
anchors.rightMargin: 0
stepSize: 1
maximumValue: 255
value: 255
onEditingFinished: updateImage()
}
}
Text {
text: "Output Levels:"
}
RowLayout {
anchors.left: parent.left
anchors.leftMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
spacing: 5
SpinBox {
id: spinboxLevelsOutMin
objectName: "spinboxLevelsOutMin"
anchors.left: parent.left
anchors.leftMargin: 0
stepSize: 1
maximumValue: 255
value: 0
onEditingFinished: updateImage()
}
SpinBox {
id: spinboxLevelsOutMax
objectName: "spinboxLevelsOutMax"
anchors.right: parent.right
anchors.rightMargin: 0
stepSize: 1
maximumValue: 255
value: 255
onEditingFinished: updateImage()
}
}
}
}
statusBar: StatusBar {
RowLayout {
anchors.fill: parent
Label {
id: labelStatus
objectName: "labelStatus"
text: "Ready"
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: rectangle1
Layout.fillWidth: true
anchors.verticalCenter: parent.verticalCenter
Label {
id: labelPercent
objectName: "labelPercent"
font.pointSize: 9
anchors.right: progressBar.left
anchors.rightMargin: 5
anchors.verticalCenter: parent.verticalCenter
}
ProgressBar {
id: progressBar
objectName: "progressBar"
visible: false
value: 0.0
minimumValue : 0.0
maximumValue : 100.0
anchors.bottom: parent.bottom
anchors.bottomMargin: -10
anchors.top: parent.top
anchors.topMargin: -10
anchors.right: labelProgress.left
anchors.rightMargin: 5
}
Label {
id: labelProgress
objectName: "labelProgress"
font.pointSize: 9
anchors.right: parent.right
anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
-550
View File
@@ -1,550 +0,0 @@
// Author: Milan Nikolic <gen2brain@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
//go:generate genqrc assets
import (
"bytes"
"compress/gzip"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"mime"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/disintegration/imaging"
"github.com/dustin/go-humanize"
"github.com/gen2brain/cbconvert"
"github.com/gographics/imagick/imagick"
"github.com/hotei/bmp"
"golang.org/x/image/tiff"
"gopkg.in/qml.v1"
)
// Model
type Comics struct {
Root qml.Object
Conv *cbconvert.Convertor
List []Comic
Len int
}
// Comic Element
type Comic struct {
Name string
Path string
Type string
Size int64
SizeHuman string
}
// Sorts by name
type ByName []Comic
func (c ByName) Len() int { return len(c) }
func (c ByName) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ByName) Less(i, j int) bool { return c[i].Name < c[j].Name }
// Sorts by size
type BySize []Comic
func (c BySize) Len() int { return len(c) }
func (c BySize) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c BySize) Less(i, j int) bool { return c[i].Size < c[j].Size }
// Sorts by type
type ByType []Comic
func (c ByType) Len() int { return len(c) }
func (c ByType) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ByType) Less(i, j int) bool { return c[i].Type < c[j].Type }
// Adds element to list
func (c *Comics) Add(comic Comic) {
c.List = append(c.List, comic)
c.Len = len(c.List)
qml.Changed(c, &c.Len)
}
// Removes element from list
func (c *Comics) Remove(i int) {
l := c.List
l = append(l[:i], l[i+1:]...)
c.List = l
c.Len = len(c.List)
qml.Changed(c, &c.Len)
}
// Removes all elements from list
func (c *Comics) RemoveAll() {
c.Len = 0
c.List = make([]Comic, 0)
qml.Changed(c, &c.Len)
}
// Sorts by name
func (c *Comics) ByName() {
sort.Sort(ByName(c.List))
c.Len++
qml.Changed(c, &c.Len)
c.Len--
qml.Changed(c, &c.Len)
}
// Sorts by size
func (c *Comics) BySize() {
sort.Sort(BySize(c.List))
c.Len++
qml.Changed(c, &c.Len)
c.Len--
qml.Changed(c, &c.Len)
}
// Sorts by type
func (c *Comics) ByType() {
sort.Sort(ByType(c.List))
c.Len++
qml.Changed(c, &c.Len)
c.Len--
qml.Changed(c, &c.Len)
}
// Returns element for given index
func (c *Comics) Get(i int) Comic {
return c.List[i]
}
// Adds elements from fileUrls to list
func (c *Comics) AddUrls(u string) {
var args []string
l := strings.Split(u, "_CBSEP_")
re := regexp.MustCompile(`^[a-zA-Z]:`)
for _, f := range l {
f = strings.Replace(f, "file://", "", -1)
f = re.ReplaceAllString(f, "")
f = re.ReplaceAllString(f, "")
args = append(args, f)
}
c.Conv.Opts = c.GetOptions()
files := c.Conv.GetFiles(args)
for _, file := range files {
stat, err := os.Stat(file)
if err != nil {
fmt.Fprintf(os.Stderr, "Error Stat AddUrls: %v\n", err.Error())
continue
}
m := mime.TypeByExtension(filepath.Ext(file))
if m == "" && stat.IsDir() {
m = "inode/directory"
}
c.Add(Comic{
filepath.Base(file),
file,
m,
stat.Size(),
humanize.IBytes(uint64(stat.Size())),
})
}
}
// Returns cbconvert options from qml
func (c *Comics) GetOptions() cbconvert.Options {
var o cbconvert.Options
o.Quiet = true
r := c.Root.ObjectByName("checkBoxRecursive")
o.Recursive = r.Bool("checked")
r = c.Root.ObjectByName("checkBoxNoRGB")
o.RGB = !r.Bool("checked")
r = c.Root.ObjectByName("checkBoxConvertCover")
o.ConvertCover = !r.Bool("checked")
r = c.Root.ObjectByName("spinboxSize")
o.Size = r.Int64("value")
r = c.Root.ObjectByName("sliderBrightness")
o.Brightness = r.Float64("value")
r = c.Root.ObjectByName("sliderContrast")
o.Contrast = r.Float64("value")
r = c.Root.ObjectByName("checkBoxGrayscale")
o.Grayscale = r.Bool("checked")
r = c.Root.ObjectByName("comboBoxFlip")
o.Flip = strings.ToLower(r.String("currentText"))
r = c.Root.ObjectByName("comboBoxRotate")
o.Rotate, _ = strconv.Atoi(r.String("currentText"))
r = c.Root.ObjectByName("textFieldOutDir")
o.Outdir = r.String("text")
r = c.Root.ObjectByName("textFieldSuffix")
o.Suffix = r.String("text")
r = c.Root.ObjectByName("checkBoxNonImage")
o.NonImage = !r.Bool("checked")
r = c.Root.ObjectByName("comboBoxFormat")
o.Format = strings.ToLower(r.String("currentText"))
r = c.Root.ObjectByName("width")
o.Width, _ = strconv.Atoi(r.String("text"))
r = c.Root.ObjectByName("height")
o.Height, _ = strconv.Atoi(r.String("text"))
r = c.Root.ObjectByName("checkBoxFit")
o.Fit = r.Bool("checked")
r = c.Root.ObjectByName("comboBoxFilter")
o.Filter = r.Int("currentIndex")
r = c.Root.ObjectByName("sliderQuality")
o.Quality = int(r.Float64("value"))
r = c.Root.ObjectByName("spinboxLevelsInMin")
o.LevelsInMin = r.Float64("value")
r = c.Root.ObjectByName("spinboxLevelsInMax")
o.LevelsInMax = r.Float64("value")
r = c.Root.ObjectByName("spinboxLevelsGamma")
o.LevelsGamma = r.Float64("value")
r = c.Root.ObjectByName("spinboxLevelsOutMin")
o.LevelsOutMin = r.Float64("value")
r = c.Root.ObjectByName("spinboxLevelsOutMax")
o.LevelsOutMax = r.Float64("value")
return o
}
// Sets "enabled" property
func (c *Comics) SetEnabled(b bool) {
c.Root.ObjectByName("checkBoxRecursive").Set("enabled", b)
c.Root.ObjectByName("checkBoxNoRGB").Set("enabled", b)
c.Root.ObjectByName("checkBoxConvertCover").Set("enabled", b)
c.Root.ObjectByName("spinboxSize").Set("enabled", b)
c.Root.ObjectByName("buttonLevels").Set("enabled", b)
c.Root.ObjectByName("sliderBrightness").Set("enabled", b)
c.Root.ObjectByName("sliderContrast").Set("enabled", b)
c.Root.ObjectByName("checkBoxGrayscale").Set("enabled", b)
c.Root.ObjectByName("comboBoxFlip").Set("enabled", b)
c.Root.ObjectByName("comboBoxRotate").Set("enabled", b)
c.Root.ObjectByName("textFieldOutDir").Set("enabled", b)
c.Root.ObjectByName("textFieldSuffix").Set("enabled", b)
c.Root.ObjectByName("checkBoxNonImage").Set("enabled", b)
c.Root.ObjectByName("comboBoxFormat").Set("enabled", b)
c.Root.ObjectByName("width").Set("enabled", b)
c.Root.ObjectByName("height").Set("enabled", b)
c.Root.ObjectByName("checkBoxFit").Set("enabled", b)
c.Root.ObjectByName("comboBoxFilter").Set("enabled", b)
c.Root.ObjectByName("sliderQuality").Set("enabled", b)
c.Root.ObjectByName("buttonAddFile").Set("enabled", b)
c.Root.ObjectByName("buttonAddDir").Set("enabled", b)
c.Root.ObjectByName("buttonRemove").Set("enabled", b)
c.Root.ObjectByName("buttonRemoveAll").Set("enabled", b)
c.Root.ObjectByName("buttonThumbnail").Set("enabled", b)
c.Root.ObjectByName("buttonCover").Set("enabled", b)
c.Root.ObjectByName("buttonConvert").Set("enabled", b)
}
// Converts comic
func (c *Comics) Convert() {
c.Conv.Opts = c.GetOptions()
c.Conv.Nfiles = c.Len
c.Conv.CurrFile = 0
c.SetEnabled(false)
go func() {
for _, e := range c.List {
stat, err := os.Stat(e.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "Error Stat Convert: %v\n", err.Error())
continue
}
c.Conv.ConvertComic(e.Path, stat)
}
}()
go c.showProgress(true, "Converting...")
}
// Extracts cover
func (c *Comics) Cover() {
c.Conv.Opts = c.GetOptions()
c.Conv.Nfiles = c.Len
c.Conv.CurrFile = 0
c.SetEnabled(false)
go func() {
for _, e := range c.List {
stat, err := os.Stat(e.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "Error Stat Cover: %v\n", err.Error())
continue
}
c.Conv.ExtractCover(e.Path, stat)
}
}()
go c.showProgress(false, "Extracting...")
}
// Extracts thumbnail
func (c *Comics) Thumbnail() {
c.Conv.Opts = c.GetOptions()
c.Conv.Nfiles = c.Len
c.Conv.CurrFile = 0
c.SetEnabled(false)
go func() {
for _, e := range c.List {
stat, err := os.Stat(e.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "Error Stat Thumbnail: %v\n", err.Error())
continue
}
c.Conv.ExtractThumbnail(e.Path, stat)
}
}()
go c.showProgress(false, "Extracting...")
}
// Shows progress
func (c *Comics) showProgress(cn bool, text string) {
c.Root.ObjectByName("labelStatus").Set("text", text)
c.Root.ObjectByName("progressBar").Set("visible", true)
for {
if c.Conv.CurrFile == c.Conv.Nfiles {
if c.Conv.CurrContent == c.Conv.Ncontents {
c.Root.ObjectByName("progressBar").Set("value", 0)
c.Root.ObjectByName("labelProgress").Set("text", "")
c.Root.ObjectByName("labelStatus").Set("text", "Ready")
c.Root.ObjectByName("labelPercent").Set("text", "")
c.Root.ObjectByName("progressBar").Set("visible", false)
c.SetEnabled(true)
break
}
}
var count, current int
if cn {
count = c.Conv.Ncontents
current = c.Conv.CurrContent
} else {
count = c.Conv.Nfiles
current = c.Conv.CurrFile
}
value := float64(current) / float64(count) * 100
c.Root.ObjectByName("progressBar").Set("value", float64(value))
c.Root.ObjectByName("labelPercent").Set("text",
fmt.Sprintf("%d/%d %.0f%%", current, count, float64(value)))
c.Root.ObjectByName("labelProgress").Set("text",
fmt.Sprintf("File %d of %d", c.Conv.CurrFile, c.Conv.Nfiles))
time.Sleep(500 * time.Millisecond)
}
}
// Provides image://cover/
func (c *Comics) CoverProvider(file string, width int, height int) image.Image {
c.Conv.Opts = c.GetOptions()
stat, err := os.Stat(file)
if err != nil {
fmt.Fprintf(os.Stderr, "Error Stat CoverProvider: %v\n", err.Error())
return image.NewRGBA(image.Rect(0, 0, width, height))
}
cover, err := c.Conv.GetCoverImage(file, stat)
if err != nil {
fmt.Fprintf(os.Stderr, "Error GetCoverImage: %v\n", err.Error())
return image.NewRGBA(image.Rect(0, 0, width, height))
}
cover = c.Conv.TransformImage(cover)
if c.Conv.Opts.LevelsInMin != 0 || c.Conv.Opts.LevelsInMax != 255 || c.Conv.Opts.LevelsGamma != 1.00 ||
c.Conv.Opts.LevelsOutMin != 0 || c.Conv.Opts.LevelsOutMax != 255 {
cover = c.Conv.LevelImage(cover)
}
// imaging is used for preview only
if c.Conv.Opts.Grayscale {
cover = imaging.Grayscale(cover)
}
// size preview
s := 0
b := new(bytes.Buffer)
w := 0
h := 0
switch c.Conv.Opts.Format {
case "jpeg":
jpeg.Encode(b, cover, &jpeg.Options{c.Conv.Opts.Quality})
s = len(b.Bytes())
cover, _ = jpeg.Decode(bytes.NewReader(b.Bytes()))
config, _, _ := image.DecodeConfig(bytes.NewReader(b.Bytes()))
w = config.Width
h = config.Height
case "png":
png.Encode(b, cover)
s = len(b.Bytes())
cover, _ = png.Decode(bytes.NewReader(b.Bytes()))
config, _, _ := image.DecodeConfig(bytes.NewReader(b.Bytes()))
w = config.Width
h = config.Height
case "gif":
mw := imagick.NewMagickWand()
defer mw.Destroy()
mw.ReadImageBlob(c.Conv.GetImageBytes(cover))
mw.SetImageFormat("GIF")
blob := mw.GetImageBlob()
s = len(blob)
cover, _ = gif.Decode(bytes.NewReader(blob))
config, _, _ := image.DecodeConfig(bytes.NewReader(blob))
w = config.Width
h = config.Height
case "tiff":
tiff.Encode(b, cover, &tiff.Options{tiff.Uncompressed, false})
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
gz.Write(b.Bytes())
gz.Close()
s = buf.Len()
cover, _ = tiff.Decode(bytes.NewReader(b.Bytes()))
config, _, _ := image.DecodeConfig(bytes.NewReader(b.Bytes()))
w = config.Width
h = config.Height
case "bmp":
mw := imagick.NewMagickWand()
defer mw.Destroy()
bb := c.Conv.GetImageBytes(cover)
mw.ReadImageBlob(bb)
wand := imagick.NewPixelWand()
wand.SetColor("black")
defer wand.Destroy()
mw.SetImageFormat("BMP3")
mw.SetImageBackgroundColor(wand)
mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_REMOVE)
mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_DEACTIVATE)
mw.SetImageMatte(false)
mw.SetImageCompression(imagick.COMPRESSION_NO)
mw.QuantizeImage(16, mw.GetImageColorspace(), 8, true, true)
var buf bytes.Buffer
blob := mw.GetImageBlob()
gz := gzip.NewWriter(&buf)
gz.Write(blob)
gz.Close()
s = buf.Len()
cover, _ = bmp.Decode(bytes.NewReader(blob))
config, _, _ := image.DecodeConfig(bytes.NewReader(bb))
w = config.Width
h = config.Height
}
if cover == nil {
return image.NewRGBA(image.Rect(0, 0, width, height))
}
human := humanize.IBytes(uint64(s))
c.Root.ObjectByName("sizePreview").Set("text", fmt.Sprintf("%s (%dx%d)", human, w, h))
return cover
}
func run() error {
qml.SetWindowIcon(":///assets/icon.png")
engine := qml.NewEngine()
c := &Comics{}
engine.Context().SetVar("c", c)
engine.AddImportPath("qrc:/assets")
engine.AddImageProvider("cover", c.CoverProvider)
q, err := engine.LoadFile("qrc:///assets/main.qml")
if err != nil {
return err
}
window := q.CreateWindow(nil)
c.Root = window.Root()
c.Conv = cbconvert.NewConvertor(c.GetOptions())
c.Root.On("closing", func(o qml.Object) {
os.RemoveAll(c.Conv.Workdir)
})
c.Root.ObjectByName("buttonConvert").On("clicked", c.Convert)
c.Root.ObjectByName("buttonCover").On("clicked", c.Cover)
c.Root.ObjectByName("buttonThumbnail").On("clicked", c.Thumbnail)
// center window
x := c.Root.Int("screenWidth")/2 - c.Root.Int("width")/2
y := c.Root.Int("screenHeight")/2 - c.Root.Int("height")/2
window.Set("x", x)
window.Set("y", y)
window.Show()
window.Wait()
return nil
}
func main() {
if err := qml.Run(run); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
-32
View File
@@ -1,32 +0,0 @@
#!/usr/bin/env bash
CHROOT="/home/milann/chroot"
MINGW="/usr/i686-w64-mingw32"
mkdir -p build
rm -f resource.syso
go generate
LIBRARY_PATH="$CHROOT/usr/lib:$CHROOT/lib" \
PKG_CONFIG_PATH="$CHROOT/usr/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$CHROOT/usr/lib/pkgconfig" \
CGO_LDFLAGS="-L$CHROOT/usr/lib -L$CHROOT/lib -L$CHROOT/usr/plugins/generic -L$CHROOT/usr/plugins/platforms -L$CHROOT/usr/plugins/qmltooling -L$CHROOT/usr/qml/QtQuick.2 -L$CHROOT/usr/qml/QtQuick/Controls -L$CHROOT/usr/qml/QtQuick/Dialogs -L$CHROOT/usr/qml/QtQuick/Layouts -L$CHROOT/usr/qml/QtQuick/Window.2 -L$CHROOT/usr/qml/Qt/labs/settings -L$CHROOT/usr/qml/QtQuick/PrivateWidgets -L$CHROOT/usr/plugins/xcbglintegrations" \
CGO_LDFLAGS="$CGO_LDFLAGS -lqxcb -lqtquick2plugin -lqtquickcontrolsplugin -ldialogplugin -lqquicklayoutsplugin -lwindowplugin -lqmlsettingsplugin -lwidgetsplugin -lqxcb-glx-integration -lqevdevkeyboardplugin -lqevdevmouseplugin" \
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -v -x -o build/cbconvert
strip build/cbconvert
goversioninfo -icon=assets/icon.ico
PKG_CONFIG="/usr/bin/i686-w64-mingw32-pkg-config" \
PKG_CONFIG_PATH="$MINGW/usr/lib/pkgconfig:$MINGW/usr/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$MINGW/usr/lib/pkgconfig:$MINGW/usr/lib/pkgconfig" \
CGO_LDFLAGS="-L$MINGW/usr/lib -L$MINGW/lib -L$MINGW/usr/plugins/generic -L$MINGW/usr/plugins/platforms -L$MINGW/usr/qml/Qt/labs/folderlistmodel -L$MINGW/usr/plugins/qmltooling -L$MINGW/usr/qml/QtQuick.2 -L$MINGW/usr/qml/QtQuick/Controls -L$MINGW/usr/qml/QtQuick/Dialogs -L$MINGW/usr/qml/QtQuick/Dialogs/Private -L$MINGW/usr/qml/QtQuick/Layouts -L$MINGW/usr/qml/QtQuick/Window.2 -L$MINGW/usr/qml/Qt/labs/settings -L$MINGW/usr/qml/QtQuick/PrivateWidgets" \
CGO_LDFLAGS="$CGO_LDFLAGS -lqwindows -lqtquick2plugin -lqtquickcontrolsplugin -lqmlfolderlistmodelplugin -ldialogplugin -ldialogsprivateplugin -lqquicklayoutsplugin -lwindowplugin -lqmlsettingsplugin -lwidgetsplugin" \
CGO_CFLAGS="-I$MINGW/usr/include -Wno-poison-system-directories" \
CGO_CXXFLAGS="-I$MINGW/usr/include -Wno-poison-system-directories" \
CGO_CPPFLAGS="-I$MINGW/usr/include -Wno-poison-system-directories" \
CC="i686-w64-mingw32-gcc" CXX="i686-w64-mingw32-g++" \
CC_FOR_TARGET="i686-w64-mingw32-gcc" CXX_FOR_TARGET="i686-w64-mingw32-g++" \
CGO_ENABLED=1 GOOS=windows GOARCH=386 go build -v -x -o build/cbconvert.exe -ldflags "-H=windowsgui -linkmode external '-extldflags=-static -Wl,--allow-multiple-definition'"
i686-w64-mingw32-strip build/cbconvert.exe
-44
View File
@@ -1,44 +0,0 @@
{
"FixedFileInfo":
{
"FileVersion": {
"Major": 0,
"Minor": 6,
"Patch": 0,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 6,
"Patch": 0,
"Build": 0
},
"FileFlagsMask": "3f",
"FileFlags ": "00",
"FileOS": "040004",
"FileType": "01",
"FileSubType": "00"
},
"StringFileInfo":
{
"Comments": "Comic Book converter",
"CompanyName": "",
"FileDescription": "CBconvert GUI",
"FileVersion": "0.6.0",
"InternalName": "",
"LegalCopyright": "",
"LegalTrademarks": "",
"OriginalFilename": "cbconvert.exe",
"PrivateBuild": "",
"ProductName": "CBconvert",
"ProductVersion": "0.6.0",
"SpecialBuild": ""
},
"VarFileInfo":
{
"Translation": {
"LangID": "0409",
"CharsetID": "04B0"
}
}
}
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