mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
346 lines
9.7 KiB
Go
346 lines
9.7 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 首个原生、全平台、纯 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...)
|
|
}
|
|
}
|