mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 12:12:40 +08:00
+3
-12
@@ -1003,7 +1003,7 @@ async function exportPDF() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setProgress("正在保存文档 PDF", 86);
|
setProgress("正在保存文档 PDF", 86);
|
||||||
const bytes = base64ToBytes(result.base64);
|
const bytes = result.bytes;
|
||||||
downloadBytes(bytes, "application/pdf", pdfFileName());
|
downloadBytes(bytes, "application/pdf", pdfFileName());
|
||||||
setStatus(`文档 PDF 已导出 ${formatBytes(result.size || bytes.length)}`);
|
setStatus(`文档 PDF 已导出 ${formatBytes(result.size || bytes.length)}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1047,7 +1047,7 @@ async function exportCurrentPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setProgress(`正在保存 ${result.label || label}`, 86);
|
setProgress(`正在保存 ${result.label || label}`, 86);
|
||||||
const bytes = base64ToBytes(result.base64);
|
const bytes = result.bytes;
|
||||||
downloadBytes(bytes, result.mime || info?.mime || "application/octet-stream", pageFileName(result.extension || info?.extension || format));
|
downloadBytes(bytes, result.mime || info?.mime || "application/octet-stream", pageFileName(result.extension || info?.extension || format));
|
||||||
setStatus(`${result.label || label} 已导出 ${formatBytes(result.size || bytes.length, result.label || label)}`);
|
setStatus(`${result.label || label} 已导出 ${formatBytes(result.size || bytes.length, result.label || label)}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -2159,7 +2159,7 @@ function callWASM(name, ...args) {
|
|||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
const result = JSON.parse(payload);
|
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new Error(result.error || "WASM 调用失败");
|
throw new Error(result.error || "WASM 调用失败");
|
||||||
}
|
}
|
||||||
@@ -2215,15 +2215,6 @@ function formatSize(value) {
|
|||||||
return value.toFixed(1);
|
return value.toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function base64ToBytes(base64) {
|
|
||||||
const binary = atob(base64 || "");
|
|
||||||
const bytes = new Uint8Array(binary.length);
|
|
||||||
for (let i = 0; i < binary.length; i += 1) {
|
|
||||||
bytes[i] = binary.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadBytes(bytes, mime, name) {
|
function downloadBytes(bytes, mime, name) {
|
||||||
const blob = new Blob([bytes], { type: mime });
|
const blob = new Blob([bytes], { type: mime });
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
|
|||||||
+40
-8
@@ -17,7 +17,6 @@
|
|||||||
package webui
|
package webui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
@@ -57,6 +56,9 @@ func registerCallback(name string, fn func([]js.Value) (any, error)) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return encodeResult(apiResult{OK: false, Error: err.Error()})
|
return encodeResult(apiResult{OK: false, Error: err.Error()})
|
||||||
}
|
}
|
||||||
|
if result, ok := data.(js.Value); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
return encodeResult(apiResult{OK: true, Data: data})
|
return encodeResult(apiResult{OK: true, Data: data})
|
||||||
})
|
})
|
||||||
js.Global().Set(name, cb)
|
js.Global().Set(name, cb)
|
||||||
@@ -113,7 +115,18 @@ func renderPage(args []js.Value) (any, error) {
|
|||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return nil, fmt.Errorf("missing page index")
|
return nil, fmt.Errorf("missing page index")
|
||||||
}
|
}
|
||||||
return currentSession.RenderPageSVG(args[0].Int())
|
page, err := currentSession.RenderPageSVG(args[0].Int())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return successResult(map[string]any{
|
||||||
|
"index": page.Index,
|
||||||
|
"number": page.Number,
|
||||||
|
"id": page.ID,
|
||||||
|
"width": page.Width,
|
||||||
|
"height": page.Height,
|
||||||
|
"svg": page.SVG,
|
||||||
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// exportFormats 获取导出格式
|
// exportFormats 获取导出格式
|
||||||
@@ -137,14 +150,14 @@ func exportPage(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return map[string]any{
|
return successResult(map[string]any{
|
||||||
"base64": base64.StdEncoding.EncodeToString(data),
|
"bytes": bytesToJS(data),
|
||||||
"size": len(data),
|
"size": len(data),
|
||||||
"format": format.Value,
|
"format": format.Value,
|
||||||
"label": format.Label,
|
"label": format.Label,
|
||||||
"extension": format.Extension,
|
"extension": format.Extension,
|
||||||
"mime": format.MIME,
|
"mime": format.MIME,
|
||||||
}, nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// exportPDF 导出OFD文档为PDF
|
// exportPDF 导出OFD文档为PDF
|
||||||
@@ -158,10 +171,29 @@ func exportPDF(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return map[string]any{
|
return successResult(map[string]any{
|
||||||
"base64": base64.StdEncoding.EncodeToString(data),
|
"bytes": bytesToJS(data),
|
||||||
"size": len(data),
|
"size": len(data),
|
||||||
}, nil
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// successResult 创建成功接口结果
|
||||||
|
// 入参: data 返回数据
|
||||||
|
// 返回: js.Value 接口结果
|
||||||
|
func successResult(data any) js.Value {
|
||||||
|
result := js.Global().Get("Object").New()
|
||||||
|
result.Set("ok", true)
|
||||||
|
result.Set("data", data)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// bytesToJS 将字节数据转换为Uint8Array
|
||||||
|
// 入参: data 字节数据
|
||||||
|
// 返回: js.Value Uint8Array对象
|
||||||
|
func bytesToJS(data []byte) js.Value {
|
||||||
|
value := js.Global().Get("Uint8Array").New(len(data))
|
||||||
|
js.CopyBytesToJS(value, data)
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// fontSystemNames 获取系统字体名称
|
// fontSystemNames 获取系统字体名称
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
|
|||||||
DrawParams: reader.drawParamCache,
|
DrawParams: reader.drawParamCache,
|
||||||
CompositeGraphicUnits: reader.compositeGraphicUnitCache,
|
CompositeGraphicUnits: reader.compositeGraphicUnitCache,
|
||||||
FontMap: make(map[string]*canvas.FontFamily),
|
FontMap: make(map[string]*canvas.FontFamily),
|
||||||
|
fontFSCache: make(map[fontFSKey]*canvas.FontFamily),
|
||||||
}
|
}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(r)
|
opt(r)
|
||||||
|
|||||||
+5
-4
@@ -34,6 +34,7 @@ type FontFile struct {
|
|||||||
type FontFS struct {
|
type FontFS struct {
|
||||||
files map[string][]byte
|
files map[string][]byte
|
||||||
names []string
|
names []string
|
||||||
|
candidates []fontFileCandidate
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFontFS 创建内存字体文件系统
|
// NewFontFS 创建内存字体文件系统
|
||||||
@@ -54,6 +55,7 @@ func NewFontFS(fonts []FontFile) *FontFS {
|
|||||||
fsys.files[name] = append([]byte(nil), font.Data...)
|
fsys.files[name] = append([]byte(nil), font.Data...)
|
||||||
}
|
}
|
||||||
sort.Strings(fsys.names)
|
sort.Strings(fsys.names)
|
||||||
|
fsys.candidates = fontFileCandidates(fsys.names, path.Base)
|
||||||
return fsys
|
return fsys
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,11 +148,10 @@ func (fsys *FontFS) matchStyle(pattern string, bold, italic bool) []string {
|
|||||||
// 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体
|
// 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体
|
||||||
// 返回: []string 字体文件列表
|
// 返回: []string 字体文件列表
|
||||||
func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string {
|
func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string {
|
||||||
files := fontFileCandidates(fsys.names, path.Base)
|
matches := make([]fontFileMatch, 0, len(fsys.candidates))
|
||||||
matches := make([]fontFileMatch, 0, len(files))
|
seen := make(map[string]int, len(fsys.candidates))
|
||||||
seen := make(map[string]int, len(files))
|
|
||||||
for _, matcher := range newFontPatternMatchers(patterns) {
|
for _, matcher := range newFontPatternMatchers(patterns) {
|
||||||
for _, file := range files {
|
for _, file := range fsys.candidates {
|
||||||
rank := matcher.rankCandidate(file)
|
rank := matcher.rankCandidate(file)
|
||||||
appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
|
appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-14
@@ -22,6 +22,13 @@ import (
|
|||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// fontFSKey 字体文件系统缓存键
|
||||||
|
type fontFSKey struct {
|
||||||
|
index int
|
||||||
|
name string
|
||||||
|
style canvas.FontStyle
|
||||||
|
}
|
||||||
|
|
||||||
// loadFont 加载字体
|
// loadFont 加载字体
|
||||||
// 入参: fontID 字体ID
|
// 入参: fontID 字体ID
|
||||||
// 返回: *canvas.FontFamily 字体族
|
// 返回: *canvas.FontFamily 字体族
|
||||||
@@ -99,27 +106,19 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, fsys := range r.fontFS {
|
for index, fsys := range r.fontFS {
|
||||||
for _, m := range fontFSMatchesStyle(fsys, patterns, boldStyle, italicStyle) {
|
for _, m := range fontFSMatchesStyle(fsys, patterns, boldStyle, italicStyle) {
|
||||||
resData, err := fs.ReadFile(fsys, m)
|
if loaded := r.loadFontFromFS(fontID, ff, index, fsys, m, fontStyle); loaded != nil {
|
||||||
if err == nil {
|
return loaded
|
||||||
if err := ff.LoadFont(resData, 0, fontStyle); err == nil {
|
|
||||||
r.FontMap[fontID] = ff
|
|
||||||
return ff
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !canLoadSystemFonts() {
|
if !canLoadSystemFonts() {
|
||||||
for _, fsys := range r.fontFS {
|
for index, fsys := range r.fontFS {
|
||||||
if matches, err := fs.Glob(fsys, "*"); err == nil {
|
if matches, err := fs.Glob(fsys, "*"); err == nil {
|
||||||
for _, m := range matches {
|
for _, m := range matches {
|
||||||
resData, err := fs.ReadFile(fsys, m)
|
if loaded := r.loadFontFromFS(fontID, ff, index, fsys, m, fontStyle); loaded != nil {
|
||||||
if err == nil {
|
return loaded
|
||||||
if err := ff.LoadFont(resData, 0, fontStyle); err == nil {
|
|
||||||
r.FontMap[fontID] = ff
|
|
||||||
return ff
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,6 +154,38 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
|||||||
return defaultFont
|
return defaultFont
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadFontFromFS 从字体文件系统加载字体
|
||||||
|
// 入参: fontID 字体ID, family 字体族, index 文件系统索引, fsys 字体文件系统, name 字体文件名, style 字体样式
|
||||||
|
// 返回: *canvas.FontFamily 字体族
|
||||||
|
func (r *Renderer) loadFontFromFS(fontID string, family *canvas.FontFamily, index int, fsys fs.FS, name string, style canvas.FontStyle) *canvas.FontFamily {
|
||||||
|
key := fontFSKey{index: index, name: name, style: style}
|
||||||
|
if cached := r.fontFSCache[key]; cached != nil {
|
||||||
|
r.FontMap[fontID] = cached
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
data, err := readFontData(fsys, name)
|
||||||
|
if err != nil || family.LoadFont(data, 0, style) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r.fontFSCache[key] = family
|
||||||
|
r.FontMap[fontID] = family
|
||||||
|
return family
|
||||||
|
}
|
||||||
|
|
||||||
|
// readFontData 读取字体文件数据
|
||||||
|
// 入参: fsys 字体文件系统, name 字体文件名
|
||||||
|
// 返回: []byte 字体文件数据, error 错误信息
|
||||||
|
func readFontData(fsys fs.FS, name string) ([]byte, error) {
|
||||||
|
if fontFS, ok := fsys.(*FontFS); ok {
|
||||||
|
data, ok := fontFS.files[cleanFontName(name)]
|
||||||
|
if !ok {
|
||||||
|
return nil, fs.ErrNotExist
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
return fs.ReadFile(fsys, name)
|
||||||
|
}
|
||||||
|
|
||||||
// matchFontFiles 查找字体文件
|
// matchFontFiles 查找字体文件
|
||||||
// 入参: dir 目录, patterns 模式列表, bold 是否粗体, italic 是否斜体
|
// 入参: dir 目录, patterns 模式列表, bold 是否粗体, italic 是否斜体
|
||||||
// 返回: []string 文件列表
|
// 返回: []string 文件列表
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type Renderer struct {
|
|||||||
FontMap map[string]*canvas.FontFamily
|
FontMap map[string]*canvas.FontFamily
|
||||||
FontGIDMap map[string]map[uint16]rune
|
FontGIDMap map[string]map[uint16]rune
|
||||||
FontCIDMap map[string]map[uint16]rune
|
FontCIDMap map[string]map[uint16]rune
|
||||||
|
fontFSCache map[fontFSKey]*canvas.FontFamily
|
||||||
fontDirs []string
|
fontDirs []string
|
||||||
fontFS []fs.FS
|
fontFS []fs.FS
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ func replacePDFProducer(data []byte) []byte {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
dst := []byte("/Producer(xiaoqidun/ofdgo)")
|
dst := []byte("/Producer(xiaoqidun/ofdgo)")
|
||||||
result := make([]byte, 0, len(data)-len(old)+len(dst))
|
copy(data[idx:idx+len(old)], dst)
|
||||||
return append(append(append(result, data[:idx]...), dst...), data[idx+len(old):]...)
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderToPDF 渲染为PDF
|
// RenderToPDF 渲染为PDF
|
||||||
|
|||||||
+16
-9
@@ -144,13 +144,21 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
}
|
}
|
||||||
for i, glyph := range glyphs {
|
for i, glyph := range glyphs {
|
||||||
str := glyph.Text
|
str := glyph.Text
|
||||||
|
drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0
|
||||||
|
var glyphPath *canvas.Path
|
||||||
|
var glyphWidth float64
|
||||||
|
if drawAsGlyphPath {
|
||||||
|
glyphPath, glyphWidth = textGlyphPath(face, glyph)
|
||||||
|
} else {
|
||||||
|
glyphWidth = textGlyphWidth(face, glyph)
|
||||||
|
}
|
||||||
if i < len(xs) {
|
if i < len(xs) {
|
||||||
cx = xs[i]
|
cx = xs[i]
|
||||||
} else if i > 0 {
|
} else if i > 0 {
|
||||||
if dx, ok := textDelta(dxs, i-1); ok {
|
if dx, ok := textDelta(dxs, i-1); ok {
|
||||||
cx += dx
|
cx += dx
|
||||||
} else if len(dys) == 0 {
|
} else if len(dys) == 0 {
|
||||||
cx += textGlyphWidth(face, glyph) * hScale
|
cx += glyphWidth * hScale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if i < len(ys) {
|
if i < len(ys) {
|
||||||
@@ -169,7 +177,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
tx, ty := ctm.Transform(cx, cy)
|
tx, ty := ctm.Transform(cx, cy)
|
||||||
canvasX, canvasY = tx+bx, pageH-(ty+by)
|
canvasX, canvasY = tx+bx, pageH-(ty+by)
|
||||||
}
|
}
|
||||||
textWidth := textGlyphWidth(face, glyph) * hScale
|
textWidth := glyphWidth * hScale
|
||||||
glyphFillPaint := fillPaint
|
glyphFillPaint := fillPaint
|
||||||
if useGlyphFillPaint {
|
if useGlyphFillPaint {
|
||||||
glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY)
|
glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY)
|
||||||
@@ -178,21 +186,20 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
if glyphFillPaint != nil {
|
if glyphFillPaint != nil {
|
||||||
ctx.SetFill(glyphFillPaint)
|
ctx.SetFill(glyphFillPaint)
|
||||||
drawGlyph := func(x, y float64) {
|
drawGlyph := func(x, y float64) {
|
||||||
if drawAsPath || glyph.GlyphID >= 0 {
|
if drawAsGlyphPath {
|
||||||
path, width := textGlyphPath(face, glyph)
|
|
||||||
scaleX := hScale
|
scaleX := hScale
|
||||||
if advanceLimit > 0 && width*scaleX > advanceLimit {
|
if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
|
||||||
scaleX = advanceLimit / width
|
scaleX = advanceLimit / glyphWidth
|
||||||
}
|
}
|
||||||
textWidth = width * scaleX
|
textWidth = glyphWidth * scaleX
|
||||||
if scaleX != 1 {
|
if scaleX != 1 {
|
||||||
ctx.Push()
|
ctx.Push()
|
||||||
ctx.Translate(x, y)
|
ctx.Translate(x, y)
|
||||||
ctx.Scale(scaleX, 1)
|
ctx.Scale(scaleX, 1)
|
||||||
ctx.DrawPath(0, 0, path)
|
ctx.DrawPath(0, 0, glyphPath)
|
||||||
ctx.Pop()
|
ctx.Pop()
|
||||||
} else {
|
} else {
|
||||||
ctx.DrawPath(x, y, path)
|
ctx.DrawPath(x, y, glyphPath)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
scaled := hScale != 1
|
scaled := hScale != 1
|
||||||
|
|||||||
Reference in New Issue
Block a user