e904b667c6
Build/Publish Develop Docs / deploy (push) Failing after 1s
PaddleOCR Code Style Check / check-code-style (push) Failing after 1s
PaddleOCR PR Tests GPU / detect-changes (push) Failing after 1s
PaddleOCR PR Tests / detect-changes (push) Failing after 1s
PaddleOCR PR Tests GPU / test-pr-gpu (push) Has been cancelled
PaddleOCR PR Tests / test-pr (push) Has been cancelled
PaddleOCR PR Tests GPU / test-pr-gpu-impl (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.13) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.8) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.9) (push) Has been cancelled
268 lines
7.7 KiB
Go
268 lines
7.7 KiB
Go
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package paddleocr
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type SaveResourceOption func(*saveResourceOptions)
|
|
|
|
type saveResourceOptions struct {
|
|
overwrite bool
|
|
}
|
|
|
|
func WithOverwrite(overwrite bool) SaveResourceOption {
|
|
return func(o *saveResourceOptions) {
|
|
o.overwrite = overwrite
|
|
}
|
|
}
|
|
|
|
// SaveResource downloads one result resource URL.
|
|
func (c *Client) SaveResource(ctx context.Context, resourceURL, dest string, opts ...SaveResourceOption) (string, error) {
|
|
if resourceURL == "" {
|
|
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "resource URL is required"}}
|
|
}
|
|
if dest == "" {
|
|
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "destination path is required"}}
|
|
}
|
|
options := saveResourceOptions{}
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
savedPath, err := resolveSavePath(resourceURL, dest)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parent := filepath.Dir(savedPath)
|
|
if st, err := os.Stat(parent); err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return "", &FileNotFoundError{Path: parent, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination parent not found: " + parent, Cause: err}}
|
|
}
|
|
return "", err
|
|
} else if !st.IsDir() {
|
|
return "", &FileNotFoundError{Path: parent, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination parent is not a directory: " + parent}}
|
|
}
|
|
if !options.overwrite {
|
|
if _, err := os.Stat(savedPath); err == nil {
|
|
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "destination already exists: " + savedPath}}
|
|
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, resourceURL, nil)
|
|
if err != nil {
|
|
return "", &NetworkError{PaddleOCRAPIError{Message: err.Error(), Cause: err}}
|
|
}
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return "", classifyHTTPError(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if err := raiseForResourceResponse(resp); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
tmp, err := os.CreateTemp(parent, "."+filepath.Base(savedPath)+".tmp-*")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tmpPath := tmp.Name()
|
|
cleanupTemp := true
|
|
defer func() {
|
|
if cleanupTemp {
|
|
_ = os.Remove(tmpPath)
|
|
}
|
|
}()
|
|
if _, err := io.Copy(tmp, resp.Body); err != nil {
|
|
_ = tmp.Close()
|
|
return "", err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return "", err
|
|
}
|
|
if options.overwrite {
|
|
if err := os.Rename(tmpPath, savedPath); err != nil {
|
|
return "", err
|
|
}
|
|
} else {
|
|
if err := os.Link(tmpPath, savedPath); err != nil {
|
|
if errors.Is(err, os.ErrExist) {
|
|
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "destination already exists: " + savedPath, Cause: err}}
|
|
}
|
|
return "", err
|
|
}
|
|
if err := os.Remove(tmpPath); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
cleanupTemp = false
|
|
return savedPath, nil
|
|
}
|
|
|
|
func (c *Client) SaveOCRResultResources(ctx context.Context, result *OCRResult, destDir string, opts ...SaveResourceOption) ([]string, error) {
|
|
if result == nil {
|
|
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "OCR result is required"}}
|
|
}
|
|
if err := requireExistingDirectory(destDir); err != nil {
|
|
return nil, err
|
|
}
|
|
saved := make([]string, 0, len(result.Pages))
|
|
for i, page := range result.Pages {
|
|
if page.OCRImageURL == "" {
|
|
continue
|
|
}
|
|
name := fmt.Sprintf("ocr-page-%d%s", i+1, safeResourceExtension(page.OCRImageURL))
|
|
if err := validateResultResourceFilename(name); err != nil {
|
|
return nil, err
|
|
}
|
|
path, err := c.SaveResource(ctx, page.OCRImageURL, filepath.Join(destDir, name), opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
saved = append(saved, path)
|
|
}
|
|
return saved, nil
|
|
}
|
|
|
|
func (c *Client) SaveDocumentParsingResultResources(ctx context.Context, result *DocParsingResult, destDir string, opts ...SaveResourceOption) ([]string, error) {
|
|
if result == nil {
|
|
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "document parsing result is required"}}
|
|
}
|
|
if err := requireExistingDirectory(destDir); err != nil {
|
|
return nil, err
|
|
}
|
|
var saved []string
|
|
for _, page := range result.Pages {
|
|
paths, err := c.saveNamedResourceMap(ctx, page.MarkdownImages, destDir, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
saved = append(saved, paths...)
|
|
paths, err = c.saveNamedResourceMap(ctx, page.OutputImages, destDir, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
saved = append(saved, paths...)
|
|
}
|
|
return saved, nil
|
|
}
|
|
|
|
func (c *Client) saveNamedResourceMap(ctx context.Context, resources map[string]string, destDir string, opts ...SaveResourceOption) ([]string, error) {
|
|
keys := make([]string, 0, len(resources))
|
|
for key := range resources {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
saved := make([]string, 0, len(keys))
|
|
for _, key := range keys {
|
|
resourceURL := resources[key]
|
|
if resourceURL == "" {
|
|
continue
|
|
}
|
|
name, err := resultResourceFilename(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
path, err := c.SaveResource(ctx, resourceURL, filepath.Join(destDir, name), opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
saved = append(saved, path)
|
|
}
|
|
return saved, nil
|
|
}
|
|
|
|
func resolveSavePath(resourceURL, dest string) (string, error) {
|
|
if st, err := os.Stat(dest); err == nil && st.IsDir() {
|
|
name := "resource"
|
|
u, parseErr := url.Parse(resourceURL)
|
|
if parseErr == nil {
|
|
if base := path.Base(u.Path); base != "." && base != "/" && base != "" {
|
|
name = base
|
|
}
|
|
}
|
|
return filepath.Join(dest, name), nil
|
|
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return "", err
|
|
}
|
|
return dest, nil
|
|
}
|
|
|
|
func raiseForResourceResponse(resp *http.Response) error {
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
return nil
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return &APIError{StatusCode: resp.StatusCode, PaddleOCRAPIError: PaddleOCRAPIError{Message: string(body)}}
|
|
}
|
|
|
|
func requireExistingDirectory(destDir string) error {
|
|
if destDir == "" {
|
|
return &InvalidRequestError{PaddleOCRAPIError{Message: "destination directory is required"}}
|
|
}
|
|
st, err := os.Stat(destDir)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return &FileNotFoundError{Path: destDir, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination directory not found: " + destDir, Cause: err}}
|
|
}
|
|
return err
|
|
}
|
|
if !st.IsDir() {
|
|
return &FileNotFoundError{Path: destDir, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination is not a directory: " + destDir}}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resultResourceFilename(key string) (string, error) {
|
|
if err := validateResultResourceFilename(key); err != nil {
|
|
return "", err
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
func validateResultResourceFilename(name string) error {
|
|
if name == "" || name == "." || name == ".." || filepath.IsAbs(name) || path.IsAbs(name) || strings.ContainsAny(name, `/\`) {
|
|
return &InvalidRequestError{PaddleOCRAPIError{Message: "unsafe resource filename: " + name}}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func safeResourceExtension(resourceURL string) string {
|
|
u, err := url.Parse(resourceURL)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
ext := path.Ext(path.Base(u.Path))
|
|
if len(ext) <= 1 {
|
|
return ""
|
|
}
|
|
if err := validateResultResourceFilename("resource" + ext); err != nil {
|
|
return ""
|
|
}
|
|
return ext
|
|
}
|