mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
974b515374
commit
67f4cf9d2c
13 files changed
+401
-44
No files matched your search
@@ -7687,13 +7687,18 @@ function chooseResourceFile(action) {
|
||||
async function changeManagedResource(action, item, file) {
|
||||
if (!item || !state.editing) return;
|
||||
releaseResourcePreview();
|
||||
const page = Number(resourceElements.Page.value), object = resourceElements.Object.value;
|
||||
const format = file?.name.includes(".") ? file.name.split(".").at(-1).toUpperCase() : "";
|
||||
const changed = item.kind === "Attachment" && action !== "bind" && action !== "unbind"
|
||||
? await changeDocument("ofdgoChangeAttachment", null, action, item.id, file ? [file.name] : [], file ? [file] : [])
|
||||
: await changeDocument("ofdgoChangeResource", null, action, item.kind, item.id, format, file || null, Number(resourceElements.Page.value), resourceElements.Object.value);
|
||||
: await changeDocument("ofdgoChangeResource", null, action, item.kind, item.id, format, file || null, page, object);
|
||||
if (changed) {
|
||||
await loadManagedResources();
|
||||
if (action === "bind" || action === "unbind") resourceElements.Status.textContent = action === "bind" ? "已绑定" : "已解绑";
|
||||
if (action === "bind" || action === "unbind") {
|
||||
resourceElements.Page.value = String(page);
|
||||
await loadResourceTargets(object);
|
||||
resourceElements.Status.textContent = action === "bind" ? "已绑定" : "已解绑";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+118
-18
@@ -17,9 +17,11 @@ package font
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-text/typesetting/font/opentype/tables"
|
||||
)
|
||||
@@ -34,10 +36,12 @@ type SFNT struct {
|
||||
reverse map[uint16][]rune
|
||||
}
|
||||
|
||||
// sfntCmap 保存编码记录及对应字符映射,标记已解析的共享子表
|
||||
// sfntCmap 保存编码记录及字符映射,连续区间与共享子表不重复展开
|
||||
type sfntCmap struct {
|
||||
platform, encoding, format uint16
|
||||
mapping map[rune]uint16
|
||||
groups []byte
|
||||
entries uint64
|
||||
duplicate bool
|
||||
}
|
||||
|
||||
@@ -90,7 +94,7 @@ func ParseSFNTTables(table map[string][]byte) (*SFNT, error) {
|
||||
if cmap.duplicate {
|
||||
continue
|
||||
}
|
||||
for _, glyph := range cmap.mapping {
|
||||
for _, glyph := range cmap.pairs() {
|
||||
if glyph >= f.count {
|
||||
return nil, fmt.Errorf("cmap glyph index out of range")
|
||||
}
|
||||
@@ -119,7 +123,7 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
|
||||
}
|
||||
count := int(binary.BigEndian.Uint16(data[2:]))
|
||||
result := make([]sfntCmap, 0, count)
|
||||
parsed := make(map[uint32]map[rune]uint16)
|
||||
parsed := make(map[uint32]sfntCmap)
|
||||
for i := range count {
|
||||
record := data[4+8*i:]
|
||||
offset := binary.BigEndian.Uint32(record[4:])
|
||||
@@ -128,23 +132,44 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
|
||||
}
|
||||
sub := data[offset:]
|
||||
format := binary.BigEndian.Uint16(sub)
|
||||
mapping, found := parsed[offset]
|
||||
cmap, found := parsed[offset]
|
||||
if !found {
|
||||
mapping = make(map[rune]uint16)
|
||||
cmap.mapping = make(map[rune]uint16)
|
||||
var err error
|
||||
switch format {
|
||||
case 0:
|
||||
parseCmapFormat0(sub, mapping)
|
||||
parseCmapFormat0(sub, cmap.mapping)
|
||||
case 4:
|
||||
parseCmapFormat4(sub, mapping)
|
||||
parseCmapFormat4(sub, cmap.mapping)
|
||||
case 6:
|
||||
parseCmapFormat6(sub, mapping)
|
||||
parseCmapFormat6(sub, cmap.mapping)
|
||||
case 12:
|
||||
parseCmapFormat12(sub, mapping)
|
||||
if len(sub) >= 16 {
|
||||
length := uint64(binary.BigEndian.Uint32(sub[4:]))
|
||||
count := uint64(binary.BigEndian.Uint32(sub[12:]))
|
||||
if length >= 16 && length <= uint64(len(sub)) && count <= (length-16)/12 {
|
||||
cmap.groups = sub[16 : 16+12*count]
|
||||
for offset := 0; offset < len(cmap.groups); offset += 12 {
|
||||
first := binary.BigEndian.Uint32(cmap.groups[offset:])
|
||||
last := binary.BigEndian.Uint32(cmap.groups[offset+4:])
|
||||
if first > last || offset != 0 && first <= binary.BigEndian.Uint32(cmap.groups[offset-8:]) {
|
||||
cmap.groups = nil
|
||||
parseCmapFormat12(sub, cmap.mapping)
|
||||
break
|
||||
}
|
||||
}
|
||||
if cmap.groups != nil {
|
||||
cmap.mapping = nil
|
||||
for range cmap.pairs() {
|
||||
cmap.entries++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
mapping, err = parseCmapFormat2(sub)
|
||||
cmap.mapping, err = parseCmapFormat2(sub)
|
||||
case 8, 10, 13:
|
||||
mapping, err = parseCmapExtended(sub)
|
||||
cmap.mapping, err = parseCmapExtended(sub)
|
||||
case 14:
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported cmap format %d", format)
|
||||
@@ -152,13 +177,67 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsed[offset] = mapping
|
||||
parsed[offset] = cmap
|
||||
}
|
||||
result = append(result, sfntCmap{platform: binary.BigEndian.Uint16(record), encoding: binary.BigEndian.Uint16(record[2:]), format: format, mapping: mapping, duplicate: found})
|
||||
cmap.platform, cmap.encoding, cmap.format, cmap.duplicate = binary.BigEndian.Uint16(record), binary.BigEndian.Uint16(record[2:]), format, found
|
||||
result = append(result, cmap)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// pairs 按原映射返回字符与字形,连续编码段不展开存储
|
||||
// 返回: iter.Seq2[rune, uint16] 非零有效字符映射
|
||||
func (c sfntCmap) pairs() iter.Seq2[rune, uint16] {
|
||||
return func(yield func(rune, uint16) bool) {
|
||||
for char, glyph := range c.mapping {
|
||||
if !yield(char, glyph) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for offset := 0; offset+12 <= len(c.groups); offset += 12 {
|
||||
group := c.groups[offset:]
|
||||
first, last, glyph := binary.BigEndian.Uint32(group), binary.BigEndian.Uint32(group[4:]), binary.BigEndian.Uint32(group[8:])
|
||||
if first > last || first > utf8.MaxRune || glyph > 65535 {
|
||||
continue
|
||||
}
|
||||
end := min(uint64(last), uint64(first)+65535-uint64(glyph), uint64(utf8.MaxRune))
|
||||
for char := uint64(first); char <= end; char++ {
|
||||
id := uint16(uint64(glyph) + char - uint64(first))
|
||||
if id != 0 && utf8.ValidRune(rune(char)) && !yield(rune(char), id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lookup 查找原字符映射,连续编码段使用有序区间查询
|
||||
// 入参: char 字符
|
||||
// 返回: uint16 字形编号, bool 是否存在映射
|
||||
func (c sfntCmap) lookup(char rune) (uint16, bool) {
|
||||
if glyph, ok := c.mapping[char]; ok {
|
||||
return glyph, true
|
||||
}
|
||||
if !utf8.ValidRune(char) {
|
||||
return 0, false
|
||||
}
|
||||
low, high := 0, len(c.groups)/12
|
||||
for low < high {
|
||||
middle := low + (high-low)/2
|
||||
group := c.groups[middle*12:]
|
||||
first, last := binary.BigEndian.Uint32(group), binary.BigEndian.Uint32(group[4:])
|
||||
if uint32(char) < first {
|
||||
high = middle
|
||||
} else if uint32(char) > last {
|
||||
low = middle + 1
|
||||
} else {
|
||||
glyph := uint64(binary.BigEndian.Uint32(group[8:])) + uint64(uint32(char)-first)
|
||||
return uint16(glyph), glyph > 0 && glyph <= 65535
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// UnitsPerEm 返回字体设计单位
|
||||
// 返回: uint16 每em单位数
|
||||
func (f *SFNT) UnitsPerEm() uint16 { return f.units }
|
||||
@@ -172,7 +251,7 @@ func (f *SFNT) NumGlyphs() uint16 { return f.count }
|
||||
func (f *SFNT) CmapEntries() uint64 {
|
||||
var count uint64
|
||||
for _, cmap := range f.cmaps {
|
||||
count += uint64(len(cmap.mapping))
|
||||
count += uint64(len(cmap.mapping)) + cmap.entries
|
||||
}
|
||||
return count
|
||||
}
|
||||
@@ -185,14 +264,14 @@ func (f *SFNT) GlyphIndex(char rune) uint16 {
|
||||
if cmap.duplicate {
|
||||
continue
|
||||
}
|
||||
if gid := cmap.mapping[char]; gid != 0 {
|
||||
if gid, ok := cmap.lookup(char); ok && gid != 0 {
|
||||
return gid
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// glyphCharacters 返回指向指定字形的全部字符
|
||||
// glyphCharacters 返回指向指定字形的全部字符,连续区间按需反查
|
||||
// 入参: gid 字形编号
|
||||
// 返回: []rune 排序后的字符
|
||||
func (f *SFNT) glyphCharacters(gid uint16) []rune {
|
||||
@@ -204,7 +283,7 @@ func (f *SFNT) glyphCharacters(gid uint16) []rune {
|
||||
continue
|
||||
}
|
||||
for char, glyph := range cmap.mapping {
|
||||
if glyph != 0 && !seen[char] {
|
||||
if glyph != 0 && !seen[char] && f.GlyphIndex(char) == glyph {
|
||||
f.reverse[glyph] = append(f.reverse[glyph], char)
|
||||
seen[char] = true
|
||||
}
|
||||
@@ -214,7 +293,28 @@ func (f *SFNT) glyphCharacters(gid uint16) []rune {
|
||||
slices.Sort(chars)
|
||||
}
|
||||
})
|
||||
return f.reverse[gid]
|
||||
if gid == 0 {
|
||||
return nil
|
||||
}
|
||||
chars := slices.Clone(f.reverse[gid])
|
||||
for _, cmap := range f.cmaps {
|
||||
if cmap.duplicate {
|
||||
continue
|
||||
}
|
||||
for offset := 0; offset+12 <= len(cmap.groups); offset += 12 {
|
||||
group := cmap.groups[offset:]
|
||||
first, last, glyph := binary.BigEndian.Uint32(group), binary.BigEndian.Uint32(group[4:]), binary.BigEndian.Uint32(group[8:])
|
||||
if first > last || uint32(gid) < glyph {
|
||||
continue
|
||||
}
|
||||
char := uint64(first) + uint64(uint32(gid)-glyph)
|
||||
if char <= uint64(last) && char <= utf8.MaxRune && utf8.ValidRune(rune(char)) && f.GlyphIndex(rune(char)) == gid {
|
||||
chars = append(chars, rune(char))
|
||||
}
|
||||
}
|
||||
}
|
||||
slices.Sort(chars)
|
||||
return slices.Compact(chars)
|
||||
}
|
||||
|
||||
// GlyphAdvance 返回水平字宽
|
||||
|
||||
@@ -197,7 +197,7 @@ func subsetSourceSFNT(sfnt *SFNT, usage *Usage, size int) []byte {
|
||||
for char := range usage.Characters {
|
||||
id := sfnt.GlyphIndex(char)
|
||||
for _, cmap := range unicodeTables {
|
||||
if alternate, ok := cmap.mapping[char]; ok && alternate != id {
|
||||
if alternate, ok := cmap.lookup(char); ok && alternate != id {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
+128
-2
@@ -291,6 +291,132 @@ func editorObjectFontScan(object GraphicObject) bool {
|
||||
// 入参: progress 保存进度回调
|
||||
// 返回: map[string][]byte 包内字体子集, error 错误信息
|
||||
func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) {
|
||||
result, err := e.subsetNewFonts(progress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := context.Background()
|
||||
if e.output != nil {
|
||||
ctx = e.output.ctx
|
||||
}
|
||||
source, err := e.subsetSnapshotFonts(ctx, progress.withContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return source, nil
|
||||
}
|
||||
maps.Copy(result, source)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// subsetSnapshotFonts 在写出时扫描快照中新增字体的完整包引用,不改动既有字体
|
||||
// 入参: ctx 取消上下文, progress 写出进度
|
||||
// 返回: map[string][]byte 新增字体的安全子集, error 扫描或裁剪错误
|
||||
func (e *Editor) subsetSnapshotFonts(ctx context.Context, progress editorProgress) (map[string][]byte, error) {
|
||||
if e.source == nil || e.backends.FontResources == nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
candidates := make(map[string]bool)
|
||||
for _, resource := range e.resources {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resource.font != nil && resource.font.FontFile != "" && e.source.reader.fileNames[resource.name] {
|
||||
candidates[resource.name] = true
|
||||
}
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
view, err := e.readerContext(ctx, progress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer view.Close()
|
||||
refs, resources, references, safe, err := scanEditorResources(ctx, view, view.files, nil, maps.Clone(view.fileNames), progress, true)
|
||||
if err != nil || !safe {
|
||||
return nil, err
|
||||
}
|
||||
scoped, err := resourceReferenceScopes(ctx, refs.documents, resources, references, "")
|
||||
if err != nil || scoped == nil {
|
||||
return nil, err
|
||||
}
|
||||
read := func(name string) ([]byte, error) { return view.readFileViewContext(ctx, name) }
|
||||
usage, err := resourceFontUsage(ctx, resources, scoped, read)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, source := range references {
|
||||
for name := range source.files {
|
||||
if font := usage[name]; font != nil && !source.versions[name] {
|
||||
font.unsafe = true
|
||||
}
|
||||
}
|
||||
for _, definition := range source.definitions {
|
||||
for name := range definition.files {
|
||||
if font := usage[name]; font != nil {
|
||||
font.unsafe = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result := make(map[string][]byte)
|
||||
for i, name := range slices.Sorted(maps.Keys(candidates)) {
|
||||
if err := progress.report("fonts", i, len(candidates)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
font := usage[name]
|
||||
if font == nil || font.unsafe || len(font.chars)+len(font.glyphs) == 0 {
|
||||
continue
|
||||
}
|
||||
data, err := read(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsed, err := e.backends.FontResources.OpenFontResource(FontFile{Data: data}, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !parsed.CanSubset {
|
||||
continue
|
||||
}
|
||||
preserve := len(font.glyphs) != 0
|
||||
glyphs := maps.Clone(font.glyphs)
|
||||
for char := range font.chars {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
glyphs[parsed.GlyphIndex(char)] = true
|
||||
}
|
||||
glyphs[0] = true
|
||||
for id := range glyphs {
|
||||
if id >= parsed.NumGlyphs() {
|
||||
font.unsafe = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if font.unsafe {
|
||||
continue
|
||||
}
|
||||
subset, err := e.backends.FontResources.SubsetFont(data, slices.Sorted(maps.Keys(glyphs)), preserve)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(subset) != 0 && len(subset) < len(data) {
|
||||
result[name] = subset
|
||||
}
|
||||
}
|
||||
return result, progress.report("fonts", len(candidates), len(candidates))
|
||||
}
|
||||
|
||||
// subsetNewFonts 解析尚未物化为源资源的新增字体,保持编辑字体完整
|
||||
// 入参: progress 写出进度
|
||||
// 返回: map[string][]byte 包内字体子集, error 读取或裁剪错误
|
||||
func (e *Editor) subsetNewFonts(progress editorProgress) (map[string][]byte, error) {
|
||||
ctx := context.Background()
|
||||
if e.output != nil {
|
||||
ctx = e.output.ctx
|
||||
@@ -303,7 +429,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
||||
return nil, err
|
||||
}
|
||||
if !slices.ContainsFunc(e.resources, func(resource editorResource) bool {
|
||||
return resource.font != nil && resource.font.FontFile != ""
|
||||
return resource.font != nil && resource.font.FontFile != "" && (e.source == nil || !e.source.reader.fileNames[resource.name])
|
||||
}) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -318,7 +444,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
||||
if err := progress.report("fonts", i, total); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resource.font.FontFile == "" {
|
||||
if resource.font.FontFile == "" || e.source != nil && e.source.reader.fileNames[resource.name] {
|
||||
continue
|
||||
}
|
||||
resourceFont := e.fonts[resource.font.ID]
|
||||
|
||||
@@ -82,6 +82,7 @@ type pdfImporter struct {
|
||||
pageActionPage int
|
||||
fontIDs map[*pdfgo.Font]string
|
||||
fonts map[*pdfgo.Font]*pdfImportedFont
|
||||
fontPrograms map[pdfFontProgramKey]*pdfImportedFont
|
||||
symbolFonts map[*pdfgo.Font]*pdfImportedFont
|
||||
fontWarnings map[string]bool
|
||||
clipTexts map[*pdfgo.TextClip][]TextObject
|
||||
|
||||
+4
-1
@@ -398,10 +398,13 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
|
||||
if p.symbolFonts == nil {
|
||||
p.symbolFonts = make(map[*pdfgo.Font]*pdfImportedFont)
|
||||
}
|
||||
if p.fontPrograms == nil {
|
||||
p.fontPrograms = make(map[pdfFontProgramKey]*pdfImportedFont)
|
||||
}
|
||||
if p.clipPaths == nil {
|
||||
p.clipPaths = resourceCache[[32]byte, pdfClipPath](p.resources, 8<<20)
|
||||
}
|
||||
local := pdfImporter{resources: p.resources, ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, matrix: p.matrix, pageBox: p.pageBox, pageWidth: p.pageWidth, pageHeight: p.pageHeight, rasterDPI: p.rasterDPI, warning: p.warning, rasterWarned: true, fontIDs: map[*pdfgo.Font]string{}, fonts: p.fonts, symbolFonts: p.symbolFonts, clipPaths: p.clipPaths, textOrigins: p.textOrigins}
|
||||
local := pdfImporter{resources: p.resources, ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, matrix: p.matrix, pageBox: p.pageBox, pageWidth: p.pageWidth, pageHeight: p.pageHeight, rasterDPI: p.rasterDPI, warning: p.warning, rasterWarned: true, fontIDs: map[*pdfgo.Font]string{}, fonts: p.fonts, fontPrograms: p.fontPrograms, symbolFonts: p.symbolFonts, clipPaths: p.clipPaths, textOrigins: p.textOrigins}
|
||||
if err := build(&local); err != nil {
|
||||
return nil, box, err
|
||||
}
|
||||
|
||||
+30
-11
@@ -38,6 +38,12 @@ type pdfImportedFont struct {
|
||||
repairLimit uint16
|
||||
}
|
||||
|
||||
// pdfFontProgramKey 区分字体封装内容及交给资源后端的来源名称
|
||||
type pdfFontProgramKey struct {
|
||||
checksum [32]byte
|
||||
name string
|
||||
}
|
||||
|
||||
// pdfTextReplacement 记录一个完整替换区段的文字数量与转换状态
|
||||
type pdfTextReplacement struct {
|
||||
text *pdfgo.ReplacementText
|
||||
@@ -661,21 +667,27 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
|
||||
if p.editor.backends.FontResources == nil {
|
||||
return nil, fmt.Errorf("PDF font %s resource: font resources: %w", source.Name, ErrBackendUnavailable)
|
||||
}
|
||||
result.resource, err = p.editor.backends.FontResources.OpenFontResource(FontFile{Name: source.Name + ".ttf", Data: program}, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("PDF font %s resource: %w", source.Name, err)
|
||||
}
|
||||
if p.editor.backends.Fonts == nil {
|
||||
return nil, fmt.Errorf("PDF font backend unavailable")
|
||||
}
|
||||
result.metrics, err = p.editor.backends.Fonts.OpenFont(result.resource.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("PDF font %s metrics: %w", source.Name, err)
|
||||
key := pdfFontProgramKey{checksum: sha256.Sum256(program), name: source.Name}
|
||||
if cached := p.fontPrograms[key]; cached != nil {
|
||||
result.resource, result.metrics, result.checksum = cached.resource, cached.metrics, cached.checksum
|
||||
} else {
|
||||
result.resource, err = p.editor.backends.FontResources.OpenFontResource(FontFile{Name: source.Name + ".ttf", Data: program}, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("PDF font %s resource: %w", source.Name, err)
|
||||
}
|
||||
result.metrics, err = p.editor.backends.Fonts.OpenFont(result.resource.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("PDF font %s metrics: %w", source.Name, err)
|
||||
}
|
||||
if _, ok := result.metrics.(FontOutlines); !ok {
|
||||
return nil, fmt.Errorf("PDF font backend does not provide glyph outlines")
|
||||
}
|
||||
result.checksum = sha256.Sum256(result.resource.Data)
|
||||
}
|
||||
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
|
||||
result.repairLimit = repair.MetricsLimit
|
||||
if source.Recovered || programRecovered {
|
||||
if err := p.preservePDFObject(p.ctx, "Font", source.Dictionary); err != nil {
|
||||
return nil, err
|
||||
@@ -690,6 +702,13 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
|
||||
if p.fonts == nil {
|
||||
p.fonts = make(map[*pdfgo.Font]*pdfImportedFont)
|
||||
}
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.fontPrograms == nil {
|
||||
p.fontPrograms = make(map[pdfFontProgramKey]*pdfImportedFont)
|
||||
}
|
||||
p.fontPrograms[key] = result
|
||||
p.fonts[source] = result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
@@ -120,7 +121,7 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima
|
||||
}
|
||||
continue
|
||||
}
|
||||
p, err := rasterCanvasPath(cmd.Path, pathData)
|
||||
p, err := rasterCanvasPath(visibleRasterPath(cmd, page), pathData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("canvas command %d: %w", i, err)
|
||||
}
|
||||
@@ -228,6 +229,54 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// visibleRasterPath 排除填充区域外的完整子路径,保留曲线控制范围与抗锯齿边缘
|
||||
// 入参: command 绘制指令, page 目标页面
|
||||
// 返回: []RasterSegment 可见子路径,描边和短路径沿用原数据
|
||||
func visibleRasterPath(command RasterCommand, page *RasterPage) []RasterSegment {
|
||||
if command.Stroke != nil || len(command.Path) < 256 || command.Path[0].Verb != RasterMove {
|
||||
return command.Path
|
||||
}
|
||||
padding := 25.4 / page.DPI
|
||||
var visible []RasterSegment
|
||||
filtered := false
|
||||
start := 0
|
||||
minX, minY, maxX, maxY := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
|
||||
flush := func(end int) {
|
||||
if minX <= page.Width+padding && maxX >= -padding && minY <= page.Height+padding && maxY >= -padding {
|
||||
if filtered {
|
||||
visible = append(visible, command.Path[start:end]...)
|
||||
}
|
||||
} else if !filtered {
|
||||
filtered = true
|
||||
visible = append(visible, command.Path[:start]...)
|
||||
}
|
||||
}
|
||||
include := func(point RasterPoint) bool {
|
||||
point = command.Transform.Apply(point)
|
||||
if !finite(point.X) || !finite(point.Y) {
|
||||
return false
|
||||
}
|
||||
minX, minY = math.Min(minX, point.X), math.Min(minY, point.Y)
|
||||
maxX, maxY = math.Max(maxX, point.X), math.Max(maxY, point.Y)
|
||||
return true
|
||||
}
|
||||
for index, segment := range command.Path {
|
||||
if segment.Verb == RasterMove && index != start {
|
||||
flush(index)
|
||||
start = index
|
||||
minX, minY, maxX, maxY = math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
|
||||
}
|
||||
if segment.Verb > RasterClose || segment.Verb != RasterClose && !include(segment.End) || (segment.Verb == RasterQuad || segment.Verb == RasterCubic) && !include(segment.Control1) || segment.Verb == RasterCubic && !include(segment.Control2) {
|
||||
return command.Path
|
||||
}
|
||||
}
|
||||
flush(len(command.Path))
|
||||
if !filtered {
|
||||
return command.Path
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
// rasterCanvasPath 预留路径容量,保留光栅指令的构建及闭合规则
|
||||
// 入参: path 只读光栅路径, buffer 可复用的独占路径缓冲
|
||||
// 返回: *canvas.Path 使用独占缓冲的绘制路径, error 不支持的指令
|
||||
|
||||
@@ -25,8 +25,9 @@ import (
|
||||
|
||||
// clipRenderer 收集图形和文字的绘制轮廓
|
||||
type clipRenderer struct {
|
||||
path *canvas.Path
|
||||
clip *canvas.Path
|
||||
path *canvas.Path
|
||||
clip *canvas.Path
|
||||
unions *renderCache[[32]byte, *canvas.Path]
|
||||
}
|
||||
|
||||
// Size 返回不限定边界的裁剪画布尺寸
|
||||
@@ -57,13 +58,23 @@ func (r *clipRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
|
||||
// 入参: img 图像, m 变换矩阵
|
||||
func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {}
|
||||
|
||||
// add 合并裁剪轮廓
|
||||
// add 合并裁剪轮廓,按完整输入复用已有并集
|
||||
// 入参: path 裁剪路径
|
||||
func (r *clipRenderer) add(path *canvas.Path) {
|
||||
if r.path == nil {
|
||||
r.path = path
|
||||
} else {
|
||||
} else if r.unions == nil || path == nil {
|
||||
r.path = unionClipPath(r.path, path)
|
||||
} else {
|
||||
key := canvasGeometryDigest(r.path, path)
|
||||
if merged, ok := r.unions.get(key); ok {
|
||||
r.path = merged.Copy()
|
||||
} else {
|
||||
r.path = unionClipPath(r.path, path)
|
||||
if cost := len(r.path.Data())*8 + 256; cost <= r.unions.limit {
|
||||
r.unions.put(key, r.path.Copy(), cost)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +107,7 @@ func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, ma
|
||||
}
|
||||
var p *canvas.Path
|
||||
for _, clip := range clips.Clip {
|
||||
renderer := &clipRenderer{}
|
||||
renderer := &clipRenderer{unions: r.canvasClipUnions()}
|
||||
for _, area := range clip.Area {
|
||||
areaCTM := matrix.Multiply(NewMatrix(area.CTM))
|
||||
for _, pathObj := range area.Path {
|
||||
|
||||
@@ -556,6 +556,14 @@ func clipLinearFillGeometry(path, clip GeometryPath) (GeometryPath, bool, error)
|
||||
return *geometryFromCanvasPath(result), true, nil
|
||||
}
|
||||
|
||||
// invertCanvasMatrix 对有限可逆矩阵求逆,保留非零小尺度变换
|
||||
// 入参: matrix 画布仿射矩阵
|
||||
// 返回: canvas.Matrix 逆矩阵, bool 是否存在有限逆矩阵
|
||||
func invertCanvasMatrix(matrix canvas.Matrix) (canvas.Matrix, bool) {
|
||||
inverse, ok := MatrixFromValues([6]float64{matrix[0][0], matrix[1][0], matrix[0][1], matrix[1][1], matrix[0][2], matrix[1][2]}).Invert()
|
||||
return canvas.Matrix{{inverse.a, inverse.c, inverse.e}, {inverse.b, inverse.d, inverse.f}}, ok
|
||||
}
|
||||
|
||||
// actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴
|
||||
// 入参: matrix 页面变换
|
||||
// 返回: canvas.Matrix 路径变换
|
||||
|
||||
@@ -227,7 +227,11 @@ func (run *TextRun) addSpan(start, end int, bounds canvas.Rect, m canvas.Matrix,
|
||||
if path.Empty() {
|
||||
return
|
||||
}
|
||||
bounds = path.Transform(m.Inv()).Bounds()
|
||||
inverse, ok := invertCanvasMatrix(m)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
bounds = path.Transform(inverse).Bounds()
|
||||
}
|
||||
if bounds.W() < 0 || bounds.H() <= 0 {
|
||||
return
|
||||
@@ -238,8 +242,8 @@ func (run *TextRun) addSpan(start, end int, bounds canvas.Rect, m canvas.Matrix,
|
||||
span = run.Spans[n-1]
|
||||
v := span.Matrix
|
||||
previous := canvas.Matrix{{v[0], v[2], v[4]}, {v[1], v[3], v[5]}}
|
||||
if previous.Det() != 0 {
|
||||
bounds = canvas.Rect{X1: 1, Y1: 1}.Add(canvas.Rect{X1: 1, Y1: 1}.Transform(previous.Inv().Mul(m)))
|
||||
if inverse, ok := invertCanvasMatrix(previous); ok {
|
||||
bounds = canvas.Rect{X1: 1, Y1: 1}.Add(canvas.Rect{X1: 1, Y1: 1}.Transform(inverse.Mul(m)))
|
||||
m = previous.Translate(bounds.X0, bounds.Y0).Scale(bounds.W(), bounds.H())
|
||||
}
|
||||
run.Spans = run.Spans[:n-1]
|
||||
|
||||
@@ -34,6 +34,9 @@ type canvasFontKey struct {
|
||||
// canvasClipCacheKey 区分不依赖文档资源的共享裁剪缓存
|
||||
type canvasClipCacheKey struct{}
|
||||
|
||||
// canvasClipUnionKey 区分文字和图形裁剪的共享并集缓存
|
||||
type canvasClipUnionKey struct{}
|
||||
|
||||
// canvasPathCacheKey 区分不依赖文档资源的共享局部路径缓存
|
||||
type canvasPathCacheKey struct{}
|
||||
|
||||
@@ -83,6 +86,21 @@ func (r *Renderer) canvasClipIntersections() *renderCache[[32]byte, *canvas.Path
|
||||
return cache
|
||||
}
|
||||
|
||||
// canvasClipUnions 复用相同输入轮廓的裁剪并集,不改变布尔运算顺序
|
||||
// 返回: *renderCache[[32]byte, *canvas.Path] 只读裁剪并集缓存
|
||||
func (r *Renderer) canvasClipUnions() *renderCache[[32]byte, *canvas.Path] {
|
||||
key := canvasClipUnionKey{}
|
||||
if state, ok := r.sharedBackendStates[key]; ok {
|
||||
return state.(*renderCache[[32]byte, *canvas.Path])
|
||||
}
|
||||
if r.sharedBackendStates == nil {
|
||||
r.sharedBackendStates = make(map[any]any)
|
||||
}
|
||||
cache := resourceCache[[32]byte, *canvas.Path](r.Reader.resources, 8<<20)
|
||||
r.sharedBackendStates[key] = cache
|
||||
return cache
|
||||
}
|
||||
|
||||
// canvasParsedPaths 获取子渲染器共享的局部路径缓存,不保留对象变换
|
||||
// 返回: *renderCache[canvasPathKey, *canvas.Path] 路径缓存
|
||||
func (r *Renderer) canvasParsedPaths() *renderCache[canvasPathKey, *canvas.Path] {
|
||||
|
||||
@@ -146,6 +146,19 @@ func (e *Editor) Snapshot(ctx context.Context) (*Editor, error) {
|
||||
}
|
||||
snapshot.revision, snapshot.serial, snapshot.saveTarget = e.revision, e.serial, e.saveTarget
|
||||
snapshot.backends, snapshot.fontDirs, snapshot.fontFS = e.backends, slices.Clone(e.fontDirs), slices.Clone(e.fontFS)
|
||||
for _, resource := range e.resources {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resource.font == nil || resource.font.FontFile == "" || !next.fileNames[resource.name] {
|
||||
continue
|
||||
}
|
||||
data, err := next.readFileViewContext(ctx, resource.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snapshot.resources = append(snapshot.resources, editorResource{name: resource.name, data: data, font: cloneEditorData(resource.font)})
|
||||
}
|
||||
if e.source != nil {
|
||||
snapshot.source.retiredAttachments = maps.Clone(e.source.retiredAttachments)
|
||||
snapshot.source.removedAttachments = maps.Clone(e.source.removedAttachments)
|
||||
|
||||
Reference in new issue
Block a user