From b3c4bbc8446e15076b54990bdb23513f93a38637 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 14 Oct 2026 09:02:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=96=87=E4=BB=B6=E6=8B=86=E5=88=86):=20?= =?UTF-8?q?=E6=8B=86=E5=88=86PDF=E6=A0=BC=E5=BC=8F=E9=80=82=E9=85=8D?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/base/threed/u3d.go | 1 - internal/codec/pdf/clip.go | 67 ++++ internal/codec/pdf/color.go | 37 ++ internal/codec/pdf/export.go | 195 ++++++++++ internal/codec/pdf/font.go | 338 ++++++++++++++++++ internal/codec/pdf/geometry.go | 101 ++++++ internal/codec/pdf/image.go | 95 +++++ internal/codec/pdf/image_cache.go | 106 ++++++ .../codec/pdf/link.go | 160 +++++---- internal/codec/pdf/media.go | 162 +++++++++ internal/codec/pdf/number.go | 26 ++ internal/codec/pdf/page.go | 40 +++ internal/codec/pdf/rendition.go | 158 ++++++++ .../codec/pdf/shading.go | 48 +-- internal/codec/pdf/threed.go | 270 ++++++++++++++ .../codec/pdf/threed_mode.go | 35 +- .../codec/pdf/threed_section.go | 10 +- ofdgo_pdf.go | 26 +- ofdgo_pdf_3d.go | 229 +----------- ofdgo_pdf_clip.go | 49 +-- ofdgo_pdf_composite.go | 39 +- ofdgo_pdf_composite_draw.go | 13 +- ofdgo_pdf_composite_pattern.go | 9 +- ofdgo_pdf_destination.go | 3 +- ofdgo_pdf_font.go | 330 +---------------- ofdgo_pdf_geometry.go | 49 +-- ofdgo_pdf_group.go | 5 +- ofdgo_pdf_image_cache.go | 79 +--- ofdgo_pdf_image_native.go | 36 +- ofdgo_pdf_link.go | 3 +- ofdgo_pdf_media.go | 144 +------- ofdgo_pdf_mesh.go | 5 +- ofdgo_pdf_movie.go | 3 +- ofdgo_pdf_object.go | 44 +-- ofdgo_pdf_rendition.go | 150 +------- ofdgo_pdf_shading.go | 9 +- ofdgo_pdf_text_path.go | 33 +- ofdgo_pdf_text_replacement.go | 3 +- ofdgo_pdf_transfer.go | 15 +- ofdgo_renderer_output_canvas.go | 5 +- ofdgo_renderer_pdf_action.go | 19 +- ofdgo_renderer_pdf_canvas.go | 9 +- ofdgo_renderer_pdf_text.go | 9 +- ofdgo_renderer_pdf_write.go | 166 +-------- ofdgo_renderer_text_canvas.go | 5 +- 45 files changed, 1885 insertions(+), 1453 deletions(-) create mode 100644 internal/codec/pdf/clip.go create mode 100644 internal/codec/pdf/color.go create mode 100644 internal/codec/pdf/export.go create mode 100644 internal/codec/pdf/font.go create mode 100644 internal/codec/pdf/geometry.go create mode 100644 internal/codec/pdf/image.go create mode 100644 internal/codec/pdf/image_cache.go rename ofdgo_renderer_pdf_link.go => internal/codec/pdf/link.go (66%) create mode 100644 internal/codec/pdf/media.go create mode 100644 internal/codec/pdf/number.go create mode 100644 internal/codec/pdf/page.go create mode 100644 internal/codec/pdf/rendition.go rename ofdgo_pdf_shading_colorants.go => internal/codec/pdf/shading.go (58%) create mode 100644 internal/codec/pdf/threed.go rename ofdgo_pdf_3d_mode.go => internal/codec/pdf/threed_mode.go (63%) rename ofdgo_pdf_3d_section.go => internal/codec/pdf/threed_section.go (83%) diff --git a/internal/base/threed/u3d.go b/internal/base/threed/u3d.go index bcc9949..dd97310 100644 --- a/internal/base/threed/u3d.go +++ b/internal/base/threed/u3d.go @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package threed 提供U3D模型解码、场景求值及软件绘制能力 package threed import ( diff --git a/internal/codec/pdf/clip.go b/internal/codec/pdf/clip.go new file mode 100644 index 0000000..fba845a --- /dev/null +++ b/internal/codec/pdf/clip.go @@ -0,0 +1,67 @@ +// 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 pdf + +import ( + "context" + "crypto/sha256" + "encoding/binary" + "math" + + "github.com/xiaoqidun/pdfgo" +) + +// ClipPathKey 按路径完整内容与变换生成键,不依赖可变切片的地址 +// 入参: ctx 取消上下文, path 裁剪路径, matrix 页面变换 +// 返回: [32]byte 内容摘要, error 取消错误 +func ClipPathKey(ctx context.Context, path pdfgo.Path, matrix pdfgo.Matrix) ([32]byte, error) { + hash := sha256.New() + var value [16]byte + for _, number := range matrix { + binary.LittleEndian.PutUint64(value[:8], math.Float64bits(number)) + hash.Write(value[:8]) + } + binary.LittleEndian.PutUint64(value[:8], uint64(len(path.Segments))) + value[8] = 0 + if path.EvenOdd { + value[8] = 1 + } + hash.Write(value[:9]) + for _, segment := range path.Segments { + if err := ctx.Err(); err != nil { + return [32]byte{}, err + } + binary.LittleEndian.PutUint64(value[:8], uint64(len(segment.Operator))) + binary.LittleEndian.PutUint64(value[8:], uint64(len(segment.Points))) + hash.Write(value[:]) + hash.Write([]byte(segment.Operator)) + for index, point := range segment.Points { + if index&255 == 0 { + if err := ctx.Err(); err != nil { + return [32]byte{}, err + } + } + binary.LittleEndian.PutUint64(value[:8], math.Float64bits(point.X)) + binary.LittleEndian.PutUint64(value[8:], math.Float64bits(point.Y)) + hash.Write(value[:]) + } + } + if err := ctx.Err(); err != nil { + return [32]byte{}, err + } + var key [32]byte + hash.Sum(key[:0]) + return key, nil +} diff --git a/internal/codec/pdf/color.go b/internal/codec/pdf/color.go new file mode 100644 index 0000000..d453c82 --- /dev/null +++ b/internal/codec/pdf/color.go @@ -0,0 +1,37 @@ +// 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 pdf + +import ( + "github.com/xiaoqidun/pdfgo" +) + +// OutputColor 将合成结果映射到OFD输出,与保留的设备四色对象使用相同显示换算 +// 入参: space 合成空间, values 非预乘分量 +// 返回: [3]float64 显示颜色, error 校准颜色转换错误 +func OutputColor(space *pdfgo.ColorSpace, values [4]float64) ([3]float64, error) { + if space.Model == "DeviceCMYK" && !space.Calibrated() { + black := 1 - values[3] + return [3]float64{(1 - values[0]) * black, (1 - values[1]) * black, (1 - values[2]) * black}, nil + } + return space.RGB(values[:space.Components()], "RelativeColorimetric") +} + +// NormalBlend 判断混合模式是否为普通覆盖 +// 入参: mode 混合模式 +// 返回: bool 是否普通覆盖 +func NormalBlend(mode pdfgo.Name) bool { + return mode == "" || mode == "Normal" || mode == "Compatible" +} diff --git a/internal/codec/pdf/export.go b/internal/codec/pdf/export.go new file mode 100644 index 0000000..67798e2 --- /dev/null +++ b/internal/codec/pdf/export.go @@ -0,0 +1,195 @@ +// 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 pdf + +import ( + "bytes" + "context" + "fmt" + "image" + "io" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" + "github.com/xiaoqidun/pdfgo" +) + +// TextPage 保存后端文字对象数量及待替换区段 +type TextPage struct { + Count int + Replacements []pdfgo.TextReplacement +} + +// WriteOptions 保存PDF最终写出所需的像素、文字和导航修正 +// Images逐页记录绘制图像,nil图像表示不替换;Links保留全部链接位置,包括矩形区域的nil项 +// PrepareRewrite在页面引用确定后填充导航动作,不得提前写出PDF +type WriteOptions struct { + Images [][]image.Image + Text []TextPage + Links map[int][]*pdfgo.LinkRegion + ExactImages bool + ExactText bool + Optimization pdfgo.OptimizeOptions + PrepareRewrite func(context.Context, *pdfgo.Reader, []pdfgo.Reference, *pdfgo.RewriteOptions) error +} + +// Write 合并图像、文字、导航修正及压缩,直接写出最终PDF +// 入参: ctx 取消上下文, data 后端PDF数据, writer 输出流, settings 写出配置 +// 返回: error 资源、压缩或写入错误 +func Write(ctx context.Context, data []byte, writer io.Writer, settings WriteOptions) error { + nativeWrite := settings.PrepareRewrite != nil + if !settings.ExactImages && !settings.ExactText && !nativeWrite && settings.Optimization.Compression.Mode == pdfgo.CompressionUnchanged { + if err := ctx.Err(); err != nil { + return err + } + n, err := writer.Write(data) + if err == nil && n != len(data) { + err = io.ErrShortWrite + } + return err + } + reader, err := pdfgo.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + return err + } + defer reader.Close() + if !settings.ExactImages && !settings.ExactText && !nativeWrite { + _, err := reader.OptimizeTo(ctx, writer, settings.Optimization) + return err + } + images := make(map[pdfgo.Reference]image.Image) + links := make(map[pdfgo.AnnotationLocation]pdfgo.LinkRegion) + texts := make(map[pdfgo.Reference][]pdfgo.TextReplacement) + references := make([]pdfgo.Reference, len(settings.Images)) + pages := 0 + err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { + if index >= len(settings.Images) { + return fmt.Errorf("unexpected PDF output page") + } + pages++ + references[index] = page.Reference + if nativeWrite { + annotations, err := page.AnnotationsContext(ctx) + if err != nil { + return err + } + expected := settings.Links[index] + if len(annotations) != len(expected) { + return fmt.Errorf("PDF output link count differs") + } + for i, link := range expected { + if annotations[i].Subtype != pdfgo.Name("Link") { + return fmt.Errorf("unexpected PDF output annotation") + } + if link != nil { + links[pdfgo.AnnotationLocation{Page: page.Reference, Index: i}] = *link + } + } + } + if !settings.ExactImages && !settings.ExactText { + return nil + } + value, err := reader.Resolve(page.Resources["XObject"]) + if err != nil { + return err + } + resources, _ := value.(pdfgo.Dictionary) + var content bytes.Buffer + if _, err := page.WriteContent(ctx, &content); err != nil { + return err + } + position := 0 + textCount := 0 + if err := pdfgo.WalkOperations(ctx, content.Bytes(), func(operation pdfgo.Operation) error { + if operation.Operator == "BT" { + textCount++ + } + if !settings.ExactImages { + return nil + } + if operation.Operator != "Do" { + return nil + } + if len(operation.Operands) != 1 || position >= len(settings.Images[index]) { + return fmt.Errorf("unexpected PDF output image") + } + name, ok := operation.Operands[0].(pdfgo.Name) + if !ok { + return fmt.Errorf("invalid PDF output image name") + } + ref, ok := resources[name].(pdfgo.Reference) + if !ok { + return fmt.Errorf("invalid PDF output image reference") + } + original := settings.Images[index][position] + position++ + if original == nil { + return nil + } + image, err := reader.ReadImage(ref) + if err != nil { + return err + } + if image.Width != original.Bounds().Dx() || image.Height != original.Bounds().Dy() { + return fmt.Errorf("PDF output image dimensions differ") + } + if previous := images[ref]; previous != nil && previous != original { + return fmt.Errorf("PDF output image reference conflicts") + } + images[ref] = original + return nil + }); err != nil { + return err + } + if settings.ExactImages && position != len(settings.Images[index]) { + return fmt.Errorf("PDF output image missing") + } + if settings.ExactText { + if index >= len(settings.Text) || textCount != settings.Text[index].Count { + return fmt.Errorf("PDF output text count differs") + } + if len(settings.Text[index].Replacements) != 0 { + texts[page.Reference] = settings.Text[index].Replacements + } + } + return nil + }) + if err != nil { + return err + } + if pages != len(settings.Images) { + return fmt.Errorf("PDF output page missing") + } + options := pdfgo.RewriteOptions{Images: images, ImageOptions: pdfgo.ImageWriteOptions{PreblendBinary: true}, LinkRegions: links, TextReplacements: texts, Optimization: settings.Optimization} + if nativeWrite { + if err := settings.PrepareRewrite(ctx, reader, references, &options); err != nil { + return err + } + } + for ref, original := range images { + if err := ctx.Err(); err != nil { + return err + } + pixels, err := baseimage.PixelDataContext(ctx, original) + if err != nil { + return err + } + if pixels == nil || pixels.Bounds() != original.Bounds() { + return fmt.Errorf("PDF output image pixel bounds differ") + } + images[ref] = pixels + } + _, err = reader.RewriteTo(ctx, writer, options) + return err +} diff --git a/internal/codec/pdf/font.go b/internal/codec/pdf/font.go new file mode 100644 index 0000000..207dee6 --- /dev/null +++ b/internal/codec/pdf/font.go @@ -0,0 +1,338 @@ +// 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 pdf + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "fmt" + "unicode/utf16" + + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" + "github.com/xiaoqidun/pdfgo" +) + +// FontRepair 保存字体封装修复范围,不混用字形度量与位置索引 +type FontRepair struct { + MetricsLimit uint16 + Locations bool +} + +// EmbeddedFontName 去除标准六位大写子集前缀,保留字体名称和样式后缀 +// 入参: name PDF或内嵌字体名称 +// 返回: string 原字体名称 +func EmbeddedFontName(name string) string { + if len(name) <= 7 || name[6] != '+' { + return name + } + for i := range 6 { + if name[i] < 'A' || name[i] > 'Z' { + return name + } + } + return name[7:] +} + +// FontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号 +// 无效字符表按PDF已解码字形重建,不改变字符对应的字形编号 +// 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析 +// 返回: []byte 封装后的字体数据, FontRepair 字体修复范围, error 错误信息 +func FontProgram(source *pdfgo.Font, type1 *basefont.Type1Program) ([]byte, FontRepair, error) { + var repair FontRepair + program := source.Program + if source.ProgramType == "FontFile" { + if type1 == nil { + parsed, err := basefont.ParseType1Program(program) + if err != nil { + return nil, repair, err + } + type1 = &parsed + } + var err error + program, err = type1.ToCFF(FontIdentity(source)) + if err != nil { + return nil, repair, err + } + } + bareCFF := len(program) >= 4 && program[0] == 1 && program[1] == 0 && program[2] >= 4 && program[3] >= 1 && program[3] <= 4 + if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" || bareCFF { + var err error + program, _, err = basefont.WrapCFFToOTF(program) + if err != nil { + return nil, repair, err + } + } + tables, err := basefont.FileTables(program, 0) + if err != nil { + return nil, repair, err + } + if len(tables["head"]) < 54 || len(tables["maxp"]) < 6 || len(tables["hhea"]) < 36 || len(tables["hmtx"]) == 0 { + return nil, repair, fmt.Errorf("embedded PDF font lacks required metrics") + } + count := binary.BigEndian.Uint16(tables["maxp"][4:6]) + if count == 0 { + return nil, repair, fmt.Errorf("embedded PDF font has no glyphs") + } + repair.Locations, err = basefont.NormalizeTrueTypeLocations(tables) + if err != nil { + return nil, repair, err + } + changed := repair.Locations + if head := tables["head"]; len(head) == 56 && binary.BigEndian.Uint32(head[:4]) == 0x00010000 && head[54] == 0 && head[55] == 0 { + tables["head"] = head[:54] + changed = true + } + if cff := tables["CFF "]; len(cff) != 0 { + sanitized, err := basefont.SanitizeCFF(cff) + if err != nil { + return nil, repair, err + } + normalized, err := basefont.NormalizeCFFCharstringsAt(sanitized, binary.BigEndian.Uint16(tables["head"][18:20])) + if err != nil { + return nil, repair, err + } + if !bytes.Equal(normalized, cff) { + tables["CFF "] = normalized + changed = true + } + } + metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36])) + if metrics == 0 || metrics > int(count) { + return nil, repair, fmt.Errorf("invalid embedded PDF font metric count") + } + metricLength := 4*metrics + 2*(int(count)-metrics) + if len(tables["hmtx"]) < metricLength { + if len(tables["hmtx"]) < 4*metrics || len(tables["hmtx"])%2 != 0 { + return nil, repair, fmt.Errorf("incomplete embedded PDF font metrics: have %d, need %d", len(tables["hmtx"]), metricLength) + } + limit := metrics + (len(tables["hmtx"])-4*metrics)/2 + missing, err := missingLeftBearings(tables, limit, int(count)) + if err != nil { + return nil, repair, err + } + tables["hmtx"] = append(bytes.Clone(tables["hmtx"]), missing...) + repair.MetricsLimit = uint16(limit) + changed = true + } + if len(tables["hmtx"]) > metricLength { + tables["hmtx"] = tables["hmtx"][:metricLength] + changed = true + } + invalidCmap := false + if source.Subtype == "TrueType" || source.Subtype == "Type0" { + mapping := basefont.ParseCmapMappings(tables["cmap"]) + for char, glyph := range mapping { + if glyph >= count { + delete(mapping, char) + invalidCmap = true + } + } + if invalidCmap && source.Subtype == "TrueType" { + tables["cmap"] = basefont.BuildCmapTable(count, mapping) + changed = true + } + } + if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && invalidCmap || (source.Subtype == "Type0" || source.Subtype == "TrueType") && CmapNeedsRebuild(tables["cmap"]) { + mapping, err := source.GlyphMap() + if err != nil { + return nil, repair, err + } + for _, glyph := range mapping { + if glyph >= count { + return nil, repair, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph, count) + } + } + basefont.AddPackedGlyphMapping(mapping, count) + tables["cmap"] = basefont.BuildCmapTable(count, mapping) + changed = true + } + if len(basefont.NamesFromTable(tables["name"])) == 0 { + fontName := FontIdentity(source) + name := utf16.Encode([]rune(fontName)) + if len(name) > 32767 { + return nil, repair, fmt.Errorf("PDF font name exceeds name table capacity") + } + data := make([]byte, 42+len(name)*2) + binary.BigEndian.PutUint16(data[2:], 3) + binary.BigEndian.PutUint16(data[4:], 42) + for n, id := range []uint16{1, 4, 6} { + record := data[6+n*12:] + binary.BigEndian.PutUint16(record, 3) + binary.BigEndian.PutUint16(record[2:], 1) + binary.BigEndian.PutUint16(record[4:], 0x409) + binary.BigEndian.PutUint16(record[6:], id) + binary.BigEndian.PutUint16(record[8:], uint16(len(name)*2)) + } + for n, v := range name { + binary.BigEndian.PutUint16(data[42+n*2:], v) + } + tables["name"] = data + changed = true + } + if len(tables["post"]) == 0 { + tables["post"] = basefont.BuildPostTable() + changed = true + } + if len(tables["OS/2"]) == 0 { + hhea := tables["hhea"] + tables["OS/2"] = basefont.BuildOS2TableWithMetrics(int16(binary.BigEndian.Uint16(hhea[4:6])), int16(binary.BigEndian.Uint16(hhea[6:8]))) + changed = true + } + for _, tag := range []string{"cvt ", "fpgm", "prep"} { + if data, ok := tables[tag]; ok && len(data) == 0 { + delete(tables, tag) + changed = true + } + } + if !changed && !sfntMissingPadding(program) { + return program, repair, nil + } + result, err := basefont.SerializeOTF(tables) + return result, repair, err +} + +// FontIdentity 返回字体名称或由原始程序生成的稳定封装标识 +// 入参: source PDF字体 +// 返回: string 字体标识 +func FontIdentity(source *pdfgo.Font) string { + if source.Name != "" { + return source.Name + } + checksum := sha256.Sum256(source.Program) + return fmt.Sprintf("PDF-%x", checksum[:28]) +} + +// CmapNeedsRebuild 检查字体封装字符表是否存在结构错误或交叠区段 +// 入参: data cmap表数据 +// 返回: bool 是否需要按PDF显式字形编号重建 +func CmapNeedsRebuild(data []byte) bool { + if len(data) < 4 || binary.BigEndian.Uint16(data) != 0 { + return true + } + count := int(binary.BigEndian.Uint16(data[2:])) + if count == 0 || count > (len(data)-4)/8 { + return true + } + for index := range count { + offset := uint64(binary.BigEndian.Uint32(data[8+8*index:])) + if offset < uint64(4+8*count) || offset > uint64(len(data)) || uint64(len(data))-offset < 2 { + return true + } + sub := data[int(offset):] + if binary.BigEndian.Uint16(sub) == 6 { + if len(sub) < 10 { + return true + } + length := int(binary.BigEndian.Uint16(sub[2:])) + first, glyphs := int(binary.BigEndian.Uint16(sub[6:])), int(binary.BigEndian.Uint16(sub[8:])) + if length%2 != 0 || length > len(sub) || 10+2*glyphs > length || first+glyphs > 65536 { + return true + } + continue + } + if binary.BigEndian.Uint16(sub) != 4 { + continue + } + if len(sub) < 16 { + return true + } + length := int(binary.BigEndian.Uint16(sub[2:])) + segmentBytes := int(binary.BigEndian.Uint16(sub[6:])) + segments := segmentBytes / 2 + if segmentBytes == 0 || segmentBytes%2 != 0 || length%2 != 0 || length > len(sub) || 16+8*segments > length { + return true + } + glyphStart := 16 + 8*segments + previous := -1 + for i := 0; i < segments; i++ { + end := int(binary.BigEndian.Uint16(sub[14+2*i:])) + start := int(binary.BigEndian.Uint16(sub[16+2*segments+2*i:])) + if end < start || start <= previous || i == segments-1 && (start != 65535 || end != 65535) { + return true + } + previous = end + position := 16 + 6*segments + 2*i + offset := int(binary.BigEndian.Uint16(sub[position:])) + if offset != 0 && (offset%2 != 0 || position+offset < glyphStart || position+offset+2*(end-start+1) > length) { + return true + } + } + } + return false +} + +// sfntMissingPadding 判断字体表目录是否引用了未写入的末尾对齐字节 +// 入参: program OpenType字体数据 +// 返回: bool 是否需要重新封装 +func sfntMissingPadding(program []byte) bool { + if len(program) < 12 { + return true + } + count := int(binary.BigEndian.Uint16(program[4:6])) + if count > (len(program)-12)/16 { + return true + } + for index := range count { + record := program[12+16*index:] + offset := int(binary.BigEndian.Uint32(record[8:12])) + length := int(binary.BigEndian.Uint32(record[12:16])) + if offset > len(program) || length > len(program)-offset || (4-length%4)%4 > len(program)-offset-length { + return true + } + } + return false +} + +// missingLeftBearings 从轮廓边界恢复缺失的尾部左侧边距 +// 入参: tables 字体表, start 首个缺失字形, count 字形总数 +// 返回: []byte 补齐的hmtx数据, error 无法读取轮廓 +func missingLeftBearings(tables map[string][]byte, start, count int) ([]byte, error) { + head, loca, glyf := tables["head"], tables["loca"], tables["glyf"] + if len(head) < 54 || len(glyf) == 0 { + return nil, fmt.Errorf("embedded PDF font lacks glyph outlines for missing metrics") + } + format := int(int16(binary.BigEndian.Uint16(head[50:52]))) + entrySize := 2 + if format == 1 { + entrySize = 4 + } else if format != 0 { + return nil, fmt.Errorf("invalid embedded PDF font location format") + } + if len(loca) < (count+1)*entrySize { + return nil, fmt.Errorf("incomplete embedded PDF font glyph locations") + } + location := func(index int) int { + if format == 0 { + return int(binary.BigEndian.Uint16(loca[index*2:])) * 2 + } + return int(binary.BigEndian.Uint32(loca[index*4:])) + } + result := make([]byte, 2*(count-start)) + for index := start; index < count; index++ { + from, to := location(index), location(index+1) + if from > to || to > len(glyf) { + return nil, fmt.Errorf("invalid embedded PDF font glyph location") + } + if from == to { + continue + } + if to-from < 10 { + return nil, fmt.Errorf("incomplete embedded PDF font glyph outline") + } + copy(result[(index-start)*2:], glyf[from+2:from+4]) + } + return result, nil +} diff --git a/internal/codec/pdf/geometry.go b/internal/codec/pdf/geometry.go new file mode 100644 index 0000000..42fc2da --- /dev/null +++ b/internal/codec/pdf/geometry.go @@ -0,0 +1,101 @@ +// 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 pdf + +import ( + "context" + "fmt" + + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" + "github.com/xiaoqidun/pdfgo" +) + +// GeometryPath 直接转换PDF路径坐标,不经文本编码或曲线展平 +// 入参: ctx 取消上下文, source PDF路径, matrix 坐标变换 +// 返回: basegeom.Path 独立几何路径, error 指令、坐标或取消错误 +func GeometryPath(ctx context.Context, source pdfgo.Path, matrix pdfgo.Matrix) (basegeom.Path, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + path := make(basegeom.Path, len(source.Segments)) + for i, segment := range source.Segments { + if err := ctx.Err(); err != nil { + return nil, err + } + var count int + switch segment.Operator { + case "M": + path[i].Verb, count = basegeom.Move, 1 + case "L": + path[i].Verb, count = basegeom.Line, 1 + case "B": + path[i].Verb, count = basegeom.Cubic, 3 + case "C": + path[i].Verb = basegeom.ClosePath + default: + return nil, fmt.Errorf("invalid PDF path operator %q", segment.Operator) + } + if len(segment.Points) != count { + return nil, fmt.Errorf("invalid PDF path operands for %s", segment.Operator) + } + var points [3]basegeom.Point + for j, point := range segment.Points { + point = matrix.Apply(point) + points[j] = basegeom.Point{X: point.X, Y: point.Y} + } + if count != 0 { + path[i].End = points[count-1] + } + if count == 3 { + path[i].Control1, path[i].Control2 = points[0], points[1] + } + } + if err := path.Validate(); err != nil { + return nil, err + } + return path, ctx.Err() +} + +// GlyphPath 将字形轮廓变换到PDF页面坐标,二次曲线精确转换为三次曲线 +// 入参: outline 字形轮廓, matrix 字形到页面的变换 +// 返回: pdfgo.Path 页面路径, error 非字体轮廓指令 +func GlyphPath(outline basegeom.Path, matrix pdfgo.Matrix) (pdfgo.Path, error) { + path := pdfgo.Path{Segments: make([]pdfgo.Segment, 0, len(outline))} + var current, start basegeom.Point + point := func(p basegeom.Point) pdfgo.Point { return matrix.Apply(pdfgo.Point{X: p.X, Y: p.Y}) } + for _, segment := range outline { + switch segment.Verb { + case basegeom.Move: + start = segment.End + path.Segments = append(path.Segments, pdfgo.Segment{Operator: "M", Points: []pdfgo.Point{point(segment.End)}}) + case basegeom.Line: + path.Segments = append(path.Segments, pdfgo.Segment{Operator: "L", Points: []pdfgo.Point{point(segment.End)}}) + case basegeom.Quad: + c1 := basegeom.Point{X: current.X + (segment.Control1.X-current.X)*2/3, Y: current.Y + (segment.Control1.Y-current.Y)*2/3} + c2 := basegeom.Point{X: segment.End.X + (segment.Control1.X-segment.End.X)*2/3, Y: segment.End.Y + (segment.Control1.Y-segment.End.Y)*2/3} + path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(c1), point(c2), point(segment.End)}}) + case basegeom.Cubic: + path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(segment.Control1), point(segment.Control2), point(segment.End)}}) + case basegeom.ClosePath: + path.Segments = append(path.Segments, pdfgo.Segment{Operator: "C"}) + current = start + continue + default: + return pdfgo.Path{}, fmt.Errorf("unsupported PDF glyph geometry verb %d", segment.Verb) + } + current = segment.End + } + return path, nil +} diff --git a/internal/codec/pdf/image.go b/internal/codec/pdf/image.go new file mode 100644 index 0000000..4c6dbae --- /dev/null +++ b/internal/codec/pdf/image.go @@ -0,0 +1,95 @@ +// 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 pdf + +import ( + "image" + "image/color" + "math" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" + "github.com/xiaoqidun/pdfgo" +) + +// StencilImage 按当前填充色读取模板图像,不分配整幅彩色样本 +type StencilImage struct { + Source image.Image + Fill color.NRGBA64 +} + +// NativeImage 按源像素输出原生过程色,不分配完整显示颜色缓冲 +type NativeImage struct { + Source *pdfgo.ImageProcess + Space *pdfgo.ColorSpace +} + +// ColorModel 保留填充色和模板的有效采样精度 +// 返回: color.Model 非预乘颜色模型 +func (s *StencilImage) ColorModel() color.Model { + model := s.Source.ColorModel() + if s.Fill.R%257 == 0 && s.Fill.G%257 == 0 && s.Fill.B%257 == 0 && (model == color.GrayModel || model == color.NRGBAModel || model == color.RGBAModel) { + return color.NRGBAModel + } + return color.NRGBA64Model +} + +// Bounds 返回模板图像的采样边界 +// 返回: image.Rectangle 图像边界 +func (s *StencilImage) Bounds() image.Rectangle { return s.Source.Bounds() } + +// At 以模板灰度计算填充覆盖率,不重复变换颜色 +// 入参: x 横向坐标, y 纵向坐标 +// 返回: color.Color 非预乘颜色 +func (s *StencilImage) At(x, y int) color.Color { + return s.NRGBA64At(x, y) +} + +// NRGBA64At 直接读取模板覆盖率,保留填充色的有效精度 +// 入参: x 横向坐标, y 纵向坐标 +// 返回: color.NRGBA64 非预乘颜色 +func (s *StencilImage) NRGBA64At(x, y int) color.NRGBA64 { + if !image.Pt(x, y).In(s.Bounds()) { + return color.NRGBA64{} + } + gray := baseimage.NRGBA64At(s.Source, x, y) + pixel := s.Fill + pixel.A = uint16(65535 - gray.R) + return pixel +} + +// ColorModel 保留16位原生分量与源透明度 +// 返回: color.Model 非预乘颜色模型 +func (s *NativeImage) ColorModel() color.Model { return color.NRGBA64Model } + +// Bounds 返回源图像的完整采样边界 +// 返回: image.Rectangle 原始像素边界 +func (s *NativeImage) Bounds() image.Rectangle { return s.Source.Bounds() } + +// At 获取按OFD输出映射的原生颜色 +// 入参: x 横坐标, y 纵坐标 +// 返回: color.Color 非预乘颜色 +func (s *NativeImage) At(x, y int) color.Color { return s.NRGBA64At(x, y) } + +// NRGBA64At 按原始精度转换设备过程色,不进行页面采样或备用变换 +// 入参: x 横坐标, y 纵坐标 +// 返回: color.NRGBA64 非预乘颜色 +func (s *NativeImage) NRGBA64At(x, y int) color.NRGBA64 { + if !image.Pt(x, y).In(s.Bounds()) { + return color.NRGBA64{} + } + values, alpha := s.Source.ValuesAt(x, y) + rgb, _ := OutputColor(s.Space, values) + return color.NRGBA64{R: uint16(math.Round(rgb[0] * 65535)), G: uint16(math.Round(rgb[1] * 65535)), B: uint16(math.Round(rgb[2] * 65535)), A: uint16(math.Round(alpha * 65535))} +} diff --git a/internal/codec/pdf/image_cache.go b/internal/codec/pdf/image_cache.go new file mode 100644 index 0000000..97ed39f --- /dev/null +++ b/internal/codec/pdf/image_cache.go @@ -0,0 +1,106 @@ +// 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 pdf + +import ( + "bytes" + "context" + "crypto/sha256" + "reflect" + + "github.com/xiaoqidun/pdfgo" +) + +// imageValueBudget 限制图像描述的检查深度和保留字节数 +type imageValueBudget struct { + nodes int + bytes int +} + +// ImageDigest 对无间接依赖的小图像生成编码摘要,超预算时不参与内容缓存 +// 入参: ctx 取消上下文, source 图像描述 +// 返回: [32]byte 编码摘要, bool 是否可复用, error 取消错误 +func ImageDigest(ctx context.Context, source *pdfgo.Image) ([32]byte, bool, error) { + if source == nil || source.Stream == nil || len(source.Stream.Data) > 1<<20 { + return [32]byte{}, false, nil + } + budget := imageValueBudget{nodes: 256, bytes: 65536} + for _, value := range []pdfgo.Object{source.Stream.Dictionary, source.ColorSpace, source.Decode, source.Mask, source.SoftMask} { + if !imageDirectValue(value, &budget) { + return [32]byte{}, false, ctx.Err() + } + } + hash := sha256.New() + data := source.Stream.Data + for len(data) != 0 { + if err := ctx.Err(); err != nil { + return [32]byte{}, false, err + } + n := min(len(data), 65536) + hash.Write(data[:n]) + data = data[n:] + } + var result [32]byte + hash.Sum(result[:0]) + return result, true, ctx.Err() +} + +// SameImage 核对完整编码与有效图像属性,不以摘要相同代替内容相同 +// 入参: left 已缓存图像, right 待复用图像,二者须包含有效编码流 +// 返回: bool 是否可使用同一图像资源 +func SameImage(left, right *pdfgo.Image) bool { + return left.Width == right.Width && left.Height == right.Height && left.BitsPerComponent == right.BitsPerComponent && + left.ImageMask == right.ImageMask && left.Interpolate == right.Interpolate && + bytes.Equal(left.Stream.Data, right.Stream.Data) && reflect.DeepEqual(left.Stream.Dictionary, right.Stream.Dictionary) && + reflect.DeepEqual(left.ColorSpace, right.ColorSpace) && reflect.DeepEqual(left.Decode, right.Decode) && + reflect.DeepEqual(left.Mask, right.Mask) && reflect.DeepEqual(left.SoftMask, right.SoftMask) +} + +// imageDirectValue 检查图像描述是否完全独立,拒绝间接引用和嵌套流 +// 入参: value 描述值, budget 描述检查预算 +// 返回: bool 是否不依赖阅读器资源 +func imageDirectValue(value pdfgo.Object, budget *imageValueBudget) bool { + if budget.nodes <= 0 || budget.bytes < 64 { + return false + } + budget.nodes-- + budget.bytes -= 64 + switch v := value.(type) { + case nil, pdfgo.Boolean, pdfgo.Integer, pdfgo.Real: + return true + case pdfgo.Name: + budget.bytes -= len(v) + return budget.bytes >= 0 + case pdfgo.String: + budget.bytes -= len(v) + return budget.bytes >= 0 + case pdfgo.Array: + for _, child := range v { + if !imageDirectValue(child, budget) { + return false + } + } + return true + case pdfgo.Dictionary: + for key, child := range v { + budget.bytes -= len(key) + if !imageDirectValue(child, budget) { + return false + } + } + return true + } + return false +} diff --git a/ofdgo_renderer_pdf_link.go b/internal/codec/pdf/link.go similarity index 66% rename from ofdgo_renderer_pdf_link.go rename to internal/codec/pdf/link.go index f6ff1ce..838f1fb 100644 --- a/ofdgo_renderer_pdf_link.go +++ b/internal/codec/pdf/link.go @@ -12,42 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package pdf import ( "context" "fmt" - basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "math" "slices" "github.com/tdewolff/canvas" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "github.com/xiaoqidun/pdfgo" ) // PDF链接区域细分上限与曲线逼近误差,误差单位为毫米 const ( - pdfLinkEdgeLimit = 4096 - pdfLinkQuadLimit = 65536 - pdfLinkTolerance = 0.01 + LinkEdgeLimit = 4096 + LinkQuadLimit = 65536 + linkTolerance = 0.01 ) -// pdfLinkEdge 保存按纵坐标排序的边及原始绕行方向 -type pdfLinkEdge struct { +// linkEdge 保存按纵坐标排序的边及原始绕行方向 +type linkEdge struct { a, b pdfgo.Point winding int } -// pdfLinkCross 保存水平扫描线与区域边的交点 -type pdfLinkCross struct { +// linkCross 保存水平扫描线与区域边的交点 +type linkCross struct { x float64 edge int } -// pdfActionRegion 将非零绕行区域分解为PDF链接四边形,保留空洞和相交轮廓 +// ActionRegion 将非零绕行区域分解为PDF链接四边形,保留空洞和相交轮廓 // 入参: ctx 取消上下文, outline 页面SVG路径, pageH 页面高度 // 返回: *pdfgo.LinkRegion 链接区域,nil表示沿用矩形, error 解析或复杂度错误 -func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo.LinkRegion, error) { +func ActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo.LinkRegion, error) { if outline == "" { return nil, nil } @@ -58,20 +58,20 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo if err != nil { return nil, err } - if path.Len() > pdfLinkEdgeLimit { + if path.Len() > LinkEdgeLimit { return nil, fmt.Errorf("PDF link region exceeds edge limit") } - if _, ok := basegeom.RectangleBounds(basegeom.Path(*geometryFromCanvasPath(path))); ok { + if _, ok := basegeom.RectangleBounds(linkGeometry(path)); ok { return nil, nil } - path, err = flattenPDFLink(ctx, path) + path, err = FlattenLink(ctx, path) if err != nil { return nil, err } - if quads := convexPDFLink(path, pageH); len(quads) != 0 { - return pdfLinkRegionBounds(quads), nil + if quads := convexLink(path, pageH); len(quads) != 0 { + return linkRegionBounds(quads), nil } - var edges []pdfLinkEdge + var edges []linkEdge var levels []float64 var start, previous pdfgo.Point open := false @@ -79,7 +79,7 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo if a.Y == b.Y { return } - edge := pdfLinkEdge{a: a, b: b, winding: 1} + edge := linkEdge{a: a, b: b, winding: 1} if a.Y > b.Y { edge.a, edge.b, edge.winding = b, a, -1 } @@ -129,7 +129,7 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo return nil, fmt.Errorf("invalid PDF link intersection") } levels = append(levels, y) - if len(levels) > pdfLinkQuadLimit { + if len(levels) > LinkQuadLimit { return nil, fmt.Errorf("PDF link region exceeds intersection limit") } } @@ -138,7 +138,7 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo slices.Sort(levels) levels = slices.Compact(levels) region := &pdfgo.LinkRegion{} - crossings := make([]pdfLinkCross, 0, len(edges)) + crossings := make([]linkCross, 0, len(edges)) for i := 1; i < len(levels); i++ { if err := ctx.Err(); err != nil { return nil, err @@ -151,10 +151,10 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo crossings = crossings[:0] for j, edge := range edges { if edge.a.Y < mid && mid < edge.b.Y { - crossings = append(crossings, pdfLinkCross{edge.x(mid), j}) + crossings = append(crossings, linkCross{edge.x(mid), j}) } } - slices.SortFunc(crossings, func(a, b pdfLinkCross) int { + slices.SortFunc(crossings, func(a, b linkCross) int { if a.x < b.x { return -1 } @@ -173,8 +173,8 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo if before != 0 && winding == 0 { a, b := edges[left], edges[cross.edge] quad := [4]pdfgo.Point{{X: a.x(low), Y: low}, {X: b.x(low), Y: low}, {X: b.x(high), Y: high}, {X: a.x(high), Y: high}} - region.Quads = appendPDFLinkQuad(region.Quads, quad) - if len(region.Quads) > pdfLinkQuadLimit { + region.Quads = appendLinkQuad(region.Quads, quad) + if len(region.Quads) > LinkQuadLimit { return nil, fmt.Errorf("PDF link region exceeds quadrilateral limit") } } @@ -183,13 +183,13 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo if len(region.Quads) == 0 { return region, nil } - return pdfLinkRegionBounds(region.Quads), nil + return linkRegionBounds(region.Quads), nil } -// convexPDFLink 直接保留单个凸四边形,三角形只分成三个四边形 +// convexLink 直接保留单个凸四边形,三角形只分成三个四边形 // 入参: path 已展开路径, pageH 页面高度 // 返回: [][4]pdfgo.Point 逆时针四边形,不适用时为空 -func convexPDFLink(path *canvas.Path, pageH float64) [][4]pdfgo.Point { +func convexLink(path *canvas.Path, pageH float64) [][4]pdfgo.Point { if path.HasSubpaths() || path.Len() > 6 { return nil } @@ -208,7 +208,7 @@ func convexPDFLink(path *canvas.Path, pageH float64) [][4]pdfgo.Point { direction := 0.0 for i, a := range vertices { b, c := vertices[(i+1)%len(vertices)], vertices[(i+2)%len(vertices)] - cross := pdfLinkTurn(a, b, c) + cross := linkTurn(a, b, c) if !finite(cross) || cross == 0 || direction != 0 && (direction < 0) != (cross < 0) { return nil } @@ -220,13 +220,13 @@ func convexPDFLink(path *canvas.Path, pageH float64) [][4]pdfgo.Point { if len(vertices) == 4 { return [][4]pdfgo.Point{storage} } - return appendPDFLinkTriangle(nil, [3]pdfgo.Point{vertices[0], vertices[1], vertices[2]}) + return appendLinkTriangle(nil, [3]pdfgo.Point{vertices[0], vertices[1], vertices[2]}) } -// pdfLinkRegionBounds 计算四边形共同的注解矩形,保证全部顶点落在矩形内 +// linkRegionBounds 计算四边形共同的注解矩形,保证全部顶点落在矩形内 // 入参: quads 链接四边形 // 返回: *pdfgo.LinkRegion 链接区域 -func pdfLinkRegionBounds(quads [][4]pdfgo.Point) *pdfgo.LinkRegion { +func linkRegionBounds(quads [][4]pdfgo.Point) *pdfgo.LinkRegion { region := &pdfgo.LinkRegion{Quads: quads, Rect: pdfgo.Rectangle{XMin: math.Inf(1), YMin: math.Inf(1), XMax: math.Inf(-1), YMax: math.Inf(-1)}} for _, quad := range quads { for _, p := range quad { @@ -237,16 +237,16 @@ func pdfLinkRegionBounds(quads [][4]pdfgo.Point) *pdfgo.LinkRegion { return region } -// flattenPDFLink 在取消和数量限制内展开动作曲线,复用标准椭圆弧转换 +// FlattenLink 在取消和数量限制内展开动作曲线,复用标准椭圆弧转换 // 入参: ctx 取消上下文, path 页面SVG路径 // 返回: *canvas.Path 闭合折线路径, error 精度或复杂度错误 -func flattenPDFLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error) { - geometry := geometryFromCanvasPath(path) +func FlattenLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error) { + geometry := linkGeometry(path) result := &canvas.Path{} - var current, start Point + var current, start basegeom.Point count := 0 - var flatten func([]Point, int) error - flatten = func(points []Point, depth int) error { + var flatten func([]basegeom.Point, int) error + flatten = func(points []basegeom.Point, depth int) error { if err := ctx.Err(); err != nil { return err } @@ -264,9 +264,9 @@ func flattenPDFLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error if !finite(length) || !finite(distance) { return fmt.Errorf("invalid PDF link curve") } - if distance <= pdfLinkTolerance/2 { + if distance <= linkTolerance/2 { count++ - if count > pdfLinkEdgeLimit { + if count > LinkEdgeLimit { return fmt.Errorf("PDF link region exceeds edge limit") } result.LineTo(last.X, -last.Y) @@ -275,7 +275,7 @@ func flattenPDFLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error if depth == 32 { return fmt.Errorf("PDF link curve tolerance cannot be met") } - var work, left, right [4]Point + var work, left, right [4]basegeom.Point copy(work[:], points) for n := len(points); n > 0; n-- { left[len(points)-n], right[n-1] = work[0], work[n-1] @@ -288,36 +288,36 @@ func flattenPDFLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error } return flatten(right[:len(points)], depth+1) } - for _, segment := range *geometry { + for _, segment := range geometry { if err := ctx.Err(); err != nil { return nil, err } switch segment.Verb { - case GeometryMove: + case basegeom.Move: result.MoveTo(segment.End.X, -segment.End.Y) current, start = segment.End, segment.End - case GeometryClose: + case basegeom.ClosePath: result.Close() current = start default: curves := basegeom.Path{segment} - if segment.Verb == GeometryArc { + if segment.Verb == basegeom.Arc { var err error - curves, err = basegeom.ArcCurves(current, segment, pdfLinkTolerance/2) + curves, err = basegeom.ArcCurves(current, segment, linkTolerance/2) if err != nil { return nil, err } - if len(curves) > pdfLinkEdgeLimit-count { + if len(curves) > LinkEdgeLimit-count { return nil, fmt.Errorf("PDF link region exceeds edge limit") } } for _, curve := range curves { - var storage [4]Point + var storage [4]basegeom.Point points := append(storage[:0], current) - if curve.Verb == GeometryQuad || curve.Verb == GeometryCubic { + if curve.Verb == basegeom.Quad || curve.Verb == basegeom.Cubic { points = append(points, curve.Control1) } - if curve.Verb == GeometryCubic { + if curve.Verb == basegeom.Cubic { points = append(points, curve.Control2) } points = append(points, curve.End) @@ -334,15 +334,15 @@ func flattenPDFLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error // x 计算非水平边在指定纵坐标处的横坐标 // 入参: y 扫描线纵坐标 // 返回: float64 交点横坐标 -func (e pdfLinkEdge) x(y float64) float64 { +func (e linkEdge) x(y float64) float64 { t := (y - e.a.Y) / (e.b.Y - e.a.Y) return e.a.X*(1-t) + e.b.X*t } -// appendPDFLinkQuad 收集梯形,尖端退化为三角形时细分为三个非退化四边形 +// appendLinkQuad 收集梯形,尖端退化为三角形时细分为三个非退化四边形 // 入参: quads 已有四边形, quad 扫描带梯形 // 返回: [][4]pdfgo.Point 四边形集合 -func appendPDFLinkQuad(quads [][4]pdfgo.Point, quad [4]pdfgo.Point) [][4]pdfgo.Point { +func appendLinkQuad(quads [][4]pdfgo.Point, quad [4]pdfgo.Point) [][4]pdfgo.Point { epsilon := 1e-12 for _, point := range quad { epsilon = max(epsilon, math.Abs(point.X)*1e-12, math.Abs(point.Y)*1e-12) @@ -365,27 +365,27 @@ func appendPDFLinkQuad(quads [][4]pdfgo.Point, quad [4]pdfgo.Point) [][4]pdfgo.P } else if quad[2] == quad[3] { triangle = [3]pdfgo.Point{quad[0], quad[1], quad[2]} } else { - return appendValidPDFLinkQuad(quads, quad) + return appendValidLinkQuad(quads, quad) } - return appendPDFLinkTriangle(quads, triangle) + return appendLinkTriangle(quads, triangle) } -// appendPDFLinkTriangle 连接三角形重心和边中点,生成覆盖相同区域的凸四边形 +// appendLinkTriangle 连接三角形重心和边中点,生成覆盖相同区域的凸四边形 // 入参: quads 已有四边形, triangle 逆时针三角形 // 返回: [][4]pdfgo.Point 四边形集合 -func appendPDFLinkTriangle(quads [][4]pdfgo.Point, triangle [3]pdfgo.Point) [][4]pdfgo.Point { +func appendLinkTriangle(quads [][4]pdfgo.Point, triangle [3]pdfgo.Point) [][4]pdfgo.Point { center := pdfgo.Point{X: (triangle[0].X + triangle[1].X + triangle[2].X) / 3, Y: (triangle[0].Y + triangle[1].Y + triangle[2].Y) / 3} for i, a := range triangle { b, c := triangle[(i+1)%3], triangle[(i+2)%3] - quads = appendValidPDFLinkQuad(quads, [4]pdfgo.Point{a, {X: (a.X + b.X) / 2, Y: (a.Y + b.Y) / 2}, center, {X: (a.X + c.X) / 2, Y: (a.Y + c.Y) / 2}}) + quads = appendValidLinkQuad(quads, [4]pdfgo.Point{a, {X: (a.X + b.X) / 2, Y: (a.Y + b.Y) / 2}, center, {X: (a.X + c.X) / 2, Y: (a.Y + c.Y) / 2}}) } return quads } -// pdfLinkTurn 计算相邻边的转向,消除近共线乘积相减产生的舍入噪声 +// linkTurn 计算相邻边的转向,消除近共线乘积相减产生的舍入噪声 // 入参: a 前顶点, b 当前顶点, c 后顶点 // 返回: float64 有向面积的两倍,数值退化时为零 -func pdfLinkTurn(a, b, c pdfgo.Point) float64 { +func linkTurn(a, b, c pdfgo.Point) float64 { u, v := (b.X-a.X)*(c.Y-b.Y), (b.Y-a.Y)*(c.X-b.X) cross := u - v if math.Abs(cross) <= (math.Abs(u)+math.Abs(v))*1e-12 { @@ -394,15 +394,51 @@ func pdfLinkTurn(a, b, c pdfgo.Point) float64 { return cross } -// appendValidPDFLinkQuad 仅保留有限且非退化的逆时针凸四边形 +// appendValidLinkQuad 仅保留有限且非退化的逆时针凸四边形 // 入参: quads 已有四边形, quad 候选四边形 // 返回: [][4]pdfgo.Point 四边形集合 -func appendValidPDFLinkQuad(quads [][4]pdfgo.Point, quad [4]pdfgo.Point) [][4]pdfgo.Point { +func appendValidLinkQuad(quads [][4]pdfgo.Point, quad [4]pdfgo.Point) [][4]pdfgo.Point { for i, a := range quad { - cross := pdfLinkTurn(a, quad[(i+1)%4], quad[(i+2)%4]) + cross := linkTurn(a, quad[(i+1)%4], quad[(i+2)%4]) if !finite(cross) || cross <= 0 { return quads } } return append(quads, quad) } + +// linkGeometry 转换画布坐标为基础几何路径,保留曲线和椭圆弧参数 +// 入参: path 画布路径 +// 返回: basegeom.Path 左上角坐标路径 +func linkGeometry(path *canvas.Path) basegeom.Path { + if path == nil { + return nil + } + result := make(basegeom.Path, 0, path.Len()) + scanner := path.Scanner() + for scanner.Scan() { + end := scanner.End() + segment := basegeom.Segment{End: basegeom.Point{X: end.X, Y: -end.Y}} + switch scanner.Cmd() { + case canvas.MoveToCmd: + segment.Verb = basegeom.Move + case canvas.LineToCmd: + segment.Verb = basegeom.Line + case canvas.QuadToCmd: + control := scanner.CP1() + segment.Verb, segment.Control1 = basegeom.Quad, basegeom.Point{X: control.X, Y: -control.Y} + case canvas.CubeToCmd: + c1, c2 := scanner.CP1(), scanner.CP2() + segment.Verb = basegeom.Cubic + segment.Control1, segment.Control2 = basegeom.Point{X: c1.X, Y: -c1.Y}, basegeom.Point{X: c2.X, Y: -c2.Y} + case canvas.ArcToCmd: + segment.Verb = basegeom.Arc + segment.RadiusX, segment.RadiusY, segment.Rotation, segment.Large, segment.Sweep = scanner.Arc() + segment.Rotation, segment.Sweep = -segment.Rotation, !segment.Sweep + case canvas.CloseCmd: + segment.Verb = basegeom.ClosePath + } + result = append(result, segment) + } + return result +} diff --git a/internal/codec/pdf/media.go b/internal/codec/pdf/media.go new file mode 100644 index 0000000..578029b --- /dev/null +++ b/internal/codec/pdf/media.go @@ -0,0 +1,162 @@ +// 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 pdf + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "math" + "net/url" + "path" + "strings" + + "github.com/xiaoqidun/pdfgo" +) + +// MediaFormat 获取媒体文件扩展名,URL参数和片段不参与格式判断 +// 入参: file PDF文件说明 +// 返回: string 媒体格式,未声明扩展名时为空 +func MediaFormat(file pdfgo.FileSpecification) string { + name := strings.ReplaceAll(file.Name, "\\", "/") + if file.FileSystem == "URL" { + if location, err := url.Parse(name); err == nil { + name = location.Path + } + } + return strings.TrimPrefix(path.Ext(name), ".") +} + +// SoundWAV 封装PDF采样音频,保持采样率、声道及数值,超过RIFF容量时使用RF64 +// 入参: ctx 取消上下文, sound 音频流及参数 +// 返回: []byte WAVE文件, error 不可表示的参数或损坏的采样数据 +func SoundWAV(ctx context.Context, sound pdfgo.Sound) ([]byte, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if sound.File != nil || sound.Compression != "" { + return nil, &pdfgo.UnsupportedError{Feature: "external or compressed sound samples"} + } + if sound.Stream == nil || sound.Channels <= 0 || math.IsNaN(sound.Rate) || math.IsInf(sound.Rate, 0) || sound.Rate <= 0 { + return nil, fmt.Errorf("invalid sound sample parameters") + } + if sound.Rate != math.Trunc(sound.Rate) || sound.Rate > math.MaxUint32 || sound.Channels > math.MaxUint16 || sound.Bits != 8 && sound.Bits != 16 && sound.Bits != 24 && sound.Bits != 32 { + return nil, &pdfgo.UnsupportedError{Feature: "sound parameters in WAVE"} + } + format := uint16(1) + if sound.Encoding == "muLaw" { + format = 7 + } else if sound.Encoding == "ALaw" { + format = 6 + } + if format != 1 && sound.Bits != 8 { + return nil, fmt.Errorf("invalid companded sound depth") + } + width := sound.Bits / 8 + align := uint64(sound.Channels) * uint64(width) + rate := uint64(sound.Rate) + if align > math.MaxUint16 || rate*align > math.MaxUint32 { + return nil, &pdfgo.UnsupportedError{Feature: "sound block alignment in WAVE"} + } + data, err := sound.Stream.DecodeContext(ctx) + if err != nil { + return nil, err + } + if uint64(len(data))%align != 0 { + return nil, fmt.Errorf("incomplete sound sample frame") + } + if format == 1 { + for offset := 0; offset < len(data); offset += width { + if offset%(1<<16) == 0 { + if err := ctx.Err(); err != nil { + return nil, err + } + } + if sound.Encoding == "Raw" && width > 1 || sound.Encoding == "Signed" && width == 1 { + data[offset] ^= 0x80 + } + for left, right := offset, offset+width-1; left < right; left, right = left+1, right-1 { + data[left], data[right] = data[right], data[left] + } + } + } + formatSize := uint32(16) + extra := uint64(0) + if format == 1 && (sound.Bits > 16 || sound.Channels > 2) { + format = 0xfffe + formatSize = 40 + extra = 36 + } else if format != 1 { + formatSize = 18 + extra = 14 + } + size := uint64(36) + extra + uint64(len(data)) + uint64(len(data)%2) + large := size > math.MaxUint32 + var out bytes.Buffer + put16 := func(v uint16) { _ = binary.Write(&out, binary.LittleEndian, v) } + put32 := func(v uint32) { _ = binary.Write(&out, binary.LittleEndian, v) } + put64 := func(v uint64) { _ = binary.Write(&out, binary.LittleEndian, v) } + if large { + out.WriteString("RF64") + put32(math.MaxUint32) + out.WriteString("WAVEds64") + put32(28) + put64(size + 36) + put64(uint64(len(data))) + put64(uint64(len(data)) / align) + put32(0) + } else { + out.WriteString("RIFF") + put32(uint32(size)) + out.WriteString("WAVE") + } + out.WriteString("fmt ") + put32(formatSize) + put16(format) + put16(uint16(sound.Channels)) + put32(uint32(rate)) + put32(uint32(rate * align)) + put16(uint16(align)) + put16(uint16(sound.Bits)) + if format != 1 { + if format == 0xfffe { + put16(22) + put16(uint16(sound.Bits)) + put32(0) + out.Write([]byte{1, 0, 0, 0, 0, 0, 0x10, 0, 0x80, 0, 0, 0xaa, 0, 0x38, 0x9b, 0x71}) + } else { + put16(0) + } + out.WriteString("fact") + put32(4) + if large { + put32(math.MaxUint32) + } else { + put32(uint32(uint64(len(data)) / align)) + } + } + out.WriteString("data") + if large { + put32(math.MaxUint32) + } else { + put32(uint32(len(data))) + } + out.Write(data) + if len(data)%2 != 0 { + out.WriteByte(0) + } + return out.Bytes(), ctx.Err() +} diff --git a/internal/codec/pdf/number.go b/internal/codec/pdf/number.go new file mode 100644 index 0000000..cf3bfe1 --- /dev/null +++ b/internal/codec/pdf/number.go @@ -0,0 +1,26 @@ +// 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 pdf + +import ( + "math" +) + +// finite 判断数值是否有限 +// 入参: value 待检查数值 +// 返回: bool 是否为有限数值 +func finite(value float64) bool { + return !math.IsNaN(value) && !math.IsInf(value, 0) +} diff --git a/internal/codec/pdf/page.go b/internal/codec/pdf/page.go new file mode 100644 index 0000000..f825656 --- /dev/null +++ b/internal/codec/pdf/page.go @@ -0,0 +1,40 @@ +// 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 pdf + +import ( + "github.com/xiaoqidun/pdfgo" +) + +// PageMatrix 将裁剪框、用户单位及页面旋转转换为OFD毫米坐标 +// 入参: page PDF页面 +// 返回: pdfgo.Matrix 坐标变换, float64 页面宽度, float64 页面高度 +func PageMatrix(page *pdfgo.Page) (pdfgo.Matrix, float64, float64) { + b := page.CropBox + scale := 25.4 / 72 * page.UserUnit + width, height := (b.XMax-b.XMin)*scale, (b.YMax-b.YMin)*scale + m := pdfgo.Matrix{scale, 0, 0, -scale, -b.XMin * scale, b.YMax * scale} + switch page.Rotation { + case 90: + m = pdfgo.Matrix{0, 1, -1, 0, height, 0}.Mul(m) + width, height = height, width + case 180: + m = pdfgo.Matrix{-1, 0, 0, -1, width, height}.Mul(m) + case 270: + m = pdfgo.Matrix{0, -1, 1, 0, 0, width}.Mul(m) + width, height = height, width + } + return m, width, height +} diff --git a/internal/codec/pdf/rendition.go b/internal/codec/pdf/rendition.go new file mode 100644 index 0000000..f0b8387 --- /dev/null +++ b/internal/codec/pdf/rendition.go @@ -0,0 +1,158 @@ +// 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 pdf + +import ( + "fmt" + + "github.com/xiaoqidun/pdfgo" +) + +// RenditionFloatingWindow 校验浮窗必需的像素尺寸,呈现偏好不写成视频固有尺寸 +// 入参: reader PDF阅读器, screen 媒体屏幕参数 +// 返回: bool 是否具备合法尺寸, error 引用错误 +func RenditionFloatingWindow(reader *pdfgo.Reader, screen pdfgo.Dictionary) (bool, error) { + value, err := reader.Resolve(screen["BE"]) + if err != nil { + return false, err + } + preferences, _ := value.(pdfgo.Dictionary) + value, err = reader.Resolve(preferences["F"]) + if err != nil { + return false, err + } + window, ok := value.(pdfgo.Dictionary) + if !ok { + return false, nil + } + value, err = reader.Resolve(window["D"]) + if err != nil { + return false, err + } + size, ok := value.(pdfgo.Array) + if !ok || len(size) != 2 { + return false, nil + } + for _, part := range size { + value, err = reader.Resolve(part) + if err != nil { + return false, err + } + number, ok := value.(pdfgo.Integer) + if !ok || number < 0 { + return false, nil + } + } + return true, nil +} + +// RenditionCriteria 检查媒体环境条件,未知尽力处理字段不影响候选可用性 +// 入参: reader PDF阅读器, rendition 媒体呈现或选择器 +// 返回: bool 是否可用, error 条件字典或引用错误 +func RenditionCriteria(reader *pdfgo.Reader, rendition *pdfgo.Rendition) (bool, error) { + for _, constraints := range []struct { + dict pdfgo.Dictionary + required bool + }{{rendition.MustHonor, true}, {rendition.BestEffort, false}} { + for key, object := range constraints.dict { + if key != "C" { + if constraints.required { + return false, nil + } + continue + } + value, err := reader.Resolve(object) + if err != nil { + return false, err + } + criteria, ok := value.(pdfgo.Dictionary) + if !ok { + return false, fmt.Errorf("invalid media criteria") + } + for name, object := range criteria { + value, err := reader.Resolve(object) + if err != nil { + return false, err + } + if name == "Type" && value == pdfgo.Name("MediaCriteria") { + continue + } + switch name { + case "Type", "A", "C", "O", "S", "R", "D", "Z", "V", "P", "L": + return false, nil + default: + if constraints.required { + return false, nil + } + } + } + } + } + return true, nil +} + +// RenditionParameters 合并可等价表达的播放参数,不支持的偏好不阻断播放 +// 入参: reader PDF阅读器, dict 参数字典, supported 字段及可表达值的检查 +// 返回: pdfgo.Dictionary 合并后的值, bool 是否可用, error 字典或引用错误 +func RenditionParameters(reader *pdfgo.Reader, dict pdfgo.Dictionary, supported map[pdfgo.Name]func(pdfgo.Object) bool) (pdfgo.Dictionary, bool, error) { + values := make(pdfgo.Dictionary) + for key := range dict { + if key != "Type" && key != "MH" && key != "BE" { + return nil, false, nil + } + } + value, err := reader.Resolve(dict["MH"]) + if err != nil { + return nil, false, err + } + mandatory, _ := value.(pdfgo.Dictionary) + for _, key := range []pdfgo.Name{"BE", "MH"} { + value, err := reader.Resolve(dict[key]) + if err != nil { + return nil, false, err + } + if value == nil { + continue + } + parameters, ok := value.(pdfgo.Dictionary) + if !ok { + return nil, false, fmt.Errorf("invalid media playback constraints") + } + for name, object := range parameters { + if _, overridden := mandatory[name]; key == "BE" && overridden { + continue + } + check := supported[name] + if check == nil { + if key == "MH" { + return nil, false, nil + } + continue + } + resolved, err := reader.Resolve(object) + if err != nil { + return nil, false, err + } + if !check(resolved) { + if key == "MH" { + return nil, false, nil + } + continue + } + values[name] = resolved + } + } + return values, true, nil +} diff --git a/ofdgo_pdf_shading_colorants.go b/internal/codec/pdf/shading.go similarity index 58% rename from ofdgo_pdf_shading_colorants.go rename to internal/codec/pdf/shading.go index 771a7ec..63cd84c 100644 --- a/ofdgo_pdf_shading_colorants.go +++ b/internal/codec/pdf/shading.go @@ -12,22 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package pdf -import "github.com/xiaoqidun/pdfgo" +import ( + "github.com/xiaoqidun/pdfgo" +) -// pdfShadingColorants 复用原始浓度缓冲及原生通道映射,不跨并发采样共享 -type pdfShadingColorants struct { +// ShadingColorants 复用原始浓度缓冲及原生通道映射,不跨并发采样共享 +type ShadingColorants struct { paint pdfgo.Paint - process *pdfgo.ColorantProcess + Process *pdfgo.ColorantProcess tints []float64 mesh *pdfgo.MeshSampler } -// pdfShadingColorantSpace 返回渐变的源色料定义,不使用备用颜色推断通道 +// ShadingColorantSpace 返回渐变的源色料定义,不使用备用颜色推断通道 // 入参: paint 渐变画刷 // 返回: *pdfgo.ColorantSpace 原始色料定义,普通渐变为nil -func pdfShadingColorantSpace(paint pdfgo.Paint) *pdfgo.ColorantSpace { +func ShadingColorantSpace(paint pdfgo.Paint) *pdfgo.ColorantSpace { switch { case paint.Axial != nil: return paint.Axial.ColorantSpace() @@ -41,26 +43,26 @@ func pdfShadingColorantSpace(paint pdfgo.Paint) *pdfgo.ColorantSpace { return nil } -// pdfPrepareShadingColorants 为当前组建立原生过程采样,备用色保留原路径 +// PrepareShadingColorants 为当前组建立原生过程采样,备用色保留原路径 // 入参: paint 渐变画刷, space 组空间, softMask 是否用于软蒙版 -// 返回: *pdfShadingColorants 独立采样状态,不可原生映射时为nil -func pdfPrepareShadingColorants(paint pdfgo.Paint, space *pdfgo.ColorSpace, softMask bool) *pdfShadingColorants { - colorants := pdfShadingColorantSpace(paint) - process := colorants.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, softMask) - if process == nil || process.Mask() == ([4]bool{}) { +// 返回: *ShadingColorants 独立采样状态,不可原生映射时为nil +func PrepareShadingColorants(paint pdfgo.Paint, space *pdfgo.ColorSpace, softMask bool) *ShadingColorants { + colorants := ShadingColorantSpace(paint) + Process := colorants.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, softMask) + if Process == nil || Process.Mask() == ([4]bool{}) { return nil } - s := &pdfShadingColorants{paint: paint, process: process, tints: make([]float64, len(colorants.Names))} + s := &ShadingColorants{paint: paint, Process: Process, tints: make([]float64, len(colorants.Names))} if paint.Mesh != nil { s.mesh = paint.Mesh.NewSampler() } return s } -// at 在归一化位置求值轴向或径向原始浓度 +// At 在归一化位置求值轴向或径向原始浓度 // 入参: position 归一化渐变参数 // 返回: [4]float64 原生过程分量, error 求值错误 -func (s *pdfShadingColorants) at(position float64) ([4]float64, error) { +func (s *ShadingColorants) At(position float64) ([4]float64, error) { var err error if s.paint.Axial != nil { err = s.paint.Axial.ColorantValuesAt(position, s.tints) @@ -70,25 +72,25 @@ func (s *pdfShadingColorants) at(position float64) ([4]float64, error) { if err != nil { return [4]float64{}, err } - return s.process.Convert(s.tints) + return s.Process.Convert(s.tints) } -// pointAt 在函数定义域内求值原始浓度 +// PointAt 在函数定义域内求值原始浓度 // 入参: point 函数坐标 // 返回: [4]float64 原生过程分量, error 求值错误 -func (s *pdfShadingColorants) pointAt(point pdfgo.Point) ([4]float64, error) { +func (s *ShadingColorants) PointAt(point pdfgo.Point) ([4]float64, error) { if err := s.paint.Function.ColorantValuesAt(point, s.tints); err != nil { return [4]float64{}, err } - return s.process.Convert(s.tints) + return s.Process.Convert(s.tints) } -// meshAt 插值网格原始浓度并复用函数结果 +// MeshAt 插值网格原始浓度并复用函数结果 // 入参: patch 网格序号, u 横向参数, v 纵向参数 // 返回: [4]float64 原生过程分量, error 求值错误 -func (s *pdfShadingColorants) meshAt(patch int, u, v float64) ([4]float64, error) { +func (s *ShadingColorants) MeshAt(patch int, u, v float64) ([4]float64, error) { if err := s.mesh.ColorantValuesAt(patch, u, v, s.tints); err != nil { return [4]float64{}, err } - return s.process.Convert(s.tints) + return s.Process.Convert(s.tints) } diff --git a/internal/codec/pdf/threed.go b/internal/codec/pdf/threed.go new file mode 100644 index 0000000..e3d6aa3 --- /dev/null +++ b/internal/codec/pdf/threed.go @@ -0,0 +1,270 @@ +// 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 pdf + +import ( + "context" + "errors" + "fmt" + "image" + "math" + + basethreed "github.com/xiaoqidun/ofdgo/internal/base/threed" + "github.com/xiaoqidun/pdfgo" +) + +// ThreeDRenderOptions 控制PDF三维视口的静态绘制,不改变注解激活状态 +// 缓存与实例上限为0时采用basethreed.U3DRenderOptions的默认值 +// ResolveTexture按原始URL提供外部纹理,空值不访问外部资源 +// Nodes按名称替换PDF视图中的节点状态,可指定动作轨道与采样时间,不改变原始注解 +// Time以秒指定模型动画的采样时刻,零值为初始时刻 +type ThreeDRenderOptions struct { + Width, Height int + MaxMemoryBytes int64 + MaxInstances int + ResolveTexture basethreed.U3DTextureResolver + Nodes []basethreed.U3DNodeState + Time float64 +} + +// RenderThreeD 按PDF相机、投影、背景与灯光绘制指定时刻的3DB区域,不执行脚本 +// 返回图像不含3DB以外的注解背景,未支持的视图效果返回明确错误 +// 每次从传入场景建立独立节点状态,不继承此前绘制的运行时状态 +// 入参: ctx 取消上下文, model 解码后的U3D模型, source PDF三维参数, options 输出与资源限制 +// 返回: *image.RGBA 不透明图像, error 参数、能力、预算或取消错误 +func RenderThreeD(ctx context.Context, model *basethreed.U3DModel, source pdfgo.ThreeD, options ThreeDRenderOptions) (*image.RGBA, error) { + if ctx == nil { + return nil, fmt.Errorf("invalid PDF 3D render context") + } + if err := ctx.Err(); err != nil { + return nil, err + } + if source.Format != "U3D" { + return nil, &pdfgo.UnsupportedError{Feature: "3D model format " + string(source.Format)} + } + if len(source.Measurements) != 0 { + return nil, &pdfgo.UnsupportedError{Feature: "3D view measurement rendering"} + } + presentation := source.Presentation + if presentation.Overlay != nil { + return nil, &pdfgo.UnsupportedError{Feature: "3D view overlay"} + } + if source.Background.RGB == nil { + return nil, &pdfgo.UnsupportedError{Feature: "3D background color space"} + } + box := source.ViewBox + width, height := box.XMax-box.XMin, box.YMax-box.YMin + if !finite(width) || !finite(height) || width <= 0 || height <= 0 { + return nil, fmt.Errorf("invalid PDF 3D viewport") + } + projection := pdfgo.ThreeDProjection{Subtype: "P", Clipping: "ANF", FieldOfView: 90, PerspectiveBinding: "W"} + if source.Projection != nil { + projection = *source.Projection + } + render := basethreed.U3DRenderOptions{Width: options.Width, Height: options.Height, MaxMemoryBytes: options.MaxMemoryBytes, MaxInstances: options.MaxInstances, Background: source.Background.RGB, ResolveTexture: options.ResolveTexture, Time: options.Time} + if err := threeDRenderMode(source, &render); err != nil { + return nil, err + } + if err := threeDNodeStates(ctx, presentation.Nodes, &render); err != nil { + return nil, err + } + if err := threeDSections(ctx, presentation.CrossSections, &render); err != nil { + return nil, err + } + r, err := basethreed.NewRenderer(ctx, model, render, options.Nodes) + if err != nil { + return nil, err + } + camera, view, err := threeDCamera(ctx, model, r, source.Camera) + if err != nil { + return nil, err + } + render.View = view + camera.ScaleX, camera.ScaleY, camera.Near = 1, 1, 1 + render.Camera = &camera + passes, err := r.SelectView(render) + if err != nil { + return nil, err + } + if err := r.PrepareSections(render.Sections); err != nil { + return nil, err + } + scale, err := projection.ScaleFactor(source.ViewBox, source.AnnotationBox) + if err != nil { + return nil, err + } + camera.Perspective = projection.Subtype == "P" + camera.ScaleX = scale * float64(options.Width) / width + camera.ScaleY = scale * float64(options.Height) / height + switch projection.Clipping { + case "XNF": + camera.Near = 0 + if projection.Near != nil { + camera.Near = *projection.Near + } + if !finite(camera.Near) || camera.Near < 0 || camera.Perspective && camera.Near == 0 { + return nil, fmt.Errorf("invalid PDF 3D near distance") + } + if projection.Far != nil { + camera.ClipFar = true + camera.Far = *projection.Far + if !finite(camera.Far) { + return nil, fmt.Errorf("invalid PDF 3D far distance") + } + if camera.Far < camera.Near { + passes, camera.Far = nil, 0 + camera.ClipFar = false + } + } + case "ANF": + minimum, maximum, found, err := r.DepthRange() + if err != nil { + return nil, err + } + if !found { + passes = nil + } + camera.Near = minimum + if camera.Perspective { + if maximum <= 0 { + passes = nil + } + camera.Near = max(1e-300, maximum*1e-9) + if minimum > 0 { + camera.Near = max(1e-300, minimum/2) + } + } + camera.Far = math.Nextafter(maximum, math.Inf(1)) + if !finite(camera.Far) || camera.Far <= camera.Near { + camera.Far = 0 + } + default: + return nil, fmt.Errorf("invalid PDF 3D clipping style") + } + if _, err := r.SelectView(render); err != nil { + return nil, err + } + if source.Lighting.Subtype != "Artwork" && source.Lighting.Subtype != "" { + if err := r.Reserve(uint64(len(source.Lighting.Lights)), 56); err != nil { + return nil, err + } + render.Lighting = &basethreed.U3DLighting{DiffuseOffset: source.Lighting.Ambient, Lights: make([]basethreed.U3DDirectionalLight, len(source.Lighting.Lights))} + for i, light := range source.Lighting.Lights { + if i%256 == 0 { + if err := ctx.Err(); err != nil { + return nil, err + } + } + direction := light.Direction + if light.CameraAttached { + direction = [3]float64{0, 0, 1} + } + render.Lighting.Lights[i] = basethreed.U3DDirectionalLight{Color: light.Color, Direction: direction, CameraRelative: light.CameraAttached} + } + } + return r.Render(render, passes) +} + +// threeDCamera 解析PDF相机来源并保留模型视图的绘制轮次 +// 入参: ctx 取消上下文, model 三维模型, renderer 场景实例, source 相机来源 +// 返回: basethreed.U3DCamera 相机变换, string 视图节点, error 路径或变换错误 +func threeDCamera(ctx context.Context, model *basethreed.U3DModel, renderer *basethreed.Renderer, source pdfgo.ThreeDCamera) (basethreed.U3DCamera, string, error) { + var camera basethreed.U3DCamera + selected := -1 + for i, node := range model.Nodes { + if i%256 == 0 { + if err := ctx.Err(); err != nil { + return camera, "", err + } + } + if node.Kind == "View" { + selected = i + break + } + } + if source.Source == "M" { + m := source.Matrix + camera.ToWorld = [16]float64{m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1} + } else { + if source.Source != "" && source.Source != "U3D" || source.Source == "U3D" && len(source.Path) == 0 { + return camera, "", fmt.Errorf("invalid PDF 3D camera source") + } + if source.Source == "U3D" { + selected = -1 + for i, node := range model.Nodes { + if i%256 == 0 { + if err := ctx.Err(); err != nil { + return camera, "", err + } + } + if node.Name == source.Path[len(source.Path)-1] { + selected = i + break + } + } + } + if selected < 0 || model.Nodes[selected].Kind != "View" { + return camera, "", fmt.Errorf("missing PDF 3D camera node") + } + var err error + camera.ToWorld, err = renderer.ViewCamera(selected, source.Path, source.Source == "U3D") + if errors.Is(err, basethreed.ErrAmbiguousCamera) { + return camera, "", &pdfgo.UnsupportedError{Feature: "ambiguous 3D camera instance"} + } + if errors.Is(err, basethreed.ErrCameraNotFound) { + return camera, "", fmt.Errorf("missing PDF 3D camera path") + } + if err != nil { + return camera, "", err + } + } + view := "" + if selected >= 0 { + view = model.Nodes[selected].Name + } + return camera, view, nil +} + +// threeDNodeStates 将PDF节点参数转为场景覆盖,转换缓存计入绘制预算 +// 入参: ctx 取消上下文, nodes 节点参数, options 绘制选项 +// 返回: error 预算或取消错误 +func threeDNodeStates(ctx context.Context, nodes []pdfgo.ThreeDNode, options *basethreed.U3DRenderOptions) error { + if len(nodes) == 0 { + return nil + } + limit := options.MaxMemoryBytes + if limit == 0 { + limit = basethreed.DefaultBytes + } + if limit <= 0 || uint64(len(nodes)) >= uint64(limit)/168 { + return fmt.Errorf("PDF 3D node memory exceeds limit") + } + options.MaxMemoryBytes = limit - int64(len(nodes))*168 + options.Nodes = make([]basethreed.U3DNodeState, len(nodes)) + for i, node := range nodes { + if i%256 == 0 { + if err := ctx.Err(); err != nil { + return err + } + } + state := basethreed.U3DNodeState{Name: node.Name, Opacity: node.Opacity, Visible: node.Visible} + if node.Matrix != nil { + m := node.Matrix + state.Matrix = &[16]float64{m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1} + } + options.Nodes[i] = state + } + return nil +} diff --git a/ofdgo_pdf_3d_mode.go b/internal/codec/pdf/threed_mode.go similarity index 63% rename from ofdgo_pdf_3d_mode.go rename to internal/codec/pdf/threed_mode.go index 4302748..60c4d9c 100644 --- a/ofdgo_pdf_3d_mode.go +++ b/internal/codec/pdf/threed_mode.go @@ -12,14 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package pdf -import "github.com/xiaoqidun/pdfgo" +import ( + basethreed "github.com/xiaoqidun/ofdgo/internal/base/threed" + "github.com/xiaoqidun/pdfgo" +) -// pdfThreeDRenderMode 将PDF标准绘制模式映射到通用几何绘制参数 +// threeDRenderMode 将PDF标准绘制模式映射到通用几何绘制参数 // 入参: source 三维参数, options 绘制选项 // 返回: error 未支持的模式错误 -func pdfThreeDRenderMode(source pdfgo.ThreeD, options *U3DRenderOptions) error { +func threeDRenderMode(source pdfgo.ThreeD, options *basethreed.U3DRenderOptions) error { mode := source.Presentation.RenderMode if mode == nil { return nil @@ -29,35 +32,35 @@ func pdfThreeDRenderMode(source pdfgo.ThreeD, options *U3DRenderOptions) error { case "Transparent": options.Opacity = &mode.Opacity case "SolidWireframe", "TransparentWireframe": - options.Mode, options.AuxiliaryColor = U3DRenderSolidWireframe, &mode.AuxiliaryColor + options.Mode, options.AuxiliaryColor = basethreed.U3DRenderSolidWireframe, &mode.AuxiliaryColor if mode.Subtype == "TransparentWireframe" { options.Opacity = &mode.Opacity } case "Wireframe": - options.Mode, options.AuxiliaryColor = U3DRenderWireframe, &mode.AuxiliaryColor + options.Mode, options.AuxiliaryColor = basethreed.U3DRenderWireframe, &mode.AuxiliaryColor case "ShadedWireframe": - options.Mode = U3DRenderShadedWireframe + options.Mode = basethreed.U3DRenderShadedWireframe case "HiddenWireframe": - options.Mode, options.AuxiliaryColor = U3DRenderHiddenWireframe, &mode.AuxiliaryColor + options.Mode, options.AuxiliaryColor = basethreed.U3DRenderHiddenWireframe, &mode.AuxiliaryColor case "Vertices": - options.Mode, options.AuxiliaryColor = U3DRenderVertices, &mode.AuxiliaryColor + options.Mode, options.AuxiliaryColor = basethreed.U3DRenderVertices, &mode.AuxiliaryColor case "ShadedVertices": - options.Mode = U3DRenderShadedVertices + options.Mode = basethreed.U3DRenderShadedVertices case "BoundingBox": - options.Mode, options.AuxiliaryColor = U3DRenderBoundingBox, &mode.AuxiliaryColor + options.Mode, options.AuxiliaryColor = basethreed.U3DRenderBoundingBox, &mode.AuxiliaryColor case "TransparentBoundingBox", "TransparentBoundingBoxOutline": - options.Mode = U3DRenderBoundingBoxFaces + options.Mode = basethreed.U3DRenderBoundingBoxFaces options.FaceColor, options.Opacity = mode.FaceColor, &mode.Opacity if mode.Subtype == "TransparentBoundingBoxOutline" { - options.Mode, options.AuxiliaryColor = U3DRenderBoundingBoxOutline, &mode.AuxiliaryColor + options.Mode, options.AuxiliaryColor = basethreed.U3DRenderBoundingBoxOutline, &mode.AuxiliaryColor } case "Illustration": - options.Mode = U3DRenderIllustration + options.Mode = basethreed.U3DRenderIllustration options.AuxiliaryColor, options.FaceColor, options.CreaseAngle = &mode.AuxiliaryColor, mode.FaceColor, &mode.CreaseAngle case "SolidOutline", "ShadedIllustration": - options.Mode = U3DRenderSolidOutline + options.Mode = basethreed.U3DRenderSolidOutline if mode.Subtype == "ShadedIllustration" { - options.Mode = U3DRenderShadedIllustration + options.Mode = basethreed.U3DRenderShadedIllustration } options.AuxiliaryColor, options.CreaseAngle = &mode.AuxiliaryColor, &mode.CreaseAngle default: diff --git a/ofdgo_pdf_3d_section.go b/internal/codec/pdf/threed_section.go similarity index 83% rename from ofdgo_pdf_3d_section.go rename to internal/codec/pdf/threed_section.go index 89e027d..2626476 100644 --- a/ofdgo_pdf_3d_section.go +++ b/internal/codec/pdf/threed_section.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package pdf import ( "context" @@ -23,10 +23,10 @@ import ( "github.com/xiaoqidun/pdfgo" ) -// pdfThreeDSections 按XYZ顺序构造剖切方向,中心保持为世界坐标固定点 +// threeDSections 按XYZ顺序构造剖切方向,中心保持为世界坐标固定点 // 入参: ctx 取消上下文, sections PDF剖切面, options 绘制选项 // 返回: error 方向、预算或取消错误 -func pdfThreeDSections(ctx context.Context, sections []pdfgo.ThreeDCrossSection, options *U3DRenderOptions) error { +func threeDSections(ctx context.Context, sections []pdfgo.ThreeDCrossSection, options *basethreed.U3DRenderOptions) error { if len(sections) == 0 { return nil } @@ -38,7 +38,7 @@ func pdfThreeDSections(ctx context.Context, sections []pdfgo.ThreeDCrossSection, return fmt.Errorf("PDF 3D section memory exceeds limit") } options.MaxMemoryBytes = limit - int64(len(sections))*136 - options.Sections = make([]U3DCrossSection, len(sections)) + options.Sections = make([]basethreed.U3DCrossSection, len(sections)) for i, source := range sections { if err := ctx.Err(); err != nil { return err @@ -46,7 +46,7 @@ func pdfThreeDSections(ctx context.Context, sections []pdfgo.ThreeDCrossSection, if source.Axis < 0 || source.Axis > 2 { return fmt.Errorf("invalid PDF 3D section axis") } - section := U3DCrossSection{Center: source.Center, Color: source.Color, Opacity: source.Opacity} + section := basethreed.U3DCrossSection{Center: source.Center, Color: source.Color, Opacity: source.Opacity} section.AxisU[(source.Axis+1)%3], section.AxisV[(source.Axis+2)%3] = 1, 1 for axis, angle := range source.Rotation { if axis == source.Axis { diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index e3afcbf..0d576f1 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -22,6 +22,7 @@ import ( "reflect" "strconv" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -204,7 +205,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI } } err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { - _, width, height := pdfPageMatrix(page) + _, width, height := codecpdf.PageMatrix(page) if _, err := editor.AddPage(width, height); err != nil { return err } @@ -239,7 +240,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI } } err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { - matrix, width, height := pdfPageMatrix(page) + matrix, width, height := codecpdf.PageMatrix(page) importer.matrix = matrix importer.page = index importer.rasterWarned = false @@ -345,27 +346,6 @@ func (p *pdfImporter) commitObjects() error { return nil } -// pdfPageMatrix 将裁剪框、用户单位及页面旋转转换为OFD毫米坐标 -// 入参: page PDF页面 -// 返回: pdfgo.Matrix 坐标变换, float64 页面宽度, float64 页面高度 -func pdfPageMatrix(page *pdfgo.Page) (pdfgo.Matrix, float64, float64) { - b := page.CropBox - scale := 25.4 / 72 * page.UserUnit - width, height := (b.XMax-b.XMin)*scale, (b.YMax-b.YMin)*scale - m := pdfgo.Matrix{scale, 0, 0, -scale, -b.XMin * scale, b.YMax * scale} - switch page.Rotation { - case 90: - m = pdfgo.Matrix{0, 1, -1, 0, height, 0}.Mul(m) - width, height = height, width - case 180: - m = pdfgo.Matrix{-1, 0, 0, -1, width, height}.Mul(m) - case 270: - m = pdfgo.Matrix{0, -1, 1, 0, 0, width}.Mul(m) - width, height = height, width - } - return m, width, height -} - // pdfNumbers 按OFD数值格式序列化坐标 // 入参: values 数值列表 // 返回: string 空格分隔的数值 diff --git a/ofdgo_pdf_3d.go b/ofdgo_pdf_3d.go index 0bb6055..28917a1 100644 --- a/ofdgo_pdf_3d.go +++ b/ofdgo_pdf_3d.go @@ -22,7 +22,7 @@ import ( "math" "strings" - basethreed "github.com/xiaoqidun/ofdgo/internal/base/threed" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -185,226 +185,9 @@ func (p *pdfImporter) threeDPainter(ctx context.Context, page *pdfgo.Page, sourc // 入参: ctx 取消上下文, model 解码后的U3D模型, source PDF三维参数, options 输出与资源限制 // 返回: *image.RGBA 不透明图像, error 参数、能力、预算或取消错误 func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD, options PDFThreeDRenderOptions) (*image.RGBA, error) { - if ctx == nil { - return nil, fmt.Errorf("invalid PDF 3D render context") - } - if err := ctx.Err(); err != nil { - return nil, err - } - if source.Format != "U3D" { - return nil, &pdfgo.UnsupportedError{Feature: "3D model format " + string(source.Format)} - } - if len(source.Measurements) != 0 { - return nil, &pdfgo.UnsupportedError{Feature: "3D view measurement rendering"} - } - presentation := source.Presentation - if presentation.Overlay != nil { - return nil, &pdfgo.UnsupportedError{Feature: "3D view overlay"} - } - if source.Background.RGB == nil { - return nil, &pdfgo.UnsupportedError{Feature: "3D background color space"} - } - box := source.ViewBox - width, height := box.XMax-box.XMin, box.YMax-box.YMin - if !finite(width) || !finite(height) || width <= 0 || height <= 0 { - return nil, fmt.Errorf("invalid PDF 3D viewport") - } - projection := pdfgo.ThreeDProjection{Subtype: "P", Clipping: "ANF", FieldOfView: 90, PerspectiveBinding: "W"} - if source.Projection != nil { - projection = *source.Projection - } - render := U3DRenderOptions{Width: options.Width, Height: options.Height, MaxMemoryBytes: options.MaxMemoryBytes, MaxInstances: options.MaxInstances, Background: source.Background.RGB, ResolveTexture: options.ResolveTexture, Time: options.Time} - if err := pdfThreeDRenderMode(source, &render); err != nil { - return nil, err - } - if err := pdfThreeDNodeStates(ctx, presentation.Nodes, &render); err != nil { - return nil, err - } - if err := pdfThreeDSections(ctx, presentation.CrossSections, &render); err != nil { - return nil, err - } - r, err := basethreed.NewRenderer(ctx, model, render, options.Nodes) - if err != nil { - return nil, err - } - camera, view, err := pdfThreeDCamera(ctx, model, r, source.Camera) - if err != nil { - return nil, err - } - render.View = view - camera.ScaleX, camera.ScaleY, camera.Near = 1, 1, 1 - render.Camera = &camera - passes, err := r.SelectView(render) - if err != nil { - return nil, err - } - if err := r.PrepareSections(render.Sections); err != nil { - return nil, err - } - scale, err := projection.ScaleFactor(source.ViewBox, source.AnnotationBox) - if err != nil { - return nil, err - } - camera.Perspective = projection.Subtype == "P" - camera.ScaleX = scale * float64(options.Width) / width - camera.ScaleY = scale * float64(options.Height) / height - switch projection.Clipping { - case "XNF": - camera.Near = 0 - if projection.Near != nil { - camera.Near = *projection.Near - } - if !finite(camera.Near) || camera.Near < 0 || camera.Perspective && camera.Near == 0 { - return nil, fmt.Errorf("invalid PDF 3D near distance") - } - if projection.Far != nil { - camera.ClipFar = true - camera.Far = *projection.Far - if !finite(camera.Far) { - return nil, fmt.Errorf("invalid PDF 3D far distance") - } - if camera.Far < camera.Near { - passes, camera.Far = nil, 0 - camera.ClipFar = false - } - } - case "ANF": - minimum, maximum, found, err := r.DepthRange() - if err != nil { - return nil, err - } - if !found { - passes = nil - } - camera.Near = minimum - if camera.Perspective { - if maximum <= 0 { - passes = nil - } - camera.Near = max(1e-300, maximum*1e-9) - if minimum > 0 { - camera.Near = max(1e-300, minimum/2) - } - } - camera.Far = math.Nextafter(maximum, math.Inf(1)) - if !finite(camera.Far) || camera.Far <= camera.Near { - camera.Far = 0 - } - default: - return nil, fmt.Errorf("invalid PDF 3D clipping style") - } - if _, err := r.SelectView(render); err != nil { - return nil, err - } - if source.Lighting.Subtype != "Artwork" && source.Lighting.Subtype != "" { - if err := r.Reserve(uint64(len(source.Lighting.Lights)), 56); err != nil { - return nil, err - } - render.Lighting = &U3DLighting{DiffuseOffset: source.Lighting.Ambient, Lights: make([]U3DDirectionalLight, len(source.Lighting.Lights))} - for i, light := range source.Lighting.Lights { - if i%256 == 0 { - if err := ctx.Err(); err != nil { - return nil, err - } - } - direction := light.Direction - if light.CameraAttached { - direction = [3]float64{0, 0, 1} - } - render.Lighting.Lights[i] = U3DDirectionalLight{Color: light.Color, Direction: direction, CameraRelative: light.CameraAttached} - } - } - return r.Render(render, passes) -} - -// pdfThreeDCamera 解析PDF相机来源并保留模型视图的绘制轮次 -// 入参: ctx 取消上下文, model 三维模型, renderer 场景实例, source 相机来源 -// 返回: U3DCamera 相机变换, string 视图节点, error 路径或变换错误 -func pdfThreeDCamera(ctx context.Context, model *U3DModel, renderer *basethreed.Renderer, source pdfgo.ThreeDCamera) (U3DCamera, string, error) { - var camera U3DCamera - selected := -1 - for i, node := range model.Nodes { - if i%256 == 0 { - if err := ctx.Err(); err != nil { - return camera, "", err - } - } - if node.Kind == "View" { - selected = i - break - } - } - if source.Source == "M" { - m := source.Matrix - camera.ToWorld = [16]float64{m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1} - } else { - if source.Source != "" && source.Source != "U3D" || source.Source == "U3D" && len(source.Path) == 0 { - return camera, "", fmt.Errorf("invalid PDF 3D camera source") - } - if source.Source == "U3D" { - selected = -1 - for i, node := range model.Nodes { - if i%256 == 0 { - if err := ctx.Err(); err != nil { - return camera, "", err - } - } - if node.Name == source.Path[len(source.Path)-1] { - selected = i - break - } - } - } - if selected < 0 || model.Nodes[selected].Kind != "View" { - return camera, "", fmt.Errorf("missing PDF 3D camera node") - } - var err error - camera.ToWorld, err = renderer.ViewCamera(selected, source.Path, source.Source == "U3D") - if errors.Is(err, basethreed.ErrAmbiguousCamera) { - return camera, "", &pdfgo.UnsupportedError{Feature: "ambiguous 3D camera instance"} - } - if errors.Is(err, basethreed.ErrCameraNotFound) { - return camera, "", fmt.Errorf("missing PDF 3D camera path") - } - if err != nil { - return camera, "", err - } - } - view := "" - if selected >= 0 { - view = model.Nodes[selected].Name - } - return camera, view, nil -} - -// pdfThreeDNodeStates 将PDF节点参数转为场景覆盖,转换缓存计入绘制预算 -// 入参: ctx 取消上下文, nodes 节点参数, options 绘制选项 -// 返回: error 预算或取消错误 -func pdfThreeDNodeStates(ctx context.Context, nodes []pdfgo.ThreeDNode, options *U3DRenderOptions) error { - if len(nodes) == 0 { - return nil - } - limit := options.MaxMemoryBytes - if limit == 0 { - limit = basethreed.DefaultBytes - } - if limit <= 0 || uint64(len(nodes)) >= uint64(limit)/168 { - return fmt.Errorf("PDF 3D node memory exceeds limit") - } - options.MaxMemoryBytes = limit - int64(len(nodes))*168 - options.Nodes = make([]U3DNodeState, len(nodes)) - for i, node := range nodes { - if i%256 == 0 { - if err := ctx.Err(); err != nil { - return err - } - } - state := U3DNodeState{Name: node.Name, Opacity: node.Opacity, Visible: node.Visible} - if node.Matrix != nil { - m := node.Matrix - state.Matrix = &[16]float64{m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1} - } - options.Nodes[i] = state - } - return nil + return codecpdf.RenderThreeD(ctx, model, source, codecpdf.ThreeDRenderOptions{ + Width: options.Width, Height: options.Height, + MaxMemoryBytes: options.MaxMemoryBytes, MaxInstances: options.MaxInstances, + ResolveTexture: options.ResolveTexture, Nodes: options.Nodes, Time: options.Time, + }) } diff --git a/ofdgo_pdf_clip.go b/ofdgo_pdf_clip.go index 060086a..c214029 100644 --- a/ofdgo_pdf_clip.go +++ b/ofdgo_pdf_clip.go @@ -15,11 +15,9 @@ package ofdgo import ( - "context" - "crypto/sha256" - "encoding/binary" "math" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -79,54 +77,11 @@ func pdfClipContainsBoundary(path pdfgo.Path, matrix pdfgo.Matrix, boundary Box) return left < right && top < bottom && boundary.X >= left && boundary.Y >= top && boundary.X+boundary.W <= right && boundary.Y+boundary.H <= bottom } -// pdfClipPathKey 按路径完整内容与变换生成键,不依赖可变切片的地址 -// 入参: ctx 取消上下文, path 裁剪路径, matrix 页面变换 -// 返回: [32]byte 内容摘要, error 取消错误 -func pdfClipPathKey(ctx context.Context, path pdfgo.Path, matrix pdfgo.Matrix) ([32]byte, error) { - hash := sha256.New() - var value [16]byte - for _, number := range matrix { - binary.LittleEndian.PutUint64(value[:8], math.Float64bits(number)) - hash.Write(value[:8]) - } - binary.LittleEndian.PutUint64(value[:8], uint64(len(path.Segments))) - value[8] = 0 - if path.EvenOdd { - value[8] = 1 - } - hash.Write(value[:9]) - for _, segment := range path.Segments { - if err := ctx.Err(); err != nil { - return [32]byte{}, err - } - binary.LittleEndian.PutUint64(value[:8], uint64(len(segment.Operator))) - binary.LittleEndian.PutUint64(value[8:], uint64(len(segment.Points))) - hash.Write(value[:]) - hash.Write([]byte(segment.Operator)) - for index, point := range segment.Points { - if index&255 == 0 { - if err := ctx.Err(); err != nil { - return [32]byte{}, err - } - } - binary.LittleEndian.PutUint64(value[:8], math.Float64bits(point.X)) - binary.LittleEndian.PutUint64(value[8:], math.Float64bits(point.Y)) - hash.Write(value[:]) - } - } - if err := ctx.Err(); err != nil { - return [32]byte{}, err - } - var key [32]byte - hash.Sum(key[:0]) - return key, nil -} - // clipPath 复用页面内重复路径编码,按估算字节预算淘汰且不缓存错误 // 入参: path 裁剪路径 // 返回: pdfClipPath 变换后的边界及编码, error 取消错误 func (p *pdfImporter) clipPath(path pdfgo.Path) (pdfClipPath, error) { - key, err := pdfClipPathKey(p.ctx, path, p.matrix) + key, err := codecpdf.ClipPathKey(p.ctx, path, p.matrix) if err != nil { return pdfClipPath{}, err } diff --git a/ofdgo_pdf_composite.go b/ofdgo_pdf_composite.go index eeeff6d..1e83fc9 100644 --- a/ofdgo_pdf_composite.go +++ b/ofdgo_pdf_composite.go @@ -22,6 +22,7 @@ import ( "runtime" basefont "github.com/xiaoqidun/ofdgo/internal/base/font" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -93,7 +94,7 @@ type pdfCompositeColorSampler struct { values map[float64][4]float64 position pdfgo.GradientPosition converter pdfgo.ColorConverter - colorants *pdfShadingColorants + colorants *codecpdf.ShadingColorants prepared bool converted bool } @@ -417,7 +418,7 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error, widg if externalNames == nil { externalNames = make(map[string]bool) } - name := pdfEmbeddedFontName(mark.Font.Name) + name := codecpdf.EmbeddedFontName(mark.Font.Name) if !externalNames[basefont.NormalizeName(name)] { for _, candidate := range fontExactCandidateNames(name) { externalNames[basefont.NormalizeName(candidate)] = true @@ -441,7 +442,7 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error, widg if err := p.halftone(mark.Style); err != nil { return err } - if len(mark.Glyphs) > 1 && (mark.Object != nil && !mark.Object.Knockout || mark.Style.AlphaIsShape || mark.Style.Fill.Alpha != 1 || mark.Style.Stroke.Alpha != 1 || mark.Style.SoftMask != nil || !pdfNormalBlend(mark.Style.BlendMode) || mark.Mode%4 == 1 || mark.Mode%4 == 2) { + if len(mark.Glyphs) > 1 && (mark.Object != nil && !mark.Object.Knockout || mark.Style.AlphaIsShape || mark.Style.Fill.Alpha != 1 || mark.Style.Stroke.Alpha != 1 || mark.Style.SoftMask != nil || !codecpdf.NormalBlend(mark.Style.BlendMode) || mark.Mode%4 == 1 || mark.Mode%4 == 2) { for i := range mark.Glyphs { glyph := mark glyph.Glyphs, glyph.Positions, glyph.Clip = mark.Glyphs[i:i+1], mark.Positions[i:i+1], nil @@ -514,7 +515,7 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error, widg return nil, err } for font := range embeddedFonts { - if !externalNames[basefont.NormalizeName(pdfEmbeddedFontName(font.Name))] { + if !externalNames[basefont.NormalizeName(codecpdf.EmbeddedFontName(font.Name))] { continue } if err := p.ctx.Err(); err != nil { @@ -640,7 +641,7 @@ func (p *pdfImporter) opaque(n pdfCompositeNode, space *pdfgo.ColorSpace) (bool, } if n.group != nil { g := n.group - if g.Knockout && n.textObject == nil || g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) { + if g.Knockout && n.textObject == nil || g.Alpha != 1 || g.SoftMask != nil || !codecpdf.NormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) { return false, nil } for _, child := range n.children { @@ -654,7 +655,7 @@ func (p *pdfImporter) opaque(n pdfCompositeNode, space *pdfgo.ColorSpace) (bool, if n.image != nil && style.FillOverprint { return false, nil } - if style.SoftMask != nil || !pdfNormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 { + if style.SoftMask != nil || !codecpdf.NormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 { return false, nil } if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) { @@ -675,7 +676,7 @@ func (p *pdfImporter) opaque(n pdfCompositeNode, space *pdfgo.ColorSpace) (bool, func (n pdfCompositeNode) direct() bool { if g := n.group; g != nil { single := n.textObject != nil && len(n.children) == 1 && (n.children[0].glyph != nil || n.children[0].text != nil && len(n.children[0].text.Glyphs) == 1) - if g.Knockout && !single || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.SRGBEquivalent() { + if g.Knockout && !single || !codecpdf.NormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.SRGBEquivalent() { return false } for _, child := range n.children { @@ -701,7 +702,7 @@ func (n pdfCompositeNode) direct() bool { if n.image != nil && s.FillOverprint { return false } - return pdfNormalBlend(s.BlendMode) && !(fill && s.FillOverprint && pdfOverprintNeedsSeparation(s.Fill) || stroke && s.StrokeOverprint && pdfOverprintNeedsSeparation(s.Stroke)) + return codecpdf.NormalBlend(s.BlendMode) && !(fill && s.FillOverprint && pdfOverprintNeedsSeparation(s.Fill) || stroke && s.StrokeOverprint && pdfOverprintNeedsSeparation(s.Stroke)) } // style 获取图元实际使用的图形状态 @@ -932,12 +933,12 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo } values, err = c.transfers[i].Apply(values[:], device, "") if err == nil { - rgb, err = pdfCompositeOutputColor(space, values) + rgb, err = codecpdf.OutputColor(space, values) } } else if c.transfers != nil { rgb, err = space.RGB(pixel.values[:space.Components()], "RelativeColorimetric") } else { - rgb, err = pdfCompositeOutputColor(space, pixel.values) + rgb, err = codecpdf.OutputColor(space, pixel.values) } if err != nil { return err @@ -965,21 +966,3 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo } return p.appendRasterGroup(output, box, 1) } - -// pdfCompositeOutputColor 将合成结果映射到OFD输出,与保留的设备四色对象使用相同显示换算 -// 入参: space 合成空间, values 非预乘分量 -// 返回: [3]float64 显示颜色, error 校准颜色转换错误 -func pdfCompositeOutputColor(space *pdfgo.ColorSpace, values [4]float64) ([3]float64, error) { - if space.Model == "DeviceCMYK" && !space.Calibrated() { - black := 1 - values[3] - return [3]float64{(1 - values[0]) * black, (1 - values[1]) * black, (1 - values[2]) * black}, nil - } - return space.RGB(values[:space.Components()], "RelativeColorimetric") -} - -// pdfNormalBlend 判断混合模式是否为普通覆盖 -// 入参: mode 混合模式 -// 返回: bool 是否普通覆盖 -func pdfNormalBlend(mode pdfgo.Name) bool { - return mode == "" || mode == "Normal" || mode == "Compatible" -} diff --git a/ofdgo_pdf_composite_draw.go b/ofdgo_pdf_composite_draw.go index 6c0e9af..756be70 100644 --- a/ofdgo_pdf_composite_draw.go +++ b/ofdgo_pdf_composite_draw.go @@ -21,6 +21,7 @@ import ( "math" baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -162,11 +163,11 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf } } } - var colorants *pdfShadingColorants + var colorants *codecpdf.ShadingColorants if node.image == nil || node.image.Image.ImageMask { - colorants = pdfPrepareShadingColorants(paint, space, c.softMask) + colorants = codecpdf.PrepareShadingColorants(paint, space, c.softMask) if colorants != nil && overprint { - processMask, processOverprint = colorants.process.Mask(), true + processMask, processOverprint = colorants.Process.Mask(), true } } step := 25.4 / c.importer.rasterDPI @@ -449,7 +450,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi } region := c.pixelBounds(b) blocked := c.transferBlocked - c.transferBlocked = blocked || g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) + c.transferBlocked = blocked || g.Alpha != 1 || g.SoftMask != nil || !codecpdf.NormalBlend(g.BlendMode) defer func() { c.transferBlocked = blocked }() if c.transfers != nil && node.textObject != nil && !c.transferBlocked { for _, child := range node.children { @@ -469,7 +470,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi } sameSpace := space.Equal(parent) sharedInitial := !g.Isolated && !g.Knockout && sameSpace - if sharedInitial && g.Alpha == 1 && g.SoftMask == nil && pdfNormalBlend(g.BlendMode) { + if sharedInitial && g.Alpha == 1 && g.SoftMask == nil && codecpdf.NormalBlend(g.BlendMode) { return c.draw(node.children, pixels, space) } initial := pixels @@ -1015,7 +1016,7 @@ func (s *pdfCompositeColorSampler) color(point pdfgo.Point) ([4]float64, bool, e var intent pdfgo.Name var err error if s.colorants != nil { - values, err = s.colorants.at(position) + values, err = s.colorants.At(position) } else if g != nil { values, err = g.ValuesAt(position) source, intent = g.Space, g.Intent diff --git a/ofdgo_pdf_composite_pattern.go b/ofdgo_pdf_composite_pattern.go index f411b09..d0885ac 100644 --- a/ofdgo_pdf_composite_pattern.go +++ b/ofdgo_pdf_composite_pattern.go @@ -18,6 +18,7 @@ import ( "fmt" "math" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -237,8 +238,8 @@ func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.Co if i == 0 && !fill || i == 1 && !stroke { continue } - if native := pdfPrepareShadingColorants(paint, space, false); native != nil { - if !native.process.AlternateEquivalent() || i == 0 && style.FillOverprint || i == 1 && style.StrokeOverprint { + if native := codecpdf.PrepareShadingColorants(paint, space, false); native != nil { + if !native.Process.AlternateEquivalent() || i == 0 && style.FillOverprint || i == 1 && style.StrokeOverprint { return false, nil } } @@ -287,7 +288,7 @@ func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.Co } } directGroup := false - if group := node.group; group != nil && !group.Knockout && group.Alpha == 1 && group.SoftMask == nil && pdfNormalBlend(group.BlendMode) && (group.ColorSpace == nil || group.ColorSpace.SRGBEquivalent()) { + if group := node.group; group != nil && !group.Knockout && group.Alpha == 1 && group.SoftMask == nil && codecpdf.NormalBlend(group.BlendMode) && (group.ColorSpace == nil || group.ColorSpace.SRGBEquivalent()) { directGroup = true } opaque, err := p.opaque(node, space) @@ -367,7 +368,7 @@ func (p *pdfImporter) processOverprint(nodes []pdfCompositeNode) (bool, error) { } for i, paint := range []pdfgo.Paint{style.Fill, style.Stroke} { if i == 0 && fill && style.FillOverprint || i == 1 && stroke && style.StrokeOverprint { - if native := pdfPrepareShadingColorants(paint, &pdfgo.ColorSpace{Model: "DeviceCMYK"}, false); native != nil { + if native := codecpdf.PrepareShadingColorants(paint, &pdfgo.ColorSpace{Model: "DeviceCMYK"}, false); native != nil { return true, nil } native, _, err := pdfCompositeNativePaint(paint, &pdfgo.ColorSpace{Model: "DeviceCMYK"}, style.RenderingIntent) diff --git a/ofdgo_pdf_destination.go b/ofdgo_pdf_destination.go index b506eea..68db474 100644 --- a/ofdgo_pdf_destination.go +++ b/ofdgo_pdf_destination.go @@ -18,6 +18,7 @@ import ( "fmt" "math" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -52,7 +53,7 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) { p.page = index defer func() { p.page = previous }() } - matrix, width, height := pdfPageMatrix(page) + matrix, width, height := codecpdf.PageMatrix(page) editor := NewEditor() editor.SetRenderBackends(p.editor.Backends()) editor.SetFontDirs(p.editor.fontDirs...) diff --git a/ofdgo_pdf_font.go b/ofdgo_pdf_font.go index 7fa0945..e35813a 100644 --- a/ofdgo_pdf_font.go +++ b/ofdgo_pdf_font.go @@ -15,15 +15,13 @@ package ofdgo import ( - "bytes" "context" "crypto/sha256" - "encoding/binary" "fmt" - "unicode/utf16" "unicode/utf8" basefont "github.com/xiaoqidun/ofdgo/internal/base/font" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -36,12 +34,6 @@ type pdfImportedFont struct { repairLimit uint16 } -// pdfFontRepair 保存字体封装修复范围,不混用字形度量与位置索引 -type pdfFontRepair struct { - metricsLimit uint16 - locations bool -} - // shapePDFText 使用导入字体整形注释文字,将毫米坐标约定转换为PDF文字空间方向 // 入参: ctx 取消上下文, source 源字体, text 单行原文, rightToLeft 段落方向 // 返回: []pdfgo.ShapedGlyph 千分之一字号的定位字形, error 字体、整形或取消错误 @@ -86,13 +78,13 @@ func (p *pdfImporter) shapePDFText(ctx context.Context, source *pdfgo.Font, text // 入参: source 外部字体, glyphs 当前文字字形 // 返回: *pdfImportedFont 可复用字体,无可靠匹配时为空 func (p *pdfImporter) embeddedTextFont(source *pdfgo.Font, glyphs []pdfgo.Glyph) *pdfImportedFont { - name := pdfEmbeddedFontName(source.Name) + name := codecpdf.EmbeddedFontName(source.Name) if name == "" || len(glyphs) == 0 { return nil } var matched *pdfImportedFont for _, imported := range p.fonts { - if pdfEmbeddedFontName(imported.resource.Font.FontName) != name { + if codecpdf.EmbeddedFontName(imported.resource.Font.FontName) != name { continue } if matched != nil && matched.checksum != imported.checksum { @@ -116,21 +108,6 @@ func (p *pdfImporter) embeddedTextFont(source *pdfgo.Font, glyphs []pdfgo.Glyph) return matched } -// pdfEmbeddedFontName 去除标准六位大写子集前缀,保留字体名称和样式后缀 -// 入参: name PDF或内嵌字体名称 -// 返回: string 原字体名称 -func pdfEmbeddedFontName(name string) string { - if len(name) <= 7 || name[6] != '+' { - return name - } - for i := range 6 { - if name[i] < 'A' || name[i] > 'Z' { - return name - } - } - return name[7:] -} - // importedFont 按原PDF字体复用包装和解析结果,失败结果不进入缓存 // 入参: source 源字体 // 返回: *pdfImportedFont 只读导入字体, error 字体程序或后端错误 @@ -148,11 +125,11 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error) type1 = &parsed result.type1Glyphs = parsed.GlyphIDs() } - program, repair, err := pdfFontProgram(source, type1) + program, repair, err := codecpdf.FontProgram(source, type1) if err != nil { return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err) } - if repair.locations && p.warning == nil { + if repair.Locations && p.warning == nil { return nil, fmt.Errorf("PDF font %s program: %w", source.Name, basefont.ErrTTInvalidLocations) } if p.editor.backends.FontResources == nil { @@ -172,8 +149,8 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error) if _, ok := result.metrics.(FontOutlines); !ok { return nil, fmt.Errorf("PDF font backend does not provide glyph outlines") } - result.checksum, result.repairLimit = sha256.Sum256(result.resource.Data), repair.metricsLimit - if repair.locations { + result.checksum, result.repairLimit = sha256.Sum256(result.resource.Data), repair.MetricsLimit + if repair.Locations { p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF font %s has invalid glyph locations; index rebuilt with original glyph IDs", source.Name)}) } if p.fonts == nil { @@ -187,7 +164,7 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error) // 入参: source 未嵌入的PDF字体 // 返回: *pdfImportedFont 可用的符号字体,无匹配时为空, error 字体或取消错误 func (p *pdfImporter) symbolFont(source *pdfgo.Font) (*pdfImportedFont, error) { - name := pdfEmbeddedFontName(source.Name) + name := codecpdf.EmbeddedFontName(source.Name) if source.Subtype != "Type1" || name != "Symbol" && name != "ZapfDingbats" { return nil, nil } @@ -231,294 +208,3 @@ func (p *pdfImporter) symbolFont(source *pdfgo.Font) (*pdfImportedFont, error) { p.symbolFonts[source] = result return result, nil } - -// pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号 -// 无效字符表按PDF已解码字形重建,不改变字符对应的字形编号 -// 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析 -// 返回: []byte 封装后的字体数据, pdfFontRepair 字体修复范围, error 错误信息 -func pdfFontProgram(source *pdfgo.Font, type1 *basefont.Type1Program) ([]byte, pdfFontRepair, error) { - var repair pdfFontRepair - program := source.Program - if source.ProgramType == "FontFile" { - if type1 == nil { - parsed, err := basefont.ParseType1Program(program) - if err != nil { - return nil, repair, err - } - type1 = &parsed - } - var err error - program, err = type1.ToCFF(pdfFontIdentity(source)) - if err != nil { - return nil, repair, err - } - } - bareCFF := len(program) >= 4 && program[0] == 1 && program[1] == 0 && program[2] >= 4 && program[3] >= 1 && program[3] <= 4 - if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" || bareCFF { - var err error - program, _, err = basefont.WrapCFFToOTF(program) - if err != nil { - return nil, repair, err - } - } - tables, err := basefont.FileTables(program, 0) - if err != nil { - return nil, repair, err - } - if len(tables["head"]) < 54 || len(tables["maxp"]) < 6 || len(tables["hhea"]) < 36 || len(tables["hmtx"]) == 0 { - return nil, repair, fmt.Errorf("embedded PDF font lacks required metrics") - } - count := binary.BigEndian.Uint16(tables["maxp"][4:6]) - if count == 0 { - return nil, repair, fmt.Errorf("embedded PDF font has no glyphs") - } - repair.locations, err = basefont.NormalizeTrueTypeLocations(tables) - if err != nil { - return nil, repair, err - } - changed := repair.locations - if head := tables["head"]; len(head) == 56 && binary.BigEndian.Uint32(head[:4]) == 0x00010000 && head[54] == 0 && head[55] == 0 { - tables["head"] = head[:54] - changed = true - } - if cff := tables["CFF "]; len(cff) != 0 { - sanitized, err := basefont.SanitizeCFF(cff) - if err != nil { - return nil, repair, err - } - normalized, err := basefont.NormalizeCFFCharstringsAt(sanitized, binary.BigEndian.Uint16(tables["head"][18:20])) - if err != nil { - return nil, repair, err - } - if !bytes.Equal(normalized, cff) { - tables["CFF "] = normalized - changed = true - } - } - metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36])) - if metrics == 0 || metrics > int(count) { - return nil, repair, fmt.Errorf("invalid embedded PDF font metric count") - } - metricLength := 4*metrics + 2*(int(count)-metrics) - if len(tables["hmtx"]) < metricLength { - if len(tables["hmtx"]) < 4*metrics || len(tables["hmtx"])%2 != 0 { - return nil, repair, fmt.Errorf("incomplete embedded PDF font metrics: have %d, need %d", len(tables["hmtx"]), metricLength) - } - limit := metrics + (len(tables["hmtx"])-4*metrics)/2 - missing, err := pdfMissingLeftBearings(tables, limit, int(count)) - if err != nil { - return nil, repair, err - } - tables["hmtx"] = append(bytes.Clone(tables["hmtx"]), missing...) - repair.metricsLimit = uint16(limit) - changed = true - } - if len(tables["hmtx"]) > metricLength { - tables["hmtx"] = tables["hmtx"][:metricLength] - changed = true - } - invalidCmap := false - if source.Subtype == "TrueType" || source.Subtype == "Type0" { - mapping := basefont.ParseCmapMappings(tables["cmap"]) - for char, glyph := range mapping { - if glyph >= count { - delete(mapping, char) - invalidCmap = true - } - } - if invalidCmap && source.Subtype == "TrueType" { - tables["cmap"] = basefont.BuildCmapTable(count, mapping) - changed = true - } - } - if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && invalidCmap || (source.Subtype == "Type0" || source.Subtype == "TrueType") && pdfCmapNeedsRebuild(tables["cmap"]) { - mapping, err := source.GlyphMap() - if err != nil { - return nil, repair, err - } - for _, glyph := range mapping { - if glyph >= count { - return nil, repair, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph, count) - } - } - basefont.AddPackedGlyphMapping(mapping, count) - tables["cmap"] = basefont.BuildCmapTable(count, mapping) - changed = true - } - if len(basefont.NamesFromTable(tables["name"])) == 0 { - fontName := pdfFontIdentity(source) - name := utf16.Encode([]rune(fontName)) - if len(name) > 32767 { - return nil, repair, fmt.Errorf("PDF font name exceeds name table capacity") - } - data := make([]byte, 42+len(name)*2) - binary.BigEndian.PutUint16(data[2:], 3) - binary.BigEndian.PutUint16(data[4:], 42) - for n, id := range []uint16{1, 4, 6} { - record := data[6+n*12:] - binary.BigEndian.PutUint16(record, 3) - binary.BigEndian.PutUint16(record[2:], 1) - binary.BigEndian.PutUint16(record[4:], 0x409) - binary.BigEndian.PutUint16(record[6:], id) - binary.BigEndian.PutUint16(record[8:], uint16(len(name)*2)) - } - for n, v := range name { - binary.BigEndian.PutUint16(data[42+n*2:], v) - } - tables["name"] = data - changed = true - } - if len(tables["post"]) == 0 { - tables["post"] = basefont.BuildPostTable() - changed = true - } - if len(tables["OS/2"]) == 0 { - hhea := tables["hhea"] - tables["OS/2"] = basefont.BuildOS2TableWithMetrics(int16(binary.BigEndian.Uint16(hhea[4:6])), int16(binary.BigEndian.Uint16(hhea[6:8]))) - changed = true - } - for _, tag := range []string{"cvt ", "fpgm", "prep"} { - if data, ok := tables[tag]; ok && len(data) == 0 { - delete(tables, tag) - changed = true - } - } - if !changed && !pdfSFNTMissingPadding(program) { - return program, repair, nil - } - result, err := basefont.SerializeOTF(tables) - return result, repair, err -} - -// pdfFontIdentity 返回字体名称或由原始程序生成的稳定封装标识 -// 入参: source PDF字体 -// 返回: string 字体标识 -func pdfFontIdentity(source *pdfgo.Font) string { - if source.Name != "" { - return source.Name - } - checksum := sha256.Sum256(source.Program) - return fmt.Sprintf("PDF-%x", checksum[:28]) -} - -// pdfCmapNeedsRebuild 检查字体封装字符表是否存在结构错误或交叠区段 -// 入参: data cmap表数据 -// 返回: bool 是否需要按PDF显式字形编号重建 -func pdfCmapNeedsRebuild(data []byte) bool { - if len(data) < 4 || binary.BigEndian.Uint16(data) != 0 { - return true - } - count := int(binary.BigEndian.Uint16(data[2:])) - if count == 0 || count > (len(data)-4)/8 { - return true - } - for index := range count { - offset := uint64(binary.BigEndian.Uint32(data[8+8*index:])) - if offset < uint64(4+8*count) || offset > uint64(len(data)) || uint64(len(data))-offset < 2 { - return true - } - sub := data[int(offset):] - if binary.BigEndian.Uint16(sub) == 6 { - if len(sub) < 10 { - return true - } - length := int(binary.BigEndian.Uint16(sub[2:])) - first, glyphs := int(binary.BigEndian.Uint16(sub[6:])), int(binary.BigEndian.Uint16(sub[8:])) - if length%2 != 0 || length > len(sub) || 10+2*glyphs > length || first+glyphs > 65536 { - return true - } - continue - } - if binary.BigEndian.Uint16(sub) != 4 { - continue - } - if len(sub) < 16 { - return true - } - length := int(binary.BigEndian.Uint16(sub[2:])) - segmentBytes := int(binary.BigEndian.Uint16(sub[6:])) - segments := segmentBytes / 2 - if segmentBytes == 0 || segmentBytes%2 != 0 || length%2 != 0 || length > len(sub) || 16+8*segments > length { - return true - } - glyphStart := 16 + 8*segments - previous := -1 - for i := 0; i < segments; i++ { - end := int(binary.BigEndian.Uint16(sub[14+2*i:])) - start := int(binary.BigEndian.Uint16(sub[16+2*segments+2*i:])) - if end < start || start <= previous || i == segments-1 && (start != 65535 || end != 65535) { - return true - } - previous = end - position := 16 + 6*segments + 2*i - offset := int(binary.BigEndian.Uint16(sub[position:])) - if offset != 0 && (offset%2 != 0 || position+offset < glyphStart || position+offset+2*(end-start+1) > length) { - return true - } - } - } - return false -} - -// pdfSFNTMissingPadding 判断字体表目录是否引用了未写入的末尾对齐字节 -// 入参: program OpenType字体数据 -// 返回: bool 是否需要重新封装 -func pdfSFNTMissingPadding(program []byte) bool { - if len(program) < 12 { - return true - } - count := int(binary.BigEndian.Uint16(program[4:6])) - if count > (len(program)-12)/16 { - return true - } - for index := range count { - record := program[12+16*index:] - offset := int(binary.BigEndian.Uint32(record[8:12])) - length := int(binary.BigEndian.Uint32(record[12:16])) - if offset > len(program) || length > len(program)-offset || (4-length%4)%4 > len(program)-offset-length { - return true - } - } - return false -} - -// pdfMissingLeftBearings 从轮廓边界恢复缺失的尾部左侧边距 -// 入参: tables 字体表, start 首个缺失字形, count 字形总数 -// 返回: []byte 补齐的hmtx数据, error 无法读取轮廓 -func pdfMissingLeftBearings(tables map[string][]byte, start, count int) ([]byte, error) { - head, loca, glyf := tables["head"], tables["loca"], tables["glyf"] - if len(head) < 54 || len(glyf) == 0 { - return nil, fmt.Errorf("embedded PDF font lacks glyph outlines for missing metrics") - } - format := int(int16(binary.BigEndian.Uint16(head[50:52]))) - entrySize := 2 - if format == 1 { - entrySize = 4 - } else if format != 0 { - return nil, fmt.Errorf("invalid embedded PDF font location format") - } - if len(loca) < (count+1)*entrySize { - return nil, fmt.Errorf("incomplete embedded PDF font glyph locations") - } - location := func(index int) int { - if format == 0 { - return int(binary.BigEndian.Uint16(loca[index*2:])) * 2 - } - return int(binary.BigEndian.Uint32(loca[index*4:])) - } - result := make([]byte, 2*(count-start)) - for index := start; index < count; index++ { - from, to := location(index), location(index+1) - if from > to || to > len(glyf) { - return nil, fmt.Errorf("invalid embedded PDF font glyph location") - } - if from == to { - continue - } - if to-from < 10 { - return nil, fmt.Errorf("incomplete embedded PDF font glyph outline") - } - copy(result[(index-start)*2:], glyf[from+2:from+4]) - } - return result, nil -} diff --git a/ofdgo_pdf_geometry.go b/ofdgo_pdf_geometry.go index b6b0841..d9c0c12 100644 --- a/ofdgo_pdf_geometry.go +++ b/ofdgo_pdf_geometry.go @@ -19,57 +19,20 @@ import ( "crypto/sha256" "encoding/binary" "fmt" - basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "image/color" "math" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) -// pdfGeometryPath 直接转换PDF路径坐标,不经文本编码或曲线展平 +// pdfGeometryPath 转换PDF路径并衔接OFD几何类型 // 入参: ctx 取消上下文, source PDF路径, matrix 坐标变换 // 返回: GeometryPath 独立几何路径, error 指令、坐标或取消错误 func pdfGeometryPath(ctx context.Context, source pdfgo.Path, matrix pdfgo.Matrix) (GeometryPath, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - path := make(GeometryPath, len(source.Segments)) - for i, segment := range source.Segments { - if err := ctx.Err(); err != nil { - return nil, err - } - var count int - switch segment.Operator { - case "M": - path[i].Verb, count = GeometryMove, 1 - case "L": - path[i].Verb, count = GeometryLine, 1 - case "B": - path[i].Verb, count = GeometryCubic, 3 - case "C": - path[i].Verb = GeometryClose - default: - return nil, fmt.Errorf("invalid PDF path operator %q", segment.Operator) - } - if len(segment.Points) != count { - return nil, fmt.Errorf("invalid PDF path operands for %s", segment.Operator) - } - var points [3]Point - for j, point := range segment.Points { - point = matrix.Apply(point) - points[j] = Point{X: point.X, Y: point.Y} - } - if count != 0 { - path[i].End = points[count-1] - } - if count == 3 { - path[i].Control1, path[i].Control2 = points[0], points[1] - } - } - if err := path.validate(); err != nil { - return nil, err - } - return path, ctx.Err() + path, err := codecpdf.GeometryPath(ctx, source, matrix) + return GeometryPath(path), err } // compileCoverage 直接编译无文字裁剪的填充覆盖,其他语义交给配置的页面编译器 @@ -200,7 +163,7 @@ func (p *pdfImporter) coverageClip(paths []pdfgo.Path, geometry GeometryBackend) binary.LittleEndian.PutUint64(size[8:], math.Float64bits(p.pageHeight)) hash.Write(size[:]) for _, path := range paths { - key, err := pdfClipPathKey(p.ctx, path, p.matrix) + key, err := codecpdf.ClipPathKey(p.ctx, path, p.matrix) if err != nil { return nil, err } diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index 69a596b..25ab7d7 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -22,6 +22,7 @@ import ( "math" "reflect" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -634,7 +635,7 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) { return &pdfgo.UnsupportedError{Feature: "image mask graphic"} } visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { - if mark.Alpha != 1 || mark.SoftMask != nil || !pdfNormalBlend(mark.BlendMode) || mark.ColorSpace != nil && !mark.ColorSpace.Equal(mask.ColorSpace) { + if mark.Alpha != 1 || mark.SoftMask != nil || !codecpdf.NormalBlend(mark.BlendMode) || mark.ColorSpace != nil && !mark.ColorSpace.Equal(mask.ColorSpace) { return &pdfgo.UnsupportedError{Feature: "transparent mask group"} } return walk(visitor) @@ -644,7 +645,7 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) { return err } paint := mark.Style.Fill - if !mark.Fill || mark.Stroke || paint.Alpha != 1 || paint.CMYK != nil || paint.Axial != nil || paint.Radial != nil || paint.Function != nil || paint.Mesh != nil || paint.Tiling != nil || paint.Shading != nil || paint.Space != nil && paint.Space.Calibrated() || paint.RGB[0] != paint.RGB[1] || paint.RGB[1] != paint.RGB[2] || mark.Style.SoftMask != nil || !pdfNormalBlend(mark.Style.BlendMode) { + if !mark.Fill || mark.Stroke || paint.Alpha != 1 || paint.CMYK != nil || paint.Axial != nil || paint.Radial != nil || paint.Function != nil || paint.Mesh != nil || paint.Tiling != nil || paint.Shading != nil || paint.Space != nil && paint.Space.Calibrated() || paint.RGB[0] != paint.RGB[1] || paint.RGB[1] != paint.RGB[2] || mark.Style.SoftMask != nil || !codecpdf.NormalBlend(mark.Style.BlendMode) { return &pdfgo.UnsupportedError{Feature: "nonbinary mask graphic"} } visible, err := opacity(paint.RGB[0]) diff --git a/ofdgo_pdf_image_cache.go b/ofdgo_pdf_image_cache.go index fccb72a..a7ebbfd 100644 --- a/ofdgo_pdf_image_cache.go +++ b/ofdgo_pdf_image_cache.go @@ -15,12 +15,10 @@ package ofdgo import ( - "bytes" - "crypto/sha256" "image" "image/color" - "reflect" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -39,85 +37,20 @@ type pdfImageContent struct { id string } -// pdfImageValueBudget 限制图像描述的检查深度和保留字节数 -type pdfImageValueBudget struct { - nodes int - bytes int -} - // imageContentKey 对无间接依赖的小图像生成内容键,超大图像仍沿用流缓存 // 入参: source 图像描述, key 流缓存键 // 返回: pdfImageContentKey 内容键, bool 是否可复用, error 取消错误 func (p *pdfImporter) imageContentKey(source *pdfgo.Image, key pdfImageKey) (pdfImageContentKey, bool, error) { - if source == nil || source.Stream == nil || len(source.Stream.Data) > 1<<20 { - return pdfImageContentKey{}, false, nil + digest, reusable, err := codecpdf.ImageDigest(p.ctx, source) + if !reusable { + return pdfImageContentKey{}, reusable, err } - budget := pdfImageValueBudget{nodes: 256, bytes: 65536} - for _, value := range []pdfgo.Object{source.Stream.Dictionary, source.ColorSpace, source.Decode, source.Mask, source.SoftMask} { - if !pdfImageDirectValue(value, &budget) { - return pdfImageContentKey{}, false, p.ctx.Err() - } - } - hash := sha256.New() - data := source.Stream.Data - for len(data) != 0 { - if err := p.ctx.Err(); err != nil { - return pdfImageContentKey{}, false, err - } - n := min(len(data), 65536) - hash.Write(data[:n]) - data = data[n:] - } - result := pdfImageContentKey{intent: key.intent, process: key.process, tint: key.tint, size: key.size} - hash.Sum(result.data[:0]) - return result, true, p.ctx.Err() -} - -// pdfImageDirectValue 检查图像描述是否完全独立,拒绝间接引用和嵌套流 -// 入参: value 描述值, budget 描述检查预算 -// 返回: bool 是否不依赖阅读器资源 -func pdfImageDirectValue(value pdfgo.Object, budget *pdfImageValueBudget) bool { - if budget.nodes <= 0 || budget.bytes < 64 { - return false - } - budget.nodes-- - budget.bytes -= 64 - switch v := value.(type) { - case nil, pdfgo.Boolean, pdfgo.Integer, pdfgo.Real: - return true - case pdfgo.Name: - budget.bytes -= len(v) - return budget.bytes >= 0 - case pdfgo.String: - budget.bytes -= len(v) - return budget.bytes >= 0 - case pdfgo.Array: - for _, child := range v { - if !pdfImageDirectValue(child, budget) { - return false - } - } - return true - case pdfgo.Dictionary: - for key, child := range v { - budget.bytes -= len(key) - if !pdfImageDirectValue(child, budget) { - return false - } - } - return true - } - return false + return pdfImageContentKey{data: digest, intent: key.intent, process: key.process, tint: key.tint, size: key.size}, true, err } // matches 核对完整编码与有效图像属性,不以摘要相同代替内容相同 // 入参: source 待复用图像 // 返回: bool 是否可使用同一图像资源 func (c pdfImageContent) matches(source *pdfgo.Image) bool { - s := c.source - return s.Width == source.Width && s.Height == source.Height && s.BitsPerComponent == source.BitsPerComponent && - s.ImageMask == source.ImageMask && s.Interpolate == source.Interpolate && - bytes.Equal(s.Stream.Data, source.Stream.Data) && reflect.DeepEqual(s.Stream.Dictionary, source.Stream.Dictionary) && - reflect.DeepEqual(s.ColorSpace, source.ColorSpace) && reflect.DeepEqual(s.Decode, source.Decode) && - reflect.DeepEqual(s.Mask, source.Mask) && reflect.DeepEqual(s.SoftMask, source.SoftMask) + return codecpdf.SameImage(c.source, source) } diff --git a/ofdgo_pdf_image_native.go b/ofdgo_pdf_image_native.go index 12abd95..a121b1b 100644 --- a/ofdgo_pdf_image_native.go +++ b/ofdgo_pdf_image_native.go @@ -17,18 +17,11 @@ package ofdgo import ( "fmt" "image" - "image/color" - "math" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) -// pdfNativeImage 按源像素输出原生过程色,不分配完整显示颜色缓冲 -type pdfNativeImage struct { - source *pdfgo.ImageProcess - space *pdfgo.ColorSpace -} - // imageProcessModel 选择与页面原生过程色一致的图像输出,已等价的备用色沿用现有编码路径 // 入参: source 源图像 // 返回: pdfgo.Name 原生输出空间,无需重映射时为空, error 色料解析错误 @@ -61,7 +54,7 @@ func (p *pdfImporter) imageProcess(source *pdfgo.Image, model pdfgo.Name) (image if process == nil { return nil, fmt.Errorf("invalid native PDF image process space %s", model) } - return &pdfNativeImage{source: process, space: space}, nil + return &codecpdf.NativeImage{Source: process, Space: space}, nil } // imageProcess 复用同组原生采样,仅在需要备用颜色时保留完整分量 @@ -89,28 +82,3 @@ func (c *pdfCompositeCache) imageProcess(importer *pdfImporter, source *pdfgo.Im c.imageProcesses[key] = process return process, nil } - -// ColorModel 保留16位原生分量与源透明度 -// 返回: color.Model 非预乘颜色模型 -func (s *pdfNativeImage) ColorModel() color.Model { return color.NRGBA64Model } - -// Bounds 返回源图像的完整采样边界 -// 返回: image.Rectangle 原始像素边界 -func (s *pdfNativeImage) Bounds() image.Rectangle { return s.source.Bounds() } - -// At 获取按OFD输出映射的原生颜色 -// 入参: x 横坐标, y 纵坐标 -// 返回: color.Color 非预乘颜色 -func (s *pdfNativeImage) At(x, y int) color.Color { return s.NRGBA64At(x, y) } - -// NRGBA64At 按原始精度转换设备过程色,不进行页面采样或备用变换 -// 入参: x 横坐标, y 纵坐标 -// 返回: color.NRGBA64 非预乘颜色 -func (s *pdfNativeImage) NRGBA64At(x, y int) color.NRGBA64 { - if !image.Pt(x, y).In(s.Bounds()) { - return color.NRGBA64{} - } - values, alpha := s.source.ValuesAt(x, y) - rgb, _ := pdfCompositeOutputColor(s.space, values) - return color.NRGBA64{R: uint16(math.Round(rgb[0] * 65535)), G: uint16(math.Round(rgb[1] * 65535)), B: uint16(math.Round(rgb[2] * 65535)), A: uint16(math.Round(alpha * 65535))} -} diff --git a/ofdgo_pdf_link.go b/ofdgo_pdf_link.go index 133359e..740eb6b 100644 --- a/ofdgo_pdf_link.go +++ b/ofdgo_pdf_link.go @@ -20,6 +20,7 @@ import ( "fmt" "maps" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -497,7 +498,7 @@ func (p *pdfImporter) linkAction(annotation pdfgo.Annotation, strict bool, curre } return nil, fmt.Errorf("PDF destination page missing") } - matrix, _, _ := pdfPageMatrix(targetPage) + matrix, _, _ := codecpdf.PageMatrix(targetPage) if destination.Mode == "FitBH" || destination.Mode == "FitBV" { return nil, &pdfgo.UnsupportedError{Feature: "destination mode " + string(destination.Mode)} } diff --git a/ofdgo_pdf_media.go b/ofdgo_pdf_media.go index f9efe3d..80d5635 100644 --- a/ofdgo_pdf_media.go +++ b/ofdgo_pdf_media.go @@ -15,17 +15,15 @@ package ofdgo import ( - "bytes" "context" - "encoding/binary" "errors" "fmt" "math" - "net/url" "path" "reflect" "strings" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -134,9 +132,9 @@ func (p *pdfImporter) soundResource(ctx context.Context, sound pdfgo.Sound) (str if err != nil || !available { return "", err } - format = pdfMediaFormat(*sound.File) + format = codecpdf.MediaFormat(*sound.File) } else { - data, err = pdfSoundWAV(ctx, sound) + data, err = codecpdf.SoundWAV(ctx, sound) } if err != nil { return "", err @@ -220,7 +218,7 @@ func (p *pdfImporter) interactiveAnnotation(ctx context.Context, page *pdfgo.Pag if config.Subtype == "Sound" { kind = "Audio" } - id, err := p.editor.AddMedia(kind, pdfMediaFormat(file), data) + id, err := p.editor.AddMedia(kind, codecpdf.MediaFormat(file), data) if err != nil { return err } @@ -315,137 +313,3 @@ func (p *pdfImporter) mediaData(ctx context.Context, file pdfgo.FileSpecificatio } return data, true, nil } - -// pdfMediaFormat 获取媒体文件扩展名,URL参数和片段不参与格式判断 -// 入参: file PDF文件说明 -// 返回: string 媒体格式,未声明扩展名时为空 -func pdfMediaFormat(file pdfgo.FileSpecification) string { - name := strings.ReplaceAll(file.Name, "\\", "/") - if file.FileSystem == "URL" { - if location, err := url.Parse(name); err == nil { - name = location.Path - } - } - return strings.TrimPrefix(path.Ext(name), ".") -} - -// pdfSoundWAV 封装PDF采样音频,保持采样率、声道及数值,超过RIFF容量时使用RF64 -// 入参: ctx 取消上下文, sound 音频流及参数 -// 返回: []byte WAVE文件, error 不可表示的参数或损坏的采样数据 -func pdfSoundWAV(ctx context.Context, sound pdfgo.Sound) ([]byte, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - if sound.File != nil || sound.Compression != "" { - return nil, &pdfgo.UnsupportedError{Feature: "external or compressed sound samples"} - } - if sound.Stream == nil || sound.Channels <= 0 || math.IsNaN(sound.Rate) || math.IsInf(sound.Rate, 0) || sound.Rate <= 0 { - return nil, fmt.Errorf("invalid sound sample parameters") - } - if sound.Rate != math.Trunc(sound.Rate) || sound.Rate > math.MaxUint32 || sound.Channels > math.MaxUint16 || sound.Bits != 8 && sound.Bits != 16 && sound.Bits != 24 && sound.Bits != 32 { - return nil, &pdfgo.UnsupportedError{Feature: "sound parameters in WAVE"} - } - format := uint16(1) - if sound.Encoding == "muLaw" { - format = 7 - } else if sound.Encoding == "ALaw" { - format = 6 - } - if format != 1 && sound.Bits != 8 { - return nil, fmt.Errorf("invalid companded sound depth") - } - width := sound.Bits / 8 - align := uint64(sound.Channels) * uint64(width) - rate := uint64(sound.Rate) - if align > math.MaxUint16 || rate*align > math.MaxUint32 { - return nil, &pdfgo.UnsupportedError{Feature: "sound block alignment in WAVE"} - } - data, err := sound.Stream.DecodeContext(ctx) - if err != nil { - return nil, err - } - if uint64(len(data))%align != 0 { - return nil, fmt.Errorf("incomplete sound sample frame") - } - if format == 1 { - for offset := 0; offset < len(data); offset += width { - if offset%(1<<16) == 0 { - if err := ctx.Err(); err != nil { - return nil, err - } - } - if sound.Encoding == "Raw" && width > 1 || sound.Encoding == "Signed" && width == 1 { - data[offset] ^= 0x80 - } - for left, right := offset, offset+width-1; left < right; left, right = left+1, right-1 { - data[left], data[right] = data[right], data[left] - } - } - } - formatSize := uint32(16) - extra := uint64(0) - if format == 1 && (sound.Bits > 16 || sound.Channels > 2) { - format = 0xfffe - formatSize = 40 - extra = 36 - } else if format != 1 { - formatSize = 18 - extra = 14 - } - size := uint64(36) + extra + uint64(len(data)) + uint64(len(data)%2) - large := size > math.MaxUint32 - var out bytes.Buffer - put16 := func(v uint16) { _ = binary.Write(&out, binary.LittleEndian, v) } - put32 := func(v uint32) { _ = binary.Write(&out, binary.LittleEndian, v) } - put64 := func(v uint64) { _ = binary.Write(&out, binary.LittleEndian, v) } - if large { - out.WriteString("RF64") - put32(math.MaxUint32) - out.WriteString("WAVEds64") - put32(28) - put64(size + 36) - put64(uint64(len(data))) - put64(uint64(len(data)) / align) - put32(0) - } else { - out.WriteString("RIFF") - put32(uint32(size)) - out.WriteString("WAVE") - } - out.WriteString("fmt ") - put32(formatSize) - put16(format) - put16(uint16(sound.Channels)) - put32(uint32(rate)) - put32(uint32(rate * align)) - put16(uint16(align)) - put16(uint16(sound.Bits)) - if format != 1 { - if format == 0xfffe { - put16(22) - put16(uint16(sound.Bits)) - put32(0) - out.Write([]byte{1, 0, 0, 0, 0, 0, 0x10, 0, 0x80, 0, 0, 0xaa, 0, 0x38, 0x9b, 0x71}) - } else { - put16(0) - } - out.WriteString("fact") - put32(4) - if large { - put32(math.MaxUint32) - } else { - put32(uint32(uint64(len(data)) / align)) - } - } - out.WriteString("data") - if large { - put32(math.MaxUint32) - } else { - put32(uint32(len(data))) - } - out.Write(data) - if len(data)%2 != 0 { - out.WriteByte(0) - } - return out.Bytes(), ctx.Err() -} diff --git a/ofdgo_pdf_mesh.go b/ofdgo_pdf_mesh.go index 89a77a4..871680d 100644 --- a/ofdgo_pdf_mesh.go +++ b/ofdgo_pdf_mesh.go @@ -21,6 +21,7 @@ import ( "slices" "sync" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -380,7 +381,7 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace, } sampleRows := func(start, end int) error { colors := make(map[[4]float64][4]float64) - colorants := pdfPrepareShadingColorants(pdfgo.Paint{Mesh: mesh}, space, c.softMask) + colorants := codecpdf.PrepareShadingColorants(pdfgo.Paint{Mesh: mesh}, space, c.softMask) var sampler *pdfgo.MeshSampler if colorants == nil { sampler = mesh.NewSampler() @@ -404,7 +405,7 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace, if sample.patch != 0 { var err error if colorants != nil { - values, err = colorants.meshAt(sample.patch-1, sample.u, sample.v) + values, err = colorants.MeshAt(sample.patch-1, sample.u, sample.v) } else { values, err = sampler.ValuesAt(sample.patch-1, sample.u, sample.v) } diff --git a/ofdgo_pdf_movie.go b/ofdgo_pdf_movie.go index 2530bd4..c393317 100644 --- a/ofdgo_pdf_movie.go +++ b/ofdgo_pdf_movie.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -26,7 +27,7 @@ import ( // 入参: ctx 取消上下文, file 文件说明, attachment 是否作为附件保存 // 返回: string 媒体或附件标识,缺少外部资源时为空, error 读取、注册或取消错误 func (p *pdfImporter) movieResource(ctx context.Context, file pdfgo.FileSpecification, attachment bool) (string, error) { - return p.mediaResource(ctx, file, "Video", pdfMediaFormat(file), attachment) + return p.mediaResource(ctx, file, "Video", codecpdf.MediaFormat(file), attachment) } // movieAnnotation 保留视频外观及可表示的播放动作,无法等价转换时按策略处理 diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index 59fae44..3feb9ed 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -27,7 +27,7 @@ import ( "strconv" "unicode/utf8" - baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -46,46 +46,6 @@ type pdfImageResource struct { id string } -// pdfStencilImage 按当前填充色读取模板图像,不分配整幅彩色样本 -type pdfStencilImage struct { - source image.Image - fill color.NRGBA64 -} - -// ColorModel 保留填充色和模板的有效采样精度 -// 返回: color.Model 非预乘颜色模型 -func (s *pdfStencilImage) ColorModel() color.Model { - model := s.source.ColorModel() - if s.fill.R%257 == 0 && s.fill.G%257 == 0 && s.fill.B%257 == 0 && (model == color.GrayModel || model == color.NRGBAModel || model == color.RGBAModel) { - return color.NRGBAModel - } - return color.NRGBA64Model -} - -// Bounds 返回模板图像的采样边界 -// 返回: image.Rectangle 图像边界 -func (s *pdfStencilImage) Bounds() image.Rectangle { return s.source.Bounds() } - -// At 以模板灰度计算填充覆盖率,不重复变换颜色 -// 入参: x 横向坐标, y 纵向坐标 -// 返回: color.Color 非预乘颜色 -func (s *pdfStencilImage) At(x, y int) color.Color { - return s.NRGBA64At(x, y) -} - -// NRGBA64At 直接读取模板覆盖率,保留填充色的有效精度 -// 入参: x 横向坐标, y 纵向坐标 -// 返回: color.NRGBA64 非预乘颜色 -func (s *pdfStencilImage) NRGBA64At(x, y int) color.NRGBA64 { - if !image.Pt(x, y).In(s.Bounds()) { - return color.NRGBA64{} - } - gray := baseimage.NRGBA64At(s.source, x, y) - pixel := s.fill - pixel.A = uint16(65535 - gray.R) - return pixel -} - // image 转换图像样本,不将整页或其他对象栅格化 // 有损编码复用已解码像素,候选容量超限时沿用原编码路径 // 入参: mark PDF图像绘制信息 @@ -203,7 +163,7 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error { } } if source.ImageMask { - decoded = &pdfStencilImage{source: decoded, fill: key.tint} + decoded = &codecpdf.StencilImage{Source: decoded, Fill: key.tint} } data := jbig2Original if len(data) == 0 { diff --git a/ofdgo_pdf_rendition.go b/ofdgo_pdf_rendition.go index c9aa892..4cec0ab 100644 --- a/ofdgo_pdf_rendition.go +++ b/ofdgo_pdf_rendition.go @@ -22,6 +22,7 @@ import ( "mime" "strings" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -106,7 +107,7 @@ func (p *pdfImporter) renditionLinkActions(object pdfgo.Object, source pdfgo.Ann return nil, err } if kind == "Video" { - parameters, _, err := p.renditionParameters(selected.Screen, map[pdfgo.Name]func(pdfgo.Object) bool{ + parameters, _, err := codecpdf.RenditionParameters(p.reader, selected.Screen, map[pdfgo.Name]func(pdfgo.Object) bool{ "W": func(value pdfgo.Object) bool { return value == pdfgo.Integer(0) || value == pdfgo.Integer(3) }, }) if err != nil { @@ -155,7 +156,7 @@ func (p *pdfImporter) renditionLinkActions(object pdfgo.Object, source pdfgo.Ann // 入参: selected 已选择的原生呈现, kind 媒体类型, playback 播放目标 // 返回: string 资源标识,不可用时为空, error 文件或资源错误 func (p *pdfImporter) renditionResource(selected *pdfgo.Rendition, kind string, playback pdfgo.RenditionPlayback) (string, error) { - format := pdfMediaFormat(*selected.Clip.File) + format := codecpdf.MediaFormat(*selected.Clip.File) if format == "" { mediaType, _, _ := mime.ParseMediaType(selected.Clip.ContentType) format = strings.TrimPrefix(strings.SplitN(mediaType, "/", 2)[1], "x-") @@ -183,7 +184,7 @@ func (p *pdfImporter) renditionActionLoss(reason string, strict bool) error { // 返回: Action 播放动作, string 媒体类别, bool 是否可用, error 参数错误 func (p *pdfImporter) nativeRendition(rendition *pdfgo.Rendition) (Action, string, bool, error) { result := Action{Event: "CLICK"} - viable, err := p.renditionCriteria(rendition) + viable, err := codecpdf.RenditionCriteria(p.reader, rendition) if err != nil || !viable { return result, "", false, err } @@ -217,7 +218,7 @@ func (p *pdfImporter) nativeRendition(rendition *pdfgo.Rendition) (Action, strin return result, "", false, nil } audio := strings.HasPrefix(mediaType, "audio/") - play, viable, err := p.renditionParameters(rendition.Play, map[pdfgo.Name]func(pdfgo.Object) bool{ + play, viable, err := codecpdf.RenditionParameters(p.reader, rendition.Play, map[pdfgo.Name]func(pdfgo.Object) bool{ "V": func(value pdfgo.Object) bool { number, ok := value.(pdfgo.Integer) return ok && number >= 0 && number <= 100 && (audio || number == 100) @@ -231,14 +232,14 @@ func (p *pdfImporter) nativeRendition(rendition *pdfgo.Rendition) (Action, strin if err != nil || !viable { return result, "", false, err } - screen, viable, err := p.renditionParameters(rendition.Screen, map[pdfgo.Name]func(pdfgo.Object) bool{ + screen, viable, err := codecpdf.RenditionParameters(p.reader, rendition.Screen, map[pdfgo.Name]func(pdfgo.Object) bool{ "W": func(value pdfgo.Object) bool { return !audio && value == pdfgo.Integer(0) || value == pdfgo.Integer(3) }, }) if err != nil || !viable { return result, "", false, err } if screen["W"] == pdfgo.Integer(0) { - valid, err := p.renditionFloatingWindow(rendition.Screen) + valid, err := codecpdf.RenditionFloatingWindow(p.reader, rendition.Screen) if err != nil || !valid { return result, "", false, err } @@ -258,140 +259,3 @@ func (p *pdfImporter) nativeRendition(rendition *pdfgo.Rendition) (Action, strin result.Movie = &Movie{Operator: "Play"} return result, "Video", true, nil } - -// renditionFloatingWindow 校验浮窗必需的像素尺寸,呈现偏好不写成视频固有尺寸 -// 入参: screen 媒体屏幕参数 -// 返回: bool 是否具备合法尺寸, error 引用错误 -func (p *pdfImporter) renditionFloatingWindow(screen pdfgo.Dictionary) (bool, error) { - value, err := p.reader.Resolve(screen["BE"]) - if err != nil { - return false, err - } - preferences, _ := value.(pdfgo.Dictionary) - value, err = p.reader.Resolve(preferences["F"]) - if err != nil { - return false, err - } - window, ok := value.(pdfgo.Dictionary) - if !ok { - return false, nil - } - value, err = p.reader.Resolve(window["D"]) - if err != nil { - return false, err - } - size, ok := value.(pdfgo.Array) - if !ok || len(size) != 2 { - return false, nil - } - for _, part := range size { - value, err = p.reader.Resolve(part) - if err != nil { - return false, err - } - number, ok := value.(pdfgo.Integer) - if !ok || number < 0 { - return false, nil - } - } - return true, nil -} - -// renditionCriteria 检查媒体环境条件,未知尽力处理字段不影响候选可用性 -// 入参: rendition 媒体呈现或选择器 -// 返回: bool 是否可用, error 条件字典或引用错误 -func (p *pdfImporter) renditionCriteria(rendition *pdfgo.Rendition) (bool, error) { - for _, constraints := range []struct { - dict pdfgo.Dictionary - required bool - }{{rendition.MustHonor, true}, {rendition.BestEffort, false}} { - for key, object := range constraints.dict { - if key != "C" { - if constraints.required { - return false, nil - } - continue - } - value, err := p.reader.Resolve(object) - if err != nil { - return false, err - } - criteria, ok := value.(pdfgo.Dictionary) - if !ok { - return false, fmt.Errorf("invalid media criteria") - } - for name, object := range criteria { - value, err := p.reader.Resolve(object) - if err != nil { - return false, err - } - if name == "Type" && value == pdfgo.Name("MediaCriteria") { - continue - } - switch name { - case "Type", "A", "C", "O", "S", "R", "D", "Z", "V", "P", "L": - return false, nil - default: - if constraints.required { - return false, nil - } - } - } - } - } - return true, nil -} - -// renditionParameters 合并可等价表达的播放参数,不支持的偏好不阻断播放 -// 入参: dict 参数字典, supported 字段及可表达值的检查 -// 返回: pdfgo.Dictionary 合并后的值, bool 是否可用, error 字典或引用错误 -func (p *pdfImporter) renditionParameters(dict pdfgo.Dictionary, supported map[pdfgo.Name]func(pdfgo.Object) bool) (pdfgo.Dictionary, bool, error) { - values := make(pdfgo.Dictionary) - for key := range dict { - if key != "Type" && key != "MH" && key != "BE" { - return nil, false, nil - } - } - value, err := p.reader.Resolve(dict["MH"]) - if err != nil { - return nil, false, err - } - mandatory, _ := value.(pdfgo.Dictionary) - for _, key := range []pdfgo.Name{"BE", "MH"} { - value, err := p.reader.Resolve(dict[key]) - if err != nil { - return nil, false, err - } - if value == nil { - continue - } - parameters, ok := value.(pdfgo.Dictionary) - if !ok { - return nil, false, fmt.Errorf("invalid media playback constraints") - } - for name, object := range parameters { - if _, overridden := mandatory[name]; key == "BE" && overridden { - continue - } - check := supported[name] - if check == nil { - if key == "MH" { - return nil, false, nil - } - continue - } - resolved, err := p.reader.Resolve(object) - if err != nil { - return nil, false, err - } - if !check(resolved) { - if key == "MH" { - return nil, false, nil - } - continue - } - values[name] = resolved - } - } - return values, true, nil -} diff --git a/ofdgo_pdf_shading.go b/ofdgo_pdf_shading.go index 9753e03..aa1eb26 100644 --- a/ofdgo_pdf_shading.go +++ b/ofdgo_pdf_shading.go @@ -16,10 +16,11 @@ package ofdgo import ( "fmt" - basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "image" "math" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -593,7 +594,7 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod if err != nil { return nil, err } - colorants := pdfPrepareShadingColorants(paint, space, c.softMask) + colorants := codecpdf.PrepareShadingColorants(paint, space, c.softMask) step := 25.4 / c.importer.rasterDPI pixels := make([]pdfShadingPixel, c.width*c.height) for y := range c.height { @@ -634,7 +635,7 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod visible = domainClipped || local.X >= g.Domain.XMin && local.X <= g.Domain.XMax && local.Y >= g.Domain.YMin && local.Y <= g.Domain.YMax if visible { if colorants != nil { - values, err = colorants.pointAt(local) + values, err = colorants.PointAt(local) } else { values, err = g.ValuesAt(local) if err == nil { @@ -647,7 +648,7 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod parameter, visible = position.PositionAt(point) if visible { if colorants != nil { - values, err = colorants.at(parameter) + values, err = colorants.At(parameter) } else if paint.Axial != nil { values, err = paint.Axial.ValuesAt(parameter) } else { diff --git a/ofdgo_pdf_text_path.go b/ofdgo_pdf_text_path.go index aa1ae04..375f482 100644 --- a/ofdgo_pdf_text_path.go +++ b/ofdgo_pdf_text_path.go @@ -15,39 +15,14 @@ package ofdgo import ( - "fmt" - + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) -// pdfGlyphPath 将字形轮廓变换到PDF页面坐标,二次曲线精确转换为三次曲线 +// pdfGlyphPath 将OFD字形几何交给PDF路径适配器转换 // 入参: outline 字形轮廓, matrix 字形到页面的变换 // 返回: pdfgo.Path 页面路径, error 非字体轮廓指令 func pdfGlyphPath(outline GeometryPath, matrix pdfgo.Matrix) (pdfgo.Path, error) { - path := pdfgo.Path{Segments: make([]pdfgo.Segment, 0, len(outline))} - var current, start Point - point := func(p Point) pdfgo.Point { return matrix.Apply(pdfgo.Point{X: p.X, Y: p.Y}) } - for _, segment := range outline { - switch segment.Verb { - case GeometryMove: - start = segment.End - path.Segments = append(path.Segments, pdfgo.Segment{Operator: "M", Points: []pdfgo.Point{point(segment.End)}}) - case GeometryLine: - path.Segments = append(path.Segments, pdfgo.Segment{Operator: "L", Points: []pdfgo.Point{point(segment.End)}}) - case GeometryQuad: - c1 := Point{X: current.X + (segment.Control1.X-current.X)*2/3, Y: current.Y + (segment.Control1.Y-current.Y)*2/3} - c2 := Point{X: segment.End.X + (segment.Control1.X-segment.End.X)*2/3, Y: segment.End.Y + (segment.Control1.Y-segment.End.Y)*2/3} - path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(c1), point(c2), point(segment.End)}}) - case GeometryCubic: - path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(segment.Control1), point(segment.Control2), point(segment.End)}}) - case GeometryClose: - path.Segments = append(path.Segments, pdfgo.Segment{Operator: "C"}) - current = start - continue - default: - return pdfgo.Path{}, fmt.Errorf("unsupported PDF glyph geometry verb %d", segment.Verb) - } - current = segment.End - } - return path, nil + return codecpdf.GlyphPath(basegeom.Path(outline), matrix) } diff --git a/ofdgo_pdf_text_replacement.go b/ofdgo_pdf_text_replacement.go index 4ccf896..d3fa51a 100644 --- a/ofdgo_pdf_text_replacement.go +++ b/ofdgo_pdf_text_replacement.go @@ -20,6 +20,7 @@ import ( "strings" "unicode/utf8" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -273,7 +274,7 @@ func pdfMergeReplacement(nodes []pdfCompositeNode, replacement *pdfgo.Replacemen first := nodes[0].text style := first.Style paint := style.Fill - if first.Font == nil || len(first.Font.Program) == 0 || first.Mode != 0 || first.Size <= 0 || first.HorizontalScale <= 0 || first.Clip != nil || style.SoftMask != nil || style.Transfer != nil || style.AlphaIsShape || style.FillOverprint || paint.Alpha != 1 || !pdfNormalBlend(style.BlendMode) || paint.Tiling != nil || paint.Axial != nil || paint.Radial != nil || paint.Mesh != nil || paint.Function != nil { + if first.Font == nil || len(first.Font.Program) == 0 || first.Mode != 0 || first.Size <= 0 || first.HorizontalScale <= 0 || first.Clip != nil || style.SoftMask != nil || style.Transfer != nil || style.AlphaIsShape || style.FillOverprint || paint.Alpha != 1 || !codecpdf.NormalBlend(style.BlendMode) || paint.Tiling != nil || paint.Axial != nil || paint.Radial != nil || paint.Mesh != nil || paint.Function != nil { return nil, false } inverse, ok := first.Matrix.Inverse() diff --git a/ofdgo_pdf_transfer.go b/ofdgo_pdf_transfer.go index 31d84dd..d62a2f4 100644 --- a/ofdgo_pdf_transfer.go +++ b/ofdgo_pdf_transfer.go @@ -14,7 +14,10 @@ package ofdgo -import "github.com/xiaoqidun/pdfgo" +import ( + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" + "github.com/xiaoqidun/pdfgo" +) // 图案标记保留设备四色通道及非零对象形状 const ( @@ -99,7 +102,7 @@ func (c *pdfCompositor) selectTransfer(node pdfCompositeNode, outline bool) (*pd if outline { paint = style.Stroke } - if c.transferBlocked || paint.Alpha != 1 || style.SoftMask != nil || !pdfNormalBlend(style.BlendMode) { + if c.transferBlocked || paint.Alpha != 1 || style.SoftMask != nil || !codecpdf.NormalBlend(style.BlendMode) { return nil, nil } transfer, err := c.importer.resolveTransfer(style, c.transferModel) @@ -108,7 +111,7 @@ func (c *pdfCompositor) selectTransfer(node pdfCompositeNode, outline bool) (*pd } if pattern := paint.Shading; pattern != nil && (node.image == nil || node.image.Image.ImageMask) { inner := pattern.Style - if inner.Fill.Alpha != 1 || inner.SoftMask != nil || !pdfNormalBlend(inner.BlendMode) { + if inner.Fill.Alpha != 1 || inner.SoftMask != nil || !codecpdf.NormalBlend(inner.BlendMode) { return nil, nil } } @@ -171,7 +174,7 @@ func (p *pdfCompositePattern) opaqueTransfer() (bool, error) { // 返回: bool 是否不透明, error 图案或图片遮罩引用错误 func (p *pdfImporter) transferOpaque(node pdfCompositeNode) (bool, error) { if group := node.group; group != nil { - if group.Alpha != 1 || group.SoftMask != nil || !pdfNormalBlend(group.BlendMode) { + if group.Alpha != 1 || group.SoftMask != nil || !codecpdf.NormalBlend(group.BlendMode) { return false, nil } for _, child := range node.children { @@ -182,7 +185,7 @@ func (p *pdfImporter) transferOpaque(node pdfCompositeNode) (bool, error) { return true, nil } style, fill, stroke := node.style() - if style.SoftMask != nil || !pdfNormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 { + if style.SoftMask != nil || !codecpdf.NormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 { return false, nil } if node.image != nil { @@ -197,7 +200,7 @@ func (p *pdfImporter) transferOpaque(node pdfCompositeNode) (bool, error) { for i, paint := range [2]pdfgo.Paint{style.Fill, style.Stroke} { if pattern := paint.Shading; pattern != nil && (i == 0 && fill || i == 1 && stroke) { inner := pattern.Style - if inner.Fill.Alpha != 1 || inner.SoftMask != nil || !pdfNormalBlend(inner.BlendMode) { + if inner.Fill.Alpha != 1 || inner.SoftMask != nil || !codecpdf.NormalBlend(inner.BlendMode) { return false, nil } } diff --git a/ofdgo_renderer_output_canvas.go b/ofdgo_renderer_output_canvas.go index cd6b542..e09cc5d 100644 --- a/ofdgo_renderer_output_canvas.go +++ b/ofdgo_renderer_output_canvas.go @@ -26,6 +26,7 @@ import ( "github.com/tdewolff/canvas/renderers/pdf" "github.com/tdewolff/canvas/renderers/ps" "github.com/tdewolff/canvas/renderers/svg" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -136,7 +137,7 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i } start := buf.Len() p := pdf.New(buf, pages[0].Box.W, pages[0].Box.H, nil) - renderer := &pdfRenderer{PDF: p, glyphPaths: renderCache[*canvas.Path, *canvas.Path]{limit: 16 << 20}, images: make([][]image.Image, 1), text: make([]pdfPageText, 1), navigation: navigation} + renderer := &pdfRenderer{PDF: p, glyphPaths: renderCache[*canvas.Path, *canvas.Path]{limit: 16 << 20}, images: make([][]image.Image, 1), text: make([]codecpdf.TextPage, 1), navigation: navigation} defer func() { if renderer.imageError != nil { r.canvasState().images = renderCache[*EncodedImage, image.Image]{limit: imageCacheLimit} @@ -160,7 +161,7 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i if i > 0 { p.NewPage(page.Box.W, page.Box.H) renderer.images = append(renderer.images, nil) - renderer.text = append(renderer.text, pdfPageText{}) + renderer.text = append(renderer.text, codecpdf.TextPage{}) } navigation.apply(p, i) if err := r.renderCanvasPageToContext(canvas.NewContext(renderer), content, !r.TransparentBackground); err != nil { diff --git a/ofdgo_renderer_pdf_action.go b/ofdgo_renderer_pdf_action.go index 6ae81c7..9b9e988 100644 --- a/ofdgo_renderer_pdf_action.go +++ b/ofdgo_renderer_pdf_action.go @@ -17,10 +17,11 @@ package ofdgo import ( "context" "fmt" - basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "slices" "github.com/tdewolff/canvas" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -63,10 +64,10 @@ func (n *pdfNavigation) addActions(renderer *Renderer, page int, source actionSo if err != nil { return err } - if parsed.Len() > pdfLinkEdgeLimit { + if parsed.Len() > codecpdf.LinkEdgeLimit { return fmt.Errorf("PDF link region exceeds edge limit") } - parsed, err = flattenPDFLink(ctx, parsed) + parsed, err = codecpdf.FlattenLink(ctx, parsed) if err != nil { return err } @@ -92,7 +93,7 @@ func (n *pdfNavigation) addActions(renderer *Renderer, page int, source actionSo if err != nil { return err } - region, err := pdfActionRegion(ctx, outline, pages[page].Box.H) + region, err := codecpdf.ActionRegion(ctx, outline, pages[page].Box.H) if err != nil { return err } @@ -158,20 +159,20 @@ func partitionPDFActions(ctx context.Context, renderer *Renderer, areas []pdfAct if err != nil { return nil, err } - if len(result) > pdfLinkEdgeLimit { + if len(result) > codecpdf.LinkEdgeLimit { return nil, fmt.Errorf("PDF action partition exceeds region limit") } } if len(remaining) != 0 { result = append(result, pdfActionArea{Path: remaining, Actions: []pdfNavigationAction{action}}) } - if len(result) > pdfLinkEdgeLimit { + if len(result) > codecpdf.LinkEdgeLimit { return nil, fmt.Errorf("PDF action partition exceeds region limit") } edges := 0 for _, area := range result { edges += len(area.Path) - if edges > pdfLinkQuadLimit { + if edges > codecpdf.LinkQuadLimit { return nil, fmt.Errorf("PDF action partition exceeds total edge limit") } } @@ -185,14 +186,14 @@ func combinePDFActionRegion(ctx context.Context, geometry GeometryBackend, left, if err := ctx.Err(); err != nil { return nil, err } - if len(left) > pdfLinkEdgeLimit || len(right) > pdfLinkEdgeLimit { + if len(left) > codecpdf.LinkEdgeLimit || len(right) > codecpdf.LinkEdgeLimit { return nil, fmt.Errorf("PDF action partition exceeds edge limit") } result, err := geometry.Combine(left, right, operation) if err != nil { return nil, err } - if len(result) > pdfLinkEdgeLimit { + if len(result) > codecpdf.LinkEdgeLimit { return nil, fmt.Errorf("PDF action partition exceeds edge limit") } return result, ctx.Err() diff --git a/ofdgo_renderer_pdf_canvas.go b/ofdgo_renderer_pdf_canvas.go index ed01e37..1b62f5f 100644 --- a/ofdgo_renderer_pdf_canvas.go +++ b/ofdgo_renderer_pdf_canvas.go @@ -24,6 +24,7 @@ import ( "github.com/tdewolff/canvas" cimage "github.com/tdewolff/canvas/image" "github.com/tdewolff/canvas/renderers/pdf" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -36,16 +37,10 @@ type pdfRenderer struct { exactImages bool imageError error navigation *pdfNavigation - text []pdfPageText + text []codecpdf.TextPage exactText bool } -// pdfPageText 记录后端文字对象数量及OFD原文所属区段 -type pdfPageText struct { - count int - replacements []pdfgo.TextReplacement -} - // pdfNavigation PDF导航信息 type pdfNavigation struct { Link map[int][]pdfLink diff --git a/ofdgo_renderer_pdf_text.go b/ofdgo_renderer_pdf_text.go index d1406fe..27952e1 100644 --- a/ofdgo_renderer_pdf_text.go +++ b/ofdgo_renderer_pdf_text.go @@ -18,6 +18,7 @@ import ( "github.com/tdewolff/canvas" canvastext "github.com/tdewolff/canvas/text" basefont "github.com/xiaoqidun/ofdgo/internal/base/font" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -25,12 +26,12 @@ import ( // 入参: text 画布文字, matrix 文字变换 func (r *pdfRenderer) RenderText(text *canvas.Text, matrix canvas.Matrix) { if len(r.text) == 0 { - r.text = append(r.text, pdfPageText{}) + r.text = append(r.text, codecpdf.TextPage{}) } page := &r.text[len(r.text)-1] text.WalkSpans(func(_, _ float64, span canvas.TextSpan) { if span.IsText() { - page.count++ + page.Count++ } }) r.PDF.RenderText(text, matrix) @@ -40,8 +41,8 @@ func (r *pdfRenderer) RenderText(text *canvas.Text, matrix canvas.Matrix) { // 入参: first 起始文字对象序号, text 原文 func (r *pdfRenderer) preserveText(first int, text string) { page := &r.text[len(r.text)-1] - if page.count > first { - page.replacements = append(page.replacements, pdfgo.TextReplacement{First: first, Count: page.count - first, Text: text}) + if page.Count > first { + page.Replacements = append(page.Replacements, pdfgo.TextReplacement{First: first, Count: page.Count - first, Text: text}) r.exactText = true } } diff --git a/ofdgo_renderer_pdf_write.go b/ofdgo_renderer_pdf_write.go index 00121b7..9c65d2c 100644 --- a/ofdgo_renderer_pdf_write.go +++ b/ofdgo_renderer_pdf_write.go @@ -15,165 +15,37 @@ package ofdgo import ( - "bytes" "context" - "fmt" - "image" "io" - baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) -// writePDF 合并图像、文字、导航修正及压缩,直接写出最终PDF +// writePDF 传递后端资源记录及OFD导航修正,写出最终PDF // 入参: ctx 取消上下文, data 后端PDF数据, writer 输出流, optimization 优化配置 // 返回: error 资源、压缩或写入错误 func (r *pdfRenderer) writePDF(ctx context.Context, data []byte, writer io.Writer, optimization pdfgo.OptimizeOptions) error { if r.imageError != nil { return r.imageError } - nativeWrite := r.navigation != nil && r.navigation.nativeWrite - if !r.exactImages && !r.exactText && !nativeWrite && optimization.Compression.Mode == CompressionUnchanged { - if err := ctx.Err(); err != nil { - return err - } - n, err := writer.Write(data) - if err == nil && n != len(data) { - err = io.ErrShortWrite - } - return err + options := codecpdf.WriteOptions{ + Images: r.images, + Text: r.text, + ExactImages: r.exactImages, + ExactText: r.exactText, + Optimization: optimization, } - reader, err := pdfgo.NewReader(bytes.NewReader(data), int64(len(data))) - if err != nil { - return err + if r.navigation != nil && r.navigation.nativeWrite { + options.Links = make(map[int][]*pdfgo.LinkRegion, len(r.navigation.Link)) + for page, links := range r.navigation.Link { + regions := make([]*pdfgo.LinkRegion, len(links)) + for i, link := range links { + regions[i] = link.Region + } + options.Links[page] = regions + } + options.PrepareRewrite = r.navigation.prepareRewrite } - defer reader.Close() - if !r.exactImages && !r.exactText && !nativeWrite { - _, err := reader.OptimizeTo(ctx, writer, optimization) - return err - } - images := make(map[pdfgo.Reference]image.Image) - links := make(map[pdfgo.AnnotationLocation]pdfgo.LinkRegion) - texts := make(map[pdfgo.Reference][]pdfgo.TextReplacement) - references := make([]pdfgo.Reference, len(r.images)) - pages := 0 - err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { - if index >= len(r.images) { - return fmt.Errorf("unexpected PDF output page") - } - pages++ - references[index] = page.Reference - if nativeWrite { - annotations, err := page.AnnotationsContext(ctx) - if err != nil { - return err - } - expected := r.navigation.Link[index] - if len(annotations) != len(expected) { - return fmt.Errorf("PDF output link count differs") - } - for i, link := range expected { - if annotations[i].Subtype != pdfgo.Name("Link") { - return fmt.Errorf("unexpected PDF output annotation") - } - if link.Region != nil { - links[pdfgo.AnnotationLocation{Page: page.Reference, Index: i}] = *link.Region - } - } - } - if !r.exactImages && !r.exactText { - return nil - } - value, err := reader.Resolve(page.Resources["XObject"]) - if err != nil { - return err - } - resources, _ := value.(pdfgo.Dictionary) - var content bytes.Buffer - if _, err := page.WriteContent(ctx, &content); err != nil { - return err - } - position := 0 - textCount := 0 - if err := pdfgo.WalkOperations(ctx, content.Bytes(), func(operation pdfgo.Operation) error { - if operation.Operator == "BT" { - textCount++ - } - if !r.exactImages { - return nil - } - if operation.Operator != "Do" { - return nil - } - if len(operation.Operands) != 1 || position >= len(r.images[index]) { - return fmt.Errorf("unexpected PDF output image") - } - name, ok := operation.Operands[0].(pdfgo.Name) - if !ok { - return fmt.Errorf("invalid PDF output image name") - } - ref, ok := resources[name].(pdfgo.Reference) - if !ok { - return fmt.Errorf("invalid PDF output image reference") - } - original := r.images[index][position] - position++ - if original == nil { - return nil - } - image, err := reader.ReadImage(ref) - if err != nil { - return err - } - if image.Width != original.Bounds().Dx() || image.Height != original.Bounds().Dy() { - return fmt.Errorf("PDF output image dimensions differ") - } - if previous := images[ref]; previous != nil && previous != original { - return fmt.Errorf("PDF output image reference conflicts") - } - images[ref] = original - return nil - }); err != nil { - return err - } - if r.exactImages && position != len(r.images[index]) { - return fmt.Errorf("PDF output image missing") - } - if r.exactText { - if index >= len(r.text) || textCount != r.text[index].count { - return fmt.Errorf("PDF output text count differs") - } - if len(r.text[index].replacements) != 0 { - texts[page.Reference] = r.text[index].replacements - } - } - return nil - }) - if err != nil { - return err - } - if pages != len(r.images) { - return fmt.Errorf("PDF output page missing") - } - options := pdfgo.RewriteOptions{Images: images, ImageOptions: pdfgo.ImageWriteOptions{PreblendBinary: true}, LinkRegions: links, TextReplacements: texts, Optimization: optimization} - if nativeWrite { - if err := r.navigation.prepareRewrite(ctx, reader, references, &options); err != nil { - return err - } - } - for ref, original := range images { - if err := ctx.Err(); err != nil { - return err - } - pixels, err := baseimage.PixelDataContext(ctx, original) - if err != nil { - return err - } - if pixels == nil || pixels.Bounds() != original.Bounds() { - return fmt.Errorf("PDF output image pixel bounds differ") - } - images[ref] = pixels - } - _, err = reader.RewriteTo(ctx, writer, options) - return err + return codecpdf.Write(ctx, data, writer, options) } diff --git a/ofdgo_renderer_text_canvas.go b/ofdgo_renderer_text_canvas.go index b87077a..208c411 100644 --- a/ofdgo_renderer_text_canvas.go +++ b/ofdgo_renderer_text_canvas.go @@ -18,6 +18,7 @@ import ( "strings" "github.com/tdewolff/canvas" + codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" ) // ptPerMM 为毫米转换为排版点的比例 @@ -212,9 +213,9 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } if pdfText { if len(pdfOutput.text) == 0 { - pdfOutput.text = append(pdfOutput.text, pdfPageText{}) + pdfOutput.text = append(pdfOutput.text, codecpdf.TextPage{}) } - defer pdfOutput.preserveText(pdfOutput.text[len(pdfOutput.text)-1].count, obj.Text()) + defer pdfOutput.preserveText(pdfOutput.text[len(pdfOutput.text)-1].Count, obj.Text()) } codePos := 0 textPos := 0