mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:53:44 +08:00
1 parent
983a856736
commit
98d9effc8c
18 files changed
+668
-133
No files matched your search
+32
-18
@@ -3575,19 +3575,7 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function selectLocalFonts(fonts) {
|
async function selectLocalFonts(fonts) {
|
||||||
const available = new Map();
|
const available = fontManager.localEntries(fonts);
|
||||||
for (const font of fonts) {
|
|
||||||
const names = [
|
|
||||||
fontManager.localName(font),
|
|
||||||
font.postscriptName && `${font.postscriptName}.ttf`,
|
|
||||||
font.family && `${[font.family, font.style].filter(Boolean).join(" ")}.ttf`,
|
|
||||||
];
|
|
||||||
for (const name of names) {
|
|
||||||
if (name && !available.has(name)) {
|
|
||||||
available.set(name, font);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const matched = await callWASM("ofdgoMatchFontFiles", [...available.keys()]);
|
const matched = await callWASM("ofdgoMatchFontFiles", [...available.keys()]);
|
||||||
return [...new Set(matched.map((name) => available.get(name)))];
|
return [...new Set(matched.map((name) => available.get(name)))];
|
||||||
}
|
}
|
||||||
@@ -4502,11 +4490,15 @@ function updateBatchControls() {
|
|||||||
batchElements.Cancel.disabled = batch.canceled;
|
batchElements.Cancel.disabled = batch.canceled;
|
||||||
batchElements.Close.textContent = batch.running ? "收起" : "关闭";
|
batchElements.Close.textContent = batch.running ? "收起" : "关闭";
|
||||||
batchElements.Count.textContent = batch.items.length ? `${batch.items.length}个文件` : "";
|
batchElements.Count.textContent = batch.items.length ? `${batch.items.length}个文件` : "";
|
||||||
const formats = batch.items.length ? batch.items.map(item => item.format || batchElements.Format.value) : [batchElements.Format.value];
|
batchElements.DPIRow.hidden = !(batch.items.length ? batch.items : [null]).some(batchUsesDPI);
|
||||||
batchElements.DPIRow.hidden = !formats.some(value => batch.formats.find(format => format.value === value)?.raster);
|
|
||||||
batchElements.Button.toggleAttribute("data-running", batch.running);
|
batchElements.Button.toggleAttribute("data-running", batch.running);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function batchUsesDPI(item) {
|
||||||
|
const format = item?.format || batchElements.Format.value;
|
||||||
|
return Boolean(batch.formats.find(value => value.value === format)?.raster || item && /\.pdf$/i.test(item.file.name));
|
||||||
|
}
|
||||||
|
|
||||||
function batchStatus(item, status, text) {
|
function batchStatus(item, status, text) {
|
||||||
item.status = status;
|
item.status = status;
|
||||||
item.text = text;
|
item.text = text;
|
||||||
@@ -4558,12 +4550,24 @@ function startBatchWorker() {
|
|||||||
};
|
};
|
||||||
worker.onerror = event => fail(new Error(event.message || "转换引擎异常"));
|
worker.onerror = event => fail(new Error(event.message || "转换引擎异常"));
|
||||||
worker.onmessageerror = () => fail(new Error("转换数据传递失败"));
|
worker.onmessageerror = () => fail(new Error("转换数据传递失败"));
|
||||||
worker.onmessage = ({ data }) => {
|
worker.onmessage = async ({ data }) => {
|
||||||
if (data.type === "ready") { resolve(); return; }
|
if (data.type === "ready") { resolve(); return; }
|
||||||
if (data.type === "exit") { fail(new Error(data.error)); return; }
|
if (data.type === "exit") { fail(new Error(data.error)); return; }
|
||||||
if (data.type === "progress") { batchElements.Status.textContent = data.text; return; }
|
if (data.type === "progress") { batchElements.Status.textContent = data.text; return; }
|
||||||
const request = batch.requests.get(data.id);
|
const request = batch.requests.get(data.id);
|
||||||
if (!request) return;
|
if (!request) return;
|
||||||
|
if (data.type === "font") {
|
||||||
|
try {
|
||||||
|
const font = batch.localFonts.get(data.name);
|
||||||
|
if (!font) throw new Error("系统字体不可用");
|
||||||
|
if (!batch.fontData.has(font)) batch.fontData.set(font, fontManager.read(font));
|
||||||
|
const bytes = await batch.fontData.get(font);
|
||||||
|
worker.postMessage({ type: "font", request: data.request, bytes });
|
||||||
|
} catch (err) {
|
||||||
|
worker.postMessage({ type: "font", request: data.request, error: err.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data.type === "output") { request.outputName = data.name; return; }
|
if (data.type === "output") { request.outputName = data.name; return; }
|
||||||
if (data.type === "conversion") { request.onProgress?.(data); return; }
|
if (data.type === "conversion") { request.onProgress?.(data); return; }
|
||||||
batch.requests.delete(data.id);
|
batch.requests.delete(data.id);
|
||||||
@@ -4593,9 +4597,10 @@ async function runBatch() {
|
|||||||
const dpi = Number(batchElements.DPI.value);
|
const dpi = Number(batchElements.DPI.value);
|
||||||
const archive = batchElements.Destination.value === "archive";
|
const archive = batchElements.Destination.value === "archive";
|
||||||
const backend = state.renderBackend;
|
const backend = state.renderBackend;
|
||||||
|
const fontKey = [fontManager.permission, fontManager.records().map(font => [font.id, font.name, font.enabled, font.checksum])];
|
||||||
for (const item of batch.items) {
|
for (const item of batch.items) {
|
||||||
const format = batch.formats.find(format => format.value === (item.format || defaultFormat.value));
|
const format = batch.formats.find(format => format.value === (item.format || defaultFormat.value));
|
||||||
const key = JSON.stringify([format.value, pageRange(item), format.raster ? dpi : 0, archive, backend]);
|
const key = JSON.stringify([format.value, pageRange(item), batchUsesDPI(item) ? dpi : 0, archive, backend, fontKey]);
|
||||||
if (item.settingsKey !== key) {
|
if (item.settingsKey !== key) {
|
||||||
item.status = "pending";
|
item.status = "pending";
|
||||||
item.text = "待转";
|
item.text = "待转";
|
||||||
@@ -4629,6 +4634,12 @@ async function runBatch() {
|
|||||||
}
|
}
|
||||||
} else destination = await window.showDirectoryPicker({ mode: "readwrite" });
|
} else destination = await window.showDirectoryPicker({ mode: "readwrite" });
|
||||||
if (batch.canceled) return;
|
if (batch.canceled) return;
|
||||||
|
if (jobs.some(item => /\.pdf$/i.test(item.file.name) || !["ofd", "txt"].includes(item.format || defaultFormat.value))) {
|
||||||
|
await requestLocalFontsBeforeOpen();
|
||||||
|
}
|
||||||
|
if (batch.canceled) return;
|
||||||
|
batch.localFonts = fontManager.localEntries();
|
||||||
|
batch.fontData = new Map();
|
||||||
batchElements.Status.textContent = "正在准备引擎";
|
batchElements.Status.textContent = "正在准备引擎";
|
||||||
await startBatchWorker();
|
await startBatchWorker();
|
||||||
const fonts = await fontManager.files(fontManager.records());
|
const fonts = await fontManager.files(fontManager.records());
|
||||||
@@ -4642,7 +4653,8 @@ async function runBatch() {
|
|||||||
const outputName = () => (format.paged ? unique : `${unique}.${format.extension}`).toLowerCase();
|
const outputName = () => (format.paged ? unique : `${unique}.${format.extension}`).toLowerCase();
|
||||||
for (let suffix = 2; archive && names.has(outputName()); suffix++) unique = `${base} (${suffix})`;
|
for (let suffix = 2; archive && names.has(outputName()); suffix++) unique = `${base} (${suffix})`;
|
||||||
names.add(outputName());
|
names.add(outputName());
|
||||||
const options = { format: format.value, pages: pageRange(item), dpi, backend, archive, base: unique, credentials: null };
|
const options = { format: format.value, pages: pageRange(item), dpi, backend, archive, base: unique, credentials: null,
|
||||||
|
fontNames: [...batch.localFonts.keys()] };
|
||||||
try {
|
try {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
try {
|
try {
|
||||||
@@ -4694,6 +4706,8 @@ async function runBatch() {
|
|||||||
batch.worker = null;
|
batch.worker = null;
|
||||||
batch.requests.clear();
|
batch.requests.clear();
|
||||||
batch.activeID = 0;
|
batch.activeID = 0;
|
||||||
|
batch.fontData?.clear();
|
||||||
|
batch.localFonts?.clear();
|
||||||
if (storage && stagingName) {
|
if (storage && stagingName) {
|
||||||
try { await storage.removeEntry(stagingName, { recursive: true }); }
|
try { await storage.removeEntry(stagingName, { recursive: true }); }
|
||||||
catch { batchElements.Status.textContent += " · 临时文件清理失败"; }
|
catch { batchElements.Status.textContent += " · 临时文件清理失败"; }
|
||||||
|
|||||||
@@ -396,6 +396,20 @@ export class FontManager {
|
|||||||
return `${name}.ttf`;
|
return `${name}.ttf`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localEntries(fonts = this.catalog) {
|
||||||
|
const entries = new Map();
|
||||||
|
const disabled = new Set(this.localFonts.filter(font => !font.enabled).map(font => font.name));
|
||||||
|
for (const font of fonts) {
|
||||||
|
if (disabled.has(this.localName(font))) continue;
|
||||||
|
const names = [this.localName(font), font.postscriptName && `${font.postscriptName}.ttf`,
|
||||||
|
font.family && `${[font.family, font.style].filter(Boolean).join(" ")}.ttf`];
|
||||||
|
for (const name of names) {
|
||||||
|
if (name && !entries.has(name)) entries.set(name, font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
canReadLocal() {
|
canReadLocal() {
|
||||||
return typeof window.queryLocalFonts === "function";
|
return typeof window.queryLocalFonts === "function";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,14 @@ importScripts("./wasm_exec.js");
|
|||||||
|
|
||||||
let pending = Promise.resolve();
|
let pending = Promise.resolve();
|
||||||
const operations = new Map();
|
const operations = new Map();
|
||||||
|
const fontRequests = new Map();
|
||||||
|
let fontSequence = 0;
|
||||||
self.onmessage = ({ data }) => {
|
self.onmessage = ({ data }) => {
|
||||||
|
if (data.type === "font") {
|
||||||
|
fontRequests.get(data.request)?.(data);
|
||||||
|
fontRequests.delete(data.request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data.type === "cancel") {
|
if (data.type === "cancel") {
|
||||||
operations.get(data.id)?.abort();
|
operations.get(data.id)?.abort();
|
||||||
return;
|
return;
|
||||||
@@ -96,7 +103,20 @@ async function handleBatchMessage({ id, name, args }) {
|
|||||||
finish(done);
|
finish(done);
|
||||||
},
|
},
|
||||||
(action, pageName, done) => boundary(action, pageName).then(() => finish(done), err => finish(done, err)),
|
(action, pageName, done) => boundary(action, pageName).then(() => finish(done), err => finish(done, err)),
|
||||||
done => finish(done), fonts);
|
done => finish(done), fonts, (name, done) => {
|
||||||
|
if (signal.aborted) { done(null, "", true); return; }
|
||||||
|
const request = ++fontSequence;
|
||||||
|
const cancel = () => {
|
||||||
|
fontRequests.delete(request);
|
||||||
|
done(null, "", true);
|
||||||
|
};
|
||||||
|
signal.addEventListener("abort", cancel, { once: true });
|
||||||
|
fontRequests.set(request, result => {
|
||||||
|
signal.removeEventListener("abort", cancel);
|
||||||
|
done(result.bytes, result.error || "", signal.aborted);
|
||||||
|
});
|
||||||
|
self.postMessage({ id, type: "font", request, name });
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const [entries, handle] = args;
|
const [entries, handle] = args;
|
||||||
if (handle) output = await handle.createWritable();
|
if (handle) output = await handle.createWritable();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1
|
github.com/xiaoqidun/jbig2 v1.1.1
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260926132451-00d165f66ad7
|
github.com/xiaoqidun/pdfgo v0.0.0-20260926152052-3a4cb2e9266a
|
||||||
golang.org/x/crypto v0.57.0
|
golang.org/x/crypto v0.57.0
|
||||||
golang.org/x/image v0.46.0
|
golang.org/x/image v0.46.0
|
||||||
golang.org/x/text v0.42.0
|
golang.org/x/text v0.42.0
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
|||||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260926132451-00d165f66ad7 h1:yRhuqMZm146rOe1682qJJoBld8zwNYg3RwvdvWkbwzk=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260926152052-3a4cb2e9266a h1:BT4VQisXVY8OlUdZFE6WTOIktGk8fCQOfkWlSxvL+cc=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260926132451-00d165f66ad7/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260926152052-3a4cb2e9266a/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
|
|||||||
+32
-4
@@ -398,10 +398,10 @@ func convertPDFDocument(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convertFile 使用库转换独立文件,不读取或修改当前阅读和编辑会话
|
// convertFile 使用库转换独立文件,不读取或修改当前阅读和编辑会话
|
||||||
// 入参: args 随机读取、文件大小、转换选项、写出、进度、文件边界、取消检查及字体
|
// 入参: args 随机读取、文件大小、转换选项、写出、进度、文件边界、取消检查、字体及按需字体读取
|
||||||
// 返回: any 转换报告, error 转换错误
|
// 返回: any 转换报告, error 转换错误
|
||||||
func convertFile(args []js.Value) (any, error) {
|
func convertFile(args []js.Value) (any, error) {
|
||||||
if len(args) != 8 {
|
if len(args) != 9 {
|
||||||
return nil, fmt.Errorf("missing conversion arguments")
|
return nil, fmt.Errorf("missing conversion arguments")
|
||||||
}
|
}
|
||||||
settings := args[2]
|
settings := args[2]
|
||||||
@@ -422,9 +422,13 @@ func convertFile(args []js.Value) (any, error) {
|
|||||||
return awaitExport(args[4], progress.Stage, progress.Completed, progress.Total)
|
return awaitExport(args[4], progress.Stage, progress.Completed, progress.Total)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if len(fonts) > 0 {
|
fontSources := []fs.FS{ofdgo.NewFontFS(fonts)}
|
||||||
options.RendererOptions = append(options.RendererOptions, ofdgo.WithFontFS(ofdgo.NewFontFS(fonts)))
|
if names := settings.Get("fontNames"); !names.IsUndefined() && names.Length() > 0 {
|
||||||
|
fontSources = append(fontSources, ofdgo.NewLazyFontFS(stringsFromJS(names), func(name string) ([]byte, error) {
|
||||||
|
return loadConversionFont(args[8], name)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
options.RendererOptions = append(options.RendererOptions, ofdgo.WithFontFS(fontSources...))
|
||||||
if value := settings.Get("credentials"); !value.IsNull() && !value.IsUndefined() {
|
if value := settings.Get("credentials"); !value.IsNull() && !value.IsUndefined() {
|
||||||
credentials, err := credentialsFromJS(value)
|
credentials, err := credentialsFromJS(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -476,6 +480,30 @@ func convertFile(args []js.Value) (any, error) {
|
|||||||
return report, nil
|
return report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadConversionFont 按需读取浏览器已授权字体,不预读系统字体目录
|
||||||
|
// 入参: load 浏览器读取函数, name 字体文件名
|
||||||
|
// 返回: []byte 字体内容, error 读取错误或取消原因
|
||||||
|
func loadConversionFont(load js.Value, name string) ([]byte, error) {
|
||||||
|
done := make(chan error, 1)
|
||||||
|
var data []byte
|
||||||
|
callback := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
|
var err error
|
||||||
|
if args[2].Bool() {
|
||||||
|
err = context.Canceled
|
||||||
|
} else if message := args[1].String(); message != "" {
|
||||||
|
err = fmt.Errorf("%s", message)
|
||||||
|
} else {
|
||||||
|
data, err = bytesFromJS(args[0])
|
||||||
|
}
|
||||||
|
done <- err
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer callback.Release()
|
||||||
|
load.Invoke(name, callback)
|
||||||
|
err := <-done
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
// packFiles 将已经成功转换的文件逐个打包,不重复转换或读取整个文件
|
// packFiles 将已经成功转换的文件逐个打包,不重复转换或读取整个文件
|
||||||
// 入参: args 文件列表、分块读取、分块写出、进度及取消检查
|
// 入参: args 文件列表、分块读取、分块写出、进度及取消检查
|
||||||
// 返回: any 输出格式, error 打包错误
|
// 返回: any 输出格式, error 打包错误
|
||||||
|
|||||||
@@ -783,15 +783,17 @@ func (e *Editor) editorFont(id string) (FontMetrics, error) {
|
|||||||
return nil, &EditError{Code: EditFontUnavailable, Err: fmt.Errorf("fonts: %w", ErrBackendUnavailable)}
|
return nil, &EditError{Code: EditFontUnavailable, Err: fmt.Errorf("fonts: %w", ErrBackendUnavailable)}
|
||||||
}
|
}
|
||||||
var data []byte
|
var data []byte
|
||||||
createdExternal := false
|
var external *Font
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
if resource.font != nil && resource.font.ID == id {
|
if resource.font != nil && resource.font.ID == id {
|
||||||
data = resource.data
|
data = resource.data
|
||||||
createdExternal = resource.font.FontFile == ""
|
if resource.font.FontFile == "" {
|
||||||
|
external = resource.font
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if data == nil && e.source != nil && !createdExternal {
|
if data == nil && e.source != nil && external == nil {
|
||||||
if e.fontRenderer == nil {
|
if e.fontRenderer == nil {
|
||||||
e.fontRenderer = e.newRenderer(e.source.reader)
|
e.fontRenderer = e.newRenderer(e.source.reader)
|
||||||
}
|
}
|
||||||
@@ -801,12 +803,9 @@ func (e *Editor) editorFont(id string) (FontMetrics, error) {
|
|||||||
}
|
}
|
||||||
data = resolved.Data
|
data = resolved.Data
|
||||||
}
|
}
|
||||||
if data == nil && createdExternal {
|
if data == nil && external != nil {
|
||||||
reader, err := e.Reader()
|
reader := &Reader{doc: &Document{}, fontResourcesRead: true, fontCache: map[string]*Font{id: external}}
|
||||||
if err != nil {
|
resolved, err := e.newRenderer(reader).ResolveFont(id, true)
|
||||||
return nil, &EditError{Code: EditFontUnavailable, Err: err}
|
|
||||||
}
|
|
||||||
resolved, err := e.newRenderer(reader).ResolveFont(id, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &EditError{Code: EditFontUnavailable, Err: err}
|
return nil, &EditError{Code: EditFontUnavailable, Err: err}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-4
@@ -21,6 +21,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,9 +31,10 @@ type FontFile struct {
|
|||||||
Data []byte
|
Data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// FontFS 内存字体文件系统
|
// FontFS 缓存字体数据及名称索引的只读文件系统
|
||||||
type FontFS struct {
|
type FontFS struct {
|
||||||
files map[string][]byte
|
files map[string][]byte
|
||||||
|
loaders map[string]func() ([]byte, error)
|
||||||
names []string
|
names []string
|
||||||
candidates []fontFileCandidate
|
candidates []fontFileCandidate
|
||||||
}
|
}
|
||||||
@@ -62,6 +64,32 @@ func NewFontFS(fonts []FontFile) *FontFS {
|
|||||||
return fsys
|
return fsys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewLazyFontFS 创建按需读取的字体文件系统,按文件名匹配,不预读字体内容
|
||||||
|
// 每个文件的读取结果仅缓存一次;目录条目大小为0,打开后可获取实际大小
|
||||||
|
// 入参: names 包含字体名称和样式的文件名, load 按原文件名读取独立字体的非nil函数
|
||||||
|
// 返回: *FontFS 字体文件系统
|
||||||
|
func NewLazyFontFS(names []string, load func(string) ([]byte, error)) *FontFS {
|
||||||
|
fsys := &FontFS{files: make(map[string][]byte), loaders: make(map[string]func() ([]byte, error))}
|
||||||
|
for _, original := range names {
|
||||||
|
name := cleanFontName(original)
|
||||||
|
if name == "." || !isFontFileName(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := fsys.files[name]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fsys.names = append(fsys.names, name)
|
||||||
|
fsys.files[name] = nil
|
||||||
|
fsys.loaders[name] = sync.OnceValues(func() ([]byte, error) {
|
||||||
|
data, err := load(original)
|
||||||
|
return bytes.Clone(data), err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Strings(fsys.names)
|
||||||
|
fsys.candidates = fontFileCandidates(fsys.names, path.Base)
|
||||||
|
return fsys
|
||||||
|
}
|
||||||
|
|
||||||
// Len 获取字体文件数量
|
// Len 获取字体文件数量
|
||||||
// 返回: int 字体文件数量
|
// 返回: int 字体文件数量
|
||||||
func (fsys *FontFS) Len() int {
|
func (fsys *FontFS) Len() int {
|
||||||
@@ -85,6 +113,13 @@ func (fsys *FontFS) Open(name string) (fs.File, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||||
}
|
}
|
||||||
|
if load := fsys.loaders[strings.ToLower(name)]; load != nil {
|
||||||
|
var err error
|
||||||
|
data, err = load()
|
||||||
|
if err != nil {
|
||||||
|
return nil, &fs.PathError{Op: "open", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
return &fontMemFile{
|
return &fontMemFile{
|
||||||
Reader: bytes.NewReader(data),
|
Reader: bytes.NewReader(data),
|
||||||
info: fontFileInfo{
|
info: fontFileInfo{
|
||||||
@@ -281,7 +316,9 @@ type fontFileCandidate struct {
|
|||||||
face int
|
face int
|
||||||
base string
|
base string
|
||||||
lowerBase string
|
lowerBase string
|
||||||
|
filename string
|
||||||
normalized string
|
normalized string
|
||||||
|
names []string
|
||||||
fold string
|
fold string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,11 +330,13 @@ func fontFileCandidates(names []string, base func(string) string) []fontFileCand
|
|||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
if isFontFileName(name) {
|
if isFontFileName(name) {
|
||||||
baseName := base(name)
|
baseName := base(name)
|
||||||
|
normalized := fontNormalizeName(strings.TrimSuffix(baseName, path.Ext(baseName)))
|
||||||
files = append(files, fontFileCandidate{
|
files = append(files, fontFileCandidate{
|
||||||
name: name,
|
name: name,
|
||||||
base: baseName,
|
base: baseName,
|
||||||
lowerBase: strings.ToLower(baseName),
|
lowerBase: strings.ToLower(baseName),
|
||||||
normalized: fontNormalizeName(baseName),
|
filename: normalized,
|
||||||
|
normalized: normalized,
|
||||||
fold: strings.ToLower(name),
|
fold: strings.ToLower(name),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -370,7 +409,7 @@ func (m fontPatternMatcher) rankCandidate(file fontFileCandidate) int {
|
|||||||
}
|
}
|
||||||
return fontMatchNone
|
return fontMatchNone
|
||||||
}
|
}
|
||||||
name := file.normalized
|
name := m.candidateName(file)
|
||||||
if name == m.stem {
|
if name == m.stem {
|
||||||
return fontMatchExact
|
return fontMatchExact
|
||||||
}
|
}
|
||||||
@@ -401,6 +440,26 @@ func (m fontPatternMatcher) rankCandidate(file fontFileCandidate) int {
|
|||||||
return fontMatchNone
|
return fontMatchNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// candidateName 保留同族内部名称的区分,只有无关联时使用调用方文件别名
|
||||||
|
// 入参: file 字体文件候选
|
||||||
|
// 返回: string 用于统一匹配名称与样式的规范名称
|
||||||
|
func (m fontPatternMatcher) candidateName(file fontFileCandidate) string {
|
||||||
|
if len(file.names) == 0 || file.base == "" || m.stem == "" {
|
||||||
|
return file.normalized
|
||||||
|
}
|
||||||
|
for _, name := range file.names {
|
||||||
|
if strings.HasPrefix(name, m.stem) {
|
||||||
|
return file.normalized
|
||||||
|
}
|
||||||
|
for _, alias := range m.aliases {
|
||||||
|
if strings.HasPrefix(name, alias) {
|
||||||
|
return file.normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file.filename
|
||||||
|
}
|
||||||
|
|
||||||
// styleSuffixNormalized 获取规范字体名称的样式后缀
|
// styleSuffixNormalized 获取规范字体名称的样式后缀
|
||||||
// 入参: name 规范字体名称
|
// 入参: name 规范字体名称
|
||||||
// 返回: string 样式后缀
|
// 返回: string 样式后缀
|
||||||
@@ -441,7 +500,7 @@ func appendFontFileMatch(matches *[]fontFileMatch, seen map[fontFileKey]int, mat
|
|||||||
if rank == fontMatchNone {
|
if rank == fontMatchNone {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
suffix := matcher.styleSuffixNormalized(file.normalized)
|
suffix := matcher.styleSuffixNormalized(matcher.candidateName(file))
|
||||||
if rank == fontMatchFuzzy {
|
if rank == fontMatchFuzzy {
|
||||||
suffix = matcher.styleSuffixNormalized(fontNormalizeName(file.base))
|
suffix = matcher.styleSuffixNormalized(fontNormalizeName(file.base))
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-3
@@ -33,7 +33,7 @@ type fontNameRecord struct {
|
|||||||
Offset uint16
|
Offset uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendFontFileNames 使用字体内部名称匹配,缺失时保留文件名
|
// appendFontFileNames 建立各字体的内部名称与文件别名匹配候选
|
||||||
// 入参: candidates 字体候选, index 文件候选索引, faces 各字体内部名称
|
// 入参: candidates 字体候选, index 文件候选索引, faces 各字体内部名称
|
||||||
// 返回: []fontFileCandidate 字体候选列表
|
// 返回: []fontFileCandidate 字体候选列表
|
||||||
func appendFontFileNames(candidates []fontFileCandidate, index int, faces [][]string) []fontFileCandidate {
|
func appendFontFileNames(candidates []fontFileCandidate, index int, faces [][]string) []fontFileCandidate {
|
||||||
@@ -41,8 +41,12 @@ func appendFontFileNames(candidates []fontFileCandidate, index int, faces [][]st
|
|||||||
replaced := false
|
replaced := false
|
||||||
for face, names := range faces {
|
for face, names := range faces {
|
||||||
file.face = face
|
file.face = face
|
||||||
for _, name := range names {
|
file.names = make([]string, len(names))
|
||||||
file.normalized = fontNormalizeName(name)
|
for i, name := range names {
|
||||||
|
file.names[i] = fontNormalizeName(name)
|
||||||
|
}
|
||||||
|
for _, name := range file.names {
|
||||||
|
file.normalized = name
|
||||||
if !replaced {
|
if !replaced {
|
||||||
candidates[index] = file
|
candidates[index] = file
|
||||||
replaced = true
|
replaced = true
|
||||||
|
|||||||
@@ -210,12 +210,12 @@ func (r *Renderer) fontSources(fontID string, font *Font) []fontSource {
|
|||||||
seen := make(map[fontSourceKey]bool)
|
seen := make(map[fontSourceKey]bool)
|
||||||
for _, dir := range r.fontDirs {
|
for _, dir := range r.fontDirs {
|
||||||
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
|
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
|
||||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: true, priority: min(match.priority, 0)})
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: match.rank == fontMatchExact, priority: min(match.priority, 0)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for index := range r.fontFS {
|
for index := range r.fontFS {
|
||||||
for _, match := range r.matchFontFS(index, names, bold, italic) {
|
for _, match := range r.matchFontFS(index, names, bold, italic) {
|
||||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face, exact: true, priority: min(match.priority, 0)})
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face, exact: match.rank == fontMatchExact, priority: min(match.priority, 0)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sortFontSources(sources)
|
sortFontSources(sources)
|
||||||
@@ -223,7 +223,7 @@ func (r *Renderer) fontSources(fontID string, font *Font) []fontSource {
|
|||||||
if canLoadSystemFonts() {
|
if canLoadSystemFonts() {
|
||||||
for _, dir := range systemFontDirs() {
|
for _, dir := range systemFontDirs() {
|
||||||
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
|
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
|
||||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: true, priority: min(match.priority, 0)})
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: match.rank == fontMatchExact, priority: min(match.priority, 0)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, name := range fontSystemNames(names...) {
|
for _, name := range fontSystemNames(names...) {
|
||||||
|
|||||||
+77
-15
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -31,6 +32,29 @@ func (r *Renderer) ResolveColor(value string, index *int, space string, alpha *i
|
|||||||
// 入参: value 颜色值, index 调色板索引, space 颜色空间标识, alpha 透明度
|
// 入参: value 颜色值, index 调色板索引, space 颜色空间标识, alpha 透明度
|
||||||
// 返回: color.Color 颜色对象
|
// 返回: color.Color 颜色对象
|
||||||
func (r *Renderer) parseColorWithAlpha(value string, index *int, space string, alpha *int) color.Color {
|
func (r *Renderer) parseColorWithAlpha(value string, index *int, space string, alpha *int) color.Color {
|
||||||
|
kind, values := r.colorComponents(value, index, space)
|
||||||
|
var components [4]uint8
|
||||||
|
for i, value := range values {
|
||||||
|
components[i] = uint8(math.Round(value * 255))
|
||||||
|
}
|
||||||
|
red, green, blue := components[0], components[1], components[2]
|
||||||
|
switch kind {
|
||||||
|
case "GRAY":
|
||||||
|
green, blue = red, red
|
||||||
|
case "CMYK":
|
||||||
|
red, green, blue = color.CMYKToRGB(components[0], components[1], components[2], components[3])
|
||||||
|
}
|
||||||
|
a := 255
|
||||||
|
if alpha != nil {
|
||||||
|
a = clampColor(*alpha)
|
||||||
|
}
|
||||||
|
return color.RGBA{R: uint8(int(red) * a / 255), G: uint8(int(green) * a / 255), B: uint8(int(blue) * a / 255), A: uint8(a)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// colorComponents 读取颜色空间及归一化分量,保留源位深供渐变插值
|
||||||
|
// 入参: value 颜色值, index 调色板索引, space 颜色空间标识
|
||||||
|
// 返回: string 颜色模型, [4]float64 单位分量
|
||||||
|
func (r *Renderer) colorComponents(value string, index *int, space string) (string, [4]float64) {
|
||||||
if space == "" && r.Reader.doc != nil {
|
if space == "" && r.Reader.doc != nil {
|
||||||
space = strconv.Itoa(r.Reader.doc.CommonData.DefaultCS)
|
space = strconv.Itoa(r.Reader.doc.CommonData.DefaultCS)
|
||||||
}
|
}
|
||||||
@@ -53,30 +77,19 @@ func (r *Renderer) parseColorWithAlpha(value string, index *int, space string, a
|
|||||||
count = 4
|
count = 4
|
||||||
}
|
}
|
||||||
parts := strings.Fields(value)
|
parts := strings.Fields(value)
|
||||||
var components [4]uint8
|
var components [4]float64
|
||||||
limit := 1<<bits - 1
|
limit := 1<<bits - 1
|
||||||
if len(parts) >= count {
|
if len(parts) >= count {
|
||||||
for i, part := range parts[:count] {
|
for i, part := range parts[:count] {
|
||||||
v, err := parseColorComponent(part)
|
v, err := parseColorComponent(part)
|
||||||
if err != nil || v < 0 || limit < v {
|
if err != nil || v < 0 || limit < v {
|
||||||
components = [4]uint8{}
|
components = [4]float64{}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
components[i] = uint8((v*255 + limit/2) / limit)
|
components[i] = float64(v) / float64(limit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
red, green, blue := components[0], components[1], components[2]
|
return kind, components
|
||||||
switch kind {
|
|
||||||
case "GRAY":
|
|
||||||
green, blue = red, red
|
|
||||||
case "CMYK":
|
|
||||||
red, green, blue = color.CMYKToRGB(components[0], components[1], components[2], components[3])
|
|
||||||
}
|
|
||||||
a := 255
|
|
||||||
if alpha != nil {
|
|
||||||
a = clampColor(*alpha)
|
|
||||||
}
|
|
||||||
return color.RGBA{R: uint8(int(red) * a / 255), G: uint8(int(green) * a / 255), B: uint8(int(blue) * a / 255), A: uint8(a)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseColorComponent 解析颜色分量
|
// parseColorComponent 解析颜色分量
|
||||||
@@ -232,6 +245,55 @@ func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop
|
|||||||
return gradient
|
return gradient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renderGradientStops 在CMYK分量空间插值,将RGB分段误差控制在半个8位灰阶内
|
||||||
|
// 入参: segments 原始颜色分段, alpha 对象透明度
|
||||||
|
// 返回: []ColorStop 仅供绘制的颜色节点,不改变原始分段
|
||||||
|
func (r *Renderer) renderGradientStops(segments []ShdSegment, alpha *int) []ColorStop {
|
||||||
|
stops := r.GradientStops(segments, alpha)
|
||||||
|
if len(stops) < 2 {
|
||||||
|
return stops
|
||||||
|
}
|
||||||
|
var result []ColorStop
|
||||||
|
for i, stop := range stops {
|
||||||
|
if i > 0 && stops[i-1].Offset < stop.Offset {
|
||||||
|
a, b := segments[i-1].Color, segments[i].Color
|
||||||
|
leftKind, left := r.colorComponents(a.Value, a.Index, a.ColorSpace)
|
||||||
|
rightKind, right := r.colorComponents(b.Value, b.Index, b.ColorSpace)
|
||||||
|
if leftKind == "CMYK" && rightKind == "CMYK" {
|
||||||
|
a0, a1 := float64(stops[i-1].Color.A)/255, float64(stop.Color.A)/255
|
||||||
|
dk, da := right[3]-left[3], a1-a0
|
||||||
|
bound := 0.0
|
||||||
|
for c := 0; c < 3; c++ {
|
||||||
|
dc := right[c] - left[c]
|
||||||
|
bound = math.Max(bound, 2*math.Abs(dc*dk)+2*math.Abs(da)*(math.Abs(dc)+math.Abs(dk)))
|
||||||
|
}
|
||||||
|
steps := int(math.Ceil(math.Sqrt(bound * 255 / 4)))
|
||||||
|
if steps > 1 && result == nil {
|
||||||
|
result = append(make([]ColorStop, 0, len(stops)+steps-1), stops[:i]...)
|
||||||
|
}
|
||||||
|
for j := 1; j < steps; j++ {
|
||||||
|
t := float64(j) / float64(steps)
|
||||||
|
var values [4]float64
|
||||||
|
for c := range values {
|
||||||
|
values[c] = left[c]*(1-t) + right[c]*t
|
||||||
|
}
|
||||||
|
opacity := a0*(1-t) + a1*t
|
||||||
|
black := (1 - values[3]) * opacity * 255
|
||||||
|
color := color.RGBA{R: uint8(math.Round((1 - values[0]) * black)), G: uint8(math.Round((1 - values[1]) * black)), B: uint8(math.Round((1 - values[2]) * black)), A: uint8(math.Round(opacity * 255))}
|
||||||
|
result = append(result, ColorStop{Offset: stops[i-1].Offset*(1-t) + stop.Offset*t, Color: color})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result != nil {
|
||||||
|
result = append(result, stop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
return stops
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// gradientPositions 按显式锚点补全渐变分段位置,保留原顺序
|
// gradientPositions 按显式锚点补全渐变分段位置,保留原顺序
|
||||||
// 入参: segments 渐变分段
|
// 入参: segments 渐变分段
|
||||||
// 返回: []float64 分段位置
|
// 返回: []float64 分段位置
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func (r *Renderer) ResolvePaint(fill *FillColor) Paint {
|
|||||||
paint := Paint{Kind: PaintSolid, Color: colorToRGBA(base)}
|
paint := Paint{Kind: PaintSolid, Color: colorToRGBA(base)}
|
||||||
if node := fill.AxialShd; node != nil {
|
if node := fill.AxialShd; node != nil {
|
||||||
start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint)
|
start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint)
|
||||||
stops := r.GradientStops(node.Segment, fill.Alpha)
|
stops := r.renderGradientStops(node.Segment, fill.Alpha)
|
||||||
if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 && (!geometryEqual(start[0], end[0]) || !geometryEqual(start[1], end[1])) {
|
if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 && (!geometryEqual(start[0], end[0]) || !geometryEqual(start[1], end[1])) {
|
||||||
paint.Kind = PaintLinear
|
paint.Kind = PaintLinear
|
||||||
paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit}
|
paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit}
|
||||||
@@ -77,7 +77,7 @@ func (r *Renderer) ResolvePaint(fill *FillColor) Paint {
|
|||||||
}
|
}
|
||||||
if node := fill.RadialShd; node != nil && node.EndRadius > 0 {
|
if node := fill.RadialShd; node != nil && node.EndRadius > 0 {
|
||||||
start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint)
|
start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint)
|
||||||
stops := r.GradientStops(node.Segment, fill.Alpha)
|
stops := r.renderGradientStops(node.Segment, fill.Alpha)
|
||||||
if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 {
|
if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 {
|
||||||
paint.Kind = PaintRadial
|
paint.Kind = PaintRadial
|
||||||
paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, StartRadius: node.StartRadius, EndRadius: node.EndRadius, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit, Eccentricity: node.Eccentricity, Angle: node.Angle}
|
paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, StartRadius: node.StartRadius, EndRadius: node.EndRadius, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit, Eccentricity: node.Eccentricity, Angle: node.Angle}
|
||||||
|
|||||||
+21
-16
@@ -164,14 +164,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path)
|
importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path)
|
||||||
importer.compositeCache = nil
|
importer.compositeCache = nil
|
||||||
importer.clipTexts = make(map[*pdfgo.TextClip]TextObject)
|
importer.clipTexts = make(map[*pdfgo.TextClip]TextObject)
|
||||||
visitor := pdfgo.Visitor{Path: importer.path, Text: importer.text, Image: importer.image, Warning: importer.warning}
|
if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return reader.WalkPage(ctx, page, visitor) }); err != nil {
|
||||||
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
|
||||||
if mark.Page && mark.ColorSpace != nil && mark.ColorSpace.Model == "DeviceCMYK" && !mark.ColorSpace.Calibrated() {
|
|
||||||
return importer.compositePage(mark.ColorSpace, walk)
|
|
||||||
}
|
|
||||||
return importer.group(mark, walk, visitor)
|
|
||||||
}
|
|
||||||
if err := reader.WalkPage(ctx, page, visitor); err != nil {
|
|
||||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
if err := importer.flushPath(); err != nil {
|
if err := importer.flushPath(); err != nil {
|
||||||
@@ -466,9 +459,6 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid PDF stroke matrix")
|
return fmt.Errorf("invalid PDF stroke matrix")
|
||||||
}
|
}
|
||||||
if mark.Style.Fill.Radial != nil || mark.Style.Stroke.Radial != nil {
|
|
||||||
return &pdfgo.UnsupportedError{Feature: "radial gradient with anisotropic path stroke"}
|
|
||||||
}
|
|
||||||
drawing.matrix, origin, scale = inverse, Box{}, 1
|
drawing.matrix, origin, scale = inverse, Box{}, 1
|
||||||
m := p.matrix.Mul(mark.StrokeMatrix)
|
m := p.matrix.Mul(mark.StrokeMatrix)
|
||||||
marginScale = math.Max(math.Hypot(m[0], m[2]), math.Hypot(m[1], m[3]))
|
marginScale = math.Max(math.Hypot(m[0], m[2]), math.Hypot(m[1], m[3]))
|
||||||
@@ -531,6 +521,9 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
|||||||
// 入参: paint PDF画刷, box 对象边界
|
// 入参: paint PDF画刷, box 对象边界
|
||||||
// 返回: *FillColor OFD颜色或渐变, error 图案转换错误
|
// 返回: *FillColor OFD颜色或渐变, error 图案转换错误
|
||||||
func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error) {
|
func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error) {
|
||||||
|
if paint.Mesh != nil {
|
||||||
|
return nil, &pdfgo.UnsupportedError{Feature: "bicubic mesh conversion"}
|
||||||
|
}
|
||||||
color := p.color(paint)
|
color := p.color(paint)
|
||||||
if paint.Tiling != nil {
|
if paint.Tiling != nil {
|
||||||
pattern, err := p.tilingPattern(paint.Tiling, paint)
|
pattern, err := p.tilingPattern(paint.Tiling, paint)
|
||||||
@@ -542,8 +535,20 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
|||||||
}
|
}
|
||||||
if paint.Radial != nil {
|
if paint.Radial != nil {
|
||||||
gradient := paint.Radial
|
gradient := paint.Radial
|
||||||
start, end := p.matrix.Apply(gradient.Start), p.matrix.Apply(gradient.End)
|
matrix := p.matrix
|
||||||
scale := math.Hypot(p.matrix[0], p.matrix[1])
|
if gradient.Matrix != (pdfgo.Matrix{}) {
|
||||||
|
matrix = matrix.Mul(gradient.Matrix)
|
||||||
|
}
|
||||||
|
start, end := matrix.Apply(gradient.Start), matrix.Apply(gradient.End)
|
||||||
|
xx, yy, xy := matrix[0]*matrix[0]+matrix[2]*matrix[2], matrix[1]*matrix[1]+matrix[3]*matrix[3], matrix[0]*matrix[1]+matrix[2]*matrix[3]
|
||||||
|
major := (xx + yy + math.Hypot(xx-yy, 2*xy)) / 2
|
||||||
|
determinant := matrix[0]*matrix[3] - matrix[1]*matrix[2]
|
||||||
|
if major <= 0 || determinant == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid PDF radial gradient matrix")
|
||||||
|
}
|
||||||
|
scale := math.Sqrt(major)
|
||||||
|
eccentricity := math.Sqrt(math.Max(0, 1-determinant*determinant/(major*major)))
|
||||||
|
angle := math.Atan2(2*xy, xx-yy) * 90 / math.Pi
|
||||||
extend := 0
|
extend := 0
|
||||||
if gradient.Extend[0] {
|
if gradient.Extend[0] {
|
||||||
extend |= 1
|
extend |= 1
|
||||||
@@ -551,9 +556,9 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
|||||||
if gradient.Extend[1] {
|
if gradient.Extend[1] {
|
||||||
extend |= 2
|
extend |= 2
|
||||||
}
|
}
|
||||||
shading := &RadialShd{StartPoint: pdfNumbers(start.X-box.X, start.Y-box.Y), EndPoint: pdfNumbers(end.X-box.X, end.Y-box.Y), StartRadius: gradient.StartRadius * scale, EndRadius: gradient.EndRadius * scale, Extend: strconv.Itoa(extend)}
|
shading := &RadialShd{StartPoint: pdfNumbers(start.X-box.X, start.Y-box.Y), EndPoint: pdfNumbers(end.X-box.X, end.Y-box.Y), StartRadius: gradient.StartRadius * scale, EndRadius: gradient.EndRadius * scale, Eccentricity: eccentricity, Angle: angle, Extend: strconv.Itoa(extend)}
|
||||||
var err error
|
var err error
|
||||||
shading.Segment, err = pdfGradientSegments(gradient.Stops, gradient.Space)
|
shading.Segment, err = p.gradientSegments(gradient.Stops, gradient.Space)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -587,7 +592,7 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
|||||||
}
|
}
|
||||||
shading := &AxialShd{StartPoint: pdfNumbers(start.X-box.X, start.Y-box.Y), EndPoint: pdfNumbers(end.X-box.X, end.Y-box.Y), Extend: strconv.Itoa(extend)}
|
shading := &AxialShd{StartPoint: pdfNumbers(start.X-box.X, start.Y-box.Y), EndPoint: pdfNumbers(end.X-box.X, end.Y-box.Y), Extend: strconv.Itoa(extend)}
|
||||||
var err error
|
var err error
|
||||||
shading.Segment, err = pdfGradientSegments(gradient.Stops, gradient.Space)
|
shading.Segment, err = p.gradientSegments(gradient.Stops, gradient.Space)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-3
@@ -37,6 +37,7 @@ type pdfCompositePixel struct {
|
|||||||
values [4]float64
|
values [4]float64
|
||||||
alpha float64
|
alpha float64
|
||||||
effect float64
|
effect float64
|
||||||
|
shape float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfCompositor 在指定局部区域中按源颜色空间合成PDF图元
|
// pdfCompositor 在指定局部区域中按源颜色空间合成PDF图元
|
||||||
@@ -47,6 +48,7 @@ type pdfCompositor struct {
|
|||||||
inverse pdfgo.Matrix
|
inverse pdfgo.Matrix
|
||||||
cache *pdfCompositeCache
|
cache *pdfCompositeCache
|
||||||
masks map[*pdfgo.SoftMask][]float64
|
masks map[*pdfgo.SoftMask][]float64
|
||||||
|
meshes map[pdfMeshKey][]pdfMeshPixel
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfCompositeKey 区分图元的填充与描边几何
|
// pdfCompositeKey 区分图元的填充与描边几何
|
||||||
@@ -68,13 +70,14 @@ type pdfCompositeCache struct {
|
|||||||
geometry map[pdfCompositeKey]pdfCompositeGeometry
|
geometry map[pdfCompositeKey]pdfCompositeGeometry
|
||||||
images map[*pdfgo.Image]*pdfgo.ImageComponents
|
images map[*pdfgo.Image]*pdfgo.ImageComponents
|
||||||
masks map[*pdfgo.SoftMask][]pdfCompositeNode
|
masks map[*pdfgo.SoftMask][]pdfCompositeNode
|
||||||
|
meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle
|
||||||
}
|
}
|
||||||
|
|
||||||
// compositingCache 按需创建单页合成缓存,页面切换时由导入器释放
|
// compositingCache 按需创建单页合成缓存,页面切换时由导入器释放
|
||||||
// 返回: *pdfCompositeCache 当前页面缓存
|
// 返回: *pdfCompositeCache 当前页面缓存
|
||||||
func (p *pdfImporter) compositingCache() *pdfCompositeCache {
|
func (p *pdfImporter) compositingCache() *pdfCompositeCache {
|
||||||
if p.compositeCache == nil {
|
if p.compositeCache == nil {
|
||||||
p.compositeCache = &pdfCompositeCache{geometry: map[pdfCompositeKey]pdfCompositeGeometry{}, images: map[*pdfgo.Image]*pdfgo.ImageComponents{}, masks: map[*pdfgo.SoftMask][]pdfCompositeNode{}}
|
p.compositeCache = &pdfCompositeCache{geometry: map[pdfCompositeKey]pdfCompositeGeometry{}, images: map[*pdfgo.Image]*pdfgo.ImageComponents{}, masks: map[*pdfgo.SoftMask][]pdfCompositeNode{}, meshes: map[*pdfgo.MeshGradient][]pdfMeshTriangle{}}
|
||||||
}
|
}
|
||||||
return p.compositeCache
|
return p.compositeCache
|
||||||
}
|
}
|
||||||
@@ -162,8 +165,17 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(nodes) == 1 && nodes[0].group != nil && nodes[0].group.Page {
|
||||||
|
space = nodes[0].group.ColorSpace
|
||||||
|
if !nodes[0].group.Knockout {
|
||||||
|
nodes = nodes[0].children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if space == nil {
|
||||||
|
space = &pdfgo.ColorSpace{Model: "DeviceRGB"}
|
||||||
|
}
|
||||||
for i, node := range nodes {
|
for i, node := range nodes {
|
||||||
if node.opaque(space) {
|
if node.opaque(space) || space.SRGBEquivalent() && node.direct() {
|
||||||
if err := node.emit(p.visitor()); err != nil {
|
if err := node.emit(p.visitor()); err != nil {
|
||||||
return fmt.Errorf("convert graphic %d: %w", i+1, err)
|
return fmt.Errorf("convert graphic %d: %w", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -188,7 +200,7 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis
|
|||||||
func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool {
|
func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool {
|
||||||
if n.group != nil {
|
if n.group != nil {
|
||||||
g := n.group
|
g := n.group
|
||||||
if g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) {
|
if g.Knockout || g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, child := range n.children {
|
for _, child := range n.children {
|
||||||
@@ -214,6 +226,24 @@ func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// direct 检查sRGB图元能否使用标准OFD透明度而无需背景合成
|
||||||
|
// 返回: bool 是否可直接转换
|
||||||
|
func (n pdfCompositeNode) direct() bool {
|
||||||
|
if g := n.group; g != nil {
|
||||||
|
if g.Knockout || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.SRGBEquivalent() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, child := range n.children {
|
||||||
|
if !child.direct() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
s, fill, stroke := n.style()
|
||||||
|
return pdfNormalBlend(s.BlendMode) && !(fill && s.FillOverprint && pdfOverprintNeedsSeparation(s.Fill) || stroke && s.StrokeOverprint && pdfOverprintNeedsSeparation(s.Stroke))
|
||||||
|
}
|
||||||
|
|
||||||
// style 获取图元实际使用的图形状态
|
// style 获取图元实际使用的图形状态
|
||||||
// 返回: pdfgo.Style 图形状态, bool 是否填充, bool 是否描边
|
// 返回: pdfgo.Style 图形状态, bool 是否填充, bool 是否描边
|
||||||
func (n pdfCompositeNode) style() (pdfgo.Style, bool, bool) {
|
func (n pdfCompositeNode) style() (pdfgo.Style, bool, bool) {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
|
|||||||
result = append([]pdfCompositePixel(nil), pixels...)
|
result = append([]pdfCompositePixel(nil), pixels...)
|
||||||
for i := range result {
|
for i := range result {
|
||||||
result[i].effect = 0
|
result[i].effect = 0
|
||||||
|
result[i].shape = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +100,14 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
style, _, _ := node.style()
|
style, _, _ := node.style()
|
||||||
compatible := overprint && style.OverprintMode == 1 && node.image == nil && space.Model == "DeviceCMYK" && !space.Calibrated() && (paint.CMYK != nil || paint.Space != nil && paint.Space.Model == "DeviceCMYK" && !paint.Space.Calibrated())
|
compatible := overprint && style.OverprintMode == 1 && node.image == nil && space.Model == "DeviceCMYK" && !space.Calibrated() && (paint.CMYK != nil || paint.Space != nil && paint.Space.Model == "DeviceCMYK" && !paint.Space.Calibrated())
|
||||||
step := 25.4 / c.importer.rasterDPI
|
step := 25.4 / c.importer.rasterDPI
|
||||||
uniform := node.image == nil && paint.Axial == nil && paint.Radial == nil && paint.Tiling == nil
|
uniform := node.image == nil && paint.Axial == nil && paint.Radial == nil && paint.Tiling == nil && paint.Mesh == nil
|
||||||
|
var mesh []pdfMeshPixel
|
||||||
|
if paint.Mesh != nil {
|
||||||
|
mesh, err = c.mesh(paint.Mesh, space)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
var uniformValues [4]float64
|
var uniformValues [4]float64
|
||||||
uniformReady := false
|
uniformReady := false
|
||||||
for y := 0; y < c.height; y++ {
|
for y := 0; y < c.height; y++ {
|
||||||
@@ -117,7 +125,10 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
opacity *= mask[index]
|
opacity *= mask[index]
|
||||||
}
|
}
|
||||||
var values [4]float64
|
var values [4]float64
|
||||||
if uniform {
|
if mesh != nil {
|
||||||
|
values = mesh[index].values
|
||||||
|
shape *= mesh[index].shape
|
||||||
|
} else if uniform {
|
||||||
if !uniformReady {
|
if !uniformReady {
|
||||||
uniformValues, _, err = pdfCompositeColor(paint, pdfgo.Point{}, space, style.RenderingIntent)
|
uniformValues, _, err = pdfCompositeColor(paint, pdfgo.Point{}, space, style.RenderingIntent)
|
||||||
uniformReady = true
|
uniformReady = true
|
||||||
@@ -140,10 +151,10 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if initial != nil {
|
|
||||||
if style.AlphaIsShape {
|
if style.AlphaIsShape {
|
||||||
shape, opacity = shape*opacity, 1
|
shape, opacity = shape*opacity, 1
|
||||||
}
|
}
|
||||||
|
if initial != nil {
|
||||||
pixel := initial[index]
|
pixel := initial[index]
|
||||||
if err := pdfCompositePaintOver(&pixel, values, opacity, space, mode, compatible); err != nil {
|
if err := pdfCompositePaintOver(&pixel, values, opacity, space, mode, compatible); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -152,6 +163,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
} else if err := pdfCompositePaintOver(&pixels[index], values, shape*opacity, space, mode, compatible); err != nil {
|
} else if err := pdfCompositePaintOver(&pixels[index], values, shape*opacity, space, mode, compatible); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
pixels[index].shape += shape * (1 - pixels[index].shape)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -190,21 +202,71 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
result := append([]pdfCompositePixel(nil), initial...)
|
result := append([]pdfCompositePixel(nil), initial...)
|
||||||
|
if g.Knockout {
|
||||||
|
if err := c.drawKnockout(node.children, initial, result, space); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if err := c.draw(node.children, result, space); err != nil {
|
if err := c.draw(node.children, result, space); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
mask, err := c.mask(g.SoftMask, parent)
|
mask, err := c.mask(g.SoftMask, parent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if g.AlphaIsShape {
|
||||||
|
for i := range result {
|
||||||
|
result[i].shape *= g.Alpha
|
||||||
|
if mask != nil {
|
||||||
|
result[i].shape *= mask[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return pdfCompositeGroup(pixels, initial, result, space, parent, g.Alpha, mask, g.BlendMode)
|
return pdfCompositeGroup(pixels, initial, result, space, parent, g.Alpha, mask, g.BlendMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// drawKnockout 将每个对象与组初始背景合成,再按对象形状替换先前对象
|
||||||
|
// 入参: nodes 组图元, initial 初始背景, pixels 组输出, space 混合空间
|
||||||
|
// 返回: error 图元或合成错误
|
||||||
|
func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error {
|
||||||
|
candidate := make([]pdfCompositePixel, len(initial))
|
||||||
|
for _, node := range nodes {
|
||||||
|
copy(candidate, initial)
|
||||||
|
if err := c.draw([]pdfCompositeNode{node}, candidate, space); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, source := range candidate {
|
||||||
|
pdfCompositeKnockout(&pixels[i], initial[i], source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositeKnockout 移除未覆盖区域的初始背景贡献,保留形状与不透明度差异
|
||||||
|
// 入参: target 累计组结果, initial 初始背景, source 单个对象合成结果
|
||||||
|
func pdfCompositeKnockout(target *pdfCompositePixel, initial, source pdfCompositePixel) {
|
||||||
|
f := source.shape
|
||||||
|
if f == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
alpha := target.alpha*(1-f) + source.alpha - initial.alpha*(1-f)
|
||||||
|
if alpha > 0 {
|
||||||
|
for j := range target.values {
|
||||||
|
target.values[j] = math.Max(0, math.Min(1, (target.values[j]*target.alpha*(1-f)+source.values[j]*source.alpha-initial.values[j]*initial.alpha*(1-f))/alpha))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target.alpha = math.Max(0, math.Min(1, alpha))
|
||||||
|
target.effect = target.effect*(1-f) + source.effect
|
||||||
|
target.shape += f * (1 - target.shape)
|
||||||
|
}
|
||||||
|
|
||||||
// pdfCompositeGroup 移除组初始背景贡献后,将组结果合成到父空间
|
// pdfCompositeGroup 移除组初始背景贡献后,将组结果合成到父空间
|
||||||
// 入参: pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式
|
// 入参: pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式
|
||||||
// 返回: error 颜色或混合错误
|
// 返回: error 颜色或混合错误
|
||||||
func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode pdfgo.Name) error {
|
func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode pdfgo.Name) error {
|
||||||
for i, pixel := range result {
|
for i, pixel := range result {
|
||||||
|
pixels[i].shape += pixel.shape * (1 - pixels[i].shape)
|
||||||
if pixel.effect == 0 {
|
if pixel.effect == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -39,6 +39,9 @@ type pdfPaintRegion struct {
|
|||||||
// 入参: mark 组边界状态, walk 组内容访问函数, visitor 页面访问器
|
// 入参: mark 组边界状态, walk 组内容访问函数, visitor 页面访问器
|
||||||
// 返回: error 不支持的组内容或转换错误
|
// 返回: error 不支持的组内容或转换错误
|
||||||
func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error, visitor pdfgo.Visitor) error {
|
func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error, visitor pdfgo.Visitor) error {
|
||||||
|
if mark.Knockout {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "knockout group outside page compositing"}
|
||||||
|
}
|
||||||
if mark.BlendMode != "" && mark.BlendMode != "Normal" && mark.BlendMode != "Compatible" {
|
if mark.BlendMode != "" && mark.BlendMode != "Normal" && mark.BlendMode != "Compatible" {
|
||||||
return &pdfgo.UnsupportedError{Feature: "group blend mode " + string(mark.BlendMode)}
|
return &pdfgo.UnsupportedError{Feature: "group blend mode " + string(mark.BlendMode)}
|
||||||
}
|
}
|
||||||
@@ -152,7 +155,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
|||||||
if stroke {
|
if stroke {
|
||||||
color, overprint = path.Style.Stroke, path.Style.StrokeOverprint
|
color, overprint = path.Style.Stroke, path.Style.StrokeOverprint
|
||||||
}
|
}
|
||||||
if color.Tiling != nil || color.Axial != nil || color.Radial != nil || overprint && pdfOverprintNeedsSeparation(color) {
|
if color.Tiling != nil || color.Axial != nil || color.Radial != nil || color.Mesh != nil || overprint && pdfOverprintNeedsSeparation(color) {
|
||||||
return fmt.Errorf("%w: patterned or overprinted group content", errPDFGroupRaster)
|
return fmt.Errorf("%w: patterned or overprinted group content", errPDFGroupRaster)
|
||||||
}
|
}
|
||||||
if color.Alpha == 0 {
|
if color.Alpha == 0 {
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pdfMeshVertex 保存局部毫米坐标及原曲面参数
|
||||||
|
type pdfMeshVertex struct {
|
||||||
|
point pdfgo.Point
|
||||||
|
u, v float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfMeshTriangle 保存按输出精度细分的曲面网格
|
||||||
|
type pdfMeshTriangle struct {
|
||||||
|
vertices [3]pdfMeshVertex
|
||||||
|
patch int
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfMeshKey 区分网格及其输出混合空间
|
||||||
|
type pdfMeshKey struct {
|
||||||
|
mesh *pdfgo.MeshGradient
|
||||||
|
space *pdfgo.ColorSpace
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfMeshPixel 保存抗锯齿覆盖率与源空间合成分量
|
||||||
|
type pdfMeshPixel struct {
|
||||||
|
values [4]float64
|
||||||
|
shape float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfMeshSample 保存采样点参数,patch为一基编号,零表示未覆盖
|
||||||
|
type pdfMeshSample struct {
|
||||||
|
u, v float64
|
||||||
|
patch int
|
||||||
|
}
|
||||||
|
|
||||||
|
// meshTriangles 按曲面二阶导数界细分,将几何误差控制在输出像素的十六分之一内
|
||||||
|
// 入参: mesh 原始曲面
|
||||||
|
// 返回: []pdfMeshTriangle 细分结果, error 取消或坐标错误
|
||||||
|
func (p *pdfImporter) meshTriangles(mesh *pdfgo.MeshGradient) ([]pdfMeshTriangle, error) {
|
||||||
|
matrix := p.matrix.Mul(mesh.Matrix)
|
||||||
|
var triangles []pdfMeshTriangle
|
||||||
|
for index, patch := range mesh.Patches {
|
||||||
|
for i := range patch.Points {
|
||||||
|
for j := range patch.Points[i] {
|
||||||
|
patch.Points[i][j] = matrix.Apply(patch.Points[i][j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var duu, dvv, duv float64
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
for j := 0; j < 4; j++ {
|
||||||
|
a := patch.Points[i][j]
|
||||||
|
if i < 2 {
|
||||||
|
b, c := patch.Points[i+1][j], patch.Points[i+2][j]
|
||||||
|
duu = math.Max(duu, 6*math.Hypot(c.X-2*b.X+a.X, c.Y-2*b.Y+a.Y))
|
||||||
|
}
|
||||||
|
if j < 2 {
|
||||||
|
b, c := patch.Points[i][j+1], patch.Points[i][j+2]
|
||||||
|
dvv = math.Max(dvv, 6*math.Hypot(c.X-2*b.X+a.X, c.Y-2*b.Y+a.Y))
|
||||||
|
}
|
||||||
|
if i < 3 && j < 3 {
|
||||||
|
b, c, d := patch.Points[i+1][j], patch.Points[i][j+1], patch.Points[i+1][j+1]
|
||||||
|
duv = math.Max(duv, 9*math.Hypot(d.X-b.X-c.X+a.X, d.Y-b.Y-c.Y+a.Y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps := math.Max(1, math.Ceil(math.Sqrt(8*(duu+dvv+2*duv)*p.rasterDPI/25.4)))
|
||||||
|
if !finite(steps) || steps >= math.Sqrt(float64(int(^uint(0)>>1)/2)) {
|
||||||
|
return nil, fmt.Errorf("mesh subdivision exceeds platform integer range")
|
||||||
|
}
|
||||||
|
n := int(steps)
|
||||||
|
row := make([]pdfMeshVertex, n+1)
|
||||||
|
for i := range row {
|
||||||
|
u := float64(i) / steps
|
||||||
|
row[i] = pdfMeshVertex{patch.PointAt(u, 0), u, 0}
|
||||||
|
}
|
||||||
|
for j := 1; j <= n; j++ {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v := float64(j) / steps
|
||||||
|
left := pdfMeshVertex{patch.PointAt(0, v), 0, v}
|
||||||
|
for i := 1; i <= n; i++ {
|
||||||
|
u := float64(i) / steps
|
||||||
|
right := pdfMeshVertex{patch.PointAt(u, v), u, v}
|
||||||
|
a, b := row[i-1], row[i]
|
||||||
|
triangles = append(triangles, pdfMeshTriangle{[3]pdfMeshVertex{a, b, left}, index}, pdfMeshTriangle{[3]pdfMeshVertex{b, right, left}, index})
|
||||||
|
row[i-1], left = left, right
|
||||||
|
}
|
||||||
|
row[n] = left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return triangles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mesh 对当前图块执行四倍超采样,曲面重叠按后补片及较大v、u取值
|
||||||
|
// 入参: mesh 原曲面, space 输出混合空间
|
||||||
|
// 返回: []pdfMeshPixel 颜色及覆盖, error 几何或颜色错误
|
||||||
|
func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace) ([]pdfMeshPixel, error) {
|
||||||
|
key := pdfMeshKey{mesh, space}
|
||||||
|
if pixels, ok := c.meshes[key]; ok {
|
||||||
|
return pixels, nil
|
||||||
|
}
|
||||||
|
triangles, ok := c.cache.meshes[mesh]
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
triangles, err = c.importer.meshTriangles(mesh)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c.cache.meshes == nil {
|
||||||
|
c.cache.meshes = map[*pdfgo.MeshGradient][]pdfMeshTriangle{}
|
||||||
|
}
|
||||||
|
c.cache.meshes[mesh] = triangles
|
||||||
|
}
|
||||||
|
const samples = 4
|
||||||
|
w, h := c.width*samples, c.height*samples
|
||||||
|
grid := make([]pdfMeshSample, w*h)
|
||||||
|
scale := c.importer.rasterDPI / 25.4 * samples
|
||||||
|
for _, triangle := range triangles {
|
||||||
|
if err := c.importer.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
vertices := triangle.vertices
|
||||||
|
for i := range vertices {
|
||||||
|
vertices[i].point.X = (vertices[i].point.X - c.box.X) * scale
|
||||||
|
vertices[i].point.Y = (vertices[i].point.Y - c.box.Y) * scale
|
||||||
|
}
|
||||||
|
a, b, d := vertices[0].point, vertices[1].point, vertices[2].point
|
||||||
|
denominator := (b.Y-d.Y)*(a.X-d.X) + (d.X-b.X)*(a.Y-d.Y)
|
||||||
|
if denominator == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
minX, maxX := math.Min(a.X, math.Min(b.X, d.X)), math.Max(a.X, math.Max(b.X, d.X))
|
||||||
|
minY, maxY := math.Min(a.Y, math.Min(b.Y, d.Y)), math.Max(a.Y, math.Max(b.Y, d.Y))
|
||||||
|
if maxX < 0 || maxY < 0 || minX >= float64(w) || minY >= float64(h) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
endX := int(math.Min(float64(w), math.Ceil(maxX)))
|
||||||
|
endY := int(math.Min(float64(h), math.Ceil(maxY)))
|
||||||
|
for y := int(math.Max(0, math.Floor(minY))); y < endY; y++ {
|
||||||
|
for x := int(math.Max(0, math.Floor(minX))); x < endX; x++ {
|
||||||
|
px, py := float64(x)+.5, float64(y)+.5
|
||||||
|
l0 := ((b.Y-d.Y)*(px-d.X) + (d.X-b.X)*(py-d.Y)) / denominator
|
||||||
|
l1 := ((d.Y-a.Y)*(px-d.X) + (a.X-d.X)*(py-d.Y)) / denominator
|
||||||
|
l2 := 1 - l0 - l1
|
||||||
|
if l0 < -1e-10 || l1 < -1e-10 || l2 < -1e-10 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u := math.Max(0, math.Min(1, l0*vertices[0].u+l1*vertices[1].u+l2*vertices[2].u))
|
||||||
|
v := math.Max(0, math.Min(1, l0*vertices[0].v+l1*vertices[1].v+l2*vertices[2].v))
|
||||||
|
current := &grid[y*w+x]
|
||||||
|
if triangle.patch+1 > current.patch || triangle.patch+1 == current.patch && (v > current.v || v == current.v && u > current.u) {
|
||||||
|
*current = pdfMeshSample{u, v, triangle.patch + 1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pixels := make([]pdfMeshPixel, c.width*c.height)
|
||||||
|
for y := 0; y < h; y++ {
|
||||||
|
if err := c.importer.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
sample := grid[y*w+x]
|
||||||
|
if sample.patch == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values, err := mesh.ValuesAt(sample.patch-1, sample.u, sample.v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
values, err = space.Convert(values[:mesh.Space.Components()], mesh.Space, mesh.Intent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pixel := &pixels[(y/samples)*c.width+x/samples]
|
||||||
|
for i, value := range values {
|
||||||
|
pixel.values[i] += value
|
||||||
|
}
|
||||||
|
pixel.shape++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range pixels {
|
||||||
|
pixel := &pixels[i]
|
||||||
|
if pixel.shape == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for j := range pixel.values {
|
||||||
|
pixel.values[j] /= pixel.shape
|
||||||
|
}
|
||||||
|
pixel.shape /= samples * samples
|
||||||
|
}
|
||||||
|
if c.meshes == nil {
|
||||||
|
c.meshes = map[pdfMeshKey][]pdfMeshPixel{}
|
||||||
|
}
|
||||||
|
c.meshes[key] = pixels
|
||||||
|
return pixels, nil
|
||||||
|
}
|
||||||
+67
-48
@@ -16,72 +16,61 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// pdfGradientSegments 将线性源颜色及设备四色的截断点表达为OFD分段
|
// gradientSegments 保留线性源颜色的设备分量,校准颜色经源空间转换
|
||||||
// 入参: stops 源颜色分段, space 源颜色空间
|
// 入参: stops 源颜色分段, space 源颜色空间
|
||||||
// 返回: []ShdSegment 等价RGB分段, error 不可线性表达的变换
|
// 返回: []ShdSegment OFD颜色分段, error 不可线性表达的变换
|
||||||
func pdfGradientSegments(stops []pdfgo.GradientStop, space *pdfgo.ColorSpace) ([]ShdSegment, error) {
|
func (p *pdfImporter) gradientSegments(stops []pdfgo.GradientStop, space *pdfgo.ColorSpace) ([]ShdSegment, error) {
|
||||||
if len(stops) == 0 {
|
if err := pdfGradientStopsError(stops, space); err != nil {
|
||||||
return nil, &pdfgo.UnsupportedError{Feature: "nonlinear gradient conversion"}
|
|
||||||
}
|
|
||||||
calibrated := space != nil && space.Calibrated() && !space.SRGBEquivalent()
|
|
||||||
cmyk := space != nil && space.Model == "DeviceCMYK" && !space.Calibrated()
|
|
||||||
segments := make([]ShdSegment, 0, len(stops))
|
|
||||||
appendStop := func(position float64, rgb [3]float64) {
|
|
||||||
segments = append(segments, ShdSegment{Position: position, Color: ShdColor{Value: pdfNumbers(math.Round(rgb[0]*255), math.Round(rgb[1]*255), math.Round(rgb[2]*255))}})
|
|
||||||
}
|
|
||||||
for i, b := range stops {
|
|
||||||
if i > 0 {
|
|
||||||
a := stops[i-1]
|
|
||||||
if a.Position != b.Position && a.Values != b.Values {
|
|
||||||
if calibrated {
|
|
||||||
return nil, &pdfgo.UnsupportedError{Feature: "nonlinear ICC gradient conversion"}
|
|
||||||
}
|
|
||||||
if cmyk {
|
|
||||||
var crossings []float64
|
|
||||||
for c := 0; c < 3; c++ {
|
|
||||||
start, end := a.Values[c]+a.Values[3], b.Values[c]+b.Values[3]
|
|
||||||
if start != end {
|
|
||||||
if t := (1 - start) / (end - start); t > 0 && t < 1 {
|
|
||||||
crossings = append(crossings, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slices.Sort(crossings)
|
|
||||||
for _, t := range slices.Compact(crossings) {
|
|
||||||
var values [4]float64
|
|
||||||
for c := range values {
|
|
||||||
values[c] = a.Values[c]*(1-t) + b.Values[c]*t
|
|
||||||
}
|
|
||||||
rgb, err := space.RGB(values[:], "RelativeColorimetric")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
appendStop(a.Position+(b.Position-a.Position)*t, rgb)
|
cmyk := space != nil && space.Model == "DeviceCMYK" && !space.Calibrated()
|
||||||
|
segments := make([]ShdSegment, len(stops))
|
||||||
|
for i, stop := range stops {
|
||||||
|
paint := pdfgo.Paint{RGB: stop.RGB, Alpha: 1}
|
||||||
|
if cmyk {
|
||||||
|
paint.CMYK = &stop.Values
|
||||||
}
|
}
|
||||||
}
|
color := p.color(paint)
|
||||||
}
|
segments[i] = ShdSegment{Position: stop.Position, Color: ShdColor{Value: color.Value, ColorSpace: color.ColorSpace}}
|
||||||
}
|
|
||||||
appendStop(b.Position, b.RGB)
|
|
||||||
}
|
}
|
||||||
return segments, nil
|
return segments, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pdfGradientStopsError 检查源颜色分段是否可由OFD基本颜色表达
|
||||||
|
// 入参: stops 源颜色分段, space 源颜色空间
|
||||||
|
// 返回: error 不可线性表达的变换
|
||||||
|
func pdfGradientStopsError(stops []pdfgo.GradientStop, space *pdfgo.ColorSpace) error {
|
||||||
|
if len(stops) == 0 {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "nonlinear gradient conversion"}
|
||||||
|
}
|
||||||
|
calibrated := space != nil && space.Calibrated() && !space.SRGBEquivalent()
|
||||||
|
if calibrated {
|
||||||
|
for i := 1; i < len(stops); i++ {
|
||||||
|
a, b := stops[i-1], stops[i]
|
||||||
|
if a.Position != b.Position && a.Values != b.Values {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "nonlinear ICC gradient conversion"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// pdfGradientError 检查渐变能否精确映射为OFD线性分段
|
// pdfGradientError 检查渐变能否精确映射为OFD线性分段
|
||||||
// 入参: paint 画刷
|
// 入参: paint 画刷
|
||||||
// 返回: error 不可等价表达的渐变
|
// 返回: error 不可等价表达的渐变
|
||||||
func pdfGradientError(paint pdfgo.Paint) error {
|
func pdfGradientError(paint pdfgo.Paint) error {
|
||||||
|
if paint.Mesh != nil {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "bicubic mesh conversion"}
|
||||||
|
}
|
||||||
if paint.Axial != nil {
|
if paint.Axial != nil {
|
||||||
_, err := pdfGradientSegments(paint.Axial.Stops, paint.Axial.Space)
|
return pdfGradientStopsError(paint.Axial.Stops, paint.Axial.Space)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if paint.Radial != nil {
|
if paint.Radial != nil {
|
||||||
_, err := pdfGradientSegments(paint.Radial.Stops, paint.Radial.Space)
|
return pdfGradientStopsError(paint.Radial.Stops, paint.Radial.Space)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -90,7 +79,30 @@ func pdfGradientError(paint pdfgo.Paint) error {
|
|||||||
// 入参: mark 路径绘制信息
|
// 入参: mark 路径绘制信息
|
||||||
// 返回: error 几何或着色错误
|
// 返回: error 几何或着色错误
|
||||||
func (p *pdfImporter) gradientPath(mark pdfgo.PathMark) error {
|
func (p *pdfImporter) gradientPath(mark pdfgo.PathMark) error {
|
||||||
return p.compositeRegion(nil, pdfCompositeNode{path: &mark}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false)
|
cmyk := true
|
||||||
|
for i, paint := range []pdfgo.Paint{mark.Style.Fill, mark.Style.Stroke} {
|
||||||
|
if i == 0 && !mark.Fill || i == 1 && !mark.Stroke {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
space := paint.Space
|
||||||
|
switch {
|
||||||
|
case paint.Axial != nil:
|
||||||
|
space = paint.Axial.Space
|
||||||
|
case paint.Radial != nil:
|
||||||
|
space = paint.Radial.Space
|
||||||
|
case paint.Mesh != nil:
|
||||||
|
space = paint.Mesh.Space
|
||||||
|
}
|
||||||
|
if paint.CMYK == nil && (space == nil || space.Model != "DeviceCMYK" || space.Calibrated()) {
|
||||||
|
cmyk = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
space := &pdfgo.ColorSpace{Model: "DeviceRGB"}
|
||||||
|
if cmyk {
|
||||||
|
space.Model = "DeviceCMYK"
|
||||||
|
}
|
||||||
|
return p.compositeRegion(nil, pdfCompositeNode{path: &mark}, space, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfShadingPosition 计算渐变参数,未延伸区域及不存在非负圆半径的位置不着色
|
// pdfShadingPosition 计算渐变参数,未延伸区域及不存在非负圆半径的位置不着色
|
||||||
@@ -103,6 +115,13 @@ func pdfShadingPosition(paint pdfgo.Paint, point pdfgo.Point) (float64, bool) {
|
|||||||
return t, finite(t) && (t >= 0 || g.Extend[0]) && (t <= 1 || g.Extend[1])
|
return t, finite(t) && (t >= 0 || g.Extend[0]) && (t <= 1 || g.Extend[1])
|
||||||
}
|
}
|
||||||
g := paint.Radial
|
g := paint.Radial
|
||||||
|
if g.Matrix != (pdfgo.Matrix{}) {
|
||||||
|
inverse, ok := g.Matrix.Inverse()
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
point = inverse.Apply(point)
|
||||||
|
}
|
||||||
dx, dy, dr := g.End.X-g.Start.X, g.End.Y-g.Start.Y, g.EndRadius-g.StartRadius
|
dx, dy, dr := g.End.X-g.Start.X, g.End.Y-g.Start.Y, g.EndRadius-g.StartRadius
|
||||||
px, py := point.X-g.Start.X, point.Y-g.Start.Y
|
px, py := point.X-g.Start.X, point.Y-g.Start.Y
|
||||||
a, b, c := dx*dx+dy*dy-dr*dr, -2*(px*dx+py*dy+g.StartRadius*dr), px*px+py*py-g.StartRadius*g.StartRadius
|
a, b, c := dx*dx+dy*dy-dr*dr, -2*(px*dx+py*dy+g.StartRadius*dr), px*px+py*py-g.StartRadius*g.StartRadius
|
||||||
|
|||||||
Reference in new issue
Block a user