diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css index 333f7f7..6cc6721 100644 --- a/assets/webui/ofdgo.css +++ b/assets/webui/ofdgo.css @@ -48,6 +48,12 @@ select { font: inherit; } +select, +option { + text-align: left; + text-align-last: left; +} + button { border: 1px solid var(--line-strong); background: #ffffff; @@ -135,7 +141,6 @@ a:focus-visible { border-radius: 0; background: #fff; color: #1f2428; - text-align: center; } .editor-paragraph > .editor-field, @@ -650,7 +655,8 @@ button:not(:disabled):hover { inset: 0; display: flex; align-items: center; - justify-content: center; + justify-content: flex-start; + padding-left: 4px; padding-right: 12px; pointer-events: none; } @@ -789,7 +795,6 @@ button:not(:disabled):hover { height: var(--control-height); padding: 0 6px; vertical-align: middle; - text-align: center; font: inherit; border: 1px solid var(--line); border-radius: 0; @@ -802,6 +807,7 @@ button:not(:disabled):hover { } .convert-table input { + text-align: center; font-variant-numeric: tabular-nums; } diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 4fe37e2..42cd702 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -2912,7 +2912,7 @@ function releasePageResources() { const images = new Set(), fonts = new Set(); for (const page of state.pageCache.values()) { for (const name of page.imageNames) images.add(name); - for (const font of page.fonts) fonts.add(font.name); + for (const font of page.fonts || []) fonts.add(font.name); } for (const [name, image] of state.svgImages) { if (!images.has(name)) { @@ -5495,6 +5495,7 @@ function trimPageCache() { } } } + if (!state.pageInFlight.size) releasePageResources(); } async function loadSVGFonts(fonts, openSeq) { diff --git a/go.mod b/go.mod index 8419cef..d4f4287 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/xiaoqidun/jbig2 v1.1.1 - github.com/xiaoqidun/pdfgo v0.0.0-20260928145637-87d467fc8ef1 + github.com/xiaoqidun/pdfgo v0.0.0-20260928160629-4a36d082a27e golang.org/x/crypto v0.57.0 golang.org/x/image v0.46.0 golang.org/x/text v0.42.0 diff --git a/go.sum b/go.sum index 3842b45..adf1786 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= -github.com/xiaoqidun/pdfgo v0.0.0-20260928145637-87d467fc8ef1 h1:meZvn/eb/AAbr6qfsP41UZZsW/g1v67a2CUYGzoJpS0= -github.com/xiaoqidun/pdfgo v0.0.0-20260928145637-87d467fc8ef1/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20260928160629-4a36d082a27e h1:yc6JbpAQG36mxW6R+mNoAib9tbqTUAQxRcXjGLy6GRA= +github.com/xiaoqidun/pdfgo v0.0.0-20260928160629-4a36d082a27e/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/ofdgo.go b/ofdgo.go index f751ac1..4a0c338 100644 --- a/ofdgo.go +++ b/ofdgo.go @@ -72,7 +72,7 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer { DrawParams: reader.drawParamCache, CompositeGraphicUnits: reader.compositeGraphicUnitCache, templatePageCache: make(map[string]*PageContent), - imageCache: make(map[string]image.Image), + imageCache: renderCache[string, image.Image]{limit: imageCacheLimit}, backends: defaultRenderBackends(), backendStates: make(map[any]any), } diff --git a/ofdgo_image_resource.go b/ofdgo_image_resource.go index 04bd5fc..f9ede12 100644 --- a/ofdgo_image_resource.go +++ b/ofdgo_image_resource.go @@ -25,6 +25,9 @@ import ( "sync" ) +// imageCacheLimit 限制图片缓存的编码数据及预计解码内存 +const imageCacheLimit = 64 << 20 + // EncodedImage 保留PNG或JPEG原始编码,首次读取像素时解码 // Bytes返回只读数据,Image返回共享只读像素,解码错误由Image返回 type EncodedImage struct { @@ -110,21 +113,37 @@ func newEncodedImage(data []byte) (*EncodedImage, error) { // 返回: image.Image 图片对象, error 读取错误 func (r *Renderer) cachedImageResource(resPath string) (image.Image, error) { resPath = cleanPackagePath(r.Reader.ResPath(resPath)) - img, ok := r.imageCache[resPath] + img, ok := r.imageCache.get(resPath) if !ok { var err error img, err = r.readImageResource(resPath) if err != nil { return nil, err } - if r.imageCache == nil { - r.imageCache = make(map[string]image.Image) - } - r.imageCache[resPath] = img + r.imageCache.put(resPath, img, imageResourceCost(img, r.imageCache.limit)) } return img, nil } +// imageResourceCost 预留十六位像素展开空间,超预算图片不进入缓存 +// 入参: img 图片资源, limit 缓存预算 +// 返回: int 估算字节数或超预算标记 +func imageResourceCost(img image.Image, limit int) int { + cost := 256 + w, h := img.Bounds().Dx(), img.Bounds().Dy() + if limit < cost || w <= 0 || h <= 0 || w > (limit-cost)/8/h { + return limit + 1 + } + cost += w * h * 8 + if encoded, ok := img.(*EncodedImage); ok { + if len(encoded.data) > limit-cost { + return limit + 1 + } + cost += len(encoded.data) + } + return cost +} + // decodeImageResource 读取或复用图片资源,按输出需要解码像素 // 入参: resPath 图片资源路径 // 返回: image.Image 图片对象, error 读取或解码错误 diff --git a/ofdgo_page_range.go b/ofdgo_page_range.go index 86b4814..407e388 100644 --- a/ofdgo_page_range.go +++ b/ofdgo_page_range.go @@ -15,6 +15,7 @@ package ofdgo import ( + "cmp" "fmt" "slices" "strconv" @@ -25,7 +26,7 @@ import ( // 入参: value 页码表达式,如1-6,7,8-10, count 文档总页数 // 返回: []int 页面索引, error 错误信息 func ParsePageRange(value string, count int) ([]int, error) { - selected := make(map[int]bool) + var intervals [][2]int for _, part := range strings.Split(strings.ReplaceAll(value, ",", ","), ",") { first, last, interval := strings.Cut(strings.TrimSpace(part), "-") start, err := strconv.Atoi(strings.TrimSpace(first)) @@ -42,15 +43,27 @@ func ParsePageRange(value string, count int) ([]int, error) { if start < 1 || end < start || end > count { return nil, fmt.Errorf("page range %q outside 1-%d", part, count) } - for index := start - 1; index < end; index++ { - selected[index] = true + intervals = append(intervals, [2]int{start - 1, end}) + } + slices.SortFunc(intervals, func(a, b [2]int) int { return cmp.Compare(a[0], b[0]) }) + merged := intervals[:0] + for _, interval := range intervals { + if len(merged) != 0 && interval[0] <= merged[len(merged)-1][1] { + merged[len(merged)-1][1] = max(merged[len(merged)-1][1], interval[1]) + } else { + merged = append(merged, interval) } } - indices := make([]int, 0, len(selected)) - for index := range selected { - indices = append(indices, index) + length := 0 + for _, interval := range merged { + length += interval[1] - interval[0] + } + indices := make([]int, 0, length) + for _, interval := range merged { + for index := interval[0]; index < interval[1]; index++ { + indices = append(indices, index) + } } - slices.Sort(indices) return indices, nil } diff --git a/ofdgo_reader.go b/ofdgo_reader.go index 518ba29..00b91c5 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -55,11 +55,13 @@ type Reader struct { encryption *encryptionState } -// Close 关闭阅读器 +// Close 关闭阅读器持有的文件,重复调用不再关闭底层文件 // 返回: error 错误信息 func (r *Reader) Close() error { if r.Closer != nil { - return r.Closer.Close() + closer := r.Closer + r.Closer = nil + return closer.Close() } return nil } diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index ed9d00b..942c61e 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -34,7 +34,7 @@ type Renderer struct { DrawParams map[string]*DrawParam CompositeGraphicUnits map[string]*CompositeGraphicUnit templatePageCache map[string]*PageContent - imageCache map[string]image.Image + imageCache renderCache[string, image.Image] fontDirs []string fontFS []fs.FS decodeImages bool @@ -57,7 +57,7 @@ type RendererOption func(*Renderer) // ClearCache 释放派生字体、图片、模板与后端缓存,不改变文档和渲染配置 func (r *Renderer) ClearCache() { r.backendStates = make(map[any]any) - r.imageCache = make(map[string]image.Image) + r.imageCache = renderCache[string, image.Image]{limit: imageCacheLimit} r.templatePageCache = make(map[string]*PageContent) r.resetFontCache() } diff --git a/ofdgo_renderer_glyph_canvas.go b/ofdgo_renderer_glyph_canvas.go index 846a913..71fab11 100644 --- a/ofdgo_renderer_glyph_canvas.go +++ b/ofdgo_renderer_glyph_canvas.go @@ -74,14 +74,7 @@ func textGlyphPath(face *canvas.FontFace, glyph textGlyph, outlines *sfntOutline width = face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID)) } if face.FauxBold != 0 { - d := face.FauxBold * face.Size - if face.Font.IsTrueType { - d = -d - } - origFastStroke := canvas.FastStroke - canvas.FastStroke = true - p = p.Offset(d, canvas.Tolerance) - canvas.FastStroke = origFastStroke + p = p.Offset(face.FauxBold*face.Size, canvas.Tolerance) } if face.FauxItalic != 0 { p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0)) @@ -105,7 +98,7 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) ( direction: face.Direction, glyph: glyph, } - if cached, ok := r.canvasState().textGlyphPathCache[key]; ok { + if cached, ok := r.canvasState().textGlyphPathCache.get(key); ok { return cached.path, cached.width } outlines := r.canvasState().fontOutlines[face.Font] @@ -118,7 +111,7 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) ( r.renderError = err return &canvas.Path{}, 0 } - r.canvasState().textGlyphPathCache[key] = textGlyphPathCacheValue{path: path, width: width} + r.canvasState().textGlyphPathCache.put(key, textGlyphPathCacheValue{path: path, width: width}, 256+len(path.Data())*8+len(glyph.Text)+len(face.Language)) return path, width } diff --git a/ofdgo_renderer_image_canvas.go b/ofdgo_renderer_image_canvas.go index 4477a47..7bb2560 100644 --- a/ofdgo_renderer_image_canvas.go +++ b/ofdgo_renderer_image_canvas.go @@ -138,7 +138,7 @@ func (r *Renderer) canvasEncodedImage(img image.Image) image.Image { return img } state := r.canvasState() - if encoded, ok := state.images[source]; ok { + if encoded, ok := state.images.get(source); ok { return encoded } var encoded *canvasimage.Image @@ -148,7 +148,7 @@ func (r *Renderer) canvasEncodedImage(img image.Image) image.Image { encoded, _ = canvasimage.NewPNGImage(bytes.NewReader(source.Bytes())) } encoded.Bytes = source.Bytes() - state.images[source] = encoded + state.images.put(source, encoded, 2*imageResourceCost(source, state.images.limit/2)) return encoded } diff --git a/ofdgo_renderer_state_canvas.go b/ofdgo_renderer_state_canvas.go index 40594f3..2014881 100644 --- a/ofdgo_renderer_state_canvas.go +++ b/ofdgo_renderer_state_canvas.go @@ -22,10 +22,10 @@ import ( // canvasBackendState 保存Canvas字体、字形和编码图片适配缓存 type canvasBackendState struct { - images map[*EncodedImage]image.Image + images renderCache[*EncodedImage, image.Image] fontMap map[string]*canvas.FontFamily svgFontCache map[*canvas.Font]SVGFont - textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue + textGlyphPathCache renderCache[textGlyphPathCacheKey, textGlyphPathCacheValue] fontOutlines map[*canvas.Font]*sfntOutliner } @@ -37,7 +37,7 @@ func (r *Renderer) canvasState() *canvasBackendState { return state.(*canvasBackendState) } state := &canvasBackendState{ - images: make(map[*EncodedImage]image.Image), + images: renderCache[*EncodedImage, image.Image]{limit: imageCacheLimit}, } state.resetFonts() r.backendStates[key] = state @@ -48,6 +48,6 @@ func (r *Renderer) canvasState() *canvasBackendState { func (s *canvasBackendState) resetFonts() { s.fontMap = make(map[string]*canvas.FontFamily) s.svgFontCache = make(map[*canvas.Font]SVGFont) - s.textGlyphPathCache = make(map[textGlyphPathCacheKey]textGlyphPathCacheValue) + s.textGlyphPathCache = renderCache[textGlyphPathCacheKey, textGlyphPathCacheValue]{limit: 16 << 20} s.fontOutlines = make(map[*canvas.Font]*sfntOutliner) } diff --git a/ofdgo_sign_verify.go b/ofdgo_sign_verify.go index cd097b2..1d5ab1d 100644 --- a/ofdgo_sign_verify.go +++ b/ofdgo_sign_verify.go @@ -29,6 +29,7 @@ import ( "encoding/pem" "encoding/xml" "fmt" + "hash" "io" "math/big" "path" @@ -36,6 +37,7 @@ import ( "time" "github.com/emmansun/gmsm/sm2" + "github.com/emmansun/gmsm/sm3" "github.com/emmansun/gmsm/smx509" ) @@ -489,16 +491,36 @@ func (r *Reader) verifySignatureReference(sigPath, method string, ref SignatureR result.Error = err.Error() return result } - data, err := r.readFile(refPath) + var digest hash.Hash + switch { + case strings.TrimSpace(method) == "": + digest = md5.New() + case isSM3DigestMethod(method): + digest = sm3.New() + default: + algorithm, ok := signatureDigestHash(method) + if !ok { + result.Error = fmt.Sprintf("unsupported digest method: %s", method) + return result + } + digest = algorithm.New() + } + input, err := r.openFile(refPath) if err != nil { result.Error = err.Error() return result } - actual, err := signatureDigest(method, data) + _, err = io.Copy(digest, input) + closeErr := input.Close() if err != nil { result.Error = err.Error() return result } + if closeErr != nil { + result.Error = closeErr.Error() + return result + } + actual := digest.Sum(nil) result.CheckValue = checkValue result.Actual = actual result.Checked = true