diff --git a/cbconvert_test.go b/cbconvert_test.go index cecba62..165c4fa 100644 --- a/cbconvert_test.go +++ b/cbconvert_test.go @@ -1,9 +1,12 @@ package cbconvert import ( + "archive/zip" "fmt" + "image" "os" "path/filepath" + "strings" "testing" ) @@ -100,6 +103,397 @@ func TestThumbnail(t *testing.T) { } } +func TestConvertResize(t *testing.T) { + tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + opts := NewOptions() + opts.OutDir = tmpDir + opts.Width = 100 + + conv := New(opts) + + files, err := conv.Files([]string{"testdata/test.cbz"}) + if err != nil { + t.Fatal(err) + } + + for _, file := range files { + if err := conv.Convert(file); err != nil { + t.Fatal(err) + } + } + + img := firstPage(t, conv, filepath.Join(tmpDir, "test.cbz")) + if got := img.Bounds().Dx(); got != 100 { + t.Errorf("resized width: got %d, want 100", got) + } +} + +func TestConvertFit(t *testing.T) { + tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + opts := NewOptions() + opts.OutDir = tmpDir + opts.Width = 120 + opts.Height = 120 + opts.Fit = true + + conv := New(opts) + + files, err := conv.Files([]string{"testdata/test.cbz"}) + if err != nil { + t.Fatal(err) + } + + for _, file := range files { + if err := conv.Convert(file); err != nil { + t.Fatal(err) + } + } + + img := firstPage(t, conv, filepath.Join(tmpDir, "test.cbz")) + w, h := img.Bounds().Dx(), img.Bounds().Dy() + if w > 120 || h > 120 { + t.Errorf("fit exceeded bounds: got %dx%d, want <= 120x120", w, h) + } + if w != 120 && h != 120 { + t.Errorf("fit did not touch a bound: got %dx%d, want one side == 120", w, h) + } +} + +func TestConvertTar(t *testing.T) { + tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + opts := NewOptions() + opts.OutDir = tmpDir + opts.Archive = "tar" + + conv := New(opts) + + files, err := conv.Files([]string{"testdata/test.cbz"}) + if err != nil { + t.Fatal(err) + } + + for _, file := range files { + if err := conv.Convert(file); err != nil { + t.Fatal(err) + } + } + + out := filepath.Join(tmpDir, "test.cbt") + list, err := conv.archiveList(out) + if err != nil { + t.Fatalf("read tar output: %v", err) + } + if len(list) != 2 { + t.Errorf("expected 2 pages in tar output, got %d: %v", len(list), list) + } +} + +func TestImageTransforms(t *testing.T) { + conv := New(NewOptions()) + + f, err := os.Open("testdata/test/00.jpg") + if err != nil { + t.Fatal(err) + } + defer f.Close() + + src, err := conv.imageDecode(f) + if err != nil { + t.Fatal(err) + } + srcW, srcH := src.Bounds().Dx(), src.Bounds().Dy() + + conv.Opts.Rotate = 90 + rotated := conv.imageTransform(src) + if rotated.Bounds().Dx() != srcH || rotated.Bounds().Dy() != srcW { + t.Errorf("rotate 90: got %dx%d, want %dx%d", rotated.Bounds().Dx(), rotated.Bounds().Dy(), srcH, srcW) + } + + conv.Opts = NewOptions() + conv.Opts.Grayscale = true + gray := conv.imageTransform(src) + if !isGrayScale(gray) { + t.Errorf("grayscale: result is not grayscale") + } + + conv.Opts = NewOptions() + conv.Opts.Brightness = 20 + conv.Opts.Contrast = 20 + adjusted := conv.imageTransform(src) + if adjusted.Bounds().Dx() != srcW || adjusted.Bounds().Dy() != srcH { + t.Errorf("brightness/contrast changed dimensions: got %dx%d, want %dx%d", + adjusted.Bounds().Dx(), adjusted.Bounds().Dy(), srcW, srcH) + } +} + +func TestCoverName(t *testing.T) { + conv := New(NewOptions()) + + tests := []struct { + name string + images []string + want string + }{ + {"empty", nil, ""}, + {"natural sort", []string{"10.jpg", "2.jpg", "1.jpg"}, "1.jpg"}, + {"cover prefix wins", []string{"01.jpg", "cover.jpg", "02.jpg"}, "cover.jpg"}, + {"front prefix wins", []string{"01.jpg", "front.png", "00.jpg"}, "front.png"}, + {"cover suffix wins", []string{"01.jpg", "page_cover.jpg"}, "page_cover.jpg"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := conv.coverName(tt.images); got != tt.want { + t.Errorf("coverName(%v) = %q, want %q", tt.images, got, tt.want) + } + }) + } +} + +func TestCoverDirectory(t *testing.T) { + tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + opts := NewOptions() + opts.OutDir = tmpDir + + conv := New(opts) + + files, err := conv.Files([]string{"testdata/test"}) + if err != nil { + t.Fatal(err) + } + if len(files) != 1 { + t.Fatalf("expected 1 directory file, got %d", len(files)) + } + + for _, file := range files { + if err := conv.Cover(file); err != nil { + t.Fatal(err) + } + } + + if _, err := os.Stat(filepath.Join(tmpDir, "test.jpg")); err != nil { + t.Errorf("directory cover not written: %v", err) + } +} + +func TestFileType(t *testing.T) { + tests := []struct { + path string + want string + }{ + {"testdata/test.cbz", "ZIP"}, + {"testdata/test.cbr", "RAR"}, + {"testdata/test.cb7", "7Z"}, + {"testdata/test.cbt", "TAR"}, + {"testdata/test.pdf", "PDF"}, + {"testdata/test", "DIR"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + if got := FileType(tt.path); got != tt.want { + t.Errorf("FileType(%q) = %q, want %q", tt.path, got, tt.want) + } + }) + } +} + +func TestCombine(t *testing.T) { + tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + opts := NewOptions() + opts.OutDir = tmpDir + opts.OutFile = "merged" + + conv := New(opts) + + files, err := conv.Files([]string{"testdata/test.cbz", "testdata/test.cbt"}) + if err != nil { + t.Fatal(err) + } + if len(files) != 2 { + t.Fatalf("expected 2 input files, got %d", len(files)) + } + + if err := conv.Combine(files); err != nil { + t.Fatal(err) + } + + zr, err := zip.OpenReader(filepath.Join(tmpDir, "merged.cbz")) + if err != nil { + t.Fatalf("open combined archive: %v", err) + } + defer zr.Close() + + var names []string + for _, f := range zr.File { + names = append(names, f.Name) + } + + if len(names) != 4 { + t.Fatalf("expected 4 pages in combined archive, got %d: %v", len(names), names) + } + + // each input is prefixed so identically named pages do not collide + var first, second int + for _, n := range names { + switch { + case strings.HasPrefix(n, "0001_"): + first++ + case strings.HasPrefix(n, "0002_"): + second++ + } + } + if first != 2 || second != 2 { + t.Errorf("expected 2 pages from each input, got 0001_=%d 0002_=%d: %v", first, second, names) + } +} + +func TestSubfolders(t *testing.T) { + page0, err := os.ReadFile("testdata/test/00.jpg") + if err != nil { + t.Fatal(err) + } + page1, err := os.ReadFile("testdata/test/01.jpg") + if err != nil { + t.Fatal(err) + } + + inDir, err := os.MkdirTemp(os.TempDir(), "cbc-in") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(inDir) + + src := filepath.Join(inDir, "chapters.cbz") + buildZip(t, src, []zipEntry{ + {"chapter1/00.jpg", page0}, + {"chapter1/01.jpg", page1}, + {"chapter2/00.jpg", page0}, + {"chapter2/01.jpg", page1}, + }) + + tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc-out") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + opts := NewOptions() + opts.OutDir = tmpDir + + conv := New(opts) + + files, err := conv.Files([]string{src}) + if err != nil { + t.Fatal(err) + } + + for _, file := range files { + if err := conv.Convert(file); err != nil { + t.Fatal(err) + } + } + + zr, err := zip.OpenReader(filepath.Join(tmpDir, "chapters.cbz")) + if err != nil { + t.Fatalf("open output archive: %v", err) + } + defer zr.Close() + + // without subfolder preservation chapter2/00 overwrites chapter1/00 and only 2 pages survive + if len(zr.File) != 4 { + var names []string + for _, f := range zr.File { + names = append(names, f.Name) + } + t.Fatalf("expected 4 pages from numbered subfolders, got %d: %v", len(zr.File), names) + } +} + +func TestMeta(t *testing.T) { + tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // operate on a copy so the fixture stays intact + data, err := os.ReadFile("testdata/test.cbz") + if err != nil { + t.Fatal(err) + } + archive := filepath.Join(tmpDir, "meta.cbz") + if err := os.WriteFile(archive, data, 0644); err != nil { + t.Fatal(err) + } + + conv := New(NewOptions()) + + conv.Opts = NewOptions() + conv.Opts.CommentBody = "hello world" + if _, err := conv.Meta(archive); err != nil { + t.Fatalf("set comment: %v", err) + } + + conv.Opts = NewOptions() + conv.Opts.Comment = true + got, err := conv.Meta(archive) + if err != nil { + t.Fatalf("get comment: %v", err) + } + if got != "hello world" { + t.Errorf("comment roundtrip: got %q, want %q", got, "hello world") + } + + extra := filepath.Join(tmpDir, "ComicInfo.xml") + if err := os.WriteFile(extra, []byte(""), 0644); err != nil { + t.Fatal(err) + } + + conv.Opts = NewOptions() + conv.Opts.FileAdd = extra + if _, err := conv.Meta(archive); err != nil { + t.Fatalf("add file: %v", err) + } + if !archiveHas(t, conv, archive, "ComicInfo.xml") { + t.Errorf("added file not found in archive") + } + + conv.Opts = NewOptions() + conv.Opts.FileRemove = "ComicInfo.xml" + if _, err := conv.Meta(archive); err != nil { + t.Fatalf("remove file: %v", err) + } + if archiveHas(t, conv, archive, "ComicInfo.xml") { + t.Errorf("removed file still present in archive") + } +} + func TestRecursive(t *testing.T) { inDir, err := os.MkdirTemp(os.TempDir(), "cbc-in") if err != nil { @@ -152,3 +546,75 @@ func TestRecursive(t *testing.T) { t.Errorf("expected output relative to input root at %s: %v", want, err) } } + +type zipEntry struct { + name string + data []byte +} + +func buildZip(t *testing.T, path string, entries []zipEntry) { + t.Helper() + + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + zw := zip.NewWriter(f) + for _, e := range entries { + w, err := zw.Create(e.name) + if err != nil { + t.Fatal(err) + } + if _, err := w.Write(e.data); err != nil { + t.Fatal(err) + } + } + if err := zw.Close(); err != nil { + t.Fatal(err) + } +} + +func firstPage(t *testing.T, conv *Converter, archive string) image.Image { + t.Helper() + + zr, err := zip.OpenReader(archive) + if err != nil { + t.Fatal(err) + } + defer zr.Close() + + if len(zr.File) == 0 { + t.Fatalf("archive %s has no entries", archive) + } + + rc, err := zr.File[0].Open() + if err != nil { + t.Fatal(err) + } + defer rc.Close() + + img, err := conv.imageDecode(rc) + if err != nil { + t.Fatal(err) + } + + return img +} + +func archiveHas(t *testing.T, conv *Converter, archive, name string) bool { + t.Helper() + + list, err := conv.archiveList(archive) + if err != nil { + t.Fatal(err) + } + for _, n := range list { + if n == name { + return true + } + } + + return false +}