diff --git a/cbconvert.go b/cbconvert.go index 3540268..b099c2a 100644 --- a/cbconvert.go +++ b/cbconvert.go @@ -39,6 +39,8 @@ type Options struct { Height int // Best fit for required width and height Fit bool + // Document rendering resolution in DPI (PDF, EPUB, etc.); 0 uses the default + DPI int // 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos Filter int // Do not convert the cover image @@ -178,6 +180,7 @@ func (o Options) Args() []string { num("width", o.Width, def.Width) num("height", o.Height, def.Height) flag("fit", o.Fit) + num("dpi", o.DPI, def.DPI) str("format", o.Format, def.Format) str("archive", o.Archive, def.Archive) num("zip-level", o.ZipLevel, def.ZipLevel) @@ -203,6 +206,15 @@ func (o Options) Args() []string { return args } +// renderDPI returns the document rendering resolution, falling back to 300 when unset. +func (c *Converter) renderDPI() float64 { + if c.Opts.DPI > 0 { + return float64(c.Opts.DPI) + } + + return 300 +} + // Cancel cancels the operation. func (c *Converter) Cancel() { if c.OnCancel != nil { diff --git a/cbconvert_convert.go b/cbconvert_convert.go index c3afac1..aefbefe 100644 --- a/cbconvert_convert.go +++ b/cbconvert_convert.go @@ -48,7 +48,7 @@ func (c *Converter) convertDocument(ctx context.Context, fileName string) error return fmt.Errorf("convertDocument: %w", ctx.Err()) } - img, err := doc.Image(n) + img, err := doc.ImageDPI(n, c.renderDPI()) if err != nil { return fmt.Errorf("convertDocument: %w", err) } diff --git a/cbconvert_cover.go b/cbconvert_cover.go index bcb641c..00d9815 100644 --- a/cbconvert_cover.go +++ b/cbconvert_cover.go @@ -52,7 +52,7 @@ func (c *Converter) coverDocument(fileName string) (image.Image, error) { } defer doc.Close() - img, err := doc.Image(0) + img, err := doc.ImageDPI(0, c.renderDPI()) if err != nil { return nil, fmt.Errorf("coverDocument: %w", err) } diff --git a/cbconvert_test.go b/cbconvert_test.go index 67b55de..05593c0 100644 --- a/cbconvert_test.go +++ b/cbconvert_test.go @@ -114,16 +114,52 @@ func TestArgs(t *testing.T) { opts.Effort = 4 opts.Lossless = true opts.Width = 1200 + opts.DPI = 150 opts.Grayscale = true opts.OutDir = "/out" got := strings.Join(opts.Args(), " ") - want := "--width 1200 --format webp --quality 90 --effort 4 --lossless --grayscale --outdir /out" + want := "--width 1200 --dpi 150 --format webp --quality 90 --effort 4 --lossless --grayscale --outdir /out" if got != want { t.Errorf("Args() = %q, want %q", got, want) } } +func TestConvertDPI(t *testing.T) { + tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + dims := func(dpi int) int { + opts := NewOptions() + opts.OutDir = tmpDir + opts.DPI = dpi + + conv := New(opts) + + files, err := conv.Files([]string{"testdata/test.pdf"}) + if err != nil { + t.Fatal(err) + } + + for _, file := range files { + if err := conv.Convert(file); err != nil { + t.Fatal(err) + } + } + + return firstPage(t, conv, filepath.Join(tmpDir, "test.cbz")).Bounds().Dx() + } + + low := dims(150) + high := dims(600) + if low >= high { + t.Errorf("higher DPI should render larger pages: 150dpi=%d, 600dpi=%d", low, high) + } +} + func TestConvertResize(t *testing.T) { tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc") if err != nil { diff --git a/cmd/cbconvert-gui/main.go b/cmd/cbconvert-gui/main.go index 72e5beb..780eb6e 100644 --- a/cmd/cbconvert-gui/main.go +++ b/cmd/cbconvert-gui/main.go @@ -77,6 +77,7 @@ var settings = []setting{ {"Suffix", kindStr, ""}, {"Width", kindStr, ""}, {"Height", kindStr, ""}, + {"DPI", kindStr, "Default"}, {"Size", kindInt, "0"}, {"Quality", kindInt, "75"}, {"Effort", kindInt, "0"}, @@ -203,6 +204,7 @@ func options() cbconvert.Options { opts.Format = strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING")) opts.Width = iup.GetHandle("Width").GetInt("VALUE") opts.Height = iup.GetHandle("Height").GetInt("VALUE") + opts.DPI = dpiValue(iup.GetHandle("DPI").GetAttribute("VALUE")) opts.Fit = iup.GetHandle("Fit").GetAttribute("VALUE") == "ON" opts.Filter = iup.GetHandle("Filter").GetInt("VALUE") - 1 opts.Quality = iup.GetHandle("Quality").GetInt("VALUE") @@ -362,6 +364,15 @@ func zipLevel(value string) int { } } +func dpiValue(value string) int { + dpi, err := strconv.Atoi(strings.TrimSpace(value)) + if err != nil { + return 0 + } + + return dpi +} + func profileGroup(name string) string { return "Profile:" + name } @@ -846,6 +857,21 @@ func tabs() iup.Ihandle { iup.Text().SetAttributes(`SPIN=YES, SPINMAX=2048, VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Size"). SetAttributes(`TIP="Process only files larger than minimum size"`), ), + iup.Vbox( + iup.Label("Document DPI:"), + iup.List().SetAttributes(map[string]string{ + "DROPDOWN": "YES", + "EDITBOX": "YES", + "VISIBLECOLUMNS": "6", + "VALUE": "Default", + "1": "Default", + "2": "150", + "3": "300", + "4": "600", + "5": "1200", + }).SetHandle("DPI"). + SetAttribute("TIP", "Resolution for rendering documents (PDF, EPUB, etc.); Default is 300"), + ), iup.Space().SetAttributes("EXPAND=HORIZONTAL"), ).SetHandle("VboxInput").SetAttributes("NGAP=10") diff --git a/cmd/cbconvert/main.go b/cmd/cbconvert/main.go index 4a6af11..f9c9fb0 100644 --- a/cmd/cbconvert/main.go +++ b/cmd/cbconvert/main.go @@ -175,6 +175,7 @@ func parseFlags() (cbconvert.Options, []string) { convert.IntVar(&opts.Width, "width", 0, "Image width") convert.IntVar(&opts.Height, "height", 0, "Image height") convert.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height") + convert.IntVar(&opts.DPI, "dpi", 0, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)") convert.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl") convert.StringVar(&opts.Archive, "archive", "zip", "Archive format, valid values are zip, tar") convert.IntVar(&opts.ZipLevel, "zip-level", -1, "ZIP compression level, 0 disables compression, 1-9 sets deflate level (1 fastest, 9 smallest), -1 uses the default") @@ -202,6 +203,7 @@ func parseFlags() (cbconvert.Options, []string) { cover.IntVar(&opts.Width, "width", 0, "Image width") cover.IntVar(&opts.Height, "height", 0, "Image height") cover.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height") + cover.IntVar(&opts.DPI, "dpi", 0, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)") cover.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif") cover.IntVar(&opts.Quality, "quality", 75, "Image quality") cover.IntVar(&opts.Effort, "effort", -1, "Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default") @@ -216,6 +218,7 @@ func parseFlags() (cbconvert.Options, []string) { thumbnail.IntVar(&opts.Width, "width", 0, "Image width") thumbnail.IntVar(&opts.Height, "height", 0, "Image height") thumbnail.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height") + thumbnail.IntVar(&opts.DPI, "dpi", 0, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)") thumbnail.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos") thumbnail.StringVar(&opts.OutDir, "outdir", ".", "Output directory") thumbnail.StringVar(&opts.OutFile, "outfile", "", "Output file") @@ -236,7 +239,7 @@ func parseFlags() (cbconvert.Options, []string) { fmt.Fprintf(os.Stderr, "Usage: %s [] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0])) fmt.Fprintf(os.Stderr, "\nCommands:\n") fmt.Fprintf(os.Stderr, "\n convert\n \tConvert archive or document\n\n") - order := []string{"width", "height", "fit", "format", "archive", "zip-level", "quality", "effort", "lossless", "combine", "outfile", "filter", "no-cover", "no-rgb", + order := []string{"width", "height", "fit", "dpi", "format", "archive", "zip-level", "quality", "effort", "lossless", "combine", "outfile", "filter", "no-cover", "no-rgb", "no-nonimage", "no-convert", "grayscale", "rotate", "brightness", "contrast", "suffix", "outdir", "size", "recursive", "quiet"} for _, name := range order { f := convert.Lookup(name) @@ -244,14 +247,14 @@ func parseFlags() (cbconvert.Options, []string) { fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue) } fmt.Fprintf(os.Stderr, "\n cover\n \tExtract cover\n\n") - order = []string{"width", "height", "fit", "format", "quality", "effort", "lossless", "combine", "outfile", "filter", "outdir", "size", "recursive", "quiet"} + order = []string{"width", "height", "fit", "dpi", "format", "quality", "effort", "lossless", "filter", "outdir", "size", "recursive", "quiet"} for _, name := range order { f := cover.Lookup(name) fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name) fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue) } fmt.Fprintf(os.Stderr, "\n thumbnail\n \tExtract cover thumbnail (freedesktop spec.)\n\n") - order = []string{"width", "height", "fit", "filter", "outdir", "outfile", "size", "recursive", "quiet"} + order = []string{"width", "height", "fit", "dpi", "filter", "outdir", "outfile", "size", "recursive", "quiet"} for _, name := range order { f := thumbnail.Lookup(name) fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)