45eba325e2
- Introduced a new function `isIconImage` to identify small images and decorative elements based on pixel dimensions and file size. - Updated the image resolution logic to filter out identified icon images from markdown content, enhancing the quality of stored images. - Added unit tests for icon image detection to ensure accurate functionality across various image sizes. These changes improve image handling by effectively filtering out non-essential images, leading to cleaner document processing.
167 lines
4.5 KiB
Go
167 lines
4.5 KiB
Go
package docparser
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"image"
|
|
_ "image/gif"
|
|
_ "image/jpeg"
|
|
_ "image/png"
|
|
"log"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const (
|
|
// minImageDimension is the minimum width/height in pixels; images smaller
|
|
// than this on either axis are treated as icons and filtered out.
|
|
minImageDimension = 128
|
|
// minImageBytes is the minimum file size in bytes; very small images are
|
|
// almost certainly icons or decorative elements.
|
|
minImageBytes = 512 // 512 bytes
|
|
)
|
|
|
|
// isIconImage returns true if the image data looks like a small icon or
|
|
// decorative element that should be filtered out. It checks pixel dimensions
|
|
// when decodable, and falls back to raw byte size otherwise.
|
|
func isIconImage(data []byte) bool {
|
|
cfg, _, err := image.DecodeConfig(bytes.NewReader(data))
|
|
if err != nil {
|
|
// Cannot decode dimensions — fall back to size-only heuristic.
|
|
return len(data) < minImageBytes
|
|
}
|
|
if cfg.Width < minImageDimension || cfg.Height < minImageDimension {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// StoredImage describes an image that has been saved to storage.
|
|
type StoredImage struct {
|
|
OriginalRef string // reference in the original markdown
|
|
ServingURL string // provider:// URL (e.g. local://images/xxx.png, minio://bucket/key)
|
|
MimeType string
|
|
}
|
|
|
|
// ImageResolver reads images from a DocReader ReadResult (inline bytes only)
|
|
// and saves them via FileService, replacing markdown references with unified URLs.
|
|
type ImageResolver struct {
|
|
// TenantID for storage path namespacing
|
|
TenantID uint64
|
|
}
|
|
|
|
// NewImageResolver creates a resolver.
|
|
func NewImageResolver() *ImageResolver {
|
|
return &ImageResolver{}
|
|
}
|
|
|
|
// ResolveAndStore reads images from the convert result, persists them via fileSvc,
|
|
// and replaces markdown references with provider:// URLs.
|
|
// It returns the updated markdown and a list of stored images.
|
|
func (r *ImageResolver) ResolveAndStore(
|
|
ctx context.Context,
|
|
result *types.ReadResult,
|
|
fileSvc interfaces.FileService,
|
|
tenantID uint64,
|
|
) (updatedMarkdown string, images []StoredImage, err error) {
|
|
markdown := result.MarkdownContent
|
|
if len(result.ImageRefs) == 0 {
|
|
return markdown, nil, nil
|
|
}
|
|
|
|
// Build a map of original_ref -> image ref for fast lookup
|
|
refMap := make(map[string]types.ImageRef)
|
|
for _, ref := range result.ImageRefs {
|
|
refMap[ref.OriginalRef] = ref
|
|
}
|
|
|
|
// Process each image reference found in the markdown
|
|
imgPattern := regexp.MustCompile(`!\[([^\]]*)\]\(([^)]+)\)`)
|
|
matches := imgPattern.FindAllStringSubmatchIndex(markdown, -1)
|
|
|
|
// Process in reverse order to preserve positions when replacing
|
|
for i := len(matches) - 1; i >= 0; i-- {
|
|
m := matches[i]
|
|
refPath := markdown[m[4]:m[5]] // group 2: the URL/path
|
|
|
|
// Skip already-resolved URLs (http/https, unified /files/, or provider:// scheme)
|
|
if strings.HasPrefix(refPath, "http://") || strings.HasPrefix(refPath, "https://") ||
|
|
isProviderScheme(refPath) {
|
|
continue
|
|
}
|
|
|
|
// Find inline image bytes from the result
|
|
ref, found := refMap[refPath]
|
|
if !found || len(ref.ImageData) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Filter out small icons and decorative images
|
|
if isIconImage(ref.ImageData) {
|
|
// Remove the image reference from markdown entirely
|
|
markdown = markdown[:m[0]] + markdown[m[1]:]
|
|
continue
|
|
}
|
|
|
|
// Determine extension
|
|
ext := extFromMime(ref.MimeType)
|
|
if ext == "" {
|
|
ext = filepath.Ext(ref.Filename)
|
|
}
|
|
if ext == "" {
|
|
ext = ".png"
|
|
}
|
|
|
|
// Save via FileService — returns provider:// path
|
|
fileName := uuid.New().String() + ext
|
|
servingURL, saveErr := fileSvc.SaveBytes(ctx, ref.ImageData, tenantID, fileName, false)
|
|
if saveErr != nil {
|
|
log.Printf("WARN: failed to save image %s: %v", refPath, saveErr)
|
|
continue
|
|
}
|
|
|
|
images = append(images, StoredImage{
|
|
OriginalRef: refPath,
|
|
ServingURL: servingURL,
|
|
MimeType: ref.MimeType,
|
|
})
|
|
|
|
// Replace in markdown
|
|
markdown = markdown[:m[4]] + servingURL + markdown[m[5]:]
|
|
}
|
|
|
|
return markdown, images, nil
|
|
}
|
|
|
|
func extFromMime(mime string) string {
|
|
switch mime {
|
|
case "image/png":
|
|
return ".png"
|
|
case "image/jpeg":
|
|
return ".jpg"
|
|
case "image/gif":
|
|
return ".gif"
|
|
case "image/webp":
|
|
return ".webp"
|
|
case "image/bmp":
|
|
return ".bmp"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// isProviderScheme checks if the path uses a provider:// scheme (local://, minio://, cos://, tos://).
|
|
func isProviderScheme(path string) bool {
|
|
for _, prefix := range []string{"local://", "minio://", "cos://", "tos://"} {
|
|
if strings.HasPrefix(path, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|