Files
code-utils/images.go
2026-08-14 07:52:01 +08:00

172 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// images.go 处理待办/工单 Markdown 内容里的图片:
// 粘贴或选择的图片统一解码、缩放(最长边 contentImgMaxEdge后按设置存储 ——
// base64 模式返回内嵌 dataURL随内容同步到云端path 模式写入数据目录 images/ 并返回路径(仅本机可见),
// server 模式上传到 nl-pms-api 返回 http URL跨设备/团队可访问,见 fileapi.go
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"image"
"image/jpeg"
"image/png"
"os"
"path/filepath"
"strings"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
)
const (
contentImgMaxEdge = 1100 // 内容图最长边,兼顾清晰度与同步体积
contentImgMaxBytes = 20 << 20 // 原始输入上限 20MB
contentJPEGQuality = 82
)
// SaveContentImage 处理编辑器里粘贴的图片dataURL返回可直接写进 Markdown 的图片地址。
func (a *App) SaveContentImage(dataURL string) (string, error) {
if e := a.ready(); e != nil {
return "", e
}
raw, e := dataURLBytes(dataURL)
if e != nil {
return "", e
}
return a.storeContentImage(raw)
}
// PickContentImage 打开图片选择框,处理后返回 Markdown 图片地址;用户取消时返回空串。
func (a *App) PickContentImage() (string, error) {
if e := a.ready(); e != nil {
return "", e
}
p, e := application.Get().Dialog.OpenFile().
SetTitle(a.localized("选择要插入的图片", "Choose image to insert")).
AddFilter(a.localized("图片文件", "Image files"), "*.png;*.jpg;*.jpeg;*.gif;*.webp").
PromptForSingleSelection()
if e != nil || strings.TrimSpace(p) == "" {
return "", e
}
info, e := os.Stat(p)
if e != nil {
return "", errors.New("FILE_NOT_FOUND")
}
if info.Size() > contentImgMaxBytes {
return "", errors.New("IMAGE_FILE_TOO_LARGE")
}
raw, e := os.ReadFile(p)
if e != nil {
return "", errors.New("FILE_READ_FAILED")
}
return a.storeContentImage(raw)
}
// ReadContentImageAsDataURL 把 Markdown 引用的本地图片读成 dataURLpath 模式渲染时用)。
func (a *App) ReadContentImageAsDataURL(path string) (string, error) {
if e := a.ready(); e != nil {
return "", e
}
info, e := os.Stat(path)
if e != nil {
return "", errors.New("FILE_NOT_FOUND")
}
if info.Size() > contentImgMaxBytes {
return "", errors.New("IMAGE_FILE_TOO_LARGE")
}
raw, e := os.ReadFile(path)
if e != nil {
return "", errors.New("FILE_READ_FAILED")
}
data, mime, e := encodeContentImage(raw)
if e != nil {
return "", e
}
return "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(data), nil
}
// storeContentImage 缩放压缩后按设置存储server 模式上传换 http URL
// path 模式写入数据目录返回正斜杠路径,默认返回内嵌 dataURL。
func (a *App) storeContentImage(raw []byte) (string, error) {
data, mime, e := encodeContentImage(raw)
if e != nil {
return "", e
}
// 存储走向由管理员的全局配置决定上传时实时获取server 一律上传;
// local 下沿用本机 imageMode老用户的 path 数据继续可用),默认 base64。
if a.currentFileStorage().Mode == "server" {
return a.uploadImageToServer(data, mime, "content")
}
s, err := a.store.Settings()
if err == nil && s.ImageMode == "path" {
dir := filepath.Join(filepath.Dir(a.store.path), "images")
if e := os.MkdirAll(dir, 0755); e != nil {
return "", errors.New("IMAGE_SAVE_FAILED")
}
ext := ".jpg"
if mime == "image/png" {
ext = ".png"
}
p := filepath.Join(dir, fmt.Sprintf("img-%d%s", time.Now().UnixNano(), ext))
if e := os.WriteFile(p, data, 0644); e != nil {
return "", errors.New("IMAGE_SAVE_FAILED")
}
// Markdown 里用正斜杠路径,避免反斜杠被当成转义。
return filepath.ToSlash(p), nil
}
return "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(data), nil
}
// encodeContentImage 解码任意支持格式,缩放后重新编码:
// 不透明图输出 JPEG体积小带透明通道的输出 PNG。
func encodeContentImage(raw []byte) ([]byte, string, error) {
return encodeImageEdge(raw, contentImgMaxEdge)
}
// encodeImageEdge 同上,但可指定最长边(节日背景等场景用更小尺寸控制体积)。
func encodeImageEdge(raw []byte, maxEdge int) ([]byte, string, error) {
src, _, e := image.Decode(bytes.NewReader(raw))
if e != nil {
return nil, "", errors.New("IMAGE_DECODE_FAILED")
}
img := downscaleImage(src, maxEdge)
var buf bytes.Buffer
if imageOpaque(img) {
if e = jpeg.Encode(&buf, img, &jpeg.Options{Quality: contentJPEGQuality}); e != nil {
return nil, "", errors.New("IMAGE_DECODE_FAILED")
}
return buf.Bytes(), "image/jpeg", nil
}
if e = png.Encode(&buf, img); e != nil {
return nil, "", errors.New("IMAGE_DECODE_FAILED")
}
return buf.Bytes(), "image/png", nil
}
// imageOpaque 判断图片是否完全不透明;无法判断时按含透明处理(走 PNG
func imageOpaque(img image.Image) bool {
if o, ok := img.(interface{ Opaque() bool }); ok {
return o.Opaque()
}
return false
}
// dataURLBytes 解出 dataURL 里的原始字节。
func dataURLBytes(s string) ([]byte, error) {
i := strings.Index(s, ";base64,")
if !strings.HasPrefix(s, "data:image/") || i < 0 {
return nil, errors.New("IMAGE_DECODE_FAILED")
}
if len(s)-i > contentImgMaxBytes {
return nil, errors.New("IMAGE_FILE_TOO_LARGE")
}
raw, e := base64.StdEncoding.DecodeString(s[i+8:])
if e != nil {
return nil, errors.New("IMAGE_DECODE_FAILED")
}
return raw, nil
}