diff --git a/internal/epub/core.go b/internal/epub/core.go index 52436db..92c21bb 100644 --- a/internal/epub/core.go +++ b/internal/epub/core.go @@ -19,40 +19,42 @@ import ( imageconverter "go-comic-converter/internal/image-converter" ) -type Image struct { +type image struct { Id int - Data *ImageData + Data *imageData Width int Height int } -type EPub struct { - Path string - - UID string +type EpubOptions struct { + Input string + Output string Title string Author string - Publisher string - UpdatedAt string ViewWidth int ViewHeight int Quality int Crop bool LimitMb int - - Error error - - ImagesCount int - ProcessingImages func() chan *Image - TemplateProcessor *template.Template } -type EpubPart struct { - Cover *Image - Images []*Image +type ePub struct { + *EpubOptions + UID string + Publisher string + UpdatedAt string + + imagesCount int + processingImages func() chan *image + templateProcessor *template.Template } -func NewEpub(path string) *EPub { +type epubPart struct { + Cover *image + Images []*image +} + +func NewEpub(options *EpubOptions) *ePub { uid, err := uuid.NewV4() if err != nil { panic(err) @@ -64,55 +66,17 @@ func NewEpub(path string) *EPub { "zoom": func(s int, z float32) int { return int(float32(s) * z) }, }) - return &EPub{ - Path: path, - - UID: uid.String(), - Title: "Unknown title", - Author: "Unknown author", - Publisher: "GO Comic Converter", - UpdatedAt: time.Now().UTC().Format("2006-01-02T15:04:05Z"), - ViewWidth: 0, - ViewHeight: 0, - Quality: 75, - - TemplateProcessor: tmpl, + return &ePub{ + EpubOptions: options, + UID: uid.String(), + Publisher: "GO Comic Converter", + UpdatedAt: time.Now().UTC().Format("2006-01-02T15:04:05Z"), + templateProcessor: tmpl, } } -func (e *EPub) SetTitle(title string) *EPub { - e.Title = title - return e -} - -func (e *EPub) SetAuthor(author string) *EPub { - e.Author = author - return e -} - -func (e *EPub) SetSize(w, h int) *EPub { - e.ViewWidth = w - e.ViewHeight = h - return e -} - -func (e *EPub) SetQuality(q int) *EPub { - e.Quality = q - return e -} - -func (e *EPub) SetCrop(c bool) *EPub { - e.Crop = c - return e -} - -func (e *EPub) SetLimitMb(l int) *EPub { - e.LimitMb = l - return e -} - -func (e *EPub) Render(templateString string, data any) string { - tmpl, err := e.TemplateProcessor.Parse(templateString) +func (e *ePub) render(templateString string, data any) string { + tmpl, err := e.templateProcessor.Parse(templateString) if err != nil { panic(err) } @@ -124,34 +88,32 @@ func (e *EPub) Render(templateString string, data any) string { return result.String() } -func (e *EPub) Load(path string) *EPub { - fi, err := os.Stat(path) +func (e *ePub) load() error { + fi, err := os.Stat(e.Input) if err != nil { - e.Error = err - return e + return err } if fi.IsDir() { - return e.LoadDir(path) + return e.loadDir() } - switch ext := strings.ToLower(filepath.Ext(path)); ext { + switch ext := strings.ToLower(filepath.Ext(e.Input)); ext { case ".cbz": - e.LoadCBZ(path) + return e.loadCBZ() case ".cbr": - e.LoadCBR(path) + return e.loadCBR() case ".pdf": - e.LoadPDF(path) + return e.loadPDF() default: - e.Error = fmt.Errorf("unknown file format (%s): support .cbz, .cbr, .pdf", ext) + return fmt.Errorf("unknown file format (%s): support .cbz, .cbr, .pdf", ext) } - return e } -func (e *EPub) LoadCBZ(path string) *EPub { - r, err := zip.OpenReader(path) +func (e *ePub) loadCBZ() error { + r, err := zip.OpenReader(e.Input) if err != nil { - e.Error = err + return err } images := make([]*zip.File, 0) @@ -165,16 +127,15 @@ func (e *EPub) LoadCBZ(path string) *EPub { images = append(images, f) } if len(images) == 0 { - e.Error = fmt.Errorf("no images found") r.Close() - return e + return fmt.Errorf("no images found") } sort.SliceStable(images, func(i, j int) bool { return strings.Compare(images[i].Name, images[j].Name) < 0 }) - e.ImagesCount = len(images) + e.imagesCount = len(images) type Todo struct { Id int @@ -183,10 +144,10 @@ func (e *EPub) LoadCBZ(path string) *EPub { todo := make(chan *Todo) - e.ProcessingImages = func() chan *Image { + e.processingImages = func() chan *image { // defer r.Close() wg := &sync.WaitGroup{} - results := make(chan *Image) + results := make(chan *image) for i := 0; i < runtime.NumCPU(); i++ { wg.Add(1) go func() { @@ -207,9 +168,9 @@ func (e *EPub) LoadCBZ(path string) *EPub { if task.Id == 0 { name = "OEBPS/Images/cover.jpg" } - results <- &Image{ + results <- &image{ task.Id, - NewImageData(name, data), + newImageData(name, data), w, h, } @@ -229,22 +190,20 @@ func (e *EPub) LoadCBZ(path string) *EPub { return results } - return e + return nil } -func (e *EPub) LoadCBR(path string) *EPub { - e.Error = fmt.Errorf("no implemented") - return e +func (e *ePub) loadCBR() error { + return fmt.Errorf("no implemented") } -func (e *EPub) LoadPDF(path string) *EPub { - e.Error = fmt.Errorf("no implemented") - return e +func (e *ePub) loadPDF() error { + return fmt.Errorf("no implemented") } -func (e *EPub) LoadDir(dirname string) *EPub { +func (e *ePub) loadDir() error { images := make([]string, 0) - err := filepath.WalkDir(dirname, func(path string, d fs.DirEntry, err error) error { + err := filepath.WalkDir(e.Input, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } @@ -260,16 +219,14 @@ func (e *EPub) LoadDir(dirname string) *EPub { return nil }) if err != nil { - e.Error = err - return e + return err } if len(images) == 0 { - e.Error = fmt.Errorf("no images found") - return e + return fmt.Errorf("no images found") } sort.Strings(images) - e.ImagesCount = len(images) + e.imagesCount = len(images) type Todo struct { Id int @@ -278,9 +235,9 @@ func (e *EPub) LoadDir(dirname string) *EPub { todo := make(chan *Todo) - e.ProcessingImages = func() chan *Image { + e.processingImages = func() chan *image { wg := &sync.WaitGroup{} - results := make(chan *Image) + results := make(chan *image) for i := 0; i < runtime.NumCPU(); i++ { wg.Add(1) go func() { @@ -301,9 +258,9 @@ func (e *EPub) LoadDir(dirname string) *EPub { if task.Id == 0 { name = "OEBPS/Images/cover.jpg" } - results <- &Image{ + results <- &image{ task.Id, - NewImageData(name, data), + newImageData(name, data), w, h, } @@ -321,27 +278,27 @@ func (e *EPub) LoadDir(dirname string) *EPub { return results } - return e + return nil } -func (e *EPub) GetParts() []*EpubPart { - images := make([]*Image, e.ImagesCount) - bar := progressbar.Default(int64(e.ImagesCount), "Processing") - for img := range e.ProcessingImages() { +func (e *ePub) getParts() []*epubPart { + images := make([]*image, e.imagesCount) + bar := progressbar.Default(int64(e.imagesCount), "Processing") + for img := range e.processingImages() { images[img.Id] = img bar.Add(1) } bar.Close() - epubPart := make([]*EpubPart, 0) + parts := make([]*epubPart, 0) cover := images[0] images = images[1:] if e.LimitMb == 0 { - epubPart = append(epubPart, &EpubPart{ + parts = append(parts, &epubPart{ Cover: cover, Images: images, }) - return epubPart + return parts } maxSize := uint64(e.LimitMb * 1024 * 1024) @@ -351,36 +308,36 @@ func (e *EPub) GetParts() []*EpubPart { baseSize := uint64(16*1024) + cover.Data.CompressedSize() currentSize := baseSize - currentImages := make([]*Image, 0) + currentImages := make([]*image, 0) part := 1 for _, img := range images { imgSize := img.Data.CompressedSize() + xhtmlSize if len(currentImages) > 0 && currentSize+imgSize > maxSize { - epubPart = append(epubPart, &EpubPart{ + parts = append(parts, &epubPart{ Cover: cover, Images: currentImages, }) part += 1 currentSize = baseSize - currentImages = make([]*Image, 0) + currentImages = make([]*image, 0) } currentSize += imgSize currentImages = append(currentImages, img) } if len(currentImages) > 0 { - epubPart = append(epubPart, &EpubPart{ + parts = append(parts, &epubPart{ Cover: cover, Images: currentImages, }) } - return epubPart + return parts } -func (e *EPub) Write() error { - if e.Error != nil { - return e.Error +func (e *ePub) Write() error { + if err := e.load(); err != nil { + return err } type ZipContent struct { @@ -388,31 +345,31 @@ func (e *EPub) Write() error { Content any } - epubParts := e.GetParts() + epubParts := e.getParts() totalParts := len(epubParts) bar := progressbar.Default(int64(totalParts), "Writing Part") for i, part := range epubParts { - ext := filepath.Ext(e.Path) + ext := filepath.Ext(e.Output) suffix := "" if totalParts > 1 { suffix = fmt.Sprintf(" PART_%02d", i+1) } - path := fmt.Sprintf("%s%s%s", e.Path[0:len(e.Path)-len(ext)], suffix, ext) - wz, err := NewEpubZip(path) + path := fmt.Sprintf("%s%s%s", e.Output[0:len(e.Output)-len(ext)], suffix, ext) + wz, err := newEpubZip(path) if err != nil { return err } defer wz.Close() zipContent := []ZipContent{ - {"META-INF/container.xml", TEMPLATE_CONTAINER}, - {"OEBPS/content.opf", e.Render(TEMPLATE_CONTENT, map[string]any{"Info": e, "Images": part.Images})}, - {"OEBPS/toc.ncx", e.Render(TEMPLATE_TOC, map[string]any{"Info": e, "Image": part.Images[0]})}, - {"OEBPS/nav.xhtml", e.Render(TEMPLATE_NAV, map[string]any{"Info": e, "Image": part.Images[0]})}, - {"OEBPS/Text/style.css", TEMPLATE_STYLE}, - {"OEBPS/Text/part.xhtml", e.Render(TEMPLATE_PART, map[string]any{ + {"META-INF/container.xml", containerTmpl}, + {"OEBPS/content.opf", e.render(contentTmpl, map[string]any{"Info": e, "Images": part.Images})}, + {"OEBPS/toc.ncx", e.render(tocTmpl, map[string]any{"Info": e, "Image": part.Images[0]})}, + {"OEBPS/nav.xhtml", e.render(navTmpl, map[string]any{"Info": e, "Image": part.Images[0]})}, + {"OEBPS/Text/style.css", styleTmpl}, + {"OEBPS/Text/part.xhtml", e.render(partTmpl, map[string]any{ "Info": e, "Part": i + 1, "Total": totalParts, @@ -431,7 +388,7 @@ func (e *EPub) Write() error { for _, img := range part.Images { text := fmt.Sprintf("OEBPS/Text/%d.xhtml", img.Id) - if err := wz.WriteFile(text, e.Render(TEMPLATE_TEXT, img)); err != nil { + if err := wz.WriteFile(text, e.render(textTmpl, img)); err != nil { return err } if err := wz.WriteImage(img.Data); err != nil { diff --git a/internal/epub/image_data.go b/internal/epub/image_data.go index 0d61015..fc872b3 100644 --- a/internal/epub/image_data.go +++ b/internal/epub/image_data.go @@ -8,16 +8,16 @@ import ( "time" ) -type ImageData struct { +type imageData struct { Header *zip.FileHeader Data []byte } -func (img *ImageData) CompressedSize() uint64 { +func (img *imageData) CompressedSize() uint64 { return img.Header.CompressedSize64 + 30 + uint64(len(img.Header.Name)) } -func NewImageData(name string, data []byte) *ImageData { +func newImageData(name string, data []byte) *imageData { cdata := bytes.NewBuffer([]byte{}) wcdata, err := flate.NewWriter(cdata, flate.BestCompression) if err != nil { @@ -29,7 +29,7 @@ func NewImageData(name string, data []byte) *ImageData { panic(err) } t := time.Now() - return &ImageData{ + return &imageData{ &zip.FileHeader{ Name: name, CompressedSize64: uint64(cdata.Len()), diff --git a/internal/epub/templates.go b/internal/epub/templates.go index 51ac917..0fb99cb 100644 --- a/internal/epub/templates.go +++ b/internal/epub/templates.go @@ -3,22 +3,22 @@ package epub import _ "embed" //go:embed "templates/container.xml.tmpl" -var TEMPLATE_CONTAINER string +var containerTmpl string //go:embed "templates/content.opf.tmpl" -var TEMPLATE_CONTENT string +var contentTmpl string //go:embed "templates/toc.ncx.tmpl" -var TEMPLATE_TOC string +var tocTmpl string //go:embed "templates/nav.xhtml.tmpl" -var TEMPLATE_NAV string +var navTmpl string //go:embed "templates/style.css.tmpl" -var TEMPLATE_STYLE string +var styleTmpl string //go:embed "templates/part.xhtml.tmpl" -var TEMPLATE_PART string +var partTmpl string //go:embed "templates/text.xhtml.tmpl" -var TEMPLATE_TEXT string +var textTmpl string diff --git a/internal/epub/zip.go b/internal/epub/zip.go index 116310d..a026041 100644 --- a/internal/epub/zip.go +++ b/internal/epub/zip.go @@ -7,28 +7,28 @@ import ( "time" ) -type EpubZip struct { +type epubZip struct { w *os.File wz *zip.Writer } -func NewEpubZip(path string) (*EpubZip, error) { +func newEpubZip(path string) (*epubZip, error) { w, err := os.Create(path) if err != nil { return nil, err } wz := zip.NewWriter(w) - return &EpubZip{w, wz}, nil + return &epubZip{w, wz}, nil } -func (e *EpubZip) Close() error { +func (e *epubZip) Close() error { if err := e.wz.Close(); err != nil { return err } return e.w.Close() } -func (e *EpubZip) WriteMagic() error { +func (e *epubZip) WriteMagic() error { t := time.Now() fh := &zip.FileHeader{ Name: "mimetype", @@ -50,7 +50,7 @@ func (e *EpubZip) WriteMagic() error { return err } -func (e *EpubZip) WriteImage(image *ImageData) error { +func (e *epubZip) WriteImage(image *imageData) error { m, err := e.wz.CreateRaw(image.Header) if err != nil { return err @@ -59,7 +59,7 @@ func (e *EpubZip) WriteImage(image *ImageData) error { return err } -func (e *EpubZip) WriteFile(file string, data any) error { +func (e *epubZip) WriteFile(file string, data any) error { var content []byte switch b := data.(type) { case string: diff --git a/main.go b/main.go index 777cc3f..6307d72 100644 --- a/main.go +++ b/main.go @@ -124,17 +124,17 @@ func main() { fmt.Println(opt) - err := epub.NewEpub(opt.Output). - SetSize(profile.Width, profile.Height). - SetQuality(opt.Quality). - SetCrop(!opt.NoCrop). - SetLimitMb(opt.LimitMb). - SetTitle(opt.Title). - SetAuthor(opt.Author). - Load(opt.Input). - Write() - - if err != nil { + if err := epub.NewEpub(&epub.EpubOptions{ + Input: opt.Input, + Output: opt.Output, + ViewWidth: profile.Width, + ViewHeight: profile.Height, + Quality: opt.Quality, + Crop: !opt.NoCrop, + LimitMb: opt.LimitMb, + Title: opt.Title, + Author: opt.Author, + }).Write(); err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) }