mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:31:12 +08:00
608 lines
20 KiB
Go
608 lines
20 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 (
|
||
"archive/zip"
|
||
"bytes"
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"runtime"
|
||
"slices"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/xiaoqidun/pdfgo"
|
||
)
|
||
|
||
// OutputFormat 描述文档输出格式,Paged表示逐页文件,Raster表示受DPI控制的位图
|
||
type OutputFormat struct {
|
||
Value string `json:"value"`
|
||
Label string `json:"label"`
|
||
Extension string `json:"extension"`
|
||
MIME string `json:"mime"`
|
||
Paged bool `json:"paged,omitempty"`
|
||
Raster bool `json:"raster,omitempty"`
|
||
}
|
||
|
||
// ConvertProgress 回报open、pages、convert、fonts、fonts.*、prepare、write、write.*及export阶段,Total为0表示总量未知
|
||
type ConvertProgress struct {
|
||
Stage string `json:"stage"`
|
||
Completed int `json:"completed"`
|
||
Total int `json:"total"`
|
||
}
|
||
|
||
// ConvertOptions 设置文档转换格式、页码、阅读和渲染选项
|
||
// Input为空时识别源格式,Format为输出格式,Pages使用零基索引,nil表示全部页面
|
||
// PageRange使用一基页码表达式,与Pages互斥,空字符串表示全部页面
|
||
// SkipUnchanged仅在未压缩、源格式与目标相同且未选页时不写出数据
|
||
// Pool可由多个独立转换共享,nil时不限制文件间并发
|
||
// Resources提供共享计算与缓存策略,显式Pool优先控制计算调度
|
||
// 整份PDF同格式压缩直接重写PDF对象,其余PDF先转换为OFD对象再输出
|
||
// RendererOptions同时用于PDF局部合成和输出,在PDF.RendererOptions之后应用
|
||
// Backends统一配置转换与输出,优先于PDF和RendererOptions中的后端设置
|
||
// Compression统一配置输出及PDF图片导入,优先于PDF.Compression,文本输出不优化图片
|
||
// Fonts仅用于OFD输出,按字体处理全包引用,未处理原因通过报告返回
|
||
// PageOutput仅用于逐页格式,同步调用export写出一页并自行处理提交或回滚;此时output可为nil
|
||
// 未提供PageOutput时,逐页格式打包ZIP;所有回调返回错误均会停止转换
|
||
type ConvertOptions struct {
|
||
Resources *Resources
|
||
Input string
|
||
Format string
|
||
Pages []int
|
||
PageRange string
|
||
SkipUnchanged bool
|
||
Pool *ConversionPool
|
||
Compression CompressionOptions
|
||
Fonts FontMode
|
||
Backends *RenderBackends
|
||
ReaderOptions []ReaderOption
|
||
RendererOptions []RendererOption
|
||
PDF PDFImportOptions
|
||
OnProgress func(ConvertProgress) error
|
||
PageOutput func(index int, export func(io.Writer) error) error
|
||
}
|
||
|
||
// ConvertReport 汇总源格式、输出格式、页数、字节数及PDF转换诊断
|
||
// Unchanged表示无需转换或原样交付源文件,Bytes仅统计实际输出,不包含调用方额外封装
|
||
type ConvertReport struct {
|
||
Input string `json:"input"`
|
||
Format OutputFormat `json:"format"`
|
||
Pages int `json:"pages"`
|
||
Bytes int64 `json:"bytes"`
|
||
Unchanged bool `json:"unchanged,omitempty"`
|
||
PDF PDFImportReport `json:"pdf"`
|
||
Fonts []FontChange `json:"fonts,omitempty"`
|
||
}
|
||
|
||
// ConversionPoolOptions 设置共享转换并行度及图像工作集预算
|
||
// Parallelism为0时使用GOMAXPROCS,MemoryLimit为0时不指定固定预算
|
||
// AvailableMemory返回可用于新增任务的字节数,nil表示不探测,回调须支持并发调用
|
||
// 内存预算用于调度,不限制解析器及运行时的实际分配
|
||
type ConversionPoolOptions struct {
|
||
Parallelism int
|
||
MemoryLimit int64
|
||
AvailableMemory func() int64
|
||
}
|
||
|
||
// ConversionPool 协调独立转换和图像任务,配置在创建后不变,可跨会话复用
|
||
type ConversionPool struct {
|
||
options ConversionPoolOptions
|
||
mu sync.Mutex
|
||
changed chan struct{}
|
||
active int
|
||
images int
|
||
memory int64
|
||
}
|
||
|
||
// convertReader 将取消上下文传递到随机读取
|
||
type convertReader struct {
|
||
ctx context.Context
|
||
input io.ReaderAt
|
||
}
|
||
|
||
// convertWriter 将取消上下文传递到写入并统计实际字节数
|
||
type convertWriter struct {
|
||
ctx context.Context
|
||
writer io.Writer
|
||
count *int64
|
||
}
|
||
|
||
// conversionPoolKey 标识单次转换使用的共享预算
|
||
type conversionPoolKey struct{}
|
||
|
||
// OutputFormats 返回独立的输出格式列表,不依赖WebUI或具体渲染后端
|
||
// 返回: []OutputFormat 输出格式列表
|
||
func OutputFormats() []OutputFormat {
|
||
return []OutputFormat{
|
||
{Value: "ofd", Label: "OFD", Extension: "ofd", MIME: "application/ofd"},
|
||
{Value: "pdf", Label: "PDF", Extension: "pdf", MIME: "application/pdf"},
|
||
{Value: "svg", Label: "SVG", Extension: "svg", MIME: "image/svg+xml", Paged: true},
|
||
{Value: "eps", Label: "EPS", Extension: "eps", MIME: "application/postscript", Paged: true},
|
||
{Value: "png", Label: "PNG", Extension: "png", MIME: "image/png", Paged: true, Raster: true},
|
||
{Value: "jpg", Label: "JPG", Extension: "jpg", MIME: "image/jpeg", Paged: true, Raster: true},
|
||
{Value: "txt", Label: "TXT", Extension: "txt", MIME: "text/plain"},
|
||
}
|
||
}
|
||
|
||
// Convert 将PDF或OFD转换为指定格式,不关闭调用方的输入和输出
|
||
// 原始模式完整OFD另存保持源字节;选页和跨格式输出重新生成文档,压缩仅作用于输出副本
|
||
// 输入按需随机读取,输出分块写出;解析对象及后端可能缓存资源,不保证恒定内存
|
||
// 出错时报告保留已有诊断,调用方应丢弃未完成输出;只有返回nil才表示转换成功
|
||
// 入参: ctx 取消上下文, input 输入源, output 输出流, options 转换选项
|
||
// 返回: ConvertReport 转换报告, error 错误信息
|
||
func Convert(ctx context.Context, input *Input, output io.Writer, options ConvertOptions) (report ConvertReport, err error) {
|
||
if options.Resources != nil {
|
||
if options.Pool == nil {
|
||
options.Pool = options.Resources.pool
|
||
}
|
||
options.ReaderOptions = append(slices.Clone(options.ReaderOptions), WithResources(options.Resources))
|
||
options.PDF.Resources = options.Resources
|
||
}
|
||
if options.Pool != nil {
|
||
if err = options.Pool.acquire(ctx, false, 0); err != nil {
|
||
return report, err
|
||
}
|
||
defer options.Pool.release(false, 0)
|
||
ctx = context.WithValue(ctx, conversionPoolKey{}, options.Pool)
|
||
}
|
||
if err = options.Compression.Validate(); err != nil {
|
||
return report, err
|
||
}
|
||
if err = ctx.Err(); err != nil {
|
||
return report, err
|
||
}
|
||
format := strings.ToLower(strings.TrimSpace(options.Format))
|
||
if format == "jpeg" {
|
||
format = "jpg"
|
||
}
|
||
for _, candidate := range OutputFormats() {
|
||
if candidate.Value == format {
|
||
report.Format = candidate
|
||
break
|
||
}
|
||
}
|
||
if report.Format.Value == "" {
|
||
return report, fmt.Errorf("unsupported output format %s", options.Format)
|
||
}
|
||
if options.Fonts > FontExternal || options.Fonts != FontKeep && format != "ofd" {
|
||
return report, fmt.Errorf("font processing requires OFD output and a valid mode")
|
||
}
|
||
if options.PageOutput != nil && !report.Format.Paged {
|
||
return report, fmt.Errorf("page output requires a paged format")
|
||
}
|
||
if options.Pages != nil && len(options.Pages) == 0 {
|
||
return report, fmt.Errorf("no pages selected")
|
||
}
|
||
if options.Pages != nil && strings.TrimSpace(options.PageRange) != "" {
|
||
return report, fmt.Errorf("conflicting page selections")
|
||
}
|
||
progress := func(stage string, completed, total int) error {
|
||
if err := ctx.Err(); err != nil {
|
||
return err
|
||
}
|
||
if options.OnProgress != nil {
|
||
if err := options.OnProgress(ConvertProgress{Stage: stage, Completed: completed, Total: total}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return ctx.Err()
|
||
}
|
||
writeProgress := func(stage string, completed, total int) error {
|
||
if stage == "write" {
|
||
return progress(stage, completed, total)
|
||
}
|
||
return progress("write."+stage, completed, total)
|
||
}
|
||
if err = progress("open", 0, 0); err != nil {
|
||
return report, err
|
||
}
|
||
if input == nil || input.Size() <= 0 {
|
||
return report, fmt.Errorf("empty document input")
|
||
}
|
||
size := input.Size()
|
||
operationInput := &Input{reader: convertReader{ctx: ctx, input: input}, size: size}
|
||
report.Input = strings.ToLower(strings.TrimSpace(options.Input))
|
||
if report.Input == "" {
|
||
report.Input, err = DetectFormat(operationInput)
|
||
if err != nil {
|
||
return report, err
|
||
}
|
||
}
|
||
var reader *Reader
|
||
var editor *Editor
|
||
var count int
|
||
if report.Input != "pdf" && report.Input != "ofd" {
|
||
return report, fmt.Errorf("unsupported input format")
|
||
}
|
||
if options.SkipUnchanged && options.Fonts == FontKeep && options.Compression.Mode == CompressionUnchanged && report.Input == format && options.Pages == nil && strings.TrimSpace(options.PageRange) == "" {
|
||
report.Unchanged = true
|
||
return report, nil
|
||
}
|
||
if output == nil && options.PageOutput == nil {
|
||
return report, fmt.Errorf("missing output writer")
|
||
}
|
||
if report.Input == "pdf" && format == "pdf" && options.Compression.Mode != CompressionUnchanged && options.Pages == nil && strings.TrimSpace(options.PageRange) == "" {
|
||
pdfReader, openErr := pdfgo.NewReaderWithOptions(operationInput, size, pdfgo.ReaderOptions{Password: options.PDF.Password, PasswordUTF8: options.PDF.PasswordUTF8})
|
||
if openErr != nil {
|
||
return report, openErr
|
||
}
|
||
defer pdfReader.Close()
|
||
if pageErr := pdfReader.WalkPages(ctx, func(_ int, _ *pdfgo.Page) error { report.Pages++; return nil }); pageErr != nil {
|
||
return report, pageErr
|
||
}
|
||
result, optimizeErr := pdfReader.OptimizeTo(ctx, output, pdfgo.OptimizeOptions{Compression: options.Compression, Creator: ofdCreator, OnProgress: writeProgress})
|
||
report.Bytes, report.Unchanged = result.Bytes, result.Preserved
|
||
return report, optimizeErr
|
||
}
|
||
switch report.Input {
|
||
case "pdf":
|
||
pdfOptions := options.PDF
|
||
pdfOptions.Compression = options.Compression
|
||
if format == "txt" {
|
||
pdfOptions.Compression = CompressionOptions{}
|
||
}
|
||
pdfOptions.RendererOptions = append(append([]RendererOption(nil), pdfOptions.RendererOptions...), options.RendererOptions...)
|
||
if options.Backends != nil {
|
||
pdfOptions.Backends = options.Backends
|
||
}
|
||
options.RendererOptions = pdfOptions.RendererOptions
|
||
options.Backends = pdfOptions.Backends
|
||
pdfOptions.OnProgress = func(stage string, completed, total int) error {
|
||
if err := progress(stage, completed, total); err != nil {
|
||
return err
|
||
}
|
||
if options.PDF.OnProgress != nil {
|
||
return options.PDF.OnProgress(stage, completed, total)
|
||
}
|
||
return nil
|
||
}
|
||
editor, report.PDF, err = ImportPDF(ctx, operationInput, pdfOptions)
|
||
if err != nil {
|
||
return report, err
|
||
}
|
||
count = editor.PageCount()
|
||
case "ofd":
|
||
reader, err = NewReader(operationInput, options.ReaderOptions...)
|
||
if err != nil {
|
||
return report, err
|
||
}
|
||
defer reader.Close()
|
||
count, err = reader.PageCount()
|
||
if err != nil {
|
||
return report, err
|
||
}
|
||
}
|
||
if strings.TrimSpace(options.PageRange) != "" {
|
||
options.Pages, err = ParsePageRange(options.PageRange, count)
|
||
if err != nil {
|
||
return report, err
|
||
}
|
||
}
|
||
indices, err := exportPageIndices(count, options.Pages)
|
||
if err != nil {
|
||
return report, err
|
||
}
|
||
report.Pages = len(indices)
|
||
writer := convertWriter{ctx: ctx, writer: output, count: &report.Bytes}
|
||
if format == "ofd" {
|
||
if options.Pages == nil && editor == nil && options.Fonts == FontKeep && options.Compression.Mode == CompressionUnchanged {
|
||
if err = progress("write", 0, 0); err != nil {
|
||
return report, err
|
||
}
|
||
_, err = io.Copy(writer, io.NewSectionReader(operationInput, 0, size))
|
||
report.Unchanged = err == nil
|
||
return report, err
|
||
}
|
||
if editor == nil {
|
||
editor, err = reader.Editor()
|
||
if err != nil {
|
||
return report, err
|
||
}
|
||
}
|
||
editor.OnWriteProgress = writeProgress
|
||
if options.Fonts != FontKeep {
|
||
if err = progress("fonts", 0, 0); err != nil {
|
||
return report, err
|
||
}
|
||
configuration := NewRenderer(&Reader{}, options.RendererOptions...)
|
||
if options.Backends != nil {
|
||
WithRenderBackends(*options.Backends)(configuration)
|
||
}
|
||
editor.SetRenderBackends(configuration.Backends())
|
||
editor.SetFontDirs(configuration.fontDirs...)
|
||
editor.SetFontFS(configuration.fontFS...)
|
||
report.Fonts, err = editor.ProcessFontsWithOptions(ctx, FontProcessOptions{Mode: options.Fonts, OnProgress: func(stage string, completed, total int) error {
|
||
if stage == "fonts" {
|
||
return progress(stage, completed, total)
|
||
}
|
||
return progress("fonts."+stage, completed, total)
|
||
}})
|
||
if err != nil {
|
||
return report, err
|
||
}
|
||
if report.Input == "ofd" && options.Pages == nil && options.Compression.Mode == CompressionUnchanged && !slices.ContainsFunc(report.Fonts, func(change FontChange) bool { return change.Changed }) {
|
||
if err = progress("write", 0, 0); err != nil {
|
||
return report, err
|
||
}
|
||
_, err = io.Copy(writer, io.NewSectionReader(operationInput, 0, size))
|
||
report.Unchanged = err == nil
|
||
return report, err
|
||
}
|
||
}
|
||
if options.Pages == nil {
|
||
_, err = editor.WriteToWithOptions(ctx, writer, WriteOptions{Compression: options.Compression})
|
||
} else {
|
||
_, err = editor.WritePagesTo(ctx, writer, indices, WriteOptions{Compression: options.Compression})
|
||
}
|
||
return report, err
|
||
}
|
||
if reader == nil {
|
||
if err = progress("prepare", 0, count); err != nil {
|
||
return report, err
|
||
}
|
||
reader, err = editor.reader(progress)
|
||
if err != nil {
|
||
return report, err
|
||
}
|
||
defer reader.Close()
|
||
}
|
||
renderer := NewRenderer(reader, options.RendererOptions...)
|
||
renderer.Compression = options.Compression
|
||
renderer.OutputContext = ctx
|
||
if options.Backends != nil {
|
||
WithRenderBackends(*options.Backends)(renderer)
|
||
}
|
||
renderer.OnExportProgress = func(completed, total int) error {
|
||
return progress("export", completed, total)
|
||
}
|
||
switch format {
|
||
case "pdf":
|
||
err = renderer.RenderToMultiPagePDF(writer, indices...)
|
||
case "txt":
|
||
err = renderer.RenderToMultiPageText(writer, indices...)
|
||
default:
|
||
if options.PageOutput == nil {
|
||
err = renderer.RenderToZIP(writer, format, indices...)
|
||
report.Format = OutputFormat{Value: "zip", Label: "ZIP", Extension: "zip", MIME: "application/zip"}
|
||
break
|
||
}
|
||
for i, index := range indices {
|
||
if err = progress("export", i, len(indices)); err != nil {
|
||
return report, err
|
||
}
|
||
page, readErr := reader.PageContentByIndex(index)
|
||
if readErr != nil {
|
||
return report, readErr
|
||
}
|
||
if err = options.PageOutput(index, func(output io.Writer) error {
|
||
return renderer.RenderTo(page, convertWriter{ctx: ctx, writer: output, count: &report.Bytes}, format)
|
||
}); err != nil {
|
||
return report, fmt.Errorf("export page %d: %w", index+1, err)
|
||
}
|
||
}
|
||
err = progress("export", len(indices), len(indices))
|
||
}
|
||
return report, err
|
||
}
|
||
|
||
// ReadAt 读取前后响应取消,保留读取字节数和源错误
|
||
// 入参: data 目标缓冲, offset 源偏移
|
||
// 返回: int 读取字节数, error 读取错误
|
||
func (r convertReader) ReadAt(data []byte, offset int64) (int, error) {
|
||
if err := r.ctx.Err(); err != nil {
|
||
return 0, err
|
||
}
|
||
n, err := r.input.ReadAt(data, offset)
|
||
if err == nil {
|
||
err = r.ctx.Err()
|
||
}
|
||
return n, err
|
||
}
|
||
|
||
// Write 写入前后响应取消,保留写入及短写错误
|
||
// 入参: data 待写数据
|
||
// 返回: int 写入字节数, error 写入错误
|
||
func (w convertWriter) Write(data []byte) (int, error) {
|
||
if err := w.ctx.Err(); err != nil {
|
||
return 0, err
|
||
}
|
||
n, err := w.writer.Write(data)
|
||
*w.count += int64(n)
|
||
if err == nil && n != len(data) {
|
||
err = io.ErrShortWrite
|
||
}
|
||
if err == nil {
|
||
err = w.ctx.Err()
|
||
}
|
||
return n, err
|
||
}
|
||
|
||
// WriteString 写入字符串并保留底层原生写入能力,前后响应取消
|
||
// 入参: data 待写文本
|
||
// 返回: int 写入字节数, error 写入、短写或取消错误
|
||
func (w convertWriter) WriteString(data string) (int, error) {
|
||
if err := w.ctx.Err(); err != nil {
|
||
return 0, err
|
||
}
|
||
n, err := io.WriteString(w.writer, data)
|
||
*w.count += int64(n)
|
||
if err == nil && n != len(data) {
|
||
err = io.ErrShortWrite
|
||
}
|
||
if err == nil {
|
||
err = w.ctx.Err()
|
||
}
|
||
return n, err
|
||
}
|
||
|
||
// DetectFormat 按文件内容识别PDF或OFD,不读取压缩内容或关闭输入
|
||
// 识别结果不代表文档完整有效,仍需通过对应阅读器解析
|
||
// 入参: input 输入源
|
||
// 返回: string 格式标识pdf或ofd, error 读取错误或不支持的格式
|
||
func DetectFormat(input *Input) (string, error) {
|
||
if input == nil || input.Size() <= 0 {
|
||
return "", fmt.Errorf("empty document input")
|
||
}
|
||
size := input.Size()
|
||
var header [1024]byte
|
||
data := header[:min(size, int64(len(header)))]
|
||
n, err := input.ReadAt(data, 0)
|
||
if err != nil && err != io.EOF {
|
||
return "", err
|
||
}
|
||
if n != len(data) {
|
||
return "", io.ErrUnexpectedEOF
|
||
}
|
||
if bytes.HasPrefix(data, []byte("%PDF-")) {
|
||
return "pdf", nil
|
||
}
|
||
archive, err := zip.NewReader(input, size)
|
||
if err == nil {
|
||
reader := &Reader{Zip: archive}
|
||
if err := reader.indexPackage(); err != nil {
|
||
return "", err
|
||
}
|
||
if reader.fileNames["OFD.xml"] || reader.fileNames["Encryptions.xml"] {
|
||
return "ofd", nil
|
||
}
|
||
} else {
|
||
if !errors.Is(err, zip.ErrFormat) {
|
||
return "", err
|
||
}
|
||
if bytes.Contains(data, []byte("%PDF-")) {
|
||
return "pdf", nil
|
||
}
|
||
}
|
||
return "", fmt.Errorf("unsupported input format")
|
||
}
|
||
|
||
// NewConversionPool 创建共享转换调度器,不启动后台任务
|
||
// 入参: options 并行及内存选项
|
||
// 返回: *ConversionPool 调度器, error 配置错误
|
||
func NewConversionPool(options ConversionPoolOptions) (*ConversionPool, error) {
|
||
if options.Parallelism < 0 || options.MemoryLimit < 0 {
|
||
return nil, fmt.Errorf("invalid conversion pool options")
|
||
}
|
||
if options.Parallelism == 0 {
|
||
options.Parallelism = runtime.GOMAXPROCS(0)
|
||
}
|
||
return &ConversionPool{options: options, changed: make(chan struct{})}, nil
|
||
}
|
||
|
||
// Parallelism 返回同时运行的转换数量上限
|
||
// 返回: int 并行上限
|
||
func (p *ConversionPool) Parallelism() int { return p.options.Parallelism }
|
||
|
||
// memoryLimit 读取当前可分配的图像工作量预算
|
||
// 返回: int64 可用字节数,负值表示未设置预算
|
||
func (p *ConversionPool) memoryLimit() int64 {
|
||
limit := int64(-1)
|
||
if p.options.MemoryLimit > 0 {
|
||
limit = p.options.MemoryLimit
|
||
}
|
||
if p.options.AvailableMemory != nil {
|
||
available := max(0, p.options.AvailableMemory())
|
||
if limit < 0 || available < limit {
|
||
limit = available
|
||
}
|
||
}
|
||
return limit
|
||
}
|
||
|
||
// acquire 等待转换或图像预算,超预算图像可独占运行
|
||
// 入参: ctx 取消上下文, image 是否为图像任务, size 图像工作量
|
||
// 返回: error 取消错误
|
||
func (p *ConversionPool) acquire(ctx context.Context, image bool, size int64) error {
|
||
var timer *time.Timer
|
||
defer func() {
|
||
if timer != nil {
|
||
timer.Stop()
|
||
}
|
||
}()
|
||
for {
|
||
if err := ctx.Err(); err != nil {
|
||
return err
|
||
}
|
||
limit := p.memoryLimit()
|
||
p.mu.Lock()
|
||
allowed := p.active < p.options.Parallelism && (p.active == 0 || limit != 0)
|
||
if image {
|
||
allowed = p.images < p.options.Parallelism && (p.images == 0 || limit < 0 || size <= limit-p.memory)
|
||
}
|
||
if allowed {
|
||
if image {
|
||
p.images++
|
||
p.memory += size
|
||
} else {
|
||
p.active++
|
||
}
|
||
p.mu.Unlock()
|
||
return nil
|
||
}
|
||
changed := p.changed
|
||
p.mu.Unlock()
|
||
var tick <-chan time.Time
|
||
if p.options.AvailableMemory != nil {
|
||
if timer == nil {
|
||
timer = time.NewTimer(250 * time.Millisecond)
|
||
} else {
|
||
timer.Reset(250 * time.Millisecond)
|
||
}
|
||
tick = timer.C
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
case <-changed:
|
||
case <-tick:
|
||
}
|
||
if timer != nil {
|
||
timer.Stop()
|
||
}
|
||
}
|
||
}
|
||
|
||
// release 归还任务预算并唤醒等待者
|
||
// 入参: image 是否为图像任务, size 图像工作量
|
||
func (p *ConversionPool) release(image bool, size int64) {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
if image {
|
||
p.images--
|
||
p.memory -= size
|
||
} else {
|
||
p.active--
|
||
}
|
||
close(p.changed)
|
||
p.changed = make(chan struct{})
|
||
}
|
||
|
||
// conversionWorkers 按当前文件数量分配内部计算任务
|
||
// 入参: ctx 转换上下文
|
||
// 返回: int 内部并行数
|
||
func conversionWorkers(ctx context.Context) int {
|
||
p, _ := ctx.Value(conversionPoolKey{}).(*ConversionPool)
|
||
if p == nil {
|
||
return runtime.GOMAXPROCS(0)
|
||
}
|
||
p.mu.Lock()
|
||
active := p.active
|
||
p.mu.Unlock()
|
||
return max(1, p.options.Parallelism/max(1, active))
|
||
}
|