Files
ofdgo/ofdgo_compression_images.go
T
2026-10-11 18:45:54 +08:00

249 lines
8.3 KiB
Go

// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// 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 ofdgo
import (
"context"
"fmt"
"image"
"math"
)
// compressionImages 记录图片各处使用的最大像素需求
type compressionImages struct {
ctx context.Context
dpi int
sizes map[string]image.Point
renderer *Renderer
patternPage *Matrix
patterns map[*Pattern]bool
patternVisits map[compressionPatternKey]bool
}
// compressionImagePlan 为有损输出分文档分析图片,按共享路径合并最大尺寸和保护条件
// 入参: ctx 取消上下文, options 压缩配置
// 返回: map[string]bool 可有损处理的路径, map[string]image.Point 像素需求, error 读取或取消错误
func (r *Reader) compressionImagePlan(ctx context.Context, options CompressionOptions) (map[string]bool, map[string]image.Point, error) {
if options.Mode != CompressionLossy {
return nil, nil, ctx.Err()
}
images := make(map[string]bool)
sizes := make(map[string]image.Point)
blocked := make(map[string]bool)
for index := 0; index < r.DocumentCount(); index++ {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
document := r
if index != r.DocumentIndex() {
var err error
document, err = r.Document(index)
if err != nil {
return nil, nil, err
}
}
catalog, err := document.Images(ctx)
if err != nil {
if document != r {
document.Close()
}
return nil, nil, err
}
var demands map[string]image.Point
masks, geometry, err := document.compressionImageSafety(ctx, catalog)
if err == nil && geometry {
demands, err = document.compressionImageSizes(ctx, catalog, options.ImageDPI())
}
if document != r {
document.Close()
}
if err != nil {
return nil, nil, err
}
for _, img := range catalog {
name := cleanPackagePath(img.Location)
if old, ok := images[name]; !ok || old {
images[name] = !masks[name]
}
size := demands[name]
if size.X <= 0 || size.Y <= 0 || masks[name] {
blocked[name] = true
delete(sizes, name)
continue
}
if !blocked[name] {
old := sizes[name]
sizes[name] = image.Pt(max(old.X, size.X), max(old.Y, size.Y))
}
}
}
return images, sizes, ctx.Err()
}
// compressionImageSize 将毫米显示尺寸换算为像素,非法尺寸不降采样
// 整像素边界消除单位换算产生的单ULP上溢,不降低实际像素需求
// 入参: width 显示宽度, height 显示高度, dpi 分辨率上限
// 返回: image.Point 像素需求
func compressionImageSize(width, height float64, dpi int) image.Point {
w := math.Ceil(math.Nextafter(width*float64(dpi)/25.4, math.Inf(-1)))
h := math.Ceil(math.Nextafter(height*float64(dpi)/25.4, math.Inf(-1)))
if w <= 0 || h <= 0 || math.IsNaN(w) || math.IsNaN(h) || math.IsInf(w, 0) || math.IsInf(h, 0) || w > 1<<30 || h > 1<<30 {
return image.Point{}
}
return image.Pt(int(w), int(h))
}
// compressionImageSizes 按正文、模板、注解及底纹的实际变换汇总图片尺寸,包括隐藏内容
// 入参: ctx 取消上下文, images 图片资源, dpi 分辨率上限
// 返回: map[string]image.Point 资源像素需求, error 取消错误
func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo, dpi int) (map[string]image.Point, error) {
if dpi == 0 {
return nil, nil
}
count, err := r.PageCount()
if err != nil {
return nil, ctx.Err()
}
renderer := NewRenderer(r, WithRenderBackends(RenderBackends{}))
defer renderer.ClearCache()
visitor := &compressionImages{ctx: ctx, dpi: dpi, sizes: make(map[string]image.Point), renderer: renderer}
for index := 0; index < count; index++ {
if err := ctx.Err(); err != nil {
return nil, err
}
page, err := r.PageContentByIndex(index)
if err != nil {
return nil, ctx.Err()
}
for _, ref := range page.Template {
if renderer.loadTemplate(ref.TemplateID) == nil {
return nil, ctx.Err()
}
}
if err := renderer.WalkPage(page, visitor); err != nil {
return nil, ctx.Err()
}
}
for _, ref := range r.doc.CommonData.TemplatePage {
if err := ctx.Err(); err != nil {
return nil, err
}
template := renderer.loadTemplate(ref.ID)
if template == nil {
return nil, ctx.Err()
}
for order := range 3 {
if err := renderer.walkLayers(template.Content.Layer, order, visitor); err != nil {
return nil, ctx.Err()
}
}
}
result := make(map[string]image.Point)
for _, img := range images {
name := cleanPackagePath(img.Location)
size := visitor.sizes[editorResourceID(img.ID)]
old := result[name]
result[name] = image.Pt(max(old.X, size.X), max(old.Y, size.Y))
}
return result, ctx.Err()
}
// DrawObject 汇总图片、替换图及文字和路径底纹的尺寸,不使用裁剪后的局部面积
// 入参: object 图形对象, state 继承状态
// 返回: error 取消或几何错误
func (v *compressionImages) DrawObject(object *GraphicObject, state RenderState) error {
if err := v.ctx.Err(); err != nil {
return err
}
switch object.Type {
case "PathObject":
return v.objectPatterns(object.PathObject, state)
case "TextObject":
text := object.TextObject
return v.objectPatterns(PathObject{Boundary: text.Boundary, CTM: text.CTM, DrawParam: text.DrawParam, FillColor: text.FillColor, StrokeColor: text.StrokeColor}, state)
case "ImageObject":
default:
return nil
}
img := object.ImageObject
ids := [2]string{editorResourceID(img.ResourceID), editorResourceID(img.Substitution)}
if ids[0] == "" || img.Substitution != "" && ids[1] == "" {
return fmt.Errorf("invalid image resource")
}
box, err := ParseBox(img.Boundary)
if err != nil {
return err
}
local := NewMatrix(img.CTM)
if img.CTM == "" {
local = Matrix{a: box.W, d: box.H}
}
matrix, _ := renderObjectMatrix(img.Boundary, local, state)
size := compressionImageSize(math.Hypot(matrix.a, matrix.b), math.Hypot(matrix.c, matrix.d), v.dpi)
return v.recordSize(ids, size)
}
// VisitComposite 按矢量资源的完整变换统计缩略图和替换图,不以裁剪区域缩减尺寸
// 入参: object 矢量资源或复合图元, state 已包含当前图元变换的状态
// 返回: error 取消、资源标识或几何错误
func (v *compressionImages) VisitComposite(object *CompositeGraphicUnit, state RenderState) error {
if err := v.ctx.Err(); err != nil {
return err
}
if object.Thumbnail == "" && object.Substitution == "" {
return nil
}
ids := [2]string{editorResourceID(object.Thumbnail), editorResourceID(object.Substitution)}
if object.Thumbnail != "" && ids[0] == "" || object.Substitution != "" && ids[1] == "" {
return fmt.Errorf("invalid composite image resource")
}
matrix := IdentityMatrix
if state.Parent != nil {
matrix = *state.Parent
}
size := compressionImageSize(object.Width*math.Hypot(matrix.a, matrix.b), object.Height*math.Hypot(matrix.c, matrix.d), v.dpi)
return v.recordSize(ids, size)
}
// recordSize 合并同一图像资源在不同位置的最大像素需求
// 入参: ids 图像标识,空值不统计, size 当前像素需求
// 返回: error 尺寸无效时返回错误
func (v *compressionImages) recordSize(ids [2]string, size image.Point) error {
if size.X == 0 || size.Y == 0 {
return fmt.Errorf("invalid image placement")
}
for _, id := range ids {
if id == "" {
continue
}
old := v.sizes[id]
v.sizes[id] = image.Pt(max(old.X, size.X), max(old.Y, size.Y))
}
return nil
}
// DrawStamp 保留签章内容,不参与图片降采样
// 入参: stamp 印章
// 返回: error 始终为空
func (v *compressionImages) DrawStamp(stamp Stamp) error { return nil }
// IncludeHidden 将隐藏内容纳入尺寸分析,保留图片在所有使用位置的精度
// 返回: bool 始终为真
func (v *compressionImages) IncludeHidden() bool { return true }
// SkipClips 按完整图片尺寸分析,不加载裁剪几何或文字轮廓
// 返回: bool 始终为真
func (v *compressionImages) SkipClips() bool { return true }