mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:53:44 +08:00
1 parent
974b515374
commit
67f4cf9d2c
13 files changed
+391
-34
No files matched your search
@@ -7687,13 +7687,18 @@ function chooseResourceFile(action) {
|
|||||||
async function changeManagedResource(action, item, file) {
|
async function changeManagedResource(action, item, file) {
|
||||||
if (!item || !state.editing) return;
|
if (!item || !state.editing) return;
|
||||||
releaseResourcePreview();
|
releaseResourcePreview();
|
||||||
|
const page = Number(resourceElements.Page.value), object = resourceElements.Object.value;
|
||||||
const format = file?.name.includes(".") ? file.name.split(".").at(-1).toUpperCase() : "";
|
const format = file?.name.includes(".") ? file.name.split(".").at(-1).toUpperCase() : "";
|
||||||
const changed = item.kind === "Attachment" && action !== "bind" && action !== "unbind"
|
const changed = item.kind === "Attachment" && action !== "bind" && action !== "unbind"
|
||||||
? await changeDocument("ofdgoChangeAttachment", null, action, item.id, file ? [file.name] : [], file ? [file] : [])
|
? 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) {
|
if (changed) {
|
||||||
await loadManagedResources();
|
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 (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"iter"
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/go-text/typesetting/font/opentype/tables"
|
"github.com/go-text/typesetting/font/opentype/tables"
|
||||||
)
|
)
|
||||||
@@ -34,10 +36,12 @@ type SFNT struct {
|
|||||||
reverse map[uint16][]rune
|
reverse map[uint16][]rune
|
||||||
}
|
}
|
||||||
|
|
||||||
// sfntCmap 保存编码记录及对应字符映射,标记已解析的共享子表
|
// sfntCmap 保存编码记录及字符映射,连续区间与共享子表不重复展开
|
||||||
type sfntCmap struct {
|
type sfntCmap struct {
|
||||||
platform, encoding, format uint16
|
platform, encoding, format uint16
|
||||||
mapping map[rune]uint16
|
mapping map[rune]uint16
|
||||||
|
groups []byte
|
||||||
|
entries uint64
|
||||||
duplicate bool
|
duplicate bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +94,7 @@ func ParseSFNTTables(table map[string][]byte) (*SFNT, error) {
|
|||||||
if cmap.duplicate {
|
if cmap.duplicate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, glyph := range cmap.mapping {
|
for _, glyph := range cmap.pairs() {
|
||||||
if glyph >= f.count {
|
if glyph >= f.count {
|
||||||
return nil, fmt.Errorf("cmap glyph index out of range")
|
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:]))
|
count := int(binary.BigEndian.Uint16(data[2:]))
|
||||||
result := make([]sfntCmap, 0, count)
|
result := make([]sfntCmap, 0, count)
|
||||||
parsed := make(map[uint32]map[rune]uint16)
|
parsed := make(map[uint32]sfntCmap)
|
||||||
for i := range count {
|
for i := range count {
|
||||||
record := data[4+8*i:]
|
record := data[4+8*i:]
|
||||||
offset := binary.BigEndian.Uint32(record[4:])
|
offset := binary.BigEndian.Uint32(record[4:])
|
||||||
@@ -128,23 +132,44 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
|
|||||||
}
|
}
|
||||||
sub := data[offset:]
|
sub := data[offset:]
|
||||||
format := binary.BigEndian.Uint16(sub)
|
format := binary.BigEndian.Uint16(sub)
|
||||||
mapping, found := parsed[offset]
|
cmap, found := parsed[offset]
|
||||||
if !found {
|
if !found {
|
||||||
mapping = make(map[rune]uint16)
|
cmap.mapping = make(map[rune]uint16)
|
||||||
var err error
|
var err error
|
||||||
switch format {
|
switch format {
|
||||||
case 0:
|
case 0:
|
||||||
parseCmapFormat0(sub, mapping)
|
parseCmapFormat0(sub, cmap.mapping)
|
||||||
case 4:
|
case 4:
|
||||||
parseCmapFormat4(sub, mapping)
|
parseCmapFormat4(sub, cmap.mapping)
|
||||||
case 6:
|
case 6:
|
||||||
parseCmapFormat6(sub, mapping)
|
parseCmapFormat6(sub, cmap.mapping)
|
||||||
case 12:
|
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:
|
case 2:
|
||||||
mapping, err = parseCmapFormat2(sub)
|
cmap.mapping, err = parseCmapFormat2(sub)
|
||||||
case 8, 10, 13:
|
case 8, 10, 13:
|
||||||
mapping, err = parseCmapExtended(sub)
|
cmap.mapping, err = parseCmapExtended(sub)
|
||||||
case 14:
|
case 14:
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported cmap format %d", format)
|
return nil, fmt.Errorf("unsupported cmap format %d", format)
|
||||||
@@ -152,13 +177,67 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
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 返回字体设计单位
|
// UnitsPerEm 返回字体设计单位
|
||||||
// 返回: uint16 每em单位数
|
// 返回: uint16 每em单位数
|
||||||
func (f *SFNT) UnitsPerEm() uint16 { return f.units }
|
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 {
|
func (f *SFNT) CmapEntries() uint64 {
|
||||||
var count uint64
|
var count uint64
|
||||||
for _, cmap := range f.cmaps {
|
for _, cmap := range f.cmaps {
|
||||||
count += uint64(len(cmap.mapping))
|
count += uint64(len(cmap.mapping)) + cmap.entries
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
@@ -185,14 +264,14 @@ func (f *SFNT) GlyphIndex(char rune) uint16 {
|
|||||||
if cmap.duplicate {
|
if cmap.duplicate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if gid := cmap.mapping[char]; gid != 0 {
|
if gid, ok := cmap.lookup(char); ok && gid != 0 {
|
||||||
return gid
|
return gid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// glyphCharacters 返回指向指定字形的全部字符
|
// glyphCharacters 返回指向指定字形的全部字符,连续区间按需反查
|
||||||
// 入参: gid 字形编号
|
// 入参: gid 字形编号
|
||||||
// 返回: []rune 排序后的字符
|
// 返回: []rune 排序后的字符
|
||||||
func (f *SFNT) glyphCharacters(gid uint16) []rune {
|
func (f *SFNT) glyphCharacters(gid uint16) []rune {
|
||||||
@@ -204,7 +283,7 @@ func (f *SFNT) glyphCharacters(gid uint16) []rune {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for char, glyph := range cmap.mapping {
|
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)
|
f.reverse[glyph] = append(f.reverse[glyph], char)
|
||||||
seen[char] = true
|
seen[char] = true
|
||||||
}
|
}
|
||||||
@@ -214,7 +293,28 @@ func (f *SFNT) glyphCharacters(gid uint16) []rune {
|
|||||||
slices.Sort(chars)
|
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 返回水平字宽
|
// GlyphAdvance 返回水平字宽
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ func subsetSourceSFNT(sfnt *SFNT, usage *Usage, size int) []byte {
|
|||||||
for char := range usage.Characters {
|
for char := range usage.Characters {
|
||||||
id := sfnt.GlyphIndex(char)
|
id := sfnt.GlyphIndex(char)
|
||||||
for _, cmap := range unicodeTables {
|
for _, cmap := range unicodeTables {
|
||||||
if alternate, ok := cmap.mapping[char]; ok && alternate != id {
|
if alternate, ok := cmap.lookup(char); ok && alternate != id {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-2
@@ -291,6 +291,132 @@ func editorObjectFontScan(object GraphicObject) bool {
|
|||||||
// 入参: progress 保存进度回调
|
// 入参: progress 保存进度回调
|
||||||
// 返回: map[string][]byte 包内字体子集, error 错误信息
|
// 返回: map[string][]byte 包内字体子集, error 错误信息
|
||||||
func (e *Editor) subsetFonts(progress editorProgress) (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()
|
ctx := context.Background()
|
||||||
if e.output != nil {
|
if e.output != nil {
|
||||||
ctx = e.output.ctx
|
ctx = e.output.ctx
|
||||||
@@ -303,7 +429,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !slices.ContainsFunc(e.resources, func(resource editorResource) bool {
|
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
|
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 {
|
if err := progress.report("fonts", i, total); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resource.font.FontFile == "" {
|
if resource.font.FontFile == "" || e.source != nil && e.source.reader.fileNames[resource.name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
resourceFont := e.fonts[resource.font.ID]
|
resourceFont := e.fonts[resource.font.ID]
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ type pdfImporter struct {
|
|||||||
pageActionPage int
|
pageActionPage int
|
||||||
fontIDs map[*pdfgo.Font]string
|
fontIDs map[*pdfgo.Font]string
|
||||||
fonts map[*pdfgo.Font]*pdfImportedFont
|
fonts map[*pdfgo.Font]*pdfImportedFont
|
||||||
|
fontPrograms map[pdfFontProgramKey]*pdfImportedFont
|
||||||
symbolFonts map[*pdfgo.Font]*pdfImportedFont
|
symbolFonts map[*pdfgo.Font]*pdfImportedFont
|
||||||
fontWarnings map[string]bool
|
fontWarnings map[string]bool
|
||||||
clipTexts map[*pdfgo.TextClip][]TextObject
|
clipTexts map[*pdfgo.TextClip][]TextObject
|
||||||
|
|||||||
+4
-1
@@ -398,10 +398,13 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
|
|||||||
if p.symbolFonts == nil {
|
if p.symbolFonts == nil {
|
||||||
p.symbolFonts = make(map[*pdfgo.Font]*pdfImportedFont)
|
p.symbolFonts = make(map[*pdfgo.Font]*pdfImportedFont)
|
||||||
}
|
}
|
||||||
|
if p.fontPrograms == nil {
|
||||||
|
p.fontPrograms = make(map[pdfFontProgramKey]*pdfImportedFont)
|
||||||
|
}
|
||||||
if p.clipPaths == nil {
|
if p.clipPaths == nil {
|
||||||
p.clipPaths = resourceCache[[32]byte, pdfClipPath](p.resources, 8<<20)
|
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 {
|
if err := build(&local); err != nil {
|
||||||
return nil, box, err
|
return nil, box, err
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-4
@@ -38,6 +38,12 @@ type pdfImportedFont struct {
|
|||||||
repairLimit uint16
|
repairLimit uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pdfFontProgramKey 区分字体封装内容及交给资源后端的来源名称
|
||||||
|
type pdfFontProgramKey struct {
|
||||||
|
checksum [32]byte
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
// pdfTextReplacement 记录一个完整替换区段的文字数量与转换状态
|
// pdfTextReplacement 记录一个完整替换区段的文字数量与转换状态
|
||||||
type pdfTextReplacement struct {
|
type pdfTextReplacement struct {
|
||||||
text *pdfgo.ReplacementText
|
text *pdfgo.ReplacementText
|
||||||
@@ -661,13 +667,17 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
|
|||||||
if p.editor.backends.FontResources == nil {
|
if p.editor.backends.FontResources == nil {
|
||||||
return nil, fmt.Errorf("PDF font %s resource: font resources: %w", source.Name, ErrBackendUnavailable)
|
return nil, fmt.Errorf("PDF font %s resource: font resources: %w", source.Name, ErrBackendUnavailable)
|
||||||
}
|
}
|
||||||
|
if p.editor.backends.Fonts == nil {
|
||||||
|
return nil, fmt.Errorf("PDF font backend unavailable")
|
||||||
|
}
|
||||||
|
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)
|
result.resource, err = p.editor.backends.FontResources.OpenFontResource(FontFile{Name: source.Name + ".ttf", Data: program}, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("PDF font %s resource: %w", source.Name, err)
|
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)
|
result.metrics, err = p.editor.backends.Fonts.OpenFont(result.resource.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("PDF font %s metrics: %w", source.Name, err)
|
return nil, fmt.Errorf("PDF font %s metrics: %w", source.Name, err)
|
||||||
@@ -675,7 +685,9 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
|
|||||||
if _, ok := result.metrics.(FontOutlines); !ok {
|
if _, ok := result.metrics.(FontOutlines); !ok {
|
||||||
return nil, fmt.Errorf("PDF font backend does not provide glyph outlines")
|
return nil, fmt.Errorf("PDF font backend does not provide glyph outlines")
|
||||||
}
|
}
|
||||||
result.checksum, result.repairLimit = sha256.Sum256(result.resource.Data), repair.MetricsLimit
|
result.checksum = sha256.Sum256(result.resource.Data)
|
||||||
|
}
|
||||||
|
result.repairLimit = repair.MetricsLimit
|
||||||
if source.Recovered || programRecovered {
|
if source.Recovered || programRecovered {
|
||||||
if err := p.preservePDFObject(p.ctx, "Font", source.Dictionary); err != nil {
|
if err := p.preservePDFObject(p.ctx, "Font", source.Dictionary); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -690,6 +702,13 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
|
|||||||
if p.fonts == nil {
|
if p.fonts == nil {
|
||||||
p.fonts = make(map[*pdfgo.Font]*pdfImportedFont)
|
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
|
p.fonts[source] = result
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
@@ -120,7 +121,7 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
p, err := rasterCanvasPath(cmd.Path, pathData)
|
p, err := rasterCanvasPath(visibleRasterPath(cmd, page), pathData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("canvas command %d: %w", i, err)
|
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
|
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 预留路径容量,保留光栅指令的构建及闭合规则
|
// rasterCanvasPath 预留路径容量,保留光栅指令的构建及闭合规则
|
||||||
// 入参: path 只读光栅路径, buffer 可复用的独占路径缓冲
|
// 入参: path 只读光栅路径, buffer 可复用的独占路径缓冲
|
||||||
// 返回: *canvas.Path 使用独占缓冲的绘制路径, error 不支持的指令
|
// 返回: *canvas.Path 使用独占缓冲的绘制路径, error 不支持的指令
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
type clipRenderer struct {
|
type clipRenderer struct {
|
||||||
path *canvas.Path
|
path *canvas.Path
|
||||||
clip *canvas.Path
|
clip *canvas.Path
|
||||||
|
unions *renderCache[[32]byte, *canvas.Path]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size 返回不限定边界的裁剪画布尺寸
|
// Size 返回不限定边界的裁剪画布尺寸
|
||||||
@@ -57,13 +58,23 @@ func (r *clipRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
|
|||||||
// 入参: img 图像, m 变换矩阵
|
// 入参: img 图像, m 变换矩阵
|
||||||
func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {}
|
func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {}
|
||||||
|
|
||||||
// add 合并裁剪轮廓
|
// add 合并裁剪轮廓,按完整输入复用已有并集
|
||||||
// 入参: path 裁剪路径
|
// 入参: path 裁剪路径
|
||||||
func (r *clipRenderer) add(path *canvas.Path) {
|
func (r *clipRenderer) add(path *canvas.Path) {
|
||||||
if r.path == nil {
|
if r.path == nil {
|
||||||
r.path = path
|
r.path = path
|
||||||
|
} 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 {
|
} else {
|
||||||
r.path = unionClipPath(r.path, path)
|
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
|
var p *canvas.Path
|
||||||
for _, clip := range clips.Clip {
|
for _, clip := range clips.Clip {
|
||||||
renderer := &clipRenderer{}
|
renderer := &clipRenderer{unions: r.canvasClipUnions()}
|
||||||
for _, area := range clip.Area {
|
for _, area := range clip.Area {
|
||||||
areaCTM := matrix.Multiply(NewMatrix(area.CTM))
|
areaCTM := matrix.Multiply(NewMatrix(area.CTM))
|
||||||
for _, pathObj := range area.Path {
|
for _, pathObj := range area.Path {
|
||||||
|
|||||||
@@ -556,6 +556,14 @@ func clipLinearFillGeometry(path, clip GeometryPath) (GeometryPath, bool, error)
|
|||||||
return *geometryFromCanvasPath(result), true, nil
|
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轴
|
// actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴
|
||||||
// 入参: matrix 页面变换
|
// 入参: matrix 页面变换
|
||||||
// 返回: canvas.Matrix 路径变换
|
// 返回: canvas.Matrix 路径变换
|
||||||
|
|||||||
@@ -227,7 +227,11 @@ func (run *TextRun) addSpan(start, end int, bounds canvas.Rect, m canvas.Matrix,
|
|||||||
if path.Empty() {
|
if path.Empty() {
|
||||||
return
|
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 {
|
if bounds.W() < 0 || bounds.H() <= 0 {
|
||||||
return
|
return
|
||||||
@@ -238,8 +242,8 @@ func (run *TextRun) addSpan(start, end int, bounds canvas.Rect, m canvas.Matrix,
|
|||||||
span = run.Spans[n-1]
|
span = run.Spans[n-1]
|
||||||
v := span.Matrix
|
v := span.Matrix
|
||||||
previous := canvas.Matrix{{v[0], v[2], v[4]}, {v[1], v[3], v[5]}}
|
previous := canvas.Matrix{{v[0], v[2], v[4]}, {v[1], v[3], v[5]}}
|
||||||
if previous.Det() != 0 {
|
if inverse, ok := invertCanvasMatrix(previous); ok {
|
||||||
bounds = canvas.Rect{X1: 1, Y1: 1}.Add(canvas.Rect{X1: 1, Y1: 1}.Transform(previous.Inv().Mul(m)))
|
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())
|
m = previous.Translate(bounds.X0, bounds.Y0).Scale(bounds.W(), bounds.H())
|
||||||
}
|
}
|
||||||
run.Spans = run.Spans[:n-1]
|
run.Spans = run.Spans[:n-1]
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ type canvasFontKey struct {
|
|||||||
// canvasClipCacheKey 区分不依赖文档资源的共享裁剪缓存
|
// canvasClipCacheKey 区分不依赖文档资源的共享裁剪缓存
|
||||||
type canvasClipCacheKey struct{}
|
type canvasClipCacheKey struct{}
|
||||||
|
|
||||||
|
// canvasClipUnionKey 区分文字和图形裁剪的共享并集缓存
|
||||||
|
type canvasClipUnionKey struct{}
|
||||||
|
|
||||||
// canvasPathCacheKey 区分不依赖文档资源的共享局部路径缓存
|
// canvasPathCacheKey 区分不依赖文档资源的共享局部路径缓存
|
||||||
type canvasPathCacheKey struct{}
|
type canvasPathCacheKey struct{}
|
||||||
|
|
||||||
@@ -83,6 +86,21 @@ func (r *Renderer) canvasClipIntersections() *renderCache[[32]byte, *canvas.Path
|
|||||||
return cache
|
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 获取子渲染器共享的局部路径缓存,不保留对象变换
|
// canvasParsedPaths 获取子渲染器共享的局部路径缓存,不保留对象变换
|
||||||
// 返回: *renderCache[canvasPathKey, *canvas.Path] 路径缓存
|
// 返回: *renderCache[canvasPathKey, *canvas.Path] 路径缓存
|
||||||
func (r *Renderer) 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.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)
|
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 {
|
if e.source != nil {
|
||||||
snapshot.source.retiredAttachments = maps.Clone(e.source.retiredAttachments)
|
snapshot.source.retiredAttachments = maps.Clone(e.source.retiredAttachments)
|
||||||
snapshot.source.removedAttachments = maps.Clone(e.source.removedAttachments)
|
snapshot.source.removedAttachments = maps.Clone(e.source.removedAttachments)
|
||||||
|
|||||||
Reference in new issue
Block a user