From 2b4f2db183bf3b983c59a08c8a633fc7428e7b3c Mon Sep 17 00:00:00 2001 From: zhengxin Date: Tue, 5 Nov 2019 19:33:47 +0800 Subject: [PATCH] generate thumbail automatically --- .gitignore | 1 + README.md | 83 +++++++++++++++++ chapter.go | 104 +++++++++++++++++++++ examples/README_ZH_cn.md | 73 +++++++++++++++ examples/cover_example.jpg | Bin 0 -> 22510 bytes examples/example.toml | 12 +++ examples/example.txt | 26 ++++++ img_test.go | 13 +++ main.go | 103 +++++++++++++++++++++ setting.go | 184 +++++++++++++++++++++++++++++++++++++ 10 files changed, 599 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 chapter.go create mode 100644 examples/README_ZH_cn.md create mode 100644 examples/cover_example.jpg create mode 100644 examples/example.toml create mode 100644 examples/example.txt create mode 100644 img_test.go create mode 100644 main.go create mode 100644 setting.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..229e3bb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.mobi diff --git a/README.md b/README.md new file mode 100644 index 0000000..9da7c56 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# TXT2MOBI + +[README_ZH_cn.md](./examples/README_ZH_cn.md) + +`txt2mobi` is a simple tool for converting txt file to Mobi format for kindle. It's Based on golang package [mobi](https://github.com/766b/mobi/). + +`Note`: All testing were done on Kindle App for windows and Kindle Paperwhite (10th Gen) +_PS: Any html element in text file would make effect._ + +## Build + +``` +go get -u github.com/zhnxin/txt2mobi +``` + +## Usage + +1. Prepare the cover,thumbnail and txt file + +2. Create a config file witch you can refer to [example.toml](./examples/example.toml) + +3. Run the command: +```sh +[linux]$./txt2mobi -f example.toml [- o output_file_name.mobi] [-p] +#for Windows +[windows]$ txt2mobi.exe -f example.toml [- o output_file_name.mobi] [-p] +``` + +## termimal parameter +require: +- `-config`: config file + +`or` + +require: + +- `-f`: input file +- `-title`: book title +- `-author`: book author +- `-cover`: book cover and thumbnail +- `-chapter`: chater title regexp pattern + +options: + +- `-subchapter`: subchapter title regexp pattern +- `compress`: is to compress the result +- `encoding:`: use gb18030 as default + + +other options: + +- `-o`: output file name +- `-p`: is to use '\\' to pack every paragrahes. +- `-escape`: to disable html escape + +## config file + +```toml +title="Example" +author="zhengxin" +file="example.txt" +cover="cover_example.jpg" +chapter="^Chapter\\.\\d+.*$" +subchapter='^Chapter\\.\\d+\-\\d+ .*$' +compress=false +//default cover size +//cover_width=860 +//cover_hight=1200 +``` + +_Note:_ +- `title`: If the output_filename is not given, the `title` would be used as default. +- `chapter`: A regexp pattern to determin the chapter line title. +- `compress`: If it's true, the output would be compressed witch make the file smaller but take mush time to finish. +- `encoding`: Options. Use `utf-8` as default. Also support `gb18030` and `gbk`. + +## Need GUI? + +This tool is ready simple but modifis all need for me. Email me if you really desire it. + +## About BLANK + +The prefixed blank would be ignore because of html decoding. diff --git a/chapter.go b/chapter.go new file mode 100644 index 0000000..b4f0798 --- /dev/null +++ b/chapter.go @@ -0,0 +1,104 @@ +package main + +import ( + "bytes" + "html" + + "github.com/zhnxin/mobi" +) + +var ( + isEscape = true + isParagraph = false + LineFeed = []byte("
") + PStart = []byte("

") + PEnd = []byte("

