From 67d55d873e366baedfdf987f4be14ef6d7fd2d79 Mon Sep 17 00:00:00 2001 From: Spiegel Date: Tue, 12 May 2026 17:14:07 +0900 Subject: [PATCH] feat: add ImageBytes API for in-memory image download --- README.md | 8 ++++++++ webinfo.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ webinfo_test.go | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/README.md b/README.md index 739741a..ab840e6 100644 --- a/README.md +++ b/README.md @@ -76,11 +76,18 @@ thumbPath, err := info.DownloadThumbnail(ctx, "thumbnails", 150, false) if err != nil { return err } + +imgBytes, err := info.ImageBytes(ctx) +if err != nil { + return err +} +fmt.Println(len(imgBytes)) ``` ### Public API - `Fetch(ctx, rawURL, userAgent)` extracts metadata from a page. +- `(*Webinfo).ImageBytes(ctx)` downloads `Webinfo.ImageURL` into memory. - `(*Webinfo).DownloadImage(ctx, destDir, temporary)` downloads `Webinfo.ImageURL`. - `(*Webinfo).DownloadThumbnail(ctx, destDir, width, temporary)` creates a resized thumbnail. @@ -96,6 +103,7 @@ if err != nil { 3. sniff first 512 bytes (`http.DetectContentType`) 4. fallback `.img` - `DownloadThumbnail` uses width `150` when `width <= 0`. +- `ImageBytes` reads the full response body into memory; very large images can increase memory usage. ## Error handling diff --git a/webinfo.go b/webinfo.go index ba9feae..0892c60 100644 --- a/webinfo.go +++ b/webinfo.go @@ -34,6 +34,51 @@ type Webinfo struct { UserAgent string `json:"user_agent,omitempty"` // User-Agent used to fetch the page } +// ImageBytes downloads w.ImageURL and returns its contents in memory. +// +// Risk: this method reads the entire response body into memory, so very large +// images can increase memory usage. +// +// Returned errors are wrapped with context and include response close failures. +func (w *Webinfo) ImageBytes(ctx context.Context) (data []byte, err error) { + if w == nil { + err = errs.Wrap(ErrNullPointer) + return + } + if w.ImageURL == "" { + err = errs.Wrap(ErrNoImageURL) + return + } + + parsed, uerr := fetch.URL(strings.TrimSpace(w.ImageURL)) + if uerr != nil { + err = errs.Wrap(uerr, errs.WithContext("url", w.ImageURL)) + return + } + + resp, ferr := fetch.New(fetch.WithHTTPClient(newHTTPClient())).GetWithContext( + ctx, + parsed, + fetch.WithRequestHeaderSet("User-Agent", getUserAgent(w.UserAgent)), + ) + if ferr != nil { + err = errs.Wrap(ferr, errs.WithContext("url", parsed.String())) + return + } + defer func() { + if cerr := resp.Close(); cerr != nil && cerr != os.ErrClosed { + err = errs.Join(cerr, err) + } + }() + + data, err = io.ReadAll(resp.Body()) + if err != nil { + err = errs.Wrap(err, errs.WithContext("url", parsed.String())) + return + } + return +} + // DownloadImage downloads w.ImageURL and writes it under destDir. // // If temporary is true, or if the URL path has no filename, a temporary file is created. diff --git a/webinfo_test.go b/webinfo_test.go index 241759b..78ed2af 100644 --- a/webinfo_test.go +++ b/webinfo_test.go @@ -142,6 +142,41 @@ func TestDownloadImage_NilReceiver(t *testing.T) { } } +func TestImageBytes_NilReceiver(t *testing.T) { + var w *Webinfo + _, err := w.ImageBytes(context.Background()) + if err == nil { + t.Fatalf("expected error for nil receiver") + } +} + +func TestImageBytes_EmptyURL(t *testing.T) { + w := &Webinfo{} + _, err := w.ImageBytes(context.Background()) + if err == nil { + t.Fatalf("expected error for empty image URL") + } +} + +func TestImageBytes_Success(t *testing.T) { + body := makeImageBytes(10, 10, "png", 0xaa, 0xbb, 0xcc) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/png") + _, _ = w.Write(body) + }) + srv := httptest.NewServer(handler) + defer srv.Close() + + w := &Webinfo{ImageURL: srv.URL + "/img.png"} + got, err := w.ImageBytes(context.Background()) + if err != nil { + t.Fatalf("ImageBytes failed: %v", err) + } + if !bytes.Equal(got, body) { + t.Fatalf("ImageBytes content mismatch") + } +} + func TestDownloadImage_SaveWithFilename(t *testing.T) { // serve a PNG at /images/pic.png handler := func(wr http.ResponseWriter, r *http.Request) {