diff --git a/README.md b/README.md index 9c181b9..3c36732 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,29 @@ More hands on examples are available below. * [Library Github page](https://github.com/serpapi/serpapi-golang) * [API health status](https://serpapi.com/status) +### Image API + +Upload JPG/JPEG, PNG, or WebP image (up to 500 KB) to use with supported search engines. + +```golang +setting := serpapi.NewSerpApiClientSetting("") +client := serpapi.NewClient(setting) + +upload, err := client.UploadImage("/path/to/image.png") +if err != nil { + panic(err) +} + +results, err := client.Search(map[string]string{ + "engine": "google_lens", + "image_id": upload["image_id"].(string), +}) +``` + +`UploadImage` also accepts an `io.Reader`. Reader ownership remains with the caller. + +Uploaded image IDs expire after 10 minutes. See the [Image API documentation](https://serpapi.com/image-api). + ### Location API ```golang diff --git a/README.md.erb b/README.md.erb index 5cdd5c6..b5465e9 100644 --- a/README.md.erb +++ b/README.md.erb @@ -111,6 +111,29 @@ More hands on examples are available below. * [Library Github page](https://github.com/serpapi/serpapi-golang) * [API health status](https://serpapi.com/status) +### Image API + +Upload JPG/JPEG, PNG, or WebP image (up to 500 KB) to use with supported search engines. + +```golang +setting := serpapi.NewSerpApiClientSetting("") +client := serpapi.NewClient(setting) + +upload, err := client.UploadImage("/path/to/image.png") +if err != nil { + panic(err) +} + +results, err := client.Search(map[string]string{ + "engine": "google_lens", + "image_id": upload["image_id"].(string), +}) +``` + +`UploadImage` also accepts an `io.Reader`. Reader ownership remains with the caller. + +Uploaded image IDs expire after 10 minutes. See the [Image API documentation](https://serpapi.com/image-api). + ### Location API ```golang diff --git a/serpapi.go b/serpapi.go index 99e94b4..4e11966 100644 --- a/serpapi.go +++ b/serpapi.go @@ -1,12 +1,15 @@ package serpapi import ( + "bytes" "encoding/json" "errors" "fmt" "io" + "mime/multipart" "net/http" "net/url" + "os" "time" ) @@ -72,6 +75,62 @@ func (client *SerpApiClient) Search(parameter map[string]string) (map[string]int return client.decodeJSON(rsp.Body) } +// UploadImage uploads an image to SerpApi's Image API and returns an image_id +// that can be used by search engines such as Google Lens. image may be a file +// path or an io.Reader. +func (client *SerpApiClient) UploadImage(image interface{}) (map[string]interface{}, error) { + rsp, err := client.executeImageUpload(image) + if err != nil { + return nil, err + } + return client.decodeJSON(rsp.Body) +} + +// executeImageUpload sends a multipart POST request to the Image API. +func (client *SerpApiClient) executeImageUpload(image interface{}) (*http.Response, error) { + var reader io.Reader + switch value := image.(type) { + case string: + file, err := os.Open(value) + if err != nil { + return nil, err + } + defer file.Close() + reader = file + case io.Reader: + reader = value + default: + return nil, errors.New("image must be a file path or io.Reader") + } + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + part, err := writer.CreateFormFile("image", "image") + if err != nil { + return nil, err + } + if _, err = io.Copy(part, reader); err != nil { + return nil, err + } + + if client.Setting.SerpApiKey != "" { + if err = writer.WriteField("api_key", client.Setting.SerpApiKey); err != nil { + return nil, err + } + } + if err = writer.Close(); err != nil { + return nil, err + } + + request, err := http.NewRequest(http.MethodPost, BaseURL+"/image", &body) + if err != nil { + return nil, err + } + request.Header.Set("Content-Type", writer.FormDataContentType()) + + return client.HttpSearch.Do(request) +} + // Html returns raw HTML search result func (client *SerpApiClient) Html(parameter map[string]string) (*string, error) { rsp, err := client.execute("/search", "html", parameter) diff --git a/test/image_test.go b/test/image_test.go new file mode 100644 index 0000000..cb921ad --- /dev/null +++ b/test/image_test.go @@ -0,0 +1,86 @@ +package serpapi + +import ( + "io" + "net/http" + "os" + "strings" + "testing" + + "github.com/serpapi/serpapi-golang" +) + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (function roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { + return function(request) +} + +func imageClient(handler roundTripFunc) serpapi.SerpApiClient { + setting := serpapi.NewSerpApiClientSetting("client-api-key") + client := serpapi.NewClient(setting) + client.HttpSearch = &http.Client{Transport: handler} + return client +} + +func TestUploadImage(t *testing.T) { + imagePath := t.TempDir() + string(os.PathSeparator) + "image.png" + if err := os.WriteFile(imagePath, []byte("fake-image-data"), 0600); err != nil { + t.Fatal(err) + } + + for name, image := range map[string]interface{}{ + "path": imagePath, + "reader": strings.NewReader("fake-image-data"), + } { + t.Run(name, func(t *testing.T) { + client := imageClient(func(request *http.Request) (*http.Response, error) { + if request.Method != http.MethodPost || request.URL.String() != "https://serpapi.com/image" { + t.Errorf("request = %s %s", request.Method, request.URL) + } + if err := request.ParseMultipartForm(1 << 20); err != nil { + t.Fatal(err) + } + if value := request.FormValue("api_key"); value != "client-api-key" { + t.Errorf("api_key = %q, want client-api-key", value) + } + file, _, err := request.FormFile("image") + if err != nil { + t.Fatal(err) + } + defer file.Close() + contents, err := io.ReadAll(file) + if err != nil { + t.Fatal(err) + } + if string(contents) != "fake-image-data" { + t.Errorf("image = %q, want fake-image-data", contents) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"image_id":"image-123"}`)), + }, nil + }) + + result, err := client.UploadImage(image) + if err != nil { + t.Fatal(err) + } + if result["image_id"] != "image-123" { + t.Errorf("image_id = %v, want image-123", result["image_id"]) + } + }) + } +} + +func TestUploadImageRejectsUnsupportedInput(t *testing.T) { + client := imageClient(func(request *http.Request) (*http.Response, error) { + t.Fatal("request should not be sent") + return nil, nil + }) + + _, err := client.UploadImage(42) + if err == nil || err.Error() != "image must be a file path or io.Reader" { + t.Errorf("error = %v", err) + } +}