") + Blank = []byte{} +) + +func SetIsEscape(flag bool) { + isEscape = flag +} +func SetIsParagraph(flag bool) { + isParagraph = flag +} + +type chapterContent struct { + Title string + Content []byte +} + +func (c *chapterContent) Append(content []byte) { + if !isEscape { + content = []byte(html.EscapeString(string(content))) + } + if isParagraph { + if len(content) > 1 { + c.Content = append(c.Content, bytes.Join([][]byte{PStart, content, PEnd}, Blank)...) + } else { + c.Content = append(c.Content, LineFeed...) + } + } else { + c.Content = append(c.Content, LineFeed...) + if len(content) > 1 { + c.Content = append(c.Content, content...) + } + } + +} + +func (c *chapterContent) SetTitle(title string) { + c.Title = title +} + +func (c *chapterContent) Restore(title string) { + c.Title = title + c.Content = make([]byte, 0) +} + +type Chapter struct { + content chapterContent + subChapterList []chapterContent + subLength uint +} + +func NewChapter(title string) Chapter { + return Chapter{ + content: chapterContent{ + Title: title, + Content: []byte{}, + }, + subChapterList: make([]chapterContent, 0), + } +} + +func (c *Chapter) Restore(title string) { + c.subLength = 0 + c.content.Title = title + c.content.Content = make([]byte, 0) + c.subChapterList = make([]chapterContent, 0) +} + +func (c *Chapter) AddSubChapter(title string) { + subChapter := chapterContent{ + Title: title, + Content: make([]byte, 0), + } + c.subChapterList = append(c.subChapterList, subChapter) + c.subLength += 1 +} + +func (c *Chapter) Append(content []byte) { + if c.subLength < 1 { + c.content.Append(content) + } else { + c.subChapterList[c.subLength-1].Append(content) + } +} + +func (c *Chapter) Flush(mobiWriter mobi.Builder) { + chapter := mobiWriter.NewChapter(c.content.Title, c.content.Content) + for _, content := range c.subChapterList { + chapter.AddSubChapter(content.Title, content.Content) + } +} diff --git a/examples/README_ZH_cn.md b/examples/README_ZH_cn.md new file mode 100644 index 0000000..9d1ba86 --- /dev/null +++ b/examples/README_ZH_cn.md @@ -0,0 +1,73 @@ +# TXT2Mobi + +一个基于golang package [mobi](https://github.com/766b/mobi/)的简单命令行工具。能够将txt文本文件转换为Mobi格式 + +`注意`: 所有测试都是使用KindleForPC(windows)和Paperwhite (10th Gen) + +_ps: 文档中的所有html元素都会生效_ + +## 编译 + + +``` +go get -u github.com/zhnxin/txt2mobi +``` + +## Usage + +1. 准备好封面(.jpg),文本文档(.txt) +2. 根据[示例](./examples/example.toml)创建配置文件(.toml) +3. 运行 +```sh +[linux]$./txt2mobi -f example.toml [- o output_file_name.mobi] [-p] +[windows]$ txt2mobi.exe -f example.toml [- o output_file_name.mobi] [-p] +``` + +## termimal parameter +require: +- `-config`: config file + +or + +require: +- `-f`: 文档 +- `-title`: 书名 +- `-author`: 作者 +- `-cover`: 封面及缩略图 +- `-chapter`: 一级标题的正则表达式 +options: +- `-subchapter`: 二级标题的正则表达式,默认为空 +- `compress`: 是否压缩,默认不使用 +- `encoding`: 文件编码,默认为gb18030 + + +other options: + +- `-o`: 输出文件名 +- `-p`: 是否使用 '\\' 装饰段落. +- `-escape`: 关闭html转义 + +## 配置文件 +```toml +title="Example" +author="zhengxin" +file="example.txt" +cover="cover_example.jpg" +chapter="^第.*卷 .*$" +subchapter="^第\\d+章 .*$" +compress=false +``` + +_Note_: +- `title` : 如果可选的输出文件名没有提供,就会使用title作为默认文件名 +- `chapter`: 正则表达式,用于识别章节 +- `compress`: 被置为true时,会压缩生成结果,但回消耗更多的运行时间 +- `encoding`: 可选参数,默认值为`utf-8`,也支持`gb18030`和`gbk`。 + +## 需要GUI? + +这个命令行工具虽然简单(简陋?),但已经能够满足我的所有需求。如果你非常有需求,可以email联系我,让我有足够的¥动力¥来完成它。 + +## 关于开头空格 + +由于html内部解析,英文的段首空格将被忽略。 diff --git a/examples/cover_example.jpg b/examples/cover_example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b63cf2f6a7f6b652435ff33dc29fc94f6031783b GIT binary patch literal 22510 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ǩҪ

һ8MĵڸijhtmlҪ5ӽתֻҪ򵥵ʽתʵ̫ԽˡǾ˱дһߵͷ + +2 Ұrust + +ûҰrust rust㹻ҵҪȻһC/C++ʵrustûֳɵMobi⣬ֻѰԡ +ʲô˵MobiٷĵԼдߺߣǵDzӵģ + +3 golang + +mobiĿ⣬ʵ١ҵC԰汾ģԭᡣpythonûgolangֻҵһǾˡ + +ڶ + +4 ڶ + +俪ͷո⣬ҺΪѡֱصС˵ļ俪ͷԴĿոġʹ

ǩҵkindleͻڿҪ

