comments the code

This commit is contained in:
Celogeek 2023-04-26 16:23:38 +02:00
parent b268521494
commit 0263a64321
Signed by: celogeek
SSH Key Fingerprint: SHA256:njNJLzoLQdbV9PC6ehcruRb0QnEgxABoCYZ+0+aUIYc
26 changed files with 318 additions and 121 deletions

View File

@ -1,3 +1,10 @@
/*
Converter Helper to parse and prepare options for go-comic-converter.
It use goflag with additional feature:
- Keep original order
- Support section
*/
package converter
import (
@ -17,17 +24,18 @@ type Converter struct {
Options *options.Options
Cmd *flag.FlagSet
order []Order
order []converterOrder
isZeroValueErrs []error
}
// Create a new parser
func New() *Converter {
options := options.New()
cmd := flag.NewFlagSet("go-comic-converter", flag.ExitOnError)
conv := &Converter{
Options: options,
Cmd: cmd,
order: make([]Order, 0),
order: make([]converterOrder, 0),
}
var cmdOutput strings.Builder
@ -36,9 +44,9 @@ func New() *Converter {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", filepath.Base(os.Args[0]))
for _, o := range conv.order {
switch v := o.(type) {
case OrderSection:
case converterOrderSection:
fmt.Fprintf(os.Stderr, "\n%s:\n", o.Value())
case OrderName:
case converterOrderName:
fmt.Fprintln(os.Stderr, conv.Usage(v.isString, cmd.Lookup(v.Value())))
}
}
@ -50,29 +58,35 @@ func New() *Converter {
return conv
}
// Load default options (config + default)
func (c *Converter) LoadConfig() error {
return c.Options.LoadDefault()
return c.Options.LoadConfig()
}
// Create a new section of config
func (c *Converter) AddSection(section string) {
c.order = append(c.order, OrderSection{value: section})
c.order = append(c.order, converterOrderSection{value: section})
}
// Add a string parameter
func (c *Converter) AddStringParam(p *string, name string, value string, usage string) {
c.Cmd.StringVar(p, name, value, usage)
c.order = append(c.order, OrderName{value: name, isString: true})
c.order = append(c.order, converterOrderName{value: name, isString: true})
}
// Add an integer parameter
func (c *Converter) AddIntParam(p *int, name string, value int, usage string) {
c.Cmd.IntVar(p, name, value, usage)
c.order = append(c.order, OrderName{value: name})
c.order = append(c.order, converterOrderName{value: name})
}
// Add a boolean parameter
func (c *Converter) AddBoolParam(p *bool, name string, value bool, usage string) {
c.Cmd.BoolVar(p, name, value, usage)
c.order = append(c.order, OrderName{value: name})
c.order = append(c.order, converterOrderName{value: name})
}
// Initialize the parser with all section and parameter.
func (c *Converter) InitParse() {
c.AddSection("Output")
c.AddStringParam(&c.Options.Input, "input", "", "Source of comic to convert: directory, cbz, zip, cbr, rar, pdf")
@ -110,6 +124,7 @@ func (c *Converter) InitParse() {
c.AddBoolParam(&c.Options.Help, "help", false, "Show this help message")
}
// Customize version of FlagSet.PrintDefaults
func (c *Converter) Usage(isString bool, f *flag.Flag) string {
var b strings.Builder
fmt.Fprintf(&b, " -%s", f.Name) // Two spaces before -; see next two comments.
@ -144,6 +159,8 @@ func (c *Converter) Usage(isString bool, f *flag.Flag) string {
return b.String()
}
// Taken from flag package as it is private and needed for usage.
//
// isZeroValue determines whether the string represents the zero
// value for a flag.
func (c *Converter) isZeroValue(f *flag.Flag, value string) (ok bool, err error) {
@ -171,6 +188,7 @@ func (c *Converter) isZeroValue(f *flag.Flag, value string) (ok bool, err error)
return value == z.Interface().(flag.Value).String(), nil
}
// Parse all parameters
func (c *Converter) Parse() {
c.Cmd.Parse(os.Args[1:])
if c.Options.Help {
@ -184,6 +202,7 @@ func (c *Converter) Parse() {
}
}
// Check parameters
func (c *Converter) Validate() error {
// Check input
if c.Options.Input == "" {
@ -262,6 +281,7 @@ func (c *Converter) Validate() error {
return errors.New("contrast should be between -100 and 100")
}
// SortPathMode
if c.Options.SortPathMode < 0 || c.Options.SortPathMode > 2 {
return errors.New("sort should be 0, 1 or 2")
}
@ -269,6 +289,7 @@ func (c *Converter) Validate() error {
return nil
}
// Helper to show usage, err and exit 1
func (c *Converter) Fatal(err error) {
c.Cmd.Usage()
fmt.Fprintf(os.Stderr, "\nError: %s\n", err)

View File

@ -1,22 +1,27 @@
package converter
type Order interface {
// Name or Section
type converterOrder interface {
Value() string
}
type OrderSection struct {
// Section
type converterOrderSection struct {
value string
}
func (s OrderSection) Value() string {
func (s converterOrderSection) Value() string {
return s.value
}
type OrderName struct {
// Name
//
// isString is used to quote the default value.
type converterOrderName struct {
value string
isString bool
}
func (s OrderName) Value() string {
func (s converterOrderName) Value() string {
return s.value
}

View File

@ -1,3 +1,6 @@
/*
Manage options with default value from config.
*/
package options
import (
@ -50,6 +53,7 @@ type Options struct {
profiles profiles.Profiles
}
// Initialize default options.
func New() *Options {
return &Options{
Profile: "",
@ -89,16 +93,18 @@ func (o *Options) String() string {
o.Author,
o.Title,
o.Workers,
o.ShowDefault(),
o.ShowConfig(),
)
}
// Config file: ~/.go-comic-converter.yaml
func (o *Options) FileName() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".go-comic-converter.yaml")
}
func (o *Options) LoadDefault() error {
// Load config files
func (o *Options) LoadConfig() error {
f, err := os.Open(o.FileName())
if err != nil {
return nil
@ -112,7 +118,8 @@ func (o *Options) LoadDefault() error {
return nil
}
func (o *Options) ShowDefault() string {
// Get current settings for fields that can be saved
func (o *Options) ShowConfig() string {
var profileDesc, viewDesc string
profile := o.GetProfile()
if profile != nil {
@ -180,12 +187,14 @@ func (o *Options) ShowDefault() string {
)
}
func (o *Options) ResetDefault() error {
New().SaveDefault()
return o.LoadDefault()
// reset all settings to default value
func (o *Options) ResetConfig() error {
New().SaveConfig()
return o.LoadConfig()
}
func (o *Options) SaveDefault() error {
// save all current settings as futur default value
func (o *Options) SaveConfig() error {
f, err := os.Create(o.FileName())
if err != nil {
return err
@ -194,10 +203,12 @@ func (o *Options) SaveDefault() error {
return yaml.NewEncoder(f).Encode(o)
}
// shortcut to get current profile
func (o *Options) GetProfile() *profiles.Profile {
return o.profiles.Get(o.Profile)
}
// all available profiles
func (o *Options) AvailableProfiles() string {
return o.profiles.String()
}

View File

@ -1,3 +1,6 @@
/*
Manage supported profiles for go-comic-converter.
*/
package profiles
import (
@ -12,8 +15,10 @@ type Profile struct {
Height int
}
// Recommended ratio of image for perfect rendering Portrait or Landscape.
const PerfectRatio = 1.5
// Compute best dimension based on device size
func (p Profile) PerfectDim() (int, int) {
width, height := float64(p.Width), float64(p.Height)
perfectWidth, perfectHeight := height/PerfectRatio, width*PerfectRatio
@ -27,6 +32,7 @@ func (p Profile) PerfectDim() (int, int) {
type Profiles []Profile
// Initialize list of all supported profiles.
func New() Profiles {
return []Profile{
{"K1", "Kindle 1", 600, 670},
@ -70,6 +76,7 @@ func (p Profiles) String() string {
return strings.Join(s, "\n")
}
// Lookup profile by code
func (p Profiles) Get(name string) *Profile {
for _, profile := range p {
if profile.Code == name {

View File

@ -1,3 +1,6 @@
/*
Tools to create epub from images.
*/
package epub
import (
@ -11,7 +14,7 @@ import (
"time"
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
epubimageprocessing "github.com/celogeek/go-comic-converter/v2/internal/epub/image_processing"
epubimageprocessing "github.com/celogeek/go-comic-converter/v2/internal/epub/imageprocessing"
epubprogress "github.com/celogeek/go-comic-converter/v2/internal/epub/progress"
epubtemplates "github.com/celogeek/go-comic-converter/v2/internal/epub/templates"
epubtree "github.com/celogeek/go-comic-converter/v2/internal/epub/tree"
@ -48,6 +51,7 @@ type epubPart struct {
Images []*epubimage.Image
}
// initialize epub
func New(options *Options) *ePub {
uid := uuid.Must(uuid.NewV4())
tmpl := template.New("parser")
@ -65,7 +69,8 @@ func New(options *Options) *ePub {
}
}
func (e *ePub) render(templateString string, data any) string {
// render templates
func (e *ePub) render(templateString string, data map[string]any) string {
var result strings.Builder
tmpl := template.Must(e.templateProcessor.Parse(templateString))
if err := tmpl.Execute(&result, data); err != nil {
@ -74,6 +79,7 @@ func (e *ePub) render(templateString string, data any) string {
return regexp.MustCompile("\n+").ReplaceAllString(result.String(), "\n")
}
// write image to the zip
func (e *ePub) writeImage(wz *epubzip.EpubZip, img *epubimage.Image) error {
err := wz.WriteFile(
fmt.Sprintf("OEBPS/%s", img.TextPath()),
@ -92,6 +98,7 @@ func (e *ePub) writeImage(wz *epubzip.EpubZip, img *epubimage.Image) error {
return err
}
// write blank page
func (e *ePub) writeBlank(wz *epubzip.EpubZip, img *epubimage.Image) error {
return wz.WriteFile(
fmt.Sprintf("OEBPS/Text/%d_sp.xhtml", img.Id),
@ -102,6 +109,7 @@ func (e *ePub) writeBlank(wz *epubzip.EpubZip, img *epubimage.Image) error {
)
}
// extract image and split it into part
func (e *ePub) getParts() ([]*epubPart, error) {
images, err := epubimageprocessing.LoadImages(&epubimageprocessing.Options{
Input: e.Input,
@ -116,14 +124,12 @@ func (e *ePub) getParts() ([]*epubPart, error) {
return nil, err
}
// sort result by id and part
sort.Slice(images, func(i, j int) bool {
if images[i].Id < images[j].Id {
return true
} else if images[i].Id == images[j].Id {
if images[i].Id == images[j].Id {
return images[i].Part < images[j].Part
} else {
return false
}
return images[i].Id < images[j].Id
})
parts := make([]*epubPart, 0)
@ -140,8 +146,8 @@ func (e *ePub) getParts() ([]*epubPart, error) {
return parts, nil
}
// compute size of the epub part and try to be as close as possible of the target
maxSize := uint64(e.LimitMb * 1024 * 1024)
xhtmlSize := uint64(1024)
// descriptor files + title
baseSize := uint64(16*1024) + cover.Data.CompressedSize()
@ -180,6 +186,9 @@ func (e *ePub) getParts() ([]*epubPart, error) {
return parts, nil
}
// create a tree from the directories.
//
// this is used to simulate the toc.
func (e *ePub) getTree(images []*epubimage.Image, skip_files bool) string {
t := epubtree.New()
for _, img := range images {
@ -197,6 +206,7 @@ func (e *ePub) getTree(images []*epubimage.Image, skip_files bool) string {
return c.WriteString("")
}
// create the zip
func (e *ePub) Write() error {
type zipContent struct {
Name string
@ -285,7 +295,7 @@ func (e *ePub) Write() error {
return err
}
}
if err := wz.WriteImage(epubimageprocessing.LoadCoverData(part.Cover, title, e.Image.Quality)); err != nil {
if err := wz.WriteImage(epubimageprocessing.LoadCoverTitleData(part.Cover, title, e.Image.Quality)); err != nil {
return err
}

View File

@ -1,3 +1,6 @@
/*
Rotate image if the source width > height.
*/
package epubfilters
import (

View File

@ -1,3 +1,6 @@
/*
Create a title with the cover image
*/
package epubfilters
import (
@ -19,10 +22,12 @@ type coverTitle struct {
title string
}
// size is the same as source
func (p *coverTitle) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
return srcBounds
}
// blur the src image, and create a box with the title in the middle
func (p *coverTitle) Draw(dst draw.Image, src image.Image, options *gift.Options) {
// Create a blur version of the cover
g := gift.New(gift.GaussianBlur(4))

View File

@ -1,3 +1,8 @@
/*
cut a double page in 2 part: left and right.
this will cut in the middle of the page.
*/
package epubfilters
import (

View File

@ -1,3 +1,8 @@
/*
generate a blank pixel 1x1, if the size of the image is 0x0.
An image 0x0 is not a valid image, and failed to read.
*/
package epubfilters
import (
@ -27,6 +32,7 @@ func (p *pixel) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
func (p *pixel) Draw(dst draw.Image, src image.Image, options *gift.Options) {
if dst.Bounds().Dx() == 1 && dst.Bounds().Dy() == 1 {
dst.Set(0, 0, color.White)
return
}
draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src)
}

View File

@ -1,3 +1,8 @@
/*
Resize image by keeping aspect ratio.
This will reduce or enlarge image to fit into the viewWidth and viewHeight.
*/
package epubfilters
import (
@ -42,6 +47,5 @@ func (p *resizeFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
}
func (p *resizeFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
b := p.Bounds(src.Bounds())
gift.Resize(b.Dx(), b.Dy(), p.resampling).Draw(dst, src, options)
gift.Resize(dst.Bounds().Dx(), dst.Bounds().Dy(), p.resampling).Draw(dst, src, options)
}

View File

@ -1,3 +1,6 @@
/*
Image helpers to transform image.
*/
package epubimage
import (
@ -20,22 +23,35 @@ type Image struct {
Name string
}
// key name of the image
func (i *Image) Key(prefix string) string {
return fmt.Sprintf("%s_%d_p%d", prefix, i.Id, i.Part)
}
// key name of the blank plage after the image
func (i *Image) SpaceKey(prefix string) string {
return fmt.Sprintf("%s_%d_sp", prefix, i.Id)
}
// path of the blank page
func (i *Image) SpacePath() string {
return fmt.Sprintf("Text/%d_sp.xhtml", i.Id)
}
// text path linked to the image
func (i *Image) TextPath() string {
return fmt.Sprintf("Text/%d_p%d.xhtml", i.Id, i.Part)
}
// image path
func (i *Image) ImgPath() string {
return fmt.Sprintf("Images/%d_p%d.jpg", i.Id, i.Part)
}
// style to apply to the image.
//
// center by default.
// align to left or right if it's part of the splitted double page.
func (i *Image) ImgStyle(viewWidth, viewHeight int, manga bool) string {
marginW, marginH := float64(viewWidth-i.Width)/2, float64(viewHeight-i.Height)/2
left, top := marginW*100/float64(viewWidth), marginH*100/float64(viewHeight)
@ -65,7 +81,3 @@ func (i *Image) ImgStyle(viewWidth, viewHeight int, manga bool) string {
align,
)
}
func (i *Image) SpacePath() string {
return fmt.Sprintf("Text/%d_sp.xhtml", i.Id)
}

View File

@ -5,6 +5,7 @@ import (
"github.com/disintegration/gift"
)
// create filter to apply to the source
func NewGift(options *Options) *gift.GIFT {
g := gift.New()
g.SetParallelization(false)
@ -25,6 +26,7 @@ func NewGift(options *Options) *gift.GIFT {
return g
}
// create filters to cut image into 2 equal pieces
func NewGiftSplitDoublePage(options *Options) []*gift.GIFT {
gifts := make([]*gift.GIFT, 2)

View File

@ -1,5 +1,6 @@
package epubimage
// options for image transformation
type Options struct {
Crop bool
ViewWidth int

View File

@ -1,3 +1,6 @@
/*
prepare image to be store in a zip file.
*/
package epubimagedata
import (
@ -17,6 +20,7 @@ type ImageData struct {
Data []byte
}
// compressed size of the image with the header
func (img *ImageData) CompressedSize() uint64 {
return img.Header.CompressedSize64 + 30 + uint64(len(img.Header.Name))
}
@ -26,11 +30,13 @@ func exitWithError(err error) {
os.Exit(1)
}
// create a new data image with file name based on id and part
func New(id int, part int, img image.Image, quality int) *ImageData {
name := fmt.Sprintf("OEBPS/Images/%d_p%d.jpg", id, part)
return NewRaw(name, img, quality)
}
// create gzip encoded jpeg
func NewRaw(name string, img image.Image, quality int) *ImageData {
var (
data, cdata bytes.Buffer

View File

@ -1,3 +1,6 @@
/*
Extract and transform image into a compressed jpeg.
*/
package epubimageprocessing
import (
@ -26,6 +29,7 @@ type tasks struct {
Name string
}
// extract and convert images
func LoadImages(o *Options) ([]*epubimage.Image, error) {
images := make([]*epubimage.Image, 0)
@ -39,6 +43,7 @@ func LoadImages(o *Options) ([]*epubimage.Image, error) {
imageInput chan *tasks
)
// get all images though a channel of bytes
if fi.IsDir() {
imageCount, imageInput, err = o.loadDir()
} else {
@ -57,6 +62,7 @@ func LoadImages(o *Options) ([]*epubimage.Image, error) {
return nil, err
}
// dry run, skip convertion
if o.Dry {
for img := range imageInput {
images = append(images, &epubimage.Image{
@ -178,7 +184,8 @@ func LoadImages(o *Options) ([]*epubimage.Image, error) {
return images, nil
}
func LoadCoverData(img *epubimage.Image, title string, quality int) *epubimagedata.ImageData {
// create a title page with the cover
func LoadCoverTitleData(img *epubimage.Image, title string, quality int) *epubimagedata.ImageData {
// Create a blur version of the cover
g := gift.New(epubfilters.CoverTitle(title))
dst := image.NewGray(g.Bounds(img.Raw.Bounds()))

View File

@ -7,6 +7,7 @@ import (
"strings"
)
// only accept jpg, png and webp as source file
func isSupportedImage(path string) bool {
switch strings.ToLower(filepath.Ext(path)) {
case ".jpg", ".jpeg", ".png", ".webp":
@ -17,11 +18,13 @@ func isSupportedImage(path string) bool {
return false
}
// check if the color is blank enough
func colorIsBlank(c color.Color) bool {
g := color.GrayModel.Convert(c).(color.Gray)
return g.Y >= 0xf0
}
// lookup for margin (blank) around the image
func findMarging(img image.Image) image.Rectangle {
imgArea := img.Bounds()

View File

@ -30,6 +30,7 @@ type Options struct {
var errNoImagesFound = errors.New("no images found")
// ensure copy image into a buffer
func (o *Options) mustExtractImage(imageOpener func() (io.ReadCloser, error)) *bytes.Buffer {
var b bytes.Buffer
if o.Dry {
@ -51,6 +52,7 @@ func (o *Options) mustExtractImage(imageOpener func() (io.ReadCloser, error)) *b
return &b
}
// load a directory of images
func (o *Options) loadDir() (totalImages int, output chan *tasks, err error) {
images := make([]string, 0)
@ -101,6 +103,7 @@ func (o *Options) loadDir() (totalImages int, output chan *tasks, err error) {
return
}
// load a zip file that include images
func (o *Options) loadCbz() (totalImages int, output chan *tasks, err error) {
r, err := zip.OpenReader(o.Input)
if err != nil {
@ -150,6 +153,7 @@ func (o *Options) loadCbz() (totalImages int, output chan *tasks, err error) {
return
}
// load a rar file that include images
func (o *Options) loadCbr() (totalImages int, output chan *tasks, err error) {
// listing and indexing
rl, err := rardecode.OpenReader(o.Input, "")
@ -233,6 +237,7 @@ func (o *Options) loadCbr() (totalImages int, output chan *tasks, err error) {
return
}
// extract image from a pdf
func (o *Options) loadPdf() (totalImages int, output chan *tasks, err error) {
pdf := pdfread.Load(o.Input)
if pdf == nil {

View File

@ -1,3 +1,6 @@
/*
create a progress bar with custom settings.
*/
package epubprogress
import (

View File

@ -1,3 +1,6 @@
/*
Templates use to create xml files of the epub.
*/
package epubtemplates
import _ "embed"

View File

@ -28,6 +28,7 @@ type tag struct {
value string
}
// create the content file
func Content(o *ContentOptions) string {
doc := etree.NewDocument()
doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
@ -76,6 +77,7 @@ func Content(o *ContentOptions) string {
return r
}
// metadata part of the content
func getMeta(o *ContentOptions) []tag {
metas := []tag{
{"meta", tagAttrs{"property": "dcterms:modified"}, o.UpdatedAt},
@ -152,6 +154,7 @@ func getManifest(o *ContentOptions) []tag {
return items
}
// spine part of the content
func getSpine(o *ContentOptions) []tag {
isOnTheRight := !o.ImageOptions.Manga
getSpread := func(doublePageNoBlank bool) string {
@ -196,6 +199,7 @@ func getSpine(o *ContentOptions) []tag {
return spine
}
// guide part of the content
func getGuide(o *ContentOptions) []tag {
guide := []tag{}
if o.Cover != nil {

View File

@ -8,6 +8,7 @@ import (
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
)
// create toc
func Toc(title string, stripFirstDirectoryFromToc bool, images []*epubimage.Image) string {
doc := etree.NewDocument()
doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)

View File

@ -1,3 +1,21 @@
/*
Organize a list of filename with their path into a tree of directories.
Example:
- A/B/C/D.jpg
- A/B/C/E.jpg
- A/B/F/G.jpg
This is transformed like:
A
B
C
D.jpg
E.jpg
F
G.jpg
*/
package epubtree
import (
@ -14,16 +32,19 @@ type node struct {
Children []*node
}
// initilize tree with a root node
func New() *tree {
return &tree{map[string]*node{
".": {".", []*node{}},
}}
}
// root node
func (n *tree) Root() *node {
return n.Nodes["."]
}
// add the filename to the tree
func (n *tree) Add(filename string) {
cn := n.Root()
cp := ""
@ -37,6 +58,7 @@ func (n *tree) Add(filename string) {
}
}
// string version of the tree
func (n *node) WriteString(indent string) string {
r := strings.Builder{}
if indent != "" {

View File

@ -1,3 +1,8 @@
/*
Helper to write epub files.
We create a zip with the magic epub mimetype.
*/
package epubzip
import (
@ -13,6 +18,7 @@ type EpubZip struct {
wz *zip.Writer
}
// create a new epub
func New(path string) (*EpubZip, error) {
w, err := os.Create(path)
if err != nil {
@ -22,6 +28,7 @@ func New(path string) (*EpubZip, error) {
return &EpubZip{w, wz}, nil
}
// close compress pipe and file.
func (e *EpubZip) Close() error {
if err := e.wz.Close(); err != nil {
return err
@ -29,6 +36,8 @@ func (e *EpubZip) Close() error {
return e.w.Close()
}
// Write mimetype, in a very specific way.
// This will be valid with epubcheck tools.
func (e *EpubZip) WriteMagic() error {
t := time.Now()
fh := &zip.FileHeader{
@ -51,6 +60,7 @@ func (e *EpubZip) WriteMagic() error {
return err
}
// Write image. They are already compressed, so we write them down directly.
func (e *EpubZip) WriteImage(image *epubimagedata.ImageData) error {
m, err := e.wz.CreateRaw(image.Header)
if err != nil {
@ -60,6 +70,7 @@ func (e *EpubZip) WriteImage(image *epubimagedata.ImageData) error {
return err
}
// Write file. Compressed it using deflate.
func (e *EpubZip) WriteFile(file string, content []byte) error {
m, err := e.wz.CreateHeader(&zip.FileHeader{
Name: file,

View File

@ -1,95 +1,45 @@
/*
sortpath support sorting of path that may include number.
A series of path can looks like:
- Tome1/Chap1/Image1.jpg
- Tome1/Chap2/Image1.jpg
- Tome1/Chap10/Image2.jpg
The module will split the string by path,
and compare them by decomposing the string and number part.
The module support 3 mode:
- mode=0 alpha for path and file
- mode=1 alphanum for path and alpha for file
- mode=2 alphanum for path and file
Example:
files := []string{
'T1/C1/Img1.jpg',
'T1/C2/Img1.jpg',
'T1/C10/Img1.jpg',
}
sort.Sort(sortpath.By(files, 1))
*/
package sortpath
import (
"path/filepath"
"regexp"
"strconv"
"strings"
)
// Strings follow with numbers like: s1, s1.2, s2-3, ...
var split_path_regex = regexp.MustCompile(`^(.*?)(\d+(?:\.\d+)?)(?:-(\d+(?:\.\d+)?))?$`)
type part struct {
fullname string
name string
number float64
}
func (a part) compare(b part) float64 {
if a.number == 0 || b.number == 0 {
return float64(strings.Compare(a.fullname, b.fullname))
}
if a.name == b.name {
return a.number - b.number
} else {
return float64(strings.Compare(a.name, b.name))
}
}
func parsePart(p string) part {
r := split_path_regex.FindStringSubmatch(p)
if len(r) == 0 {
return part{p, p, 0}
}
n, err := strconv.ParseFloat(r[2], 64)
if err != nil {
return part{p, p, 0}
}
return part{p, r[1], n}
}
// mode=0 alpha for path and file
// mode=1 alphanum for path and alpha for file
// mode=2 alphanum for path and file
func parse(filename string, mode int) []part {
pathname, name := filepath.Split(strings.ToLower(filename))
pathname = strings.TrimSuffix(pathname, string(filepath.Separator))
ext := filepath.Ext(name)
name = name[0 : len(name)-len(ext)]
f := []part{}
for _, p := range strings.Split(pathname, string(filepath.Separator)) {
if mode > 0 { // alphanum for path
f = append(f, parsePart(p))
} else {
f = append(f, part{p, p, 0})
}
}
if mode == 2 { // alphanum for file
f = append(f, parsePart(name))
} else {
f = append(f, part{name, name, 0})
}
return f
}
func comparePart(a, b []part) float64 {
m := len(a)
if m > len(b) {
m = len(b)
}
for i := 0; i < m; i++ {
c := a[i].compare(b[i])
if c != 0 {
return c
}
}
return float64(len(a) - len(b))
}
// struct that implement interface for sort.Sort
type by struct {
filenames []string
paths [][]part
}
func (b by) Len() int { return len(b.filenames) }
func (b by) Less(i, j int) bool { return comparePart(b.paths[i], b.paths[j]) < 0 }
func (b by) Less(i, j int) bool { return compareParts(b.paths[i], b.paths[j]) < 0 }
func (b by) Swap(i, j int) {
b.filenames[i], b.filenames[j] = b.filenames[j], b.filenames[i]
b.paths[i], b.paths[j] = b.paths[j], b.paths[i]
}
// use sortpath.By with sort.Sort
func By(filenames []string, mode int) by {
p := [][]part{}
for _, filename := range filenames {

View File

@ -0,0 +1,83 @@
package sortpath
import (
"path/filepath"
"regexp"
"strconv"
"strings"
)
// Strings follow with numbers like: s1, s1.2, s2-3, ...
var split_path_regex = regexp.MustCompile(`^(.*?)(\d+(?:\.\d+)?)(?:-(\d+(?:\.\d+)?))?$`)
type part struct {
fullname string
name string
number float64
}
// compare part, first check if both include a number,
// if so, compare string part then number part, else compare there as string.
func (a part) compare(b part) float64 {
if a.number == 0 || b.number == 0 {
return float64(strings.Compare(a.fullname, b.fullname))
}
if a.name == b.name {
return a.number - b.number
} else {
return float64(strings.Compare(a.name, b.name))
}
}
// separate from the string the number part.
func parsePart(p string) part {
r := split_path_regex.FindStringSubmatch(p)
if len(r) == 0 {
return part{p, p, 0}
}
n, err := strconv.ParseFloat(r[2], 64)
if err != nil {
return part{p, p, 0}
}
return part{p, r[1], n}
}
// mode=0 alpha for path and file
// mode=1 alphanum for path and alpha for file
// mode=2 alphanum for path and file
func parse(filename string, mode int) []part {
pathname, name := filepath.Split(strings.ToLower(filename))
pathname = strings.TrimSuffix(pathname, string(filepath.Separator))
ext := filepath.Ext(name)
name = name[0 : len(name)-len(ext)]
f := []part{}
for _, p := range strings.Split(pathname, string(filepath.Separator)) {
if mode > 0 { // alphanum for path
f = append(f, parsePart(p))
} else {
f = append(f, part{p, p, 0})
}
}
if mode == 2 { // alphanum for file
f = append(f, parsePart(name))
} else {
f = append(f, part{name, name, 0})
}
return f
}
// compare 2 fullpath splitted into parts
func compareParts(a, b []part) float64 {
m := len(a)
if m > len(b) {
m = len(b)
}
for i := 0; i < m; i++ {
c := a[i].compare(b[i])
if c != 0 {
return c
}
}
return float64(len(a) - len(b))
}

17
main.go
View File

@ -1,3 +1,10 @@
/*
Convert CBZ/CBR/Dir into Epub for e-reader devices (Kindle Devices, ...)
My goal is to make a simple, crossplatform, and fast tool to convert comics into epub.
Epub is now support by Amazon through [SendToKindle](https://www.amazon.com/gp/sendtokindle/), by Email or by using the App. So I've made it simple to support the size limit constraint of those services.
*/
package main
import (
@ -57,29 +64,29 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
}
if cmd.Options.Save {
cmd.Options.SaveDefault()
cmd.Options.SaveConfig()
fmt.Fprintf(
os.Stderr,
"%s%s\n\nSaving to %s\n",
cmd.Options.Header(),
cmd.Options.ShowDefault(),
cmd.Options.ShowConfig(),
cmd.Options.FileName(),
)
return
}
if cmd.Options.Show {
fmt.Fprintln(os.Stderr, cmd.Options.Header(), cmd.Options.ShowDefault())
fmt.Fprintln(os.Stderr, cmd.Options.Header(), cmd.Options.ShowConfig())
return
}
if cmd.Options.Reset {
cmd.Options.ResetDefault()
cmd.Options.ResetConfig()
fmt.Fprintf(
os.Stderr,
"%s%s\n\nReset default to %s\n",
cmd.Options.Header(),
cmd.Options.ShowDefault(),
cmd.Options.ShowConfig(),
cmd.Options.FileName(),
)
return