From 0fa4408405e7ac53072f0886cc9d17d384dba9ca Mon Sep 17 00:00:00 2001 From: Sinuhe Tellez Date: Sat, 11 Jul 2026 19:17:10 -0400 Subject: [PATCH] feat: replace regex PDF scanner with rsc.io/pdf, extract Subject and Keywords MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace bufio-based regex PDF metadata scanner with proper rsc.io/pdf parser. Extends PDF metadata extraction to include Subject (→ description) and Keywords (→ subjects), matching the full EPUB metadata pipeline. Net -12 lines, +3 metadata fields. --- go.mod | 1 + go.sum | 2 ++ internal/service/internal_test.go | 8 ++--- internal/service/service.go | 59 ++++++++++++------------------- 4 files changed, 29 insertions(+), 41 deletions(-) diff --git a/go.mod b/go.mod index 22eee81..d552a9f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.3 require ( github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939 github.com/stretchr/testify v1.7.0 + rsc.io/pdf v0.1.1 ) require ( diff --git a/go.sum b/go.sum index 9f24b8a..1b07f82 100644 --- a/go.sum +++ b/go.sum @@ -14,3 +14,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/service/internal_test.go b/internal/service/internal_test.go index 296a09b..38b1943 100644 --- a/internal/service/internal_test.go +++ b/internal/service/internal_test.go @@ -90,10 +90,10 @@ func TestExtractMetadata(t *testing.T) { t.Logf("EPUB Title: %q, Author: %q, CoverPath: %q, Description: %q, Series: %q, SeriesIndex: %q, Subjects: %v", title, author, coverPath, description, series, seriesIndex, subjects) }) - t.Run("Parse PDF value", func(t *testing.T) { - line := "/Title (The Great Gatsby) /Author (F. Scott Fitzgerald)" - assert.Equal(t, "The Great Gatsby", parsePdfValue(line, "/Title")) - assert.Equal(t, "F. Scott Fitzgerald", parsePdfValue(line, "/Author")) + t.Run("Extract PDF", func(t *testing.T) { + path := filepath.Join("testdata", "mybook", "mybook.pdf") + title, author, description, subjects := extractPdfMetadata(path) + t.Logf("PDF Title: %q, Author: %q, Description: %q, Subjects: %v", title, author, description, subjects) }) } diff --git a/internal/service/service.go b/internal/service/service.go index e61c79f..5214a14 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -5,7 +5,6 @@ package service import ( "archive/zip" - "bufio" "bytes" "crypto/sha256" "encoding/hex" @@ -26,6 +25,7 @@ import ( "time" "github.com/dubyte/dir2opds/opds" + "rsc.io/pdf" ) func init() { @@ -271,8 +271,8 @@ func extractMetadata(path string) (string, string, string, string, string, strin case ".epub": return extractEpubMetadata(path) case ".pdf": - title, author := extractPdfMetadata(path) - return title, author, "", "", "", "", nil + title, author, description, subjects := extractPdfMetadata(path) + return title, author, "", description, "", "", subjects } return "", "", "", "", "", "", nil } @@ -437,46 +437,31 @@ func findEpubCover(r *zip.ReadCloser, items []struct { return "" } -func extractPdfMetadata(path string) (string, string) { - f, err := os.Open(path) +func extractPdfMetadata(path string) (string, string, string, []string) { + reader, err := pdf.Open(path) if err != nil { - return "", "" + return "", "", "", nil } - defer f.Close() - scanner := bufio.NewScanner(f) - var title, author string - // Only scan first 4KB to keep it fast - maxLines := 100 - for i := 0; i < maxLines && scanner.Scan(); i++ { - line := scanner.Text() - if title == "" && strings.Contains(line, "/Title") { - title = parsePdfValue(line, "/Title") - } - if author == "" && strings.Contains(line, "/Author") { - author = parsePdfValue(line, "/Author") - } - if title != "" && author != "" { - break - } + info := reader.Trailer().Key("Info") + if info.IsNull() { + return "", "", "", nil } - return title, author -} -func parsePdfValue(line, key string) string { - idx := strings.Index(line, key) - if idx == -1 { - return "" - } - start := strings.Index(line[idx:], "(") - if start == -1 { - return "" - } - end := strings.Index(line[idx+start:], ")") - if end == -1 { - return "" + title := info.Key("Title").Text() + author := info.Key("Author").Text() + description := info.Key("Subject").Text() + + var subjects []string + if kw := info.Key("Keywords").Text(); kw != "" { + for _, s := range strings.Split(kw, ",") { + if s = strings.TrimSpace(s); s != "" { + subjects = append(subjects, s) + } + } } - return line[idx+start+1 : idx+start+end] + + return title, author, description, subjects } func (s OPDS) sortEntries(entries []CatalogEntry) {