ǩԼ'-p' + +Mobiʽ֧htmlʽtxtļתΪmobiʽʽҲӦñԭá仰˵תʽлضhtmlת塣Ҫرת壬ʹ'-escape' diff --git a/img_test.go b/img_test.go new file mode 100644 index 0000000..4711f71 --- /dev/null +++ b/img_test.go @@ -0,0 +1,13 @@ +package main + +import ( + "testing" +) + +func TestGImg(t *testing.T) { + config := Config{CoverHight: 1200, CoverWidth: 860} + _, _, err := config.generateCover("examples/cover_example.jpg") + if err != nil { + t.Fatal(err) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..e86adde --- /dev/null +++ b/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "bufio" + "flag" + "fmt" + "log" + "os" + + "github.com/zhnxin/mobi" +) + +var ( + HELP = flag.Bool("h", false, "help") + ConfigFile = flag.String("config", "", "ebook config file(.toml)") + IsParagraph = flag.Bool("p", false, "[option]is to use

,use false as default") + OutputFileName = flag.String("o", "", "[option]output file name") + IsEscape = flag.Bool("escape", false, "[option]To Disable html escape") + MetaFile = flag.String("f", "", "input file") + MetaCover = flag.String("cover", "", "mobi cover") + MetaThumb = flag.String("thumb", "", "mobi thumbnail") + MetaTitle = flag.String("title", "", "mobi title") + MetaAuthor = flag.String("author", "", "EBOK author") + MetaCompress = flag.Bool("compress", false, "Is to compress") + MetaEncoding = flag.String("encoding", "gb18030", "encoding:gb18030(default),gbk,uft-8") + MetaChapter = flag.String("chapter", "^第[零一二三四五六七八九十百千两\\d]+章[  ]{0,1}.*$", "regexp pattern for chapter,default:'^第[零一二三四五六七八九十百千两\\d]+章 .*$'") + MetaSubChapter = flag.String("subchapter", "", "regexp pattern for chapter,default:'^第[零一二三四五六七八九十百千两\\d]+章[  ]{0,1}.*$'") + + CONFIG *Config +) + +func initMain() { + flag.Parse() + var err error + if *ConfigFile != "" { + CONFIG, err = NewConfigWithFile(*ConfigFile) + if err != nil { + log.Fatal(err) + } + CONFIG.Update(*MetaFile, *MetaTitle, *MetaAuthor, *MetaCover, *MetaThumb) + } else { + CONFIG = NewConfig(*MetaTitle, *MetaCover, *MetaCover, *MetaAuthor, *MetaChapter, *MetaSubChapter, *MetaEncoding, *MetaFile, *MetaCompress) + } + + if err = CONFIG.Check(); err != nil { + log.Fatal(err) + flag.Usage() + return + } +} + +func main() { + initMain() + if *HELP { + flag.Usage() + fmt.Println(`Sugesstion: + chapter: "^第[零一二三四五六七八九十百千两\\d]+[卷部][  ]{0,1}.*$" + subchapter: "^第[零一二三四五六七八九十百千两\\d]+章[  ]{0,1}.*$"`) + return + } + SetIsEscape(*IsEscape) + SetIsParagraph(*IsParagraph) + mobi.SetSkipLog(true) + + file, err := os.Open(CONFIG.File) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + mobiWriter, err := CONFIG.NewWriter(*OutputFileName) + if err != nil { + log.Fatal(err) + } + + chapter := NewChapter(CONFIG.Title) + var line []byte + for scanner.Scan() { + line, err = CONFIG.Decode(scanner.Bytes()) + if err != nil { + log.Fatal(err) + } + if CONFIG.ChapterRegex.Match(line) { + chapter.Flush(mobiWriter) + chapter.Restore(string(line)) + } else if CONFIG.SubChapterRegex != nil && CONFIG.SubChapterRegex.Match(line) { + chapter.AddSubChapter(string(line)) + } else { + chapter.Append(line) + } + } + chapter.Flush(mobiWriter) + outfile, err := os.OpenFile(CONFIG.Title+".mobi", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + log.Fatal(err) + } + defer outfile.Close() + mobiWriter.WriteTo(outfile) + if err = scanner.Err(); err != nil { + log.Fatal(err) + } +} diff --git a/setting.go b/setting.go new file mode 100644 index 0000000..1ac8364 --- /dev/null +++ b/setting.go @@ -0,0 +1,184 @@ +package main + +import ( + "errors" + "fmt" + "image" + "log" + "os" + "regexp" + "time" + + "github.com/BurntSushi/toml" + "github.com/disintegration/imaging" + "github.com/nfnt/resize" + "github.com/zhnxin/golang-image/jpeg" + "github.com/zhnxin/mobi" + "golang.org/x/text/encoding" + "golang.org/x/text/encoding/simplifiedchinese" +) + +const defaultCoverWidth = 860 +const defaultCoverHight = 1200 + +type Config struct { + Title string + Cover string + CoverWidth uint `toml:"cover_width"` + CoverHight uint `toml:"cover_hight"` + Author string + Chapter string + SubChapter string + Encoding string + File string + ChapterRegex *regexp.Regexp + SubChapterRegex *regexp.Regexp + Compress bool + decode *encoding.Decoder + Lang string +} + +func NewConfig(title, cover, thumbnail, author, chapter, subchapter, encoding, file string, compress bool) *Config { + config := &Config{ + Title: title, + Cover: cover, + CoverWidth: defaultCoverWidth, CoverHight: defaultCoverHight, + Author: author, + Chapter: chapter, + SubChapter: subchapter, + Encoding: encoding, + File: file, + Compress: compress, + ChapterRegex: nil, + SubChapterRegex: nil, + decode: nil, + Lang: "", + } + return config +} +func (c *Config) Update(file, title, author, cover, thumbnail string) { + if file != "" { + c.File = file + } + if title != "" { + c.Title = title + } + if author != "" { + c.Author = author + } + if cover != "" { + c.Cover = cover + } +} + +func (config *Config) Check() (err error) { + switch config.Encoding { + case "GB18030", "gb18030": + config.decode = simplifiedchinese.GB18030.NewDecoder() + case "GBK", "gbk": + config.decode = simplifiedchinese.GBK.NewDecoder() + case "UTF8", "utf8", "utf-8", "": + config.decode = nil + default: + return fmt.Errorf("Unsupport encoding[GB18030,GBK,UTF8(default)]:%s", config.Encoding) + } + if _, err = os.Stat(config.File); os.IsNotExist(err) { + return + } + config.ChapterRegex, err = regexp.Compile(config.Chapter) + if err == nil && config.SubChapter != "" { + config.SubChapterRegex, err = regexp.Compile(config.SubChapter) + } + if config.Cover == "" { + return errors.New("ebook need a cover") + } + return +} + +func NewConfigWithFile(configFile string) (config *Config, err error) { + config = &Config{} + _, err = toml.DecodeFile(configFile, &config) + if err != nil { + return + } + if config.CoverWidth == 0 && config.CoverHight == 0 { + config.CoverHight = defaultCoverHight + config.CoverWidth = defaultCoverWidth + } + if config.CoverWidth == 0 || config.CoverHight == 0 { + return nil, errors.New("CoverWidth or CoverHight should not be zero") + } + return +} + +func (config *Config) NewWriter(fileName string) (mobi.Builder, error) { + m := mobi.NewBuilder() + m.Title(config.Title) + if !config.Compress { + m.Compression(mobi.CompressionNone) + } + if config.Lang != "" { + m.NewExthRecord(mobi.EXTH_LANGUAGE, config.Lang) + } + if config.Cover != "" { + cover, thumb, err := config.generateCover(config.Cover) + if err != nil { + return nil, err + } + m.AddCover(cover, thumb) + defer func() { + _ = os.Remove(cover) + _ = os.Remove(thumb) + }() + } + m.NewExthRecord(mobi.EXTH_DOCTYPE, "EBOK") + m.NewExthRecord(mobi.EXTH_AUTHOR, config.Author) + return m, nil +} + +func (c *Config) Decode(content []byte) ([]byte, error) { + if c.decode != nil { + return c.decode.Bytes(content) + } + return content, nil +} + +func (c *Config) generateCover(imgPath string) (cover string, thumb string, err error) { + var img image.Image + img, err = imaging.Open(imgPath) + if err != nil { + return + } + dx := img.Bounds().Dx() + dy := img.Bounds().Dy() + ts := fmt.Sprintf("%d", time.Now().Unix()) + cover = ts + ".jpg" + thumb = ts + "_thumb.jpg" + if float32(dx)/float32(dy) >= float32(c.CoverWidth)/float32(c.CoverHight) { + dst := resize.Resize(c.CoverWidth, 0, img, resize.Lanczos3) + err = saveImg(dst, cover) + if err != nil { + return + } + dst = resize.Resize(uint(float32(c.CoverWidth)*0.3), 0, img, resize.Lanczos3) + err = saveImg(dst, thumb) + return + } + dst := resize.Resize(0, c.CoverHight, img, resize.Lanczos3) + err = saveImg(dst, cover) + if err != nil { + return + } + dst = resize.Resize(0, uint(float32(c.CoverHight)*0.3), img, resize.Lanczos3) + err = saveImg(dst, thumb) + return +} + +func saveImg(img image.Image, filename string) error { + out, err := os.Create(filename) + if err != nil { + log.Fatal(err) + } + defer out.Close() + return jpeg.EncodeWithJfif(out, img, nil, nil) +}