Add DPI option, issue #52

This commit is contained in:
Milan Nikolic
2026-06-24 20:44:29 +02:00
parent f65fc4bafa
commit 9de98599a9
6 changed files with 83 additions and 6 deletions
+12
View File
@@ -39,6 +39,8 @@ type Options struct {
Height int Height int
// Best fit for required width and height // Best fit for required width and height
Fit bool 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 // 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos
Filter int Filter int
// Do not convert the cover image // Do not convert the cover image
@@ -178,6 +180,7 @@ func (o Options) Args() []string {
num("width", o.Width, def.Width) num("width", o.Width, def.Width)
num("height", o.Height, def.Height) num("height", o.Height, def.Height)
flag("fit", o.Fit) flag("fit", o.Fit)
num("dpi", o.DPI, def.DPI)
str("format", o.Format, def.Format) str("format", o.Format, def.Format)
str("archive", o.Archive, def.Archive) str("archive", o.Archive, def.Archive)
num("zip-level", o.ZipLevel, def.ZipLevel) num("zip-level", o.ZipLevel, def.ZipLevel)
@@ -203,6 +206,15 @@ func (o Options) Args() []string {
return args 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. // Cancel cancels the operation.
func (c *Converter) Cancel() { func (c *Converter) Cancel() {
if c.OnCancel != nil { if c.OnCancel != nil {
+1 -1
View File
@@ -48,7 +48,7 @@ func (c *Converter) convertDocument(ctx context.Context, fileName string) error
return fmt.Errorf("convertDocument: %w", ctx.Err()) return fmt.Errorf("convertDocument: %w", ctx.Err())
} }
img, err := doc.Image(n) img, err := doc.ImageDPI(n, c.renderDPI())
if err != nil { if err != nil {
return fmt.Errorf("convertDocument: %w", err) return fmt.Errorf("convertDocument: %w", err)
} }
+1 -1
View File
@@ -52,7 +52,7 @@ func (c *Converter) coverDocument(fileName string) (image.Image, error) {
} }
defer doc.Close() defer doc.Close()
img, err := doc.Image(0) img, err := doc.ImageDPI(0, c.renderDPI())
if err != nil { if err != nil {
return nil, fmt.Errorf("coverDocument: %w", err) return nil, fmt.Errorf("coverDocument: %w", err)
} }
+37 -1
View File
@@ -114,16 +114,52 @@ func TestArgs(t *testing.T) {
opts.Effort = 4 opts.Effort = 4
opts.Lossless = true opts.Lossless = true
opts.Width = 1200 opts.Width = 1200
opts.DPI = 150
opts.Grayscale = true opts.Grayscale = true
opts.OutDir = "/out" opts.OutDir = "/out"
got := strings.Join(opts.Args(), " ") 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 { if got != want {
t.Errorf("Args() = %q, want %q", 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) { func TestConvertResize(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc") tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil { if err != nil {
+26
View File
@@ -77,6 +77,7 @@ var settings = []setting{
{"Suffix", kindStr, ""}, {"Suffix", kindStr, ""},
{"Width", kindStr, ""}, {"Width", kindStr, ""},
{"Height", kindStr, ""}, {"Height", kindStr, ""},
{"DPI", kindStr, "Default"},
{"Size", kindInt, "0"}, {"Size", kindInt, "0"},
{"Quality", kindInt, "75"}, {"Quality", kindInt, "75"},
{"Effort", kindInt, "0"}, {"Effort", kindInt, "0"},
@@ -203,6 +204,7 @@ func options() cbconvert.Options {
opts.Format = strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING")) opts.Format = strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING"))
opts.Width = iup.GetHandle("Width").GetInt("VALUE") opts.Width = iup.GetHandle("Width").GetInt("VALUE")
opts.Height = iup.GetHandle("Height").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.Fit = iup.GetHandle("Fit").GetAttribute("VALUE") == "ON"
opts.Filter = iup.GetHandle("Filter").GetInt("VALUE") - 1 opts.Filter = iup.GetHandle("Filter").GetInt("VALUE") - 1
opts.Quality = iup.GetHandle("Quality").GetInt("VALUE") 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 { func profileGroup(name string) string {
return "Profile:" + name return "Profile:" + name
} }
@@ -846,6 +857,21 @@ func tabs() iup.Ihandle {
iup.Text().SetAttributes(`SPIN=YES, SPINMAX=2048, VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Size"). iup.Text().SetAttributes(`SPIN=YES, SPINMAX=2048, VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Size").
SetAttributes(`TIP="Process only files larger than minimum 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"), iup.Space().SetAttributes("EXPAND=HORIZONTAL"),
).SetHandle("VboxInput").SetAttributes("NGAP=10") ).SetHandle("VboxInput").SetAttributes("NGAP=10")
+6 -3
View File
@@ -175,6 +175,7 @@ func parseFlags() (cbconvert.Options, []string) {
convert.IntVar(&opts.Width, "width", 0, "Image width") convert.IntVar(&opts.Width, "width", 0, "Image width")
convert.IntVar(&opts.Height, "height", 0, "Image height") convert.IntVar(&opts.Height, "height", 0, "Image height")
convert.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and 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.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.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") 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.Width, "width", 0, "Image width")
cover.IntVar(&opts.Height, "height", 0, "Image height") cover.IntVar(&opts.Height, "height", 0, "Image height")
cover.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and 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.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.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") 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.Width, "width", 0, "Image width")
thumbnail.IntVar(&opts.Height, "height", 0, "Image height") thumbnail.IntVar(&opts.Height, "height", 0, "Image height")
thumbnail.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and 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.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.OutDir, "outdir", ".", "Output directory")
thumbnail.StringVar(&opts.OutFile, "outfile", "", "Output file") thumbnail.StringVar(&opts.OutFile, "outfile", "", "Output file")
@@ -236,7 +239,7 @@ func parseFlags() (cbconvert.Options, []string) {
fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0])) fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0]))
fmt.Fprintf(os.Stderr, "\nCommands:\n") fmt.Fprintf(os.Stderr, "\nCommands:\n")
fmt.Fprintf(os.Stderr, "\n convert\n \tConvert archive or document\n\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"} "no-nonimage", "no-convert", "grayscale", "rotate", "brightness", "contrast", "suffix", "outdir", "size", "recursive", "quiet"}
for _, name := range order { for _, name := range order {
f := convert.Lookup(name) 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, "%v (default %q)\n", f.Usage, f.DefValue)
} }
fmt.Fprintf(os.Stderr, "\n cover\n \tExtract cover\n\n") 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 { for _, name := range order {
f := cover.Lookup(name) f := cover.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.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, "%v (default %q)\n", f.Usage, f.DefValue)
} }
fmt.Fprintf(os.Stderr, "\n thumbnail\n \tExtract cover thumbnail (freedesktop spec.)\n\n") 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 { for _, name := range order {
f := thumbnail.Lookup(name) f := thumbnail.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name) fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)