From 0263a643215f983030d52b03b79c43a0d56f92a6 Mon Sep 17 00:00:00 2001 From: celogeek <65178+celogeek@users.noreply.github.com> Date: Wed, 26 Apr 2023 16:23:38 +0200 Subject: [PATCH] comments the code --- internal/converter/converter.go | 39 +++++-- internal/converter/converter_order.go | 15 ++- .../converter/options/converter_options.go | 25 ++-- .../converter/profiles/converter_profiles.go | 7 ++ internal/epub/epub.go | 28 +++-- .../epub/filters/epub_filters_autorotate.go | 3 + .../epub/filters/epub_filters_cover_title.go | 5 + internal/epub/filters/epub_filters_crop.go | 5 + internal/epub/filters/epub_filters_pixel.go | 6 + internal/epub/filters/epub_filters_resize.go | 8 +- internal/epub/image/epub_image.go | 20 +++- internal/epub/image/epub_image_filters.go | 2 + internal/epub/image/epub_image_options.go | 1 + internal/epub/imagedata/epub_image_data.go | 6 + .../epub_image_processing.go | 9 +- .../epub_image_processing_helpers.go | 3 + .../epub_image_processing_loader.go | 5 + internal/epub/progress/epub_progress.go | 3 + internal/epub/templates/epub_templates.go | 3 + .../epub/templates/epub_templates_content.go | 4 + .../{epub_toc.go => epub_templates_toc.go} | 1 + internal/epub/tree/epub_tree.go | 22 ++++ internal/epub/zip/epub_zip.go | 11 ++ internal/sortpath/sortpath.go | 108 +++++------------- internal/sortpath/sortpath_parser.go | 83 ++++++++++++++ main.go | 17 ++- 26 files changed, 318 insertions(+), 121 deletions(-) rename internal/epub/{image_processing => imageprocessing}/epub_image_processing.go (93%) rename internal/epub/{image_processing => imageprocessing}/epub_image_processing_helpers.go (90%) rename internal/epub/{image_processing => imageprocessing}/epub_image_processing_loader.go (96%) rename internal/epub/templates/{epub_toc.go => epub_templates_toc.go} (99%) create mode 100644 internal/sortpath/sortpath_parser.go diff --git a/internal/converter/converter.go b/internal/converter/converter.go index 4b23170..cfadda1 100644 --- a/internal/converter/converter.go +++ b/internal/converter/converter.go @@ -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) diff --git a/internal/converter/converter_order.go b/internal/converter/converter_order.go index a454c9b..df431dd 100644 --- a/internal/converter/converter_order.go +++ b/internal/converter/converter_order.go @@ -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 } diff --git a/internal/converter/options/converter_options.go b/internal/converter/options/converter_options.go index 782b6b6..c16cb90 100644 --- a/internal/converter/options/converter_options.go +++ b/internal/converter/options/converter_options.go @@ -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() } diff --git a/internal/converter/profiles/converter_profiles.go b/internal/converter/profiles/converter_profiles.go index 49c68a0..a1672ea 100644 --- a/internal/converter/profiles/converter_profiles.go +++ b/internal/converter/profiles/converter_profiles.go @@ -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 { diff --git a/internal/epub/epub.go b/internal/epub/epub.go index 0a5641d..319c41d 100644 --- a/internal/epub/epub.go +++ b/internal/epub/epub.go @@ -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 } diff --git a/internal/epub/filters/epub_filters_autorotate.go b/internal/epub/filters/epub_filters_autorotate.go index b0561bc..1460c01 100644 --- a/internal/epub/filters/epub_filters_autorotate.go +++ b/internal/epub/filters/epub_filters_autorotate.go @@ -1,3 +1,6 @@ +/* +Rotate image if the source width > height. +*/ package epubfilters import ( diff --git a/internal/epub/filters/epub_filters_cover_title.go b/internal/epub/filters/epub_filters_cover_title.go index 51ce8b9..546f790 100644 --- a/internal/epub/filters/epub_filters_cover_title.go +++ b/internal/epub/filters/epub_filters_cover_title.go @@ -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)) diff --git a/internal/epub/filters/epub_filters_crop.go b/internal/epub/filters/epub_filters_crop.go index 87905f0..a3044a5 100644 --- a/internal/epub/filters/epub_filters_crop.go +++ b/internal/epub/filters/epub_filters_crop.go @@ -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 ( diff --git a/internal/epub/filters/epub_filters_pixel.go b/internal/epub/filters/epub_filters_pixel.go index 61b0a9a..ecb4b6f 100644 --- a/internal/epub/filters/epub_filters_pixel.go +++ b/internal/epub/filters/epub_filters_pixel.go @@ -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) } diff --git a/internal/epub/filters/epub_filters_resize.go b/internal/epub/filters/epub_filters_resize.go index ddde7d4..a91f179 100644 --- a/internal/epub/filters/epub_filters_resize.go +++ b/internal/epub/filters/epub_filters_resize.go @@ -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) } diff --git a/internal/epub/image/epub_image.go b/internal/epub/image/epub_image.go index e47b019..21eb47c 100644 --- a/internal/epub/image/epub_image.go +++ b/internal/epub/image/epub_image.go @@ -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) -} diff --git a/internal/epub/image/epub_image_filters.go b/internal/epub/image/epub_image_filters.go index 718e3fd..f44f2e3 100644 --- a/internal/epub/image/epub_image_filters.go +++ b/internal/epub/image/epub_image_filters.go @@ -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) diff --git a/internal/epub/image/epub_image_options.go b/internal/epub/image/epub_image_options.go index 26ed66d..3797036 100644 --- a/internal/epub/image/epub_image_options.go +++ b/internal/epub/image/epub_image_options.go @@ -1,5 +1,6 @@ package epubimage +// options for image transformation type Options struct { Crop bool ViewWidth int diff --git a/internal/epub/imagedata/epub_image_data.go b/internal/epub/imagedata/epub_image_data.go index 517ee52..60ce499 100644 --- a/internal/epub/imagedata/epub_image_data.go +++ b/internal/epub/imagedata/epub_image_data.go @@ -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 diff --git a/internal/epub/image_processing/epub_image_processing.go b/internal/epub/imageprocessing/epub_image_processing.go similarity index 93% rename from internal/epub/image_processing/epub_image_processing.go rename to internal/epub/imageprocessing/epub_image_processing.go index 2aa1d70..fdafe59 100644 --- a/internal/epub/image_processing/epub_image_processing.go +++ b/internal/epub/imageprocessing/epub_image_processing.go @@ -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())) diff --git a/internal/epub/image_processing/epub_image_processing_helpers.go b/internal/epub/imageprocessing/epub_image_processing_helpers.go similarity index 90% rename from internal/epub/image_processing/epub_image_processing_helpers.go rename to internal/epub/imageprocessing/epub_image_processing_helpers.go index 6c5a5d5..067da84 100644 --- a/internal/epub/image_processing/epub_image_processing_helpers.go +++ b/internal/epub/imageprocessing/epub_image_processing_helpers.go @@ -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() diff --git a/internal/epub/image_processing/epub_image_processing_loader.go b/internal/epub/imageprocessing/epub_image_processing_loader.go similarity index 96% rename from internal/epub/image_processing/epub_image_processing_loader.go rename to internal/epub/imageprocessing/epub_image_processing_loader.go index b19c2d5..6c9def8 100644 --- a/internal/epub/image_processing/epub_image_processing_loader.go +++ b/internal/epub/imageprocessing/epub_image_processing_loader.go @@ -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 { diff --git a/internal/epub/progress/epub_progress.go b/internal/epub/progress/epub_progress.go index a29d8a3..21dd011 100644 --- a/internal/epub/progress/epub_progress.go +++ b/internal/epub/progress/epub_progress.go @@ -1,3 +1,6 @@ +/* +create a progress bar with custom settings. +*/ package epubprogress import ( diff --git a/internal/epub/templates/epub_templates.go b/internal/epub/templates/epub_templates.go index 6976e6c..8256639 100644 --- a/internal/epub/templates/epub_templates.go +++ b/internal/epub/templates/epub_templates.go @@ -1,3 +1,6 @@ +/* +Templates use to create xml files of the epub. +*/ package epubtemplates import _ "embed" diff --git a/internal/epub/templates/epub_templates_content.go b/internal/epub/templates/epub_templates_content.go index d1b513c..6c4589f 100644 --- a/internal/epub/templates/epub_templates_content.go +++ b/internal/epub/templates/epub_templates_content.go @@ -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 { diff --git a/internal/epub/templates/epub_toc.go b/internal/epub/templates/epub_templates_toc.go similarity index 99% rename from internal/epub/templates/epub_toc.go rename to internal/epub/templates/epub_templates_toc.go index 7fc2842..be3d612 100644 --- a/internal/epub/templates/epub_toc.go +++ b/internal/epub/templates/epub_templates_toc.go @@ -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"`) diff --git a/internal/epub/tree/epub_tree.go b/internal/epub/tree/epub_tree.go index 84253cf..c4d5217 100644 --- a/internal/epub/tree/epub_tree.go +++ b/internal/epub/tree/epub_tree.go @@ -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 != "" { diff --git a/internal/epub/zip/epub_zip.go b/internal/epub/zip/epub_zip.go index 6f4ee54..8698d10 100644 --- a/internal/epub/zip/epub_zip.go +++ b/internal/epub/zip/epub_zip.go @@ -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, diff --git a/internal/sortpath/sortpath.go b/internal/sortpath/sortpath.go index bcefb6d..9b29c31 100644 --- a/internal/sortpath/sortpath.go +++ b/internal/sortpath/sortpath.go @@ -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 { diff --git a/internal/sortpath/sortpath_parser.go b/internal/sortpath/sortpath_parser.go new file mode 100644 index 0000000..a8524ba --- /dev/null +++ b/internal/sortpath/sortpath_parser.go @@ -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)) +} diff --git a/main.go b/main.go index f2e87ba..a0f937b 100644 --- a/main.go +++ b/main.go @@ -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