diff --git a/README.md b/README.md index ba13640..dca5185 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,21 @@ Kojirou can be configured to fall back on reencoded lower-quality versions of th kojirou d86cf65b-5f6c-437d-a0af-19a31f94ec55 -l en --data-saver=fallback ``` +### Rotate and split double panel pages + +Kojirou has the ability to rotate and split double panel pages into two new pages. +It's possible to only rotate or rotate and split them. + +Only rotating +``` shell +kojirou d86cf65b-5f6c-437d-a0af-19a31f94ec55 -l en --rotate +``` + +Rotating and splitting +``` shell +kojirou d86cf65b-5f6c-437d-a0af-19a31f94ec55 -l en --rotateAndSplit +``` + ## Prebuilt binaries Prebuilt binaries for Linux, Windows and MacOS on x86 and ARM processors are provided. diff --git a/cmd/business.go b/cmd/business.go index 9f90e94..65b4958 100644 --- a/cmd/business.go +++ b/cmd/business.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "sort" "github.com/leotaku/kojirou/cmd/crop" "github.com/leotaku/kojirou/cmd/filter" @@ -9,6 +10,7 @@ import ( "github.com/leotaku/kojirou/cmd/formats/disk" "github.com/leotaku/kojirou/cmd/formats/download" "github.com/leotaku/kojirou/cmd/formats/kindle" + "github.com/leotaku/kojirou/cmd/split" md "github.com/leotaku/kojirou/mangadex" "golang.org/x/text/language" ) @@ -63,6 +65,18 @@ func handleVolume(skeleton md.Manga, volume md.Volume, dir kindle.NormalizedDire return fmt.Errorf("autocrop: %w", err) } } + + if rotateAndSplitArg { + if pages, err = rotateAndSplit(pages); err != nil { + return fmt.Errorf("rotateAndSplit: %w", err) + } + } + + if rotateArg { + if err := rotateDoublePage(pages); err != nil { + return fmt.Errorf("rotateDoublePage: %w", err) + } + } mangaForVolume := skeleton.WithChapters(volume.Sorted()).WithPages(pages) mobi := kindle.GenerateMOBI(mangaForVolume) @@ -212,3 +226,76 @@ func filterAndSortFromFlags(cl md.ChapterList) (md.ChapterList, error) { return cl, nil } + +func rotateDoublePage(pages md.ImageList) error { + p := formats.VanishingProgress("Rotating..") + p.Increase(len(pages)) + + sort.Slice(pages, func(i, j int) bool { + return pages[i].ImageIdentifier < pages[j].ImageIdentifier + }) + + for i, page := range pages { + if split.IsDoublePage(page.Image) { + landscapeImage, _ := split.RotateImage(page.Image) + pages[i].Image = landscapeImage + } + p.Add(1) + } + + p.Done() + return nil +} + +func rotateAndSplit(pages md.ImageList) (md.ImageList, error) { + p := formats.VanishingProgress("Splitting..") + p.Increase(len(pages)) + + sort.Slice(pages, func(i, j int) bool { + return pages[i].ImageIdentifier < pages[j].ImageIdentifier + }) + + occupied := make(map[int]bool) + newPages := make(md.ImageList, len(pages)) + copy(newPages, pages) + + for i, page := range pages { + imgId := split.GetNextImageIdentifier(page.ImageIdentifier, occupied) + image := page.Image + + if split.IsDoublePage(image) { + landscapeImage, _ := split.RotateImage(image) + newPages[i].Image = landscapeImage + newPages[i].ImageIdentifier = imgId + + leftImage, rightImage, _ := split.SplitVertically(image) + + rightPage := md.Image{ + Image: rightImage, + ChapterIdentifier: page.ChapterIdentifier, + VolumeIdentifier: page.VolumeIdentifier, + ImageIdentifier: imgId+1, + } + + leftPage := md.Image{ + Image: leftImage, + ChapterIdentifier: page.ChapterIdentifier, + VolumeIdentifier: page.VolumeIdentifier, + ImageIdentifier: imgId+2, + } + + newPages = append(newPages, rightPage, leftPage) + occupied[rightPage.ImageIdentifier] = true + occupied[leftPage.ImageIdentifier] = true + } else { + newPages[i].Image = image + newPages[i].ImageIdentifier = imgId + } + + occupied[imgId] = true + p.Add(1) + } + + p.Done() + return newPages, nil +} \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index 968c6a4..03de7ca 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,6 +13,8 @@ var ( languageArg string rankArg string autocropArg bool + rotateAndSplitArg bool + rotateArg bool kindleFolderModeArg bool dryRunArg bool outArg string @@ -170,6 +172,8 @@ func init() { rootCmd.Flags().StringVarP(&languageArg, "language", "l", "en", "language for chapter downloads") rootCmd.Flags().StringVarP(&rankArg, "rank", "r", "most", "chapter ranking method to use") rootCmd.Flags().BoolVarP(&autocropArg, "autocrop", "a", false, "crop whitespace from pages automatically") + rootCmd.Flags().BoolVarP(&rotateAndSplitArg, "rotateAndSplit", "S", false, "rotate and split double panels pages into two new separate pages") + rootCmd.Flags().BoolVarP(&rotateArg, "rotate", "H", false, "rotate horizontally double panels pages") rootCmd.Flags().BoolVarP(&kindleFolderModeArg, "kindle-folder-mode", "k", false, "generate folder structure for Kindle devices") rootCmd.Flags().BoolVarP(&leftToRightArg, "left-to-right", "p", false, "make reading direction left to right") rootCmd.Flags().IntVarP(&fillVolumeNumberArg, "fill-volume-number", "n", 0, "fill volume number with leading zeros in title") diff --git a/cmd/split/root.go b/cmd/split/root.go new file mode 100644 index 0000000..852a24f --- /dev/null +++ b/cmd/split/root.go @@ -0,0 +1,66 @@ +package split + +import ( + "fmt" + "image" +) + +func IsDoublePage(img image.Image) bool { + bounds := img.Bounds() + return bounds.Dx() >= bounds.Dy() +} + +func GetNextImageIdentifier(current int, occupied map[int]bool) int { + // while the current identifier is already occupied, increment it + for occupied[current] { + current++ + } + return current +} + +func SplitVertically(img image.Image) (image.Image, image.Image, error) { + type subImager interface { + image.Image + SubImage(r image.Rectangle) image.Image + } + + subImg, ok := img.(subImager) + if !ok { + return nil, nil, fmt.Errorf("image does not support splitting or not a valid image") + } + + originalBounds := subImg.Bounds() + xMiddle := originalBounds.Dx() / 2 + + leftBounds := image.Rectangle{ + Min: image.Point{0, 0}, + Max: image.Point{xMiddle, originalBounds.Dy()}, + } + rightBounds := image.Rectangle{ + Min: image.Point{xMiddle, 0}, + Max: image.Point{originalBounds.Dx(), originalBounds.Dy()}, + } + + leftImage := subImg.SubImage(leftBounds) + rightImage := subImg.SubImage(rightBounds) + + return leftImage, rightImage, nil +} + +func RotateImage(img image.Image) (image.Image, error) { + originalBounds := img.Bounds() + width := originalBounds.Dx() + height := originalBounds.Dy() + + // create a new empty image with rotated dimensions + rotatedImage := image.NewRGBA(image.Rect(0, 0, height, width)) + + // rotate the image by mapping each pixel from the original to the new position + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + rotatedImage.Set(y, width-1-x, img.At(x, y)) + } + } + + return rotatedImage, nil +} \ No newline at end of file