// 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 首个原生、全平台、纯 Go 语言高性能 OFD 引擎 package ofdgo import ( "archive/zip" "bytes" "context" "errors" "fmt" "image" "io" "io/fs" "os" "runtime" "sync" basefile "github.com/xiaoqidun/ofdgo/internal/base/file" ) // Source 统一文件、内存及顺序流的随机读取,可并发读取和关闭,不可复制 // 调用方应在阅读、编辑及转换结束后调用Close,借入的数据需在此期间保持不变 // 借入的随机读取器应支持并发ReadAt,Close不会关闭调用方传入的读取器 type Source struct { mu sync.RWMutex reader io.ReaderAt size int64 path string file *os.File temp bool } // SourceOptions 设置顺序输入的缓存及大小限制,单位为字节 // MemoryLimit为0时使用8MiB,浏览器WASM仅用内存;负值表示仅用内存 // TempDir为空时使用系统临时目录,不自动创建调用方指定的目录 // MaxSize为0时不限制输入大小,OpenFile和ReadSource均检查此限制 // 无临时文件系统的平台可显式使用仅内存模式,并通过MaxSize限制输入 type SourceOptions struct { MemoryLimit int64 MaxSize int64 TempDir string } // OpenFile 打开文件输入,不复制文件内容,Close释放文件句柄 // 入参: ctx 取消上下文, path 文件路径, options 输入选项 // 返回: *Source 输入源, error 打开或大小错误 func OpenFile(ctx context.Context, path string, options SourceOptions) (*Source, error) { if err := ctx.Err(); err != nil { return nil, err } f, err := basefile.Open(path) if err != nil { return nil, err } info, err := f.Stat() if err != nil { f.Close() return nil, err } if !info.Mode().IsRegular() { f.Close() return nil, fmt.Errorf("source is not a regular file") } source, err := NewSource(f, info.Size()) if err != nil { f.Close() return nil, err } source.file, source.path = f, path if err := source.checkOptions(ctx, options); err != nil { source.Close() return nil, err } return source, nil } // NewSource 借用指定长度的随机读取源,不复制数据或接管其生命周期 // 入参: reader 随机读取器, size 数据大小 // 返回: *Source 输入源, error 参数错误 func NewSource(reader io.ReaderAt, size int64) (*Source, error) { if reader == nil || size < 0 { return nil, fmt.Errorf("invalid source") } return &Source{reader: reader, size: size}, nil } // ReadSource 从当前位置接收顺序输入,超过内存阈值后转入临时文件 // 已知大小且支持随机访问的可定位读取器直接复用,不移动其当前位置 // 不关闭传入流;取消无法中断阻塞的Read,调用方应自行设置超时或关闭流 // 入参: ctx 取消上下文, reader 输入流, options 输入选项 // 返回: *Source 输入源, error 读取、缓存或取消错误 func ReadSource(ctx context.Context, reader io.Reader, options SourceOptions) (*Source, error) { if reader == nil || options.MaxSize < 0 { return nil, fmt.Errorf("invalid source options") } if err := ctx.Err(); err != nil { return nil, err } if random, ok := reader.(io.ReaderAt); ok { if seeker, ok := reader.(io.Seeker); ok { var size int64 = -1 if sized, ok := reader.(interface{ Size() int64 }); ok { size = sized.Size() } else if stated, ok := reader.(interface{ Stat() (os.FileInfo, error) }); ok { info, err := stated.Stat() if err != nil { return nil, err } if info.Mode().IsRegular() { size = info.Size() } } if size >= 0 { offset, err := seeker.Seek(0, io.SeekCurrent) if err != nil { return nil, err } if offset < 0 || offset > size { return nil, fmt.Errorf("invalid source offset") } source, err := NewSource(io.NewSectionReader(random, offset, size-offset), size-offset) if err != nil { return nil, err } if err := source.checkOptions(ctx, options); err != nil { return nil, err } return source, nil } } } limit := options.MemoryLimit if limit == 0 { limit = 8 << 20 if runtime.GOOS == "js" { limit = -1 } } source := &Source{} success := false defer func() { if !success { source.Close() } }() var memory bytes.Buffer buffer := make([]byte, 64<<10) empty := 0 for { if err := ctx.Err(); err != nil { return nil, err } n, err := reader.Read(buffer) if n < 0 || n > len(buffer) { return nil, fmt.Errorf("invalid source read count") } if err != nil && err != io.EOF { return nil, err } if failure := ctx.Err(); failure != nil { return nil, failure } if n > 0 { empty = 0 if source.size > int64(^uint64(0)>>1)-int64(n) || options.MaxSize > 0 && int64(n) > options.MaxSize-source.size { return nil, fmt.Errorf("source exceeds size limit") } if source.file == nil && limit >= 0 && source.size+int64(n) > limit { file, failure := basefile.NewTemp(options.TempDir) if failure != nil { return nil, failure } source.file, source.temp = file, true if _, failure = file.Write(memory.Bytes()); failure != nil { return nil, failure } memory = bytes.Buffer{} } var output io.Writer = &memory if source.file != nil { output = source.file } if _, failure := output.Write(buffer[:n]); failure != nil { return nil, failure } source.size += int64(n) } else if err == nil { empty++ if empty >= 100 { return nil, io.ErrNoProgress } } if err != nil { break } } if err := ctx.Err(); err != nil { return nil, err } source.reader = bytes.NewReader(memory.Bytes()) if source.file != nil { source.reader = source.file } success = true return source, nil } // checkOptions 检查取消状态及输入大小 // 入参: ctx 取消上下文, options 输入选项 // 返回: error 取消或大小错误 func (s *Source) checkOptions(ctx context.Context, options SourceOptions) error { if options.MaxSize < 0 || options.MaxSize > 0 && s.size > options.MaxSize { return fmt.Errorf("source exceeds size limit") } return ctx.Err() } // Size 返回输入字节数,关闭后仍可查询 // 返回: int64 数据大小 func (s *Source) Size() int64 { return s.size } // ReadAt 读取指定位置的数据,关闭后返回os.ErrClosed // 入参: data 目标缓冲区, offset 字节偏移 // 返回: int 读取字节数, error 读取错误 func (s *Source) ReadAt(data []byte, offset int64) (int, error) { s.mu.RLock() defer s.mu.RUnlock() if s.reader == nil { return 0, os.ErrClosed } return io.NewSectionReader(s.reader, 0, s.size).ReadAt(data, offset) } // Close 释放自有文件和缓存,等待正在进行的读取结束,可重复调用 // 返回: error 文件关闭或临时文件删除错误 func (s *Source) Close() error { s.mu.Lock() defer s.mu.Unlock() s.reader = nil if s.file == nil { return nil } file := s.file s.file = nil err := file.Close() if s.temp { err = errors.Join(err, os.Remove(file.Name())) } return err } // NewReader 从统一输入源创建OFD阅读器,不接管输入源生命周期 // 阅读器及其派生编辑器使用结束后,调用方再关闭输入源 // 入参: source 输入源, options 阅读选项 // 返回: *Reader 阅读器实例, error 错误信息 func NewReader(source *Source, options ...ReaderOption) (*Reader, error) { if source == nil { return nil, fmt.Errorf("missing source") } zr, err := zip.NewReader(source, source.Size()) if err != nil { return nil, err } reader := &Reader{ Path: source.path, Zip: zr, } if err := reader.open(options); err != nil { return nil, err } return reader, nil } // NewRenderer 创建渲染器 // 入参: reader 阅读器, opts 渲染选项 // 返回: *Renderer 渲染器实例 func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer { r := &Renderer{ Reader: reader, DPI: 300.0, RenderAnnotations: true, DrawParams: reader.drawParamCache, CompositeGraphicUnits: reader.compositeGraphicUnitCache, templatePageCache: make(map[string]*PageContent), imageCache: &renderCache[string, image.Image]{limit: imageCacheLimit}, backends: defaultRenderBackends(), backendStates: make(map[any]any), } r.resetFontCache() for _, opt := range opts { opt(r) } return r } // WithDPI 设置渲染DPI // 入参: dpi DPI值 // 返回: RendererOption 渲染选项 func WithDPI(dpi float64) RendererOption { return func(r *Renderer) { r.DPI = dpi } } // WithAnnotations 设置是否渲染注释外观 // 入参: enable 是否渲染 // 返回: RendererOption 渲染选项 func WithAnnotations(enable bool) RendererOption { return func(r *Renderer) { r.RenderAnnotations = enable } } // WithFontDirs 设置外部字体查找目录 // 入参: dirs 字体目录列表 // 返回: RendererOption 渲染选项 func WithFontDirs(dirs ...string) RendererOption { return func(r *Renderer) { r.fontDirs = append(r.fontDirs, dirs...) } } // WithFontFS 设置外部字体文件系统 // 入参: fs 字体文件系统 // 返回: RendererOption 渲染选项 func WithFontFS(fs ...fs.FS) RendererOption { return func(r *Renderer) { r.fontFS = append(r.fontFS, fs...) } }