mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
1 parent
4fa65175bc
commit
f234d4ad10
20 files changed
+1761
-105
No files matched your search
+80
-20
@@ -910,43 +910,103 @@ button:not(:disabled):hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.convert-dialog .form-actions { flex-wrap: wrap; }
|
.convert-dialog { --control-height: 36px; }
|
||||||
.convert-options { order: -1; flex-basis: 100%; justify-content: center; margin: 0 0 4px; }
|
.convert-dialog > form {
|
||||||
#convertClose { margin-left: auto; }
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: calc(100dvh - 26px);
|
||||||
|
}
|
||||||
|
.convert-dialog .form-title,
|
||||||
|
.convert-progress,
|
||||||
|
.convert-dialog .form-actions { flex-shrink: 0; }
|
||||||
|
.convert-table {
|
||||||
|
min-height: min(120px, 18dvh);
|
||||||
|
max-height: 50dvh;
|
||||||
|
}
|
||||||
|
.convert-progress:has(#convertStatus:empty) { display: none; }
|
||||||
|
.convert-dialog .form-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto minmax(0, 1fr) auto auto;
|
||||||
|
row-gap: 12px;
|
||||||
|
}
|
||||||
|
.convert-dialog .form-actions > .button { grid-row: 2; }
|
||||||
|
.convert-options {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: 1;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.convert-options:has(#convertDPIRow[hidden]) {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
|
.convert-options .form-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.convert-options label { width: auto; }
|
||||||
|
#convertAdd { grid-column: 1; }
|
||||||
|
#convertClear { grid-column: 2; }
|
||||||
|
#convertClose { grid-column: 4; }
|
||||||
|
#convertCancel,
|
||||||
|
#convertStart { grid-column: 5; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 620px) {
|
@media (max-width: 620px) {
|
||||||
.convert-options {
|
|
||||||
grid-template-columns: calc(2em + 8px + var(--convert-control-width)) 132px;
|
|
||||||
}
|
|
||||||
.convert-table table,
|
.convert-table table,
|
||||||
.convert-table tbody { display: block; }
|
.convert-table tbody { display: block; }
|
||||||
.convert-table thead { display: none; }
|
.convert-table thead { display: none; }
|
||||||
.convert-table tr {
|
.convert-table tr {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px 12px;
|
||||||
padding: 8px;
|
padding: 12px;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.convert-table tr:last-child { border-bottom: 0; }
|
.convert-table tr:last-child { border-bottom: 0; }
|
||||||
.convert-table tr:has(+ .convert-details) { padding-bottom: 0; border-bottom: 0; }
|
.convert-table tr:has(+ .convert-details) { padding-bottom: 0; border-bottom: 0; }
|
||||||
.convert-table td { min-width: 0; padding: 0; border: 0; }
|
.convert-table td { min-width: 0; padding: 0; border: 0; }
|
||||||
.convert-table td:first-child { flex: 0 0 calc(100% - var(--convert-control-width) - var(--control-height) - 16px); }
|
.convert-table td:first-child {
|
||||||
.convert-table td:nth-child(4) { order: 1; flex: 0 0 var(--convert-control-width); }
|
grid-column: 1 / -1;
|
||||||
.convert-table td:last-child { order: 2; flex: 0 0 var(--control-height); }
|
grid-row: 1;
|
||||||
|
padding-right: calc(var(--control-height) + 12px);
|
||||||
|
}
|
||||||
|
.convert-name {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.convert-table td:last-child {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1;
|
||||||
|
align-self: start;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
.convert-table td[data-label] {
|
.convert-table td[data-label] {
|
||||||
order: 3;
|
|
||||||
flex: 1 1 0;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2em var(--convert-control-width);
|
grid-row: 2;
|
||||||
|
grid-template-rows: auto var(--control-height);
|
||||||
|
justify-items: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 6px;
|
||||||
gap: 8px;
|
}
|
||||||
|
.convert-table td[data-label]::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.convert-table input,
|
||||||
|
.convert-format,
|
||||||
|
.convert-format select { width: 100%; }
|
||||||
|
.convert-table .convert-details > td {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-self: stretch;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
.convert-table td[data-label]::before { content: attr(data-label); color: var(--text-muted); }
|
|
||||||
.convert-table .convert-details > td { flex: 0 0 100%; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#sourceTextPanel {
|
#sourceTextPanel {
|
||||||
|
|||||||
@@ -3376,7 +3376,7 @@ async function openOFD(file) {
|
|||||||
if (openSeq !== state.openSeq) return;
|
if (openSeq !== state.openSeq) return;
|
||||||
let converted;
|
let converted;
|
||||||
try {
|
try {
|
||||||
converted = await callWASM("ofdgoConvertPDF", bytes);
|
converted = await callWASM("ofdgoConvertPDF", bytes, await fontManager.files(fontManager.records()));
|
||||||
} finally {
|
} finally {
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
state.exportRequestID = 0;
|
state.exportRequestID = 0;
|
||||||
@@ -4474,6 +4474,7 @@ function renderBatchList() {
|
|||||||
item.cell = row.insertCell();
|
item.cell = row.insertCell();
|
||||||
item.cell.textContent = item.text;
|
item.cell.textContent = item.text;
|
||||||
item.cell.dataset.status = item.status;
|
item.cell.dataset.status = item.status;
|
||||||
|
item.cell.dataset.label = "状态";
|
||||||
const action = row.insertCell();
|
const action = row.insertCell();
|
||||||
const remove = document.createElement("button");
|
const remove = document.createElement("button");
|
||||||
remove.type = "button";
|
remove.type = "button";
|
||||||
|
|||||||
@@ -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-20260926072349-1d4e7a9c2616
|
github.com/xiaoqidun/pdfgo v0.0.0-20260926132451-00d165f66ad7
|
||||||
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-20260926072349-1d4e7a9c2616 h1:16JKsw9CKBf+BBVeiiHg12bg7h1ocKSxyhUpw6eRIBk=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260926132451-00d165f66ad7 h1:yRhuqMZm146rOe1682qJJoBld8zwNYg3RwvdvWkbwzk=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260926072349-1d4e7a9c2616/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260926132451-00d165f66ad7/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=
|
||||||
|
|||||||
+10
-3
@@ -367,18 +367,25 @@ func safeCall(fn func([]js.Value) (any, error), args []js.Value) (data any, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convertPDFDocument 通过库转换PDF,不替换当前文档会话
|
// convertPDFDocument 通过库转换PDF,不替换当前文档会话
|
||||||
// 入参: args PDF数据
|
// 入参: args PDF数据、外部字体及进度回调
|
||||||
// 返回: any OFD数据及转换警告, error 错误信息
|
// 返回: any OFD数据及转换警告, error 错误信息
|
||||||
func convertPDFDocument(args []js.Value) (any, error) {
|
func convertPDFDocument(args []js.Value) (any, error) {
|
||||||
if len(args) != 2 {
|
if len(args) != 3 {
|
||||||
return nil, fmt.Errorf("missing PDF data")
|
return nil, fmt.Errorf("missing PDF data")
|
||||||
}
|
}
|
||||||
data, err := bytesFromJS(args[0])
|
data, err := bytesFromJS(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
fonts, err := fontsFromJS(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
options := ofdgo.PDFImportOptions{OnProgress: editorOperationProgress(args[1])}
|
options := ofdgo.PDFImportOptions{OnProgress: editorOperationProgress(args[2])}
|
||||||
|
if len(fonts) > 0 {
|
||||||
|
options.RendererOptions = []ofdgo.RendererOption{ofdgo.WithFontFS(ofdgo.NewFontFS(fonts))}
|
||||||
|
}
|
||||||
report, err := ofdgo.ConvertPDF(context.Background(), bytes.NewReader(data), int64(len(data)), &output, options)
|
report, err := ofdgo.ConvertPDF(context.Background(), bytes.NewReader(data), int64(len(data)), &output, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type ConvertProgress struct {
|
|||||||
// PageRange使用一基页码表达式,与Pages互斥,空字符串表示全部页面
|
// PageRange使用一基页码表达式,与Pages互斥,空字符串表示全部页面
|
||||||
// SkipUnchanged在源格式与目标相同且未选页时不写出数据,不校验或重新生成源文档
|
// SkipUnchanged在源格式与目标相同且未选页时不写出数据,不校验或重新生成源文档
|
||||||
// PDF先转换为OFD对象再输出,PDF选项中的进度回调仍会调用
|
// PDF先转换为OFD对象再输出,PDF选项中的进度回调仍会调用
|
||||||
|
// RendererOptions同时用于PDF局部合成和输出,在PDF.RendererOptions之后应用
|
||||||
// Backends统一配置转换与输出,优先于PDF和RendererOptions中的后端设置
|
// Backends统一配置转换与输出,优先于PDF和RendererOptions中的后端设置
|
||||||
// PageOutput仅用于逐页格式,同步调用export写出一页并自行处理提交或回滚;此时output可为nil
|
// PageOutput仅用于逐页格式,同步调用export写出一页并自行处理提交或回滚;此时output可为nil
|
||||||
// 未提供PageOutput时,逐页格式打包ZIP;所有回调返回错误均会停止转换
|
// 未提供PageOutput时,逐页格式打包ZIP;所有回调返回错误均会停止转换
|
||||||
@@ -180,9 +181,12 @@ func Convert(ctx context.Context, source io.ReaderAt, size int64, output io.Writ
|
|||||||
switch report.Input {
|
switch report.Input {
|
||||||
case "pdf":
|
case "pdf":
|
||||||
pdfOptions := options.PDF
|
pdfOptions := options.PDF
|
||||||
|
pdfOptions.RendererOptions = append(append([]RendererOption(nil), pdfOptions.RendererOptions...), options.RendererOptions...)
|
||||||
if options.Backends != nil {
|
if options.Backends != nil {
|
||||||
pdfOptions.Backends = options.Backends
|
pdfOptions.Backends = options.Backends
|
||||||
}
|
}
|
||||||
|
options.RendererOptions = pdfOptions.RendererOptions
|
||||||
|
options.Backends = pdfOptions.Backends
|
||||||
pdfOptions.OnProgress = func(stage string, completed, total int) error {
|
pdfOptions.OnProgress = func(stage string, completed, total int) error {
|
||||||
if err := progress(stage, completed, total); err != nil {
|
if err := progress(stage, completed, total); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+11
-2
@@ -46,6 +46,7 @@ type Editor struct {
|
|||||||
pages []PageContent
|
pages []PageContent
|
||||||
resources []editorResource
|
resources []editorResource
|
||||||
fonts map[string]*FontResource
|
fonts map[string]*FontResource
|
||||||
|
fontDirs []string
|
||||||
fontFS []fs.FS
|
fontFS []fs.FS
|
||||||
fontRenderer *Renderer
|
fontRenderer *Renderer
|
||||||
fontMetrics map[string]FontMetrics
|
fontMetrics map[string]FontMetrics
|
||||||
@@ -65,8 +66,16 @@ type Editor struct {
|
|||||||
encryption *encryptionState
|
encryption *encryptionState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetFontDirs 设置编辑与几何度量使用的外部字体目录,不嵌入或替换文档字体
|
||||||
|
// 入参: dirs 字体目录,空参数清除目录配置
|
||||||
|
func (e *Editor) SetFontDirs(dirs ...string) {
|
||||||
|
e.fontDirs = slices.Clone(dirs)
|
||||||
|
e.fontRenderer = nil
|
||||||
|
e.fontMetrics = make(map[string]FontMetrics)
|
||||||
|
}
|
||||||
|
|
||||||
// SetFontFS 设置编辑与几何度量使用的外部字体来源,不嵌入或替换文档字体
|
// SetFontFS 设置编辑与几何度量使用的外部字体来源,不嵌入或替换文档字体
|
||||||
// 入参: fsys 字体文件系统,空参数恢复默认来源
|
// 入参: fsys 字体文件系统,空参数清除文件系统配置
|
||||||
func (e *Editor) SetFontFS(fsys ...fs.FS) {
|
func (e *Editor) SetFontFS(fsys ...fs.FS) {
|
||||||
e.fontFS = slices.Clone(fsys)
|
e.fontFS = slices.Clone(fsys)
|
||||||
e.fontRenderer = nil
|
e.fontRenderer = nil
|
||||||
@@ -112,7 +121,7 @@ func (e *Editor) Backends() RenderBackends {
|
|||||||
// 入参: reader 当前文档快照
|
// 入参: reader 当前文档快照
|
||||||
// 返回: *Renderer 度量器
|
// 返回: *Renderer 度量器
|
||||||
func (e *Editor) newRenderer(reader *Reader) *Renderer {
|
func (e *Editor) newRenderer(reader *Reader) *Renderer {
|
||||||
return NewRenderer(reader, WithFontFS(e.fontFS...), WithRenderBackends(e.backends))
|
return NewRenderer(reader, WithFontDirs(e.fontDirs...), WithFontFS(e.fontFS...), WithRenderBackends(e.backends))
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorResource 文档内嵌资源
|
// editorResource 文档内嵌资源
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func (e *Editor) Transaction(edit func(*Editor) error) error {
|
|||||||
}
|
}
|
||||||
changed := e.revision != before.revision
|
changed := e.revision != before.revision
|
||||||
e.history, e.historyIndex, e.historyLimit = before.history, before.historyIndex, before.historyLimit
|
e.history, e.historyIndex, e.historyLimit = before.history, before.historyIndex, before.historyLimit
|
||||||
e.backends, e.fontFS = before.backends, before.fontFS
|
e.backends, e.fontDirs, e.fontFS = before.backends, before.fontDirs, before.fontFS
|
||||||
e.fontRenderer, e.fontMetrics = before.fontRenderer, before.fontMetrics
|
e.fontRenderer, e.fontMetrics = before.fontRenderer, before.fontMetrics
|
||||||
if changed {
|
if changed {
|
||||||
e.serial, e.revision = before.serial, before.revision
|
e.serial, e.revision = before.serial, before.revision
|
||||||
@@ -83,7 +83,7 @@ func (e *Editor) restoreTransaction(state Editor) {
|
|||||||
maps.Copy(state.resourceID, e.resourceID)
|
maps.Copy(state.resourceID, e.resourceID)
|
||||||
state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit
|
state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit
|
||||||
state.serial, state.maxID = max(e.serial, state.serial), max(e.maxID, state.maxID)
|
state.serial, state.maxID = max(e.serial, state.serial), max(e.maxID, state.maxID)
|
||||||
state.backends, state.fontFS = e.backends, e.fontFS
|
state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS
|
||||||
state.fontRenderer, state.fontMetrics = e.fontRenderer, e.fontMetrics
|
state.fontRenderer, state.fontMetrics = e.fontRenderer, e.fontMetrics
|
||||||
*e = state
|
*e = state
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ type FontOutlineBatch interface {
|
|||||||
GlyphOutlines(glyphs []uint16, size float64) ([]GeometryPath, error)
|
GlyphOutlines(glyphs []uint16, size float64) ([]GeometryPath, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FontGlyphDiagnostics 提供已提取字形的恢复提示,不将提示作为轮廓解析失败
|
||||||
|
// 字形不存在或不支持的指令仍由GlyphOutline返回错误
|
||||||
|
type FontGlyphDiagnostics interface {
|
||||||
|
FontMetrics
|
||||||
|
GlyphWarning(glyph uint16) error
|
||||||
|
}
|
||||||
|
|
||||||
// FontShaper 提供可选的横向文字塑形,仅用于显式新建或重排
|
// FontShaper 提供可选的横向文字塑形,仅用于显式新建或重排
|
||||||
// 字形按逻辑簇顺序排列,Cluster使用原文的符文索引,坐标为毫米且纵轴向下
|
// 字形按逻辑簇顺序排列,Cluster使用原文的符文索引,坐标为毫米且纵轴向下
|
||||||
type FontShaper interface {
|
type FontShaper interface {
|
||||||
|
|||||||
+61
-8
@@ -39,7 +39,7 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
cffData = sanitized
|
cffData = sanitized
|
||||||
}
|
}
|
||||||
cffData, err = normalizeCFFDotsection(cffData)
|
cffData, err = normalizeCFFCharstrings(cffData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -83,10 +83,10 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) {
|
|||||||
// cffDict 使用float64存储所有数值,以统一处理整数和实数
|
// cffDict 使用float64存储所有数值,以统一处理整数和实数
|
||||||
type cffDict map[int][]float64
|
type cffDict map[int][]float64
|
||||||
|
|
||||||
// normalizeCFFDotsection 移除旧版Type2字形程序中无效果的dotsection指令
|
// normalizeCFFCharstrings 规范化Type2提示指令的等价编码,保留轮廓和提示数据
|
||||||
// 入参: data CFF字体数据
|
// 入参: data CFF字体数据
|
||||||
// 返回: []byte 标准化后的CFF数据, error 错误信息
|
// 返回: []byte 标准化后的CFF数据, error 错误信息
|
||||||
func normalizeCFFDotsection(data []byte) ([]byte, error) {
|
func normalizeCFFCharstrings(data []byte) ([]byte, error) {
|
||||||
if len(data) < 4 || int(data[2]) >= len(data) {
|
if len(data) < 4 || int(data[2]) >= len(data) {
|
||||||
return nil, fmt.Errorf("invalid CFF header")
|
return nil, fmt.Errorf("invalid CFF header")
|
||||||
}
|
}
|
||||||
@@ -107,18 +107,23 @@ func normalizeCFFDotsection(data []byte) ([]byte, error) {
|
|||||||
if charStart < topStart+topSize || charSize == 0 || charStart+charSize > len(data) {
|
if charStart < topStart+topSize || charSize == 0 || charStart+charSize > len(data) {
|
||||||
return nil, fmt.Errorf("invalid CFF charstrings")
|
return nil, fmt.Errorf("invalid CFF charstrings")
|
||||||
}
|
}
|
||||||
if !bytes.Contains(data[charStart:charStart+charSize], []byte{12, 0}) {
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
chars := readCFFIndexItems(data, charStart)
|
chars := readCFFIndexItems(data, charStart)
|
||||||
changed := false
|
changed := false
|
||||||
for index, charstring := range chars {
|
for index, charstring := range chars {
|
||||||
normalized, removed, err := stripType2Dotsection(charstring)
|
normalized, inserted, err := explicitType2CounterStems(charstring)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
changed = changed || inserted
|
||||||
|
if bytes.Contains(normalized, []byte{12, 0}) {
|
||||||
|
var removed bool
|
||||||
|
normalized, removed, err = stripType2Dotsection(normalized)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
changed = changed || removed
|
||||||
|
}
|
||||||
chars[index] = normalized
|
chars[index] = normalized
|
||||||
changed = changed || removed
|
|
||||||
}
|
}
|
||||||
if !changed {
|
if !changed {
|
||||||
return data, nil
|
return data, nil
|
||||||
@@ -165,6 +170,54 @@ func normalizeCFFDotsection(data []byte) ([]byte, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// explicitType2CounterStems 将计数掩码前省略的vstemhm补为显式指令,不改变笔画提示及掩码
|
||||||
|
// 入参: data Type2字形程序
|
||||||
|
// 返回: []byte 等价程序, bool 是否发生变更, error 字节码错误
|
||||||
|
func explicitType2CounterStems(data []byte) ([]byte, bool, error) {
|
||||||
|
operands, hints := 0, 0
|
||||||
|
for index := 0; index < len(data); {
|
||||||
|
start, op := index, data[index]
|
||||||
|
index++
|
||||||
|
switch {
|
||||||
|
case op == 28:
|
||||||
|
index += 2
|
||||||
|
operands++
|
||||||
|
case op == 255:
|
||||||
|
index += 4
|
||||||
|
operands++
|
||||||
|
case op >= 247:
|
||||||
|
index++
|
||||||
|
operands++
|
||||||
|
case op >= 32:
|
||||||
|
operands++
|
||||||
|
case op == 1 || op == 3 || op == 18 || op == 23:
|
||||||
|
hints += operands / 2
|
||||||
|
operands = 0
|
||||||
|
case op == 19 || op == 20:
|
||||||
|
hints += operands / 2
|
||||||
|
index += (hints + 7) / 8
|
||||||
|
if index > len(data) {
|
||||||
|
return nil, false, fmt.Errorf("incomplete Type2 hint mask")
|
||||||
|
}
|
||||||
|
if op == 20 && operands >= 2 {
|
||||||
|
result := make([]byte, 0, len(data)+1)
|
||||||
|
result = append(result, data[:start]...)
|
||||||
|
result = append(result, 23)
|
||||||
|
return append(result, data[start:]...), true, nil
|
||||||
|
}
|
||||||
|
operands = 0
|
||||||
|
case op == 12 && index < len(data) && data[index] == 0:
|
||||||
|
index++
|
||||||
|
default:
|
||||||
|
return data, false, nil
|
||||||
|
}
|
||||||
|
if index > len(data) {
|
||||||
|
return nil, false, fmt.Errorf("incomplete Type2 operand")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// stripType2Dotsection 保留字形指令与掩码并移除无效果的旧指令
|
// stripType2Dotsection 保留字形指令与掩码并移除无效果的旧指令
|
||||||
// 入参: data Type2字形程序
|
// 入参: data Type2字形程序
|
||||||
// 返回: []byte 标准化后的程序, bool 是否发生变更, error 错误信息
|
// 返回: []byte 标准化后的程序, bool 是否发生变更, error 错误信息
|
||||||
|
|||||||
+41
-5
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -25,10 +26,12 @@ import (
|
|||||||
|
|
||||||
// sfntOutliner 缓存字体程序并提取独立字形轮廓
|
// sfntOutliner 缓存字体程序并提取独立字形轮廓
|
||||||
type sfntOutliner struct {
|
type sfntOutliner struct {
|
||||||
font *font.SFNT
|
font *font.SFNT
|
||||||
once sync.Once
|
once sync.Once
|
||||||
program *ttInterpreter
|
program *ttInterpreter
|
||||||
err error
|
err error
|
||||||
|
mu sync.Mutex
|
||||||
|
warnings map[uint16]error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ttGlyph 保存字形轮廓及四个度量虚拟点
|
// ttGlyph 保存字形轮廓及四个度量虚拟点
|
||||||
@@ -36,6 +39,7 @@ type ttGlyph struct {
|
|||||||
points []ttPoint
|
points []ttPoint
|
||||||
ends []uint16
|
ends []uint16
|
||||||
metrics *[4]ttPoint
|
metrics *[4]ttPoint
|
||||||
|
warning error
|
||||||
}
|
}
|
||||||
|
|
||||||
// path 按TrueType指令和隐含点规则生成闭合轮廓,CFF保留原有解析
|
// path 按TrueType指令和隐含点规则生成闭合轮廓,CFF保留原有解析
|
||||||
@@ -128,6 +132,9 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
if result.warning != nil {
|
||||||
|
f.recordGlyphWarning(id, result.warning)
|
||||||
|
}
|
||||||
left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.font.Hmtx.LeftSideBearing(id))
|
left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.font.Hmtx.LeftSideBearing(id))
|
||||||
top := int32(int16(binary.BigEndian.Uint16(data[8:])))
|
top := int32(int16(binary.BigEndian.Uint16(data[8:])))
|
||||||
if result.metrics != nil {
|
if result.metrics != nil {
|
||||||
@@ -153,13 +160,39 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error)
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
vm := f.program.clone()
|
vm := f.program.clone()
|
||||||
vm.zones[1], vm.ends = result.points, result.ends
|
vm.zones[1], vm.ends = append([]ttPoint(nil), result.points...), result.ends
|
||||||
if err := vm.run(program); err != nil {
|
if err := vm.run(program); err != nil {
|
||||||
|
if errors.Is(err, errTTStackUnderflow) {
|
||||||
|
result.warning = err
|
||||||
|
f.recordGlyphWarning(id, err)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
return result, fmt.Errorf("TrueType glyph %d: %w", id, err)
|
return result, fmt.Errorf("TrueType glyph %d: %w", id, err)
|
||||||
}
|
}
|
||||||
|
result.points = vm.zones[1]
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recordGlyphWarning 记录字形恢复提示,允许并发提取轮廓
|
||||||
|
// 入参: id 字形编号, warning 恢复原因
|
||||||
|
func (f *sfntOutliner) recordGlyphWarning(id uint16, warning error) {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
if f.warnings == nil {
|
||||||
|
f.warnings = make(map[uint16]error)
|
||||||
|
}
|
||||||
|
f.warnings[id] = warning
|
||||||
|
}
|
||||||
|
|
||||||
|
// glyphWarning 返回字形恢复为执行前轮廓的原因
|
||||||
|
// 入参: id 字形编号
|
||||||
|
// 返回: error 恢复原因,无恢复时为空
|
||||||
|
func (f *sfntOutliner) glyphWarning(id uint16) error {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
return f.warnings[id]
|
||||||
|
}
|
||||||
|
|
||||||
// readTTSimple 读取简单字形的重复标志、差分坐标和字节码
|
// readTTSimple 读取简单字形的重复标志、差分坐标和字节码
|
||||||
// 入参: data 字形数据, count 轮廓数
|
// 入参: data 字形数据, count 轮廓数
|
||||||
// 返回: ttGlyph 字形轮廓, []byte 字节码, error 数据错误
|
// 返回: ttGlyph 字形轮廓, []byte 字节码, error 数据错误
|
||||||
@@ -301,6 +334,9 @@ func (f *sfntOutliner) composite(data []byte, active map[uint16]bool) (ttGlyph,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return result, nil, err
|
return result, nil, err
|
||||||
}
|
}
|
||||||
|
if part.warning != nil {
|
||||||
|
result.warning = fmt.Errorf("TrueType component %d: %w", id, part.warning)
|
||||||
|
}
|
||||||
if flags&0x200 != 0 {
|
if flags&0x200 != 0 {
|
||||||
result.metrics = new([4]ttPoint)
|
result.metrics = new([4]ttPoint)
|
||||||
copy(result.metrics[:], part.points[len(part.points)-4:])
|
copy(result.metrics[:], part.points[len(part.points)-4:])
|
||||||
|
|||||||
+9
-5
@@ -16,11 +16,15 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// errTTStackUnderflow 表示字形程序缺少必需的栈参数
|
||||||
|
var errTTStackUnderflow = errors.New("TrueType instruction stack underflow")
|
||||||
|
|
||||||
// ttPoint 保存TrueType当前坐标、原始坐标及点状态,坐标使用26.6定点数
|
// ttPoint 保存TrueType当前坐标、原始坐标及点状态,坐标使用26.6定点数
|
||||||
type ttPoint struct {
|
type ttPoint struct {
|
||||||
x, y, ox, oy int32
|
x, y, ox, oy int32
|
||||||
@@ -112,7 +116,7 @@ func (v *ttInterpreter) clone() *ttInterpreter {
|
|||||||
// 返回: int32 操作数
|
// 返回: int32 操作数
|
||||||
func (v *ttInterpreter) pop() int32 {
|
func (v *ttInterpreter) pop() int32 {
|
||||||
if len(v.stack) == 0 {
|
if len(v.stack) == 0 {
|
||||||
v.err = fmt.Errorf("TrueType instruction stack underflow")
|
v.err = errTTStackUnderflow
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
x := v.stack[len(v.stack)-1]
|
x := v.stack[len(v.stack)-1]
|
||||||
@@ -319,7 +323,7 @@ func (v *ttInterpreter) execute(code []byte, depth int) error {
|
|||||||
g.freedom = axis
|
g.freedom = axis
|
||||||
}
|
}
|
||||||
case op >= 6 && op <= 9 || op == 0x86 || op == 0x87:
|
case op >= 6 && op <= 9 || op == 0x86 || op == 0x87:
|
||||||
p2, p1 := v.point(g.zone[2], v.pop()), v.point(g.zone[1], v.pop())
|
p1, p2 := v.point(g.zone[2], v.pop()), v.point(g.zone[1], v.pop())
|
||||||
direction := func(x, y int32) [2]float64 {
|
direction := func(x, y int32) [2]float64 {
|
||||||
a, b := float64(x), float64(y)
|
a, b := float64(x), float64(y)
|
||||||
if op&1 != 0 {
|
if op&1 != 0 {
|
||||||
@@ -443,7 +447,7 @@ func (v *ttInterpreter) execute(code []byte, depth int) error {
|
|||||||
}
|
}
|
||||||
for n := int32(0); n < count; n++ {
|
for n := int32(0); n < count; n++ {
|
||||||
if err = v.execute(body, depth+1); err != nil {
|
if err = v.execute(body, depth+1); err != nil {
|
||||||
return err
|
return fmt.Errorf("TrueType function %d: %w", id, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case op == 0x2c || op == 0x89:
|
case op == 0x2c || op == 0x89:
|
||||||
@@ -563,9 +567,9 @@ func (v *ttInterpreter) execute(code []byte, depth int) error {
|
|||||||
case op == 0x49 || op == 0x4a:
|
case op == 0x49 || op == 0x4a:
|
||||||
p1, p2 := v.point(g.zone[1], v.pop()), v.point(g.zone[0], v.pop())
|
p1, p2 := v.point(g.zone[1], v.pop()), v.point(g.zone[0], v.pop())
|
||||||
if op&1 == 0 {
|
if op&1 == 0 {
|
||||||
v.push(ttProject(p1.ox-p2.ox, p1.oy-p2.oy, g.dual))
|
v.push(ttProject(p2.ox-p1.ox, p2.oy-p1.oy, g.dual))
|
||||||
} else {
|
} else {
|
||||||
v.push(ttProject(p1.x-p2.x, p1.y-p2.y, g.projection))
|
v.push(ttProject(p2.x-p1.x, p2.y-p1.y, g.projection))
|
||||||
}
|
}
|
||||||
case op == 0x4b:
|
case op == 0x4b:
|
||||||
v.push(v.units)
|
v.push(v.units)
|
||||||
|
|||||||
+74
-22
@@ -29,13 +29,17 @@ import (
|
|||||||
// PDFImportOptions 指定PDF转换的密码、运行时后端、进度通知与检查策略
|
// PDFImportOptions 指定PDF转换的密码、运行时后端、进度通知与检查策略
|
||||||
// Password按PDFDocEncoding字节传入,空值尝试默认空密码
|
// Password按PDFDocEncoding字节传入,空值尝试默认空密码
|
||||||
// Strict禁止恢复缺失资源或丢失内容语义,默认在报告中记录警告
|
// Strict禁止恢复缺失资源或丢失内容语义,默认在报告中记录警告
|
||||||
|
// RendererOptions复用渲染器字体来源和后端配置,Backends优先于其中的后端设置
|
||||||
|
// RasterDPI指定局部透明效果合成精度,0沿用渲染器DPI,默认300dpi,Strict禁用局部合成
|
||||||
// OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知
|
// OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知
|
||||||
type PDFImportOptions struct {
|
type PDFImportOptions struct {
|
||||||
Password []byte
|
Password []byte
|
||||||
Backends *RenderBackends
|
Backends *RenderBackends
|
||||||
Progress func(int) error
|
RendererOptions []RendererOption
|
||||||
OnProgress func(stage string, completed, total int) error
|
Progress func(int) error
|
||||||
Strict bool
|
OnProgress func(stage string, completed, total int) error
|
||||||
|
Strict bool
|
||||||
|
RasterDPI float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// PDFImportReport 汇总转换页数、对象数、链接数和转换警告
|
// PDFImportReport 汇总转换页数、对象数、链接数和转换警告
|
||||||
@@ -53,12 +57,14 @@ type pdfImporter struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
reader *pdfgo.Reader
|
reader *pdfgo.Reader
|
||||||
editor *Editor
|
editor *Editor
|
||||||
|
renderer *Renderer
|
||||||
report PDFImportReport
|
report PDFImportReport
|
||||||
matrix pdfgo.Matrix
|
matrix pdfgo.Matrix
|
||||||
page int
|
page int
|
||||||
fontIDs map[*pdfgo.Font]string
|
fontIDs map[*pdfgo.Font]string
|
||||||
fontMetrics map[string]FontMetrics
|
fontMetrics map[string]FontMetrics
|
||||||
fontRepairLimit map[*pdfgo.Font]uint16
|
fontRepairLimit map[*pdfgo.Font]uint16
|
||||||
|
fontWarnings map[string]bool
|
||||||
type1Glyphs map[*pdfgo.Font]map[string]uint16
|
type1Glyphs map[*pdfgo.Font]map[string]uint16
|
||||||
clipTexts map[*pdfgo.TextClip]TextObject
|
clipTexts map[*pdfgo.TextClip]TextObject
|
||||||
cmykSpace string
|
cmykSpace string
|
||||||
@@ -72,6 +78,9 @@ type pdfImporter struct {
|
|||||||
pageHeight float64
|
pageHeight float64
|
||||||
pendingPath *pdfgo.PathMark
|
pendingPath *pdfgo.PathMark
|
||||||
warning func(pdfgo.Diagnostic)
|
warning func(pdfgo.Diagnostic)
|
||||||
|
rasterDPI float64
|
||||||
|
rasterWarned bool
|
||||||
|
compositeCache *pdfCompositeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
|
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
|
||||||
@@ -81,6 +90,19 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, PDFImportReport{}, err
|
return nil, PDFImportReport{}, err
|
||||||
}
|
}
|
||||||
|
if !finite(options.RasterDPI) || options.RasterDPI < 0 {
|
||||||
|
return nil, PDFImportReport{}, fmt.Errorf("invalid PDF raster DPI")
|
||||||
|
}
|
||||||
|
renderer := NewRenderer(&Reader{}, options.RendererOptions...)
|
||||||
|
if options.Backends != nil {
|
||||||
|
WithRenderBackends(*options.Backends)(renderer)
|
||||||
|
}
|
||||||
|
if options.RasterDPI != 0 {
|
||||||
|
renderer.DPI = options.RasterDPI
|
||||||
|
}
|
||||||
|
if !finite(renderer.DPI) || renderer.DPI <= 0 {
|
||||||
|
return nil, PDFImportReport{}, fmt.Errorf("invalid PDF raster DPI")
|
||||||
|
}
|
||||||
if options.OnProgress != nil {
|
if options.OnProgress != nil {
|
||||||
if err := options.OnProgress("open", 0, 0); err != nil {
|
if err := options.OnProgress("open", 0, 0); err != nil {
|
||||||
return nil, PDFImportReport{}, err
|
return nil, PDFImportReport{}, err
|
||||||
@@ -91,10 +113,9 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
return nil, PDFImportReport{}, err
|
return nil, PDFImportReport{}, err
|
||||||
}
|
}
|
||||||
editor := NewEditor()
|
editor := NewEditor()
|
||||||
if options.Backends != nil {
|
editor.SetRenderBackends(renderer.Backends())
|
||||||
editor.SetRenderBackends(*options.Backends)
|
editor.fontDirs, editor.fontFS = renderer.FontSources()
|
||||||
}
|
importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fontMetrics: map[string]FontMetrics{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}}
|
||||||
importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, fontIDs: map[*pdfgo.Font]string{}, fontMetrics: map[string]FontMetrics{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}}
|
|
||||||
if security := reader.Encryption(); security != nil && !security.Owner && security.Permissions&0xf3c != 0xf3c {
|
if security := reader.Encryption(); security != nil && !security.Owner && security.Permissions&0xf3c != 0xf3c {
|
||||||
if options.Strict {
|
if options.Strict {
|
||||||
return nil, PDFImportReport{}, &pdfgo.UnsupportedError{Feature: "PDF access permission conversion"}
|
return nil, PDFImportReport{}, &pdfgo.UnsupportedError{Feature: "PDF access permission conversion"}
|
||||||
@@ -136,13 +157,18 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
matrix, width, height := pdfPageMatrix(page)
|
matrix, width, height := pdfPageMatrix(page)
|
||||||
importer.matrix = matrix
|
importer.matrix = matrix
|
||||||
importer.page = index
|
importer.page = index
|
||||||
|
importer.rasterWarned = false
|
||||||
importer.pageBox = page.CropBox
|
importer.pageBox = page.CropBox
|
||||||
importer.pageWidth = width
|
importer.pageWidth = width
|
||||||
importer.pageHeight = height
|
importer.pageHeight = height
|
||||||
importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path)
|
importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path)
|
||||||
|
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}
|
visitor := pdfgo.Visitor{Path: importer.path, Text: importer.text, Image: importer.image, Warning: importer.warning}
|
||||||
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
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)
|
return importer.group(mark, walk, visitor)
|
||||||
}
|
}
|
||||||
if err := reader.WalkPage(ctx, page, visitor); err != nil {
|
if err := reader.WalkPage(ctx, page, visitor); err != nil {
|
||||||
@@ -339,7 +365,16 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
|
|||||||
for _, mark := range path.Text {
|
for _, mark := range path.Text {
|
||||||
object, ok := p.clipTexts[mark]
|
object, ok := p.clipTexts[mark]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("missing PDF text clipping glyphs")
|
if err := p.flushPath(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
count := len(p.objects)
|
||||||
|
if err := p.text(pdfgo.TextMark{Font: mark.Font, Glyphs: mark.Glyphs, Positions: mark.Positions, Matrix: mark.Matrix, Size: mark.Size, HorizontalScale: mark.HorizontalScale, Mode: 7, Clip: mark, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.objects = p.objects[:count]
|
||||||
|
p.report.TextObjects--
|
||||||
|
object = p.clipTexts[mark]
|
||||||
}
|
}
|
||||||
box, err := ParseBox(object.Boundary)
|
box, err := ParseBox(object.Boundary)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -357,8 +392,8 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
|
|||||||
// 入参: mark PDF路径绘制信息
|
// 入参: mark PDF路径绘制信息
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (p *pdfImporter) path(mark pdfgo.PathMark) error {
|
func (p *pdfImporter) path(mark pdfgo.PathMark) error {
|
||||||
if mark.Style.BlendMode == "Multiply" {
|
if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" {
|
||||||
return &pdfgo.UnsupportedError{Feature: "multiply path"}
|
return &pdfgo.UnsupportedError{Feature: "path blend mode " + string(mark.Style.BlendMode)}
|
||||||
}
|
}
|
||||||
if !mark.Stroke || mark.Fill || mark.Style.Stroke.Alpha != 1 {
|
if !mark.Stroke || mark.Fill || mark.Style.Stroke.Alpha != 1 {
|
||||||
if err := p.flushPath(); err != nil {
|
if err := p.flushPath(); err != nil {
|
||||||
@@ -400,11 +435,19 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
|||||||
var err error
|
var err error
|
||||||
mark.Style, err = p.maskStyle(mark.Style)
|
mark.Style, err = p.maskStyle(mark.Style)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if p.rasterMaskAllowed(err) {
|
||||||
|
mask := mark.Style.SoftMask
|
||||||
|
mark.Style.SoftMask = nil
|
||||||
|
return p.rasterGroup(pdfgo.GroupMark{Alpha: 1, SoftMask: mask}, func(v pdfgo.Visitor) error { return v.Path(mark) })
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if mark.Fill && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || mark.Stroke && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
|
if mark.Fill && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || mark.Stroke && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
|
||||||
return &pdfgo.UnsupportedError{Feature: "color separation overprint"}
|
return &pdfgo.UnsupportedError{Feature: "color separation overprint"}
|
||||||
}
|
}
|
||||||
|
if p.warning != nil && (mark.Fill && pdfGradientError(mark.Style.Fill) != nil || mark.Stroke && pdfGradientError(mark.Style.Stroke) != nil) {
|
||||||
|
return p.gradientPath(mark)
|
||||||
|
}
|
||||||
var points []pdfgo.Point
|
var points []pdfgo.Point
|
||||||
for _, segment := range mark.Path.Segments {
|
for _, segment := range mark.Path.Segments {
|
||||||
for _, point := range segment.Points {
|
for _, point := range segment.Points {
|
||||||
@@ -447,13 +490,18 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
|||||||
}
|
}
|
||||||
return drawing.paintColor(value, origin)
|
return drawing.paintColor(value, origin)
|
||||||
}
|
}
|
||||||
fillColor, err := paint(mark.Style.Fill)
|
fillColor, strokePaint := p.color(mark.Style.Fill), p.color(mark.Style.Stroke)
|
||||||
if err != nil {
|
if mark.Fill {
|
||||||
return err
|
fillColor, err = paint(mark.Style.Fill)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
strokePaint, err := paint(mark.Style.Stroke)
|
if mark.Stroke {
|
||||||
if err != nil {
|
strokePaint, err = paint(mark.Style.Stroke)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
strokeColor := StrokeColor(*strokePaint)
|
strokeColor := StrokeColor(*strokePaint)
|
||||||
clips, err := p.clips(mark.Style.Clips, box)
|
clips, err := p.clips(mark.Style.Clips, box)
|
||||||
@@ -504,8 +552,10 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
|||||||
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, Extend: strconv.Itoa(extend)}
|
||||||
for _, stop := range gradient.Stops {
|
var err error
|
||||||
shading.Segment = append(shading.Segment, ShdSegment{Position: stop.Position, Color: ShdColor{Value: pdfNumbers(math.Round(stop.RGB[0]*255), math.Round(stop.RGB[1]*255), math.Round(stop.RGB[2]*255))}})
|
shading.Segment, err = pdfGradientSegments(gradient.Stops, gradient.Space)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
color.Value = ""
|
color.Value = ""
|
||||||
color.RadialShd = shading
|
color.RadialShd = shading
|
||||||
@@ -536,8 +586,10 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
|||||||
extend |= 2
|
extend |= 2
|
||||||
}
|
}
|
||||||
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)}
|
||||||
for _, stop := range gradient.Stops {
|
var err error
|
||||||
shading.Segment = append(shading.Segment, ShdSegment{Position: stop.Position, Color: ShdColor{Value: pdfNumbers(math.Round(stop.RGB[0]*255), math.Round(stop.RGB[1]*255), math.Round(stop.RGB[2]*255))}})
|
shading.Segment, err = pdfGradientSegments(gradient.Stops, gradient.Space)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
color.Value = ""
|
color.Value = ""
|
||||||
color.AxialShd = shading
|
color.AxialShd = shading
|
||||||
|
|||||||
@@ -0,0 +1,394 @@
|
|||||||
|
// 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"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pdfCompositeNode 保留局部合成所需的原始图元和透明组层级
|
||||||
|
type pdfCompositeNode struct {
|
||||||
|
path *pdfgo.PathMark
|
||||||
|
text *pdfgo.TextMark
|
||||||
|
image *pdfgo.ImageMark
|
||||||
|
group *pdfgo.GroupMark
|
||||||
|
children []pdfCompositeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositePixel 保存非预乘源空间分量及累计透明度
|
||||||
|
type pdfCompositePixel struct {
|
||||||
|
values [4]float64
|
||||||
|
alpha float64
|
||||||
|
effect float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositor 在指定局部区域中按源颜色空间合成PDF图元
|
||||||
|
type pdfCompositor struct {
|
||||||
|
importer *pdfImporter
|
||||||
|
box Box
|
||||||
|
width, height int
|
||||||
|
inverse pdfgo.Matrix
|
||||||
|
cache *pdfCompositeCache
|
||||||
|
masks map[*pdfgo.SoftMask][]float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositeKey 区分图元的填充与描边几何
|
||||||
|
type pdfCompositeKey struct {
|
||||||
|
path *pdfgo.PathMark
|
||||||
|
text *pdfgo.TextMark
|
||||||
|
image *pdfgo.ImageMark
|
||||||
|
stroke bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositeGeometry 保存后端中立的绘制命令及其页面边界
|
||||||
|
type pdfCompositeGeometry struct {
|
||||||
|
scene *RasterPage
|
||||||
|
box Box
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容
|
||||||
|
type pdfCompositeCache struct {
|
||||||
|
geometry map[pdfCompositeKey]pdfCompositeGeometry
|
||||||
|
images map[*pdfgo.Image]*pdfgo.ImageComponents
|
||||||
|
masks map[*pdfgo.SoftMask][]pdfCompositeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// compositingCache 按需创建单页合成缓存,页面切换时由导入器释放
|
||||||
|
// 返回: *pdfCompositeCache 当前页面缓存
|
||||||
|
func (p *pdfImporter) compositingCache() *pdfCompositeCache {
|
||||||
|
if p.compositeCache == nil {
|
||||||
|
p.compositeCache = &pdfCompositeCache{geometry: map[pdfCompositeKey]pdfCompositeGeometry{}, images: map[*pdfgo.Image]*pdfgo.ImageComponents{}, masks: map[*pdfgo.SoftMask][]pdfCompositeNode{}}
|
||||||
|
}
|
||||||
|
return p.compositeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// coverage 复用已编译的覆盖场景,跳过与当前区域无交集的图元
|
||||||
|
// 入参: node 图元, stroke 是否描边
|
||||||
|
// 返回: image.Image 当前区域覆盖或空值, error 编译或渲染错误
|
||||||
|
func (c *pdfCompositor) coverage(node pdfCompositeNode, stroke bool) (image.Image, error) {
|
||||||
|
if node.text != nil && len(node.text.Font.Program) == 0 && node.text.Font.BoundingBox != nil {
|
||||||
|
b := c.importer.compositeTextBounds(*node.text, stroke)
|
||||||
|
if b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key := pdfCompositeKey{path: node.path, text: node.text, image: node.image, stroke: stroke}
|
||||||
|
geometry, ok := c.cache.geometry[key]
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
geometry.scene, geometry.box, err = c.importer.compileGroup(func(local *pdfImporter) error { return node.coverage(local, stroke) })
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.cache.geometry[key] = geometry
|
||||||
|
}
|
||||||
|
b := geometry.box
|
||||||
|
if geometry.scene == nil || b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return c.importer.renderGroupScene(geometry.scene, c.box)
|
||||||
|
}
|
||||||
|
|
||||||
|
// compositeTextBounds 按字体描述符计算保守边界,仅用于排除不相交的外部字体
|
||||||
|
// 入参: mark 具有字体边界的文字, stroke 是否描边
|
||||||
|
// 返回: Box 包含描边和抗锯齿边缘的页面范围
|
||||||
|
func (p *pdfImporter) compositeTextBounds(mark pdfgo.TextMark, stroke bool) Box {
|
||||||
|
b := mark.Font.BoundingBox
|
||||||
|
matrix := p.matrix.Mul(mark.Matrix)
|
||||||
|
var points []pdfgo.Point
|
||||||
|
for _, origin := range mark.Positions {
|
||||||
|
for _, point := range []pdfgo.Point{{X: b.XMin, Y: b.YMin}, {X: b.XMax, Y: b.YMin}, {X: b.XMax, Y: b.YMax}, {X: b.XMin, Y: b.YMax}} {
|
||||||
|
points = append(points, matrix.Apply(pdfgo.Point{X: origin.X + point.X*mark.Size*mark.HorizontalScale/1000, Y: origin.Y + point.Y*mark.Size/1000}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
box := pdfBounds(points)
|
||||||
|
marginX, marginY := 25.4/p.rasterDPI, 25.4/p.rasterDPI
|
||||||
|
if stroke {
|
||||||
|
m := mark.StrokeMatrix
|
||||||
|
if m == (pdfgo.Matrix{}) {
|
||||||
|
m = pdfgo.Identity()
|
||||||
|
}
|
||||||
|
m = p.matrix.Mul(m)
|
||||||
|
distance := mark.Style.LineWidth / 2 * math.Max(1, mark.Style.MiterLimit)
|
||||||
|
marginX += distance * (math.Abs(m[0]) + math.Abs(m[2]))
|
||||||
|
marginY += distance * (math.Abs(m[1]) + math.Abs(m[3]))
|
||||||
|
}
|
||||||
|
return Box{X: box.X - marginX, Y: box.Y - marginY, W: box.W + 2*marginX, H: box.H + 2*marginY}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositeNodes 收集原始绘制顺序,语义警告只在首次解析时交付
|
||||||
|
// 入参: walk 内容访问函数, warning 警告回调
|
||||||
|
// 返回: []pdfCompositeNode 原始图元, error 解析错误
|
||||||
|
func pdfCompositeNodes(walk func(pdfgo.Visitor) error, warning func(pdfgo.Diagnostic)) ([]pdfCompositeNode, error) {
|
||||||
|
var nodes []pdfCompositeNode
|
||||||
|
v := pdfgo.Visitor{Warning: warning}
|
||||||
|
v.Path = func(mark pdfgo.PathMark) error { nodes = append(nodes, pdfCompositeNode{path: &mark}); return nil }
|
||||||
|
v.Text = func(mark pdfgo.TextMark) error { nodes = append(nodes, pdfCompositeNode{text: &mark}); return nil }
|
||||||
|
v.Image = func(mark pdfgo.ImageMark) error { nodes = append(nodes, pdfCompositeNode{image: &mark}); return nil }
|
||||||
|
v.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
||||||
|
children, err := pdfCompositeNodes(walk, warning)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nodes = append(nodes, pdfCompositeNode{group: &mark, children: children})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := walk(v)
|
||||||
|
return nodes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// compositePage 保留独立不透明对象,仅将需要背景参与的效果在局部区域合成
|
||||||
|
// 入参: space 页面混合空间, walk 页面内容
|
||||||
|
// 返回: error 转换错误
|
||||||
|
func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Visitor) error) error {
|
||||||
|
nodes, err := pdfCompositeNodes(walk, p.warning)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, node := range nodes {
|
||||||
|
if node.opaque(space) {
|
||||||
|
if err := node.emit(p.visitor()); err != nil {
|
||||||
|
return fmt.Errorf("convert graphic %d: %w", i+1, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if p.warning == nil {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "page color-space compositing"}
|
||||||
|
}
|
||||||
|
if err := p.flushPath(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.compositeRegion(nodes[:i], node, space, true); err != nil {
|
||||||
|
return fmt.Errorf("composite graphic %d: %w", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p.flushPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
// opaque 检查图元是否无需与页面背景混合
|
||||||
|
// 入参: space 当前混合空间
|
||||||
|
// 返回: bool 是否可直接保留
|
||||||
|
func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool {
|
||||||
|
if n.group != nil {
|
||||||
|
g := n.group
|
||||||
|
if g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, child := range n.children {
|
||||||
|
if !child.opaque(space) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
style, fill, stroke := n.style()
|
||||||
|
if style.SoftMask != nil || !pdfNormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if fill && pdfGradientError(style.Fill) != nil || stroke && pdfGradientError(style.Stroke) != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if n.image != nil && (n.image.Image.Mask != nil || n.image.Image.SoftMask != nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// style 获取图元实际使用的图形状态
|
||||||
|
// 返回: pdfgo.Style 图形状态, bool 是否填充, bool 是否描边
|
||||||
|
func (n pdfCompositeNode) style() (pdfgo.Style, bool, bool) {
|
||||||
|
if n.path != nil {
|
||||||
|
return n.path.Style, n.path.Fill, n.path.Stroke
|
||||||
|
}
|
||||||
|
if n.text != nil {
|
||||||
|
mode := n.text.Mode % 4
|
||||||
|
return n.text.Style, mode == 0 || mode == 2, mode == 1 || mode == 2
|
||||||
|
}
|
||||||
|
return n.image.Style, true, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit 向现有转换器交付原始图元
|
||||||
|
// 入参: visitor 图元访问器
|
||||||
|
// 返回: error 转换错误
|
||||||
|
func (n pdfCompositeNode) emit(visitor pdfgo.Visitor) error {
|
||||||
|
if n.path != nil {
|
||||||
|
return visitor.Path(*n.path)
|
||||||
|
}
|
||||||
|
if n.text != nil {
|
||||||
|
return visitor.Text(*n.text)
|
||||||
|
}
|
||||||
|
if n.image != nil {
|
||||||
|
return visitor.Image(*n.image)
|
||||||
|
}
|
||||||
|
return visitor.Group(*n.group, func(v pdfgo.Visitor) error {
|
||||||
|
for _, child := range n.children {
|
||||||
|
if err := child.emit(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// coverage 构建白色几何覆盖,颜色和透明度由合成器单独计算
|
||||||
|
// 入参: p 局部导入器, stroke 是否只取描边
|
||||||
|
// 返回: error 几何错误
|
||||||
|
func (n pdfCompositeNode) coverage(p *pdfImporter, stroke bool) error {
|
||||||
|
if n.group != nil {
|
||||||
|
for _, child := range n.children {
|
||||||
|
if child.group != nil {
|
||||||
|
if err := child.coverage(p, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, fill, hasStroke := child.style()
|
||||||
|
if fill {
|
||||||
|
if err := child.coverage(p, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasStroke {
|
||||||
|
if err := child.coverage(p, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
style, _, _ := n.style()
|
||||||
|
style.Fill, style.Stroke = pdfgo.Paint{RGB: [3]float64{1, 1, 1}, Alpha: 1}, pdfgo.Paint{RGB: [3]float64{1, 1, 1}, Alpha: 1}
|
||||||
|
style.SoftMask, style.BlendMode = nil, "Normal"
|
||||||
|
style.FillOverprint, style.StrokeOverprint = false, false
|
||||||
|
if n.path != nil {
|
||||||
|
mark := *n.path
|
||||||
|
mark.Style = style
|
||||||
|
mark.Fill, mark.Stroke = !stroke, stroke
|
||||||
|
return p.path(mark)
|
||||||
|
}
|
||||||
|
if n.text != nil {
|
||||||
|
mark := *n.text
|
||||||
|
mark.Style = style
|
||||||
|
mark.Mode = 0
|
||||||
|
if stroke {
|
||||||
|
mark.Mode = 1
|
||||||
|
}
|
||||||
|
return p.text(mark)
|
||||||
|
}
|
||||||
|
mark := n.image
|
||||||
|
path := pdfgo.Path{Segments: []pdfgo.Segment{
|
||||||
|
{Operator: "M", Points: []pdfgo.Point{mark.Matrix.Apply(pdfgo.Point{})}},
|
||||||
|
{Operator: "L", Points: []pdfgo.Point{mark.Matrix.Apply(pdfgo.Point{X: 1})}},
|
||||||
|
{Operator: "L", Points: []pdfgo.Point{mark.Matrix.Apply(pdfgo.Point{X: 1, Y: 1})}},
|
||||||
|
{Operator: "L", Points: []pdfgo.Point{mark.Matrix.Apply(pdfgo.Point{Y: 1})}},
|
||||||
|
{Operator: "C"},
|
||||||
|
}}
|
||||||
|
return p.path(pdfgo.PathMark{Path: path, Style: style, Fill: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// compositeRegion 按原始背景计算局部效果,不将组外文字和图形栅格化
|
||||||
|
// 入参: backdrop 前序图元, node 当前效果, space 页面混合空间, flatten 是否合并页面背景
|
||||||
|
// 返回: error 合成或保存错误
|
||||||
|
func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompositeNode, space *pdfgo.ColorSpace, flatten bool) error {
|
||||||
|
scene, box, err := p.compileGroup(func(local *pdfImporter) error {
|
||||||
|
if node.group != nil {
|
||||||
|
return node.coverage(local, false)
|
||||||
|
}
|
||||||
|
_, fill, stroke := node.style()
|
||||||
|
if fill {
|
||||||
|
if err := node.coverage(local, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stroke {
|
||||||
|
return node.coverage(local, true)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil || scene == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inverse, ok := p.matrix.Inverse()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid PDF compositing transform")
|
||||||
|
}
|
||||||
|
w, h, err := (&RasterPage{Width: box.W, Height: box.H, DPI: p.rasterDPI}).PixelSize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
output := image.NewNRGBA(image.Rect(0, 0, w, h))
|
||||||
|
cache := p.compositingCache()
|
||||||
|
step := 25.4 / p.rasterDPI
|
||||||
|
for y := 0; y < h; y += 256 {
|
||||||
|
for x := 0; x < w; x += 256 {
|
||||||
|
width, height := min(256, w-x), min(256, h-y)
|
||||||
|
c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, masks: map[*pdfgo.SoftMask][]float64{}}
|
||||||
|
pixels := make([]pdfCompositePixel, width*height)
|
||||||
|
if err := c.draw(backdrop, pixels, space); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range pixels {
|
||||||
|
pixels[i].effect = 0
|
||||||
|
}
|
||||||
|
if err := c.draw([]pdfCompositeNode{node}, pixels, space); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, pixel := range pixels {
|
||||||
|
if pixel.effect == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rgb, err := pdfCompositeOutputColor(space, pixel.values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
alpha := pixel.alpha
|
||||||
|
if flatten {
|
||||||
|
for j := range rgb {
|
||||||
|
rgb[j] = pixel.alpha*rgb[j] + 1 - pixel.alpha
|
||||||
|
}
|
||||||
|
alpha = 1
|
||||||
|
}
|
||||||
|
output.SetNRGBA(x+i%width, y+i/width, color.NRGBA{R: uint8(math.Round(rgb[0] * 255)), G: uint8(math.Round(rgb[1] * 255)), B: uint8(math.Round(rgb[2] * 255)), A: uint8(math.Round(alpha * 255))})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p.appendRasterGroup(output, box, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositeOutputColor 将合成结果映射到OFD输出,与保留的设备四色对象使用相同显示换算
|
||||||
|
// 入参: space 合成空间, values 非预乘分量
|
||||||
|
// 返回: [3]float64 显示颜色, error 校准颜色转换错误
|
||||||
|
func pdfCompositeOutputColor(space *pdfgo.ColorSpace, values [4]float64) ([3]float64, error) {
|
||||||
|
if space.Model == "DeviceCMYK" && !space.Calibrated() {
|
||||||
|
black := 1 - values[3]
|
||||||
|
return [3]float64{(1 - values[0]) * black, (1 - values[1]) * black, (1 - values[2]) * black}, nil
|
||||||
|
}
|
||||||
|
return space.RGB(values[:space.Components()], "RelativeColorimetric")
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfNormalBlend 判断混合模式是否为普通覆盖
|
||||||
|
// 入参: mode 混合模式
|
||||||
|
// 返回: bool 是否普通覆盖
|
||||||
|
func pdfNormalBlend(mode pdfgo.Name) bool {
|
||||||
|
return mode == "" || mode == "Normal" || mode == "Compatible"
|
||||||
|
}
|
||||||
@@ -0,0 +1,480 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// draw 按原始顺序在当前颜色空间绘制图元及透明组
|
||||||
|
// 入参: nodes 图元, pixels 背景与输出像素, space 混合空间
|
||||||
|
// 返回: error 几何或颜色错误
|
||||||
|
func (c *pdfCompositor) draw(nodes []pdfCompositeNode, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error {
|
||||||
|
for _, node := range nodes {
|
||||||
|
if err := c.importer.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if node.group != nil {
|
||||||
|
if err := c.drawGroup(node, pixels, space); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := c.drawMark(node, pixels, space); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawMark 将填充与描边作为同一对象,保留重叠区域的挖空和叠印语义
|
||||||
|
// 入参: node 图元, pixels 背景与输出像素, space 混合空间
|
||||||
|
// 返回: error 几何或颜色错误
|
||||||
|
func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error {
|
||||||
|
style, fill, stroke := node.style()
|
||||||
|
mask, err := c.mask(style.SoftMask, space)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var initial []pdfCompositePixel
|
||||||
|
result := pixels
|
||||||
|
grouped := fill && stroke && (style.FillOverprint || style.StrokeOverprint) && style.Fill.Alpha == style.Stroke.Alpha
|
||||||
|
if fill && stroke {
|
||||||
|
initial = append([]pdfCompositePixel(nil), pixels...)
|
||||||
|
if grouped {
|
||||||
|
result = append([]pdfCompositePixel(nil), pixels...)
|
||||||
|
for i := range result {
|
||||||
|
result[i].effect = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, outline := range []bool{false, true} {
|
||||||
|
if outline && !stroke || !outline && !fill {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
paint, overprint := style.Fill, style.FillOverprint
|
||||||
|
if outline {
|
||||||
|
paint, overprint = style.Stroke, style.StrokeOverprint
|
||||||
|
}
|
||||||
|
mode, paintMask := style.BlendMode, mask
|
||||||
|
var knockout []pdfCompositePixel
|
||||||
|
if grouped {
|
||||||
|
paint.Alpha, mode, paintMask = 1, "Normal", nil
|
||||||
|
} else if outline {
|
||||||
|
knockout = initial
|
||||||
|
}
|
||||||
|
if err := c.drawPaint(node, outline, paint, mode, overprint, paintMask, knockout, result, space); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if grouped {
|
||||||
|
return pdfCompositeGroup(pixels, initial, result, space, space, style.Fill.Alpha, mask, style.BlendMode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawPaint 结合几何覆盖与源颜色绘制一个填充或描边
|
||||||
|
// 入参: node 图元, outline 描边标志, paint 画刷, mode 混合模式, overprint 叠印标志, mask 蒙版, initial 挖空初始背景, pixels 输出, space 混合空间
|
||||||
|
// 返回: error 几何或颜色错误
|
||||||
|
func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdfgo.Paint, mode pdfgo.Name, overprint bool, mask []float64, initial, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error {
|
||||||
|
coverage, err := c.coverage(node, outline)
|
||||||
|
if err != nil || coverage == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
step := 25.4 / c.importer.rasterDPI
|
||||||
|
uniform := node.image == nil && paint.Axial == nil && paint.Radial == nil && paint.Tiling == nil
|
||||||
|
var uniformValues [4]float64
|
||||||
|
uniformReady := false
|
||||||
|
for y := 0; y < c.height; y++ {
|
||||||
|
if err := c.importer.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for x := 0; x < c.width; x++ {
|
||||||
|
_, _, _, a := coverage.At(x, y).RGBA()
|
||||||
|
if a == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
index := y*c.width + x
|
||||||
|
shape, opacity := float64(a)/65535, paint.Alpha
|
||||||
|
if mask != nil {
|
||||||
|
opacity *= mask[index]
|
||||||
|
}
|
||||||
|
var values [4]float64
|
||||||
|
if uniform {
|
||||||
|
if !uniformReady {
|
||||||
|
uniformValues, _, err = pdfCompositeColor(paint, pdfgo.Point{}, space, style.RenderingIntent)
|
||||||
|
uniformReady = true
|
||||||
|
}
|
||||||
|
values = uniformValues
|
||||||
|
} else {
|
||||||
|
point := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)*step, Y: c.box.Y + (float64(y)+.5)*step})
|
||||||
|
if node.image != nil {
|
||||||
|
var alpha float64
|
||||||
|
values, alpha, err = c.imageColor(*node.image, point, space)
|
||||||
|
opacity *= alpha
|
||||||
|
} else {
|
||||||
|
var visible bool
|
||||||
|
values, visible, err = pdfCompositeColor(paint, point, space, style.RenderingIntent)
|
||||||
|
if !visible {
|
||||||
|
shape = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if initial != nil {
|
||||||
|
if style.AlphaIsShape {
|
||||||
|
shape, opacity = shape*opacity, 1
|
||||||
|
}
|
||||||
|
pixel := initial[index]
|
||||||
|
if err := pdfCompositePaintOver(&pixel, values, opacity, space, mode, compatible); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pdfCompositeInterpolate(&pixels[index], pixel, shape)
|
||||||
|
} else if err := pdfCompositePaintOver(&pixels[index], values, shape*opacity, space, mode, compatible); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositeInterpolate 按形状覆盖混合预乘颜色及累计透明度
|
||||||
|
// 入参: target 原像素与输出, source 替换像素, shape 覆盖率
|
||||||
|
func pdfCompositeInterpolate(target *pdfCompositePixel, source pdfCompositePixel, shape float64) {
|
||||||
|
alpha := target.alpha*(1-shape) + source.alpha*shape
|
||||||
|
if alpha != 0 {
|
||||||
|
for i := range target.values {
|
||||||
|
target.values[i] = (target.values[i]*target.alpha*(1-shape) + source.values[i]*source.alpha*shape) / alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target.alpha = alpha
|
||||||
|
target.effect = target.effect*(1-shape) + source.effect*shape
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawGroup 按隔离标志保留初始背景,移除背景贡献后应用组透明度
|
||||||
|
// 入参: node 透明组, pixels 父组像素, parent 父混合空间
|
||||||
|
// 返回: error 颜色或合成错误
|
||||||
|
func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePixel, parent *pdfgo.ColorSpace) error {
|
||||||
|
g := node.group
|
||||||
|
space := g.ColorSpace
|
||||||
|
if space == nil {
|
||||||
|
space = parent
|
||||||
|
}
|
||||||
|
initial := make([]pdfCompositePixel, len(pixels))
|
||||||
|
if !g.Isolated {
|
||||||
|
for i, pixel := range pixels {
|
||||||
|
values, err := space.Convert(pixel.values[:parent.Components()], parent, "RelativeColorimetric")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
initial[i] = pdfCompositePixel{values: values, alpha: pixel.alpha}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := append([]pdfCompositePixel(nil), initial...)
|
||||||
|
if err := c.draw(node.children, result, space); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mask, err := c.mask(g.SoftMask, parent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return pdfCompositeGroup(pixels, initial, result, space, parent, g.Alpha, mask, g.BlendMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositeGroup 移除组初始背景贡献后,将组结果合成到父空间
|
||||||
|
// 入参: pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式
|
||||||
|
// 返回: error 颜色或混合错误
|
||||||
|
func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode pdfgo.Name) error {
|
||||||
|
for i, pixel := range result {
|
||||||
|
if pixel.effect == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values := pixel.values
|
||||||
|
for j := 0; j < space.Components(); j++ {
|
||||||
|
values[j] = math.Max(0, math.Min(1, (pixel.values[j]*pixel.alpha-initial[i].values[j]*initial[i].alpha*(1-pixel.effect))/pixel.effect))
|
||||||
|
}
|
||||||
|
values, err := parent.Convert(values[:space.Components()], space, "RelativeColorimetric")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
alpha := pixel.effect * opacity
|
||||||
|
if mask != nil {
|
||||||
|
alpha *= mask[i]
|
||||||
|
}
|
||||||
|
if err := pdfCompositeOver(&pixels[i], values, alpha, parent, mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mask 在组颜色空间计算蒙版,保留背景色、组隔离和亮度传递
|
||||||
|
// 入参: mask 蒙版或空值, inherited 未指定蒙版空间时继承的空间
|
||||||
|
// 返回: []float64 蒙版透明度或空值, error 蒙版错误
|
||||||
|
func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace) ([]float64, error) {
|
||||||
|
if mask == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if cached, ok := c.masks[mask]; ok {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
nodes, ok := c.cache.masks[mask]
|
||||||
|
var err error
|
||||||
|
if !ok {
|
||||||
|
nodes, err = pdfCompositeNodes(mask.Walk, c.importer.warning)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.cache.masks[mask] = nodes
|
||||||
|
}
|
||||||
|
space := mask.ColorSpace
|
||||||
|
if space == nil {
|
||||||
|
space = inherited
|
||||||
|
}
|
||||||
|
pixels := make([]pdfCompositePixel, c.width*c.height)
|
||||||
|
if mask.Subtype == "Luminosity" {
|
||||||
|
for i := range pixels {
|
||||||
|
copy(pixels[i].values[:], mask.Backdrop)
|
||||||
|
pixels[i].alpha = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.draw(nodes, pixels, space); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]float64, len(pixels))
|
||||||
|
for i, pixel := range pixels {
|
||||||
|
v := pixel.alpha
|
||||||
|
if mask.Subtype == "Luminosity" {
|
||||||
|
v, err = space.Luminosity(pixel.values[:space.Components()], "RelativeColorimetric")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result[i] = math.Max(0, math.Min(1, mask.Transfer[0]+v*(mask.Transfer[1]-mask.Transfer[0])))
|
||||||
|
}
|
||||||
|
c.masks[mask] = result
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageColor 采样映射后的原始图像分量,插值在源空间内进行
|
||||||
|
// 入参: mark 图像, point 页面坐标, space 混合空间
|
||||||
|
// 返回: [4]float64 源颜色, float64 图像透明度, error 解码或变换错误
|
||||||
|
func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, space *pdfgo.ColorSpace) ([4]float64, float64, error) {
|
||||||
|
source := c.cache.images[mark.Image]
|
||||||
|
if source == nil {
|
||||||
|
var err error
|
||||||
|
source, err = mark.Image.DecodeComponents()
|
||||||
|
if err != nil {
|
||||||
|
return [4]float64{}, 0, err
|
||||||
|
}
|
||||||
|
c.cache.images[mark.Image] = source
|
||||||
|
}
|
||||||
|
inverse, ok := mark.Matrix.Inverse()
|
||||||
|
if !ok {
|
||||||
|
return [4]float64{}, 0, fmt.Errorf("invalid PDF image matrix")
|
||||||
|
}
|
||||||
|
point = inverse.Apply(point)
|
||||||
|
if point.X < 0 || point.X > 1 || point.Y < 0 || point.Y > 1 {
|
||||||
|
return [4]float64{}, 0, nil
|
||||||
|
}
|
||||||
|
x, y := point.X*float64(source.Rect.Dx())-.5, (1-point.Y)*float64(source.Rect.Dy())-.5
|
||||||
|
sample := func(x, y int) ([4]float64, float64) {
|
||||||
|
return source.ValuesAt(max(0, min(source.Rect.Dx()-1, x)), max(0, min(source.Rect.Dy()-1, y)))
|
||||||
|
}
|
||||||
|
values, alpha := sample(int(math.Floor(x+.5)), int(math.Floor(y+.5)))
|
||||||
|
if mark.Image.Interpolate {
|
||||||
|
ix, iy := int(math.Floor(x)), int(math.Floor(y))
|
||||||
|
fx, fy := x-float64(ix), y-float64(iy)
|
||||||
|
values, alpha = [4]float64{}, 0
|
||||||
|
for j := 0; j < 2; j++ {
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
weight := (1 - fx) * (1 - fy)
|
||||||
|
if i == 1 {
|
||||||
|
weight = fx * (1 - fy)
|
||||||
|
}
|
||||||
|
if j == 1 {
|
||||||
|
weight = (1 - fx) * fy
|
||||||
|
if i == 1 {
|
||||||
|
weight = fx * fy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v, a := sample(ix+i, iy+j)
|
||||||
|
alpha += a * weight
|
||||||
|
for k := 0; k < source.Space.Components(); k++ {
|
||||||
|
values[k] += v[k] * a * weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if alpha != 0 {
|
||||||
|
for k := range values {
|
||||||
|
values[k] = math.Max(0, math.Min(1, values[k]/alpha))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mark.Image.ImageMask {
|
||||||
|
color, visible, err := pdfCompositeColor(mark.Style.Fill, mark.Matrix.Apply(point), space, mark.Style.RenderingIntent)
|
||||||
|
if !visible {
|
||||||
|
return color, 0, err
|
||||||
|
}
|
||||||
|
return color, alpha * (1 - values[0]), err
|
||||||
|
}
|
||||||
|
values, err := space.Convert(values[:source.Space.Components()], source.Space, mark.Style.RenderingIntent)
|
||||||
|
return values, alpha, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositeColor 将原始画刷变换到混合空间,渐变在源空间求值
|
||||||
|
// 入参: paint 画刷, point 页面坐标, space 混合空间, intent 渲染意图
|
||||||
|
// 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误
|
||||||
|
func pdfCompositeColor(paint pdfgo.Paint, point pdfgo.Point, space *pdfgo.ColorSpace, intent pdfgo.Name) ([4]float64, bool, error) {
|
||||||
|
if paint.Tiling != nil {
|
||||||
|
return [4]float64{}, false, &pdfgo.UnsupportedError{Feature: "local tiling pattern compositing"}
|
||||||
|
}
|
||||||
|
var values [4]float64
|
||||||
|
var source *pdfgo.ColorSpace
|
||||||
|
if paint.Axial != nil || paint.Radial != nil {
|
||||||
|
t, visible := pdfShadingPosition(paint, point)
|
||||||
|
if !visible {
|
||||||
|
return values, false, nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if g := paint.Axial; g != nil {
|
||||||
|
values, err = g.ValuesAt(t)
|
||||||
|
source, intent = g.Space, g.Intent
|
||||||
|
} else {
|
||||||
|
g := paint.Radial
|
||||||
|
values, err = g.ValuesAt(t)
|
||||||
|
source, intent = g.Space, g.Intent
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return values, false, err
|
||||||
|
}
|
||||||
|
} else if paint.Space != nil {
|
||||||
|
values, source = paint.Values, paint.Space
|
||||||
|
} else if paint.CMYK != nil {
|
||||||
|
values, source = *paint.CMYK, &pdfgo.ColorSpace{Model: "DeviceCMYK"}
|
||||||
|
} else {
|
||||||
|
copy(values[:], paint.RGB[:])
|
||||||
|
source = &pdfgo.ColorSpace{Model: "DeviceRGB"}
|
||||||
|
}
|
||||||
|
values, err := space.Convert(values[:source.Components()], source, intent)
|
||||||
|
return values, true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositePaintOver 以兼容叠印计算零色料分量,再应用对象混合模式
|
||||||
|
// 入参: backdrop 背景及输出, source 源分量, alpha 源透明度, space 混合空间, mode 混合模式, compatible 是否保留零色料背景
|
||||||
|
// 返回: error 未支持的混合模式
|
||||||
|
func pdfCompositePaintOver(backdrop *pdfCompositePixel, source [4]float64, alpha float64, space *pdfgo.ColorSpace, mode pdfgo.Name, compatible bool) error {
|
||||||
|
if compatible {
|
||||||
|
for i := range source {
|
||||||
|
if source[i] == 0 {
|
||||||
|
source[i] = backdrop.values[i] * backdrop.alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pdfCompositeOver(backdrop, source, alpha, space, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositeOver 按PDF透明模型合成源颜色,四色混合使用分量补色
|
||||||
|
// 入参: backdrop 背景及输出, source 源分量, alpha 源透明度, space 混合空间, mode 混合模式
|
||||||
|
// 返回: error 未支持的混合模式
|
||||||
|
func pdfCompositeOver(backdrop *pdfCompositePixel, source [4]float64, alpha float64, space *pdfgo.ColorSpace, mode pdfgo.Name) error {
|
||||||
|
if alpha == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
combined := alpha + backdrop.alpha*(1-alpha)
|
||||||
|
for i := 0; i < space.Components(); i++ {
|
||||||
|
b, s := backdrop.values[i], source[i]
|
||||||
|
if space.Model == "DeviceCMYK" {
|
||||||
|
b, s = 1-b, 1-s
|
||||||
|
}
|
||||||
|
mixed, err := pdfSeparableBlend(b, s, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if space.Model == "DeviceCMYK" {
|
||||||
|
mixed = 1 - mixed
|
||||||
|
}
|
||||||
|
backdrop.values[i] = math.Max(0, math.Min(1, ((1-alpha)*backdrop.alpha*backdrop.values[i]+alpha*((1-backdrop.alpha)*source[i]+backdrop.alpha*mixed))/combined))
|
||||||
|
}
|
||||||
|
backdrop.alpha = combined
|
||||||
|
backdrop.effect = alpha + backdrop.effect*(1-alpha)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfSeparableBlend 计算标准可分离混合函数
|
||||||
|
// 入参: b 背景分量, s 源分量, mode 混合模式
|
||||||
|
// 返回: float64 混合分量, error 未支持的模式
|
||||||
|
func pdfSeparableBlend(b, s float64, mode pdfgo.Name) (float64, error) {
|
||||||
|
switch mode {
|
||||||
|
case "", "Normal", "Compatible":
|
||||||
|
return s, nil
|
||||||
|
case "Multiply":
|
||||||
|
return b * s, nil
|
||||||
|
case "Screen":
|
||||||
|
return b + s - b*s, nil
|
||||||
|
case "Overlay":
|
||||||
|
if b <= .5 {
|
||||||
|
return 2 * b * s, nil
|
||||||
|
}
|
||||||
|
return 1 - 2*(1-b)*(1-s), nil
|
||||||
|
case "Darken":
|
||||||
|
return math.Min(b, s), nil
|
||||||
|
case "Lighten":
|
||||||
|
return math.Max(b, s), nil
|
||||||
|
case "ColorDodge":
|
||||||
|
if b == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if s == 1 {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
return math.Min(1, b/(1-s)), nil
|
||||||
|
case "ColorBurn":
|
||||||
|
if b == 1 {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
if s == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return 1 - math.Min(1, (1-b)/s), nil
|
||||||
|
case "HardLight":
|
||||||
|
if s <= .5 {
|
||||||
|
return 2 * b * s, nil
|
||||||
|
}
|
||||||
|
return 1 - 2*(1-b)*(1-s), nil
|
||||||
|
case "SoftLight":
|
||||||
|
if s <= .5 {
|
||||||
|
return b - (1-2*s)*b*(1-b), nil
|
||||||
|
}
|
||||||
|
d := math.Sqrt(b)
|
||||||
|
if b <= .25 {
|
||||||
|
d = ((16*b-12)*b + 4) * b
|
||||||
|
}
|
||||||
|
return b + (2*s-1)*(d-b), nil
|
||||||
|
case "Difference":
|
||||||
|
return math.Abs(b - s), nil
|
||||||
|
case "Exclusion":
|
||||||
|
return b + s - 2*b*s, nil
|
||||||
|
default:
|
||||||
|
return 0, &pdfgo.UnsupportedError{Feature: "nonseparable blend mode " + string(mode)}
|
||||||
|
}
|
||||||
|
}
|
||||||
+320
-18
@@ -15,23 +15,50 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// group 将不透明图元的透明组转换为互不重叠的填充区域,保留矢量与组不透明度
|
// errPDFGroupRaster 表示透明组需要按指定精度局部合成
|
||||||
|
var errPDFGroupRaster = errors.New("PDF transparency group requires local compositing")
|
||||||
|
|
||||||
|
// pdfPaintRegion 保存透明组内的单色填充区域
|
||||||
|
type pdfPaintRegion struct {
|
||||||
|
paint pdfgo.Paint
|
||||||
|
path GeometryPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// group 将单色图元的透明组转换为互不重叠的填充区域,保留矢量与组不透明度
|
||||||
// 入参: 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.BlendMode != "" && mark.BlendMode != "Normal" && mark.BlendMode != "Compatible" {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "group blend mode " + string(mark.BlendMode)}
|
||||||
|
}
|
||||||
if err := p.flushPath(); err != nil {
|
if err := p.flushPath(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if mark.ColorSpace != nil && !mark.ColorSpace.SRGBEquivalent() {
|
||||||
|
if mark.Alpha != 1 || mark.SoftMask != nil {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "non-sRGB group color space"}
|
||||||
|
}
|
||||||
|
if err := walk(pdfOpaqueVisitor(visitor)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if mark.SoftMask != nil {
|
if mark.SoftMask != nil {
|
||||||
clip, err := p.maskClip(mark.SoftMask)
|
clip, err := p.maskClip(mark.SoftMask)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if p.rasterMaskAllowed(err) {
|
||||||
|
return p.rasterGroup(mark, walk)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
original := walk
|
original := walk
|
||||||
@@ -45,21 +72,74 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error
|
|||||||
}
|
}
|
||||||
return p.flushPath()
|
return p.flushPath()
|
||||||
}
|
}
|
||||||
|
err := p.groupPaths(mark, walk, visitor)
|
||||||
|
if errors.Is(err, errPDFGroupRaster) && p.warning != nil {
|
||||||
|
return p.rasterGroup(mark, walk)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfOpaqueVisitor 验证组内没有透明混合,允许独立的已校准颜色保留为矢量
|
||||||
|
// 入参: visitor 正式访问器,语义提示由正式遍历交付
|
||||||
|
// 返回: pdfgo.Visitor 不透明性检查器
|
||||||
|
func pdfOpaqueVisitor(visitor pdfgo.Visitor) pdfgo.Visitor {
|
||||||
|
check := func(s pdfgo.Style, fill, stroke bool) error {
|
||||||
|
if s.SoftMask != nil || fill && s.Fill.Alpha != 1 || stroke && s.Stroke.Alpha != 1 || s.BlendMode != "" && s.BlendMode != "Normal" && s.BlendMode != "Compatible" {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "non-sRGB transparent group content"}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v := pdfgo.Visitor{
|
||||||
|
Path: func(m pdfgo.PathMark) error { return check(m.Style, m.Fill, m.Stroke) },
|
||||||
|
Text: func(m pdfgo.TextMark) error {
|
||||||
|
return check(m.Style, m.Mode%4 == 0 || m.Mode%4 == 2, m.Mode%4 == 1 || m.Mode%4 == 2)
|
||||||
|
},
|
||||||
|
Image: func(m pdfgo.ImageMark) error {
|
||||||
|
if m.Image.ImageMask || m.Image.Mask != nil || m.Image.SoftMask != nil || m.Image.Stream.Dictionary["SMaskInData"] != nil {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "non-sRGB masked group image"}
|
||||||
|
}
|
||||||
|
return check(m.Style, true, false)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if visitor.Warning != nil {
|
||||||
|
v.Warning = func(pdfgo.Diagnostic) {}
|
||||||
|
}
|
||||||
|
if visitor.MarkedContent != nil {
|
||||||
|
v.MarkedContent = func(pdfgo.MarkedContentMark) error { return nil }
|
||||||
|
}
|
||||||
|
v.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
||||||
|
if mark.Alpha != 1 || mark.SoftMask != nil || mark.BlendMode != "" && mark.BlendMode != "Normal" && mark.BlendMode != "Compatible" {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "non-sRGB nested transparent group"}
|
||||||
|
}
|
||||||
|
return walk(v)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupPaths 将正常混合的透明路径组分解为标准矢量区域
|
||||||
|
// 入参: mark 组状态, walk 内容访问函数, visitor 页面访问器
|
||||||
|
// 返回: error 无法矢量表达或几何错误
|
||||||
|
func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error, visitor pdfgo.Visitor) error {
|
||||||
geometry, err := p.editor.Geometry()
|
geometry, err := p.editor.Geometry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
type paintRegion struct {
|
var regions []pdfPaintRegion
|
||||||
paint pdfgo.Paint
|
|
||||||
path GeometryPath
|
|
||||||
}
|
|
||||||
var regions []paintRegion
|
|
||||||
var combined GeometryPath
|
var combined GeometryPath
|
||||||
var paint *pdfgo.Paint
|
var paint *pdfgo.Paint
|
||||||
var clips []pdfgo.Path
|
var clips []pdfgo.Path
|
||||||
|
opaque := true
|
||||||
scale := math.Hypot(p.matrix[0], p.matrix[1])
|
scale := math.Hypot(p.matrix[0], p.matrix[1])
|
||||||
collect := pdfgo.Visitor{Warning: visitor.Warning}
|
collect := pdfgo.Visitor{
|
||||||
|
Warning: visitor.Warning,
|
||||||
|
Text: func(pdfgo.TextMark) error { return errPDFGroupRaster },
|
||||||
|
Image: func(pdfgo.ImageMark) error { return errPDFGroupRaster },
|
||||||
|
Group: func(pdfgo.GroupMark, func(pdfgo.Visitor) error) error { return errPDFGroupRaster },
|
||||||
|
}
|
||||||
collect.Path = func(path pdfgo.PathMark) error {
|
collect.Path = func(path pdfgo.PathMark) error {
|
||||||
|
if path.Style.SoftMask != nil || path.Style.BlendMode != "" && path.Style.BlendMode != "Normal" && path.Style.BlendMode != "Compatible" {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "masked or blended transparent group content"}
|
||||||
|
}
|
||||||
shape, err := ParseGeometryPath(p.pathData(path.Path, Box{}))
|
shape, err := ParseGeometryPath(p.pathData(path.Path, Box{}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -72,21 +152,28 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error
|
|||||||
if stroke {
|
if stroke {
|
||||||
color, overprint = path.Style.Stroke, path.Style.StrokeOverprint
|
color, overprint = path.Style.Stroke, path.Style.StrokeOverprint
|
||||||
}
|
}
|
||||||
if color.Alpha != 1 || color.Axial != nil || color.Radial != nil || overprint && pdfOverprintNeedsSeparation(color) {
|
if color.Tiling != nil || color.Axial != nil || color.Radial != nil || overprint && pdfOverprintNeedsSeparation(color) {
|
||||||
return &pdfgo.UnsupportedError{Feature: "transparent or overprinted group content"}
|
return fmt.Errorf("%w: patterned or overprinted group content", errPDFGroupRaster)
|
||||||
}
|
}
|
||||||
|
if color.Alpha == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opaque = opaque && color.Alpha == 1
|
||||||
if paint == nil {
|
if paint == nil {
|
||||||
paint, clips = &color, path.Style.Clips
|
paint, clips = &color, path.Style.Clips
|
||||||
} else if !reflect.DeepEqual(clips, path.Style.Clips) {
|
} else if !reflect.DeepEqual(clips, path.Style.Clips) {
|
||||||
return &pdfgo.UnsupportedError{Feature: "independently clipped transparent group"}
|
return &pdfgo.UnsupportedError{Feature: "independently clipped transparent group"}
|
||||||
} else if !reflect.DeepEqual(*paint, color) {
|
} else if color.Alpha != 1 || !reflect.DeepEqual(*paint, color) {
|
||||||
regions = append(regions, paintRegion{*paint, combined})
|
regions = append(regions, pdfPaintRegion{*paint, combined})
|
||||||
combined = nil
|
combined = nil
|
||||||
paint = &color
|
paint = &color
|
||||||
}
|
}
|
||||||
outline := shape
|
outline := shape
|
||||||
evenOdd := path.Path.EvenOdd
|
evenOdd := path.Path.EvenOdd
|
||||||
if stroke {
|
if stroke {
|
||||||
|
if path.Style.LineWidth == 0 {
|
||||||
|
return fmt.Errorf("%w: zero-width group stroke", errPDFGroupRaster)
|
||||||
|
}
|
||||||
strokeShape, strokeScale, tolerance := shape, scale, 0.001
|
strokeShape, strokeScale, tolerance := shape, scale, 0.001
|
||||||
var strokeTransform pdfgo.Matrix
|
var strokeTransform pdfgo.Matrix
|
||||||
if path.StrokeMatrix != (pdfgo.Matrix{}) {
|
if path.StrokeMatrix != (pdfgo.Matrix{}) {
|
||||||
@@ -133,18 +220,24 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error
|
|||||||
if len(combined) == 0 {
|
if len(combined) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
regions = append(regions, paintRegion{*paint, combined})
|
regions = append(regions, pdfPaintRegion{*paint, combined})
|
||||||
|
if !opaque {
|
||||||
|
regions, err = pdfCompositeRegions(geometry, regions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
var covered GeometryPath
|
var covered GeometryPath
|
||||||
for i := len(regions) - 1; i >= 0; i-- {
|
for i := len(regions) - 1; i >= 0; i-- {
|
||||||
region := regions[i]
|
region := regions[i]
|
||||||
visible := region.path
|
visible := region.path
|
||||||
if len(covered) > 0 {
|
if opaque && len(covered) > 0 {
|
||||||
visible, err = geometry.Combine(visible, covered, GeometrySubtract)
|
visible, err = geometry.Combine(visible, covered, GeometrySubtract)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if i > 0 {
|
if opaque && i > 0 {
|
||||||
covered, err = geometry.Combine(covered, region.path, GeometryUnion)
|
covered, err = geometry.Combine(covered, region.path, GeometryUnion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -168,7 +261,7 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
region.paint.Alpha = mark.Alpha
|
region.paint.Alpha *= mark.Alpha
|
||||||
fill, stroke := true, false
|
fill, stroke := true, false
|
||||||
objectClips, err := p.clips(clips, box)
|
objectClips, err := p.clips(clips, box)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -181,6 +274,205 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rasterGroup 在透明背景上合成局部图元,组透明度应用于合成结果,保留组外对象
|
||||||
|
// 入参: mark 组状态, walk 内容访问函数
|
||||||
|
// 返回: error 转换或渲染错误
|
||||||
|
func (p *pdfImporter) rasterGroup(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
||||||
|
img, box, err := p.renderGroup(func(local *pdfImporter) error { return walk(local.visitor()) }, nil)
|
||||||
|
if err != nil || img == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if mark.SoftMask != nil {
|
||||||
|
img, err = p.applySoftMask(img, box, mark.SoftMask)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p.appendRasterGroup(img, box, mark.Alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
// visitor 绑定当前导入器的图元与透明组访问函数
|
||||||
|
// 返回: pdfgo.Visitor 页面访问器
|
||||||
|
func (p *pdfImporter) visitor() pdfgo.Visitor {
|
||||||
|
v := pdfgo.Visitor{Path: p.path, Text: p.text, Image: p.image, Warning: p.warning}
|
||||||
|
v.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { return p.group(mark, walk, v) }
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderGroup 按统一像素网格渲染局部内容,保留裁剪及后端选择
|
||||||
|
// 入参: build 局部图元构建函数, viewport 指定裁取区域,nil时按图元边界计算
|
||||||
|
// 返回: image.Image 透明图像, Box 页面区域, error 构建或渲染错误
|
||||||
|
func (p *pdfImporter) renderGroup(build func(*pdfImporter) error, viewport *Box) (image.Image, Box, error) {
|
||||||
|
scene, box, err := p.compileGroup(build)
|
||||||
|
if err != nil || scene == nil && viewport == nil {
|
||||||
|
return nil, box, err
|
||||||
|
}
|
||||||
|
if viewport != nil {
|
||||||
|
box = *viewport
|
||||||
|
}
|
||||||
|
if scene == nil {
|
||||||
|
scene = &RasterPage{DPI: p.rasterDPI}
|
||||||
|
}
|
||||||
|
img, err := p.renderGroupScene(scene, box)
|
||||||
|
return img, box, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileGroup 编译局部图元并计算页面内的像素对齐边界
|
||||||
|
// 入参: build 局部图元构建函数
|
||||||
|
// 返回: *RasterPage 中立绘制场景, Box 页面区域, error 编译错误
|
||||||
|
func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage, Box, error) {
|
||||||
|
var box Box
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return nil, box, err
|
||||||
|
}
|
||||||
|
editor := NewEditor()
|
||||||
|
editor.SetRenderBackends(p.editor.Backends())
|
||||||
|
editor.SetFontDirs(p.editor.fontDirs...)
|
||||||
|
editor.SetFontFS(p.editor.fontFS...)
|
||||||
|
if _, err := editor.AddPage(p.pageWidth, p.pageHeight); err != nil {
|
||||||
|
return nil, box, err
|
||||||
|
}
|
||||||
|
local := pdfImporter{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{}, fontMetrics: map[string]FontMetrics{}}
|
||||||
|
if err := build(&local); err != nil {
|
||||||
|
return nil, box, err
|
||||||
|
}
|
||||||
|
if err := local.flushPath(); err != nil {
|
||||||
|
return nil, box, err
|
||||||
|
}
|
||||||
|
if len(local.objects) == 0 {
|
||||||
|
return nil, box, nil
|
||||||
|
}
|
||||||
|
if err := local.commitObjects(); err != nil {
|
||||||
|
return nil, box, err
|
||||||
|
}
|
||||||
|
reader, err := editor.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, box, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
page, err := reader.PageContentByIndex(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, box, err
|
||||||
|
}
|
||||||
|
renderer := p.renderer.childRenderer(reader)
|
||||||
|
renderer.TransparentBackground = true
|
||||||
|
for _, layer := range page.Content.Layer {
|
||||||
|
for _, object := range layer.Objects {
|
||||||
|
bounds, err := renderer.ObjectBounds(object, layer.DrawParam)
|
||||||
|
if err != nil {
|
||||||
|
return nil, box, err
|
||||||
|
}
|
||||||
|
box = unionTextBox(box, bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if box.W <= 0 || box.H <= 0 {
|
||||||
|
return nil, box, nil
|
||||||
|
}
|
||||||
|
step := 25.4 / p.rasterDPI
|
||||||
|
x, y := math.Max(0, math.Floor(box.X/step)*step), math.Max(0, math.Floor(box.Y/step)*step)
|
||||||
|
xend, yend := math.Ceil(math.Min(p.pageWidth, box.X+box.W)/step)*step, math.Ceil(math.Min(p.pageHeight, box.Y+box.H)/step)*step
|
||||||
|
box = Box{X: x, Y: y, W: xend - x, H: yend - y}
|
||||||
|
if box.W <= 0 || box.H <= 0 {
|
||||||
|
return nil, box, nil
|
||||||
|
}
|
||||||
|
scene, err := renderer.CompilePage(page)
|
||||||
|
return scene, box, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderGroupScene 在指定区域渲染已编译场景,不修改缓存的命令与裁剪
|
||||||
|
// 入参: source 原始场景, box 页面裁取区域
|
||||||
|
// 返回: image.Image 透明图像, error 后端渲染错误
|
||||||
|
func (p *pdfImporter) renderGroupScene(source *RasterPage, box Box) (image.Image, error) {
|
||||||
|
scene := *source
|
||||||
|
scene.Commands = append([]RasterCommand(nil), source.Commands...)
|
||||||
|
scene.Width, scene.Height = box.W, box.H
|
||||||
|
for i := range scene.Commands {
|
||||||
|
command := &scene.Commands[i]
|
||||||
|
command.Transform[4] -= box.X
|
||||||
|
command.Transform[5] -= box.Y
|
||||||
|
if command.Clip != nil {
|
||||||
|
command.Clip = append([]RasterSegment{}, command.Clip...)
|
||||||
|
for j := range command.Clip {
|
||||||
|
segment := &command.Clip[j]
|
||||||
|
segment.End.X, segment.End.Y = segment.End.X-box.X, segment.End.Y-box.Y
|
||||||
|
segment.Control1.X, segment.Control1.Y = segment.Control1.X-box.X, segment.Control1.Y-box.Y
|
||||||
|
segment.Control2.X, segment.Control2.Y = segment.Control2.X-box.X, segment.Control2.Y-box.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backend := p.editor.Backends().Raster
|
||||||
|
if backend == nil {
|
||||||
|
return nil, fmt.Errorf("PDF local compositing: %w", ErrBackendUnavailable)
|
||||||
|
}
|
||||||
|
return backend.Render(&scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendRasterGroup 保存局部合成结果并记录输出精度
|
||||||
|
// 入参: img 透明图像, box 页面区域, opacity 组透明度
|
||||||
|
// 返回: error 保存错误
|
||||||
|
func (p *pdfImporter) appendRasterGroup(img image.Image, box Box, opacity float64) error {
|
||||||
|
var data bytes.Buffer
|
||||||
|
if err := png.Encode(&data, img); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
id, err := p.editor.AddImage(data.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
alpha := int(math.Round(opacity * 255))
|
||||||
|
object := ImageObject{ResourceID: id, Boundary: pdfBoundary(box), CTM: pdfNumbers(box.W, 0, 0, box.H, 0, 0), Alpha: &alpha}
|
||||||
|
p.objects = append(p.objects, GraphicObject{Type: "ImageObject", ImageObject: object})
|
||||||
|
p.report.ImageObjects++
|
||||||
|
if !p.rasterWarned {
|
||||||
|
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF effect locally composited at %g DPI; other objects retained", p.rasterDPI)})
|
||||||
|
p.rasterWarned = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCompositeRegions 按正常混合计算互不重叠的区域,组透明度由调用方统一应用
|
||||||
|
// 入参: geometry 几何后端, regions 按绘制顺序排列的区域
|
||||||
|
// 返回: []pdfPaintRegion 合成区域, error 几何错误
|
||||||
|
func pdfCompositeRegions(geometry GeometryBackend, regions []pdfPaintRegion) ([]pdfPaintRegion, error) {
|
||||||
|
var result []pdfPaintRegion
|
||||||
|
for _, source := range regions {
|
||||||
|
remaining := source.path
|
||||||
|
var next []pdfPaintRegion
|
||||||
|
for _, backdrop := range result {
|
||||||
|
outside, err := geometry.Combine(backdrop.path, source.path, GeometrySubtract)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(outside) != 0 {
|
||||||
|
next = append(next, pdfPaintRegion{backdrop.paint, outside})
|
||||||
|
}
|
||||||
|
overlap, err := geometry.Combine(backdrop.path, source.path, GeometryIntersect)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(overlap) != 0 {
|
||||||
|
paint := pdfgo.Paint{Alpha: source.paint.Alpha + backdrop.paint.Alpha*(1-source.paint.Alpha)}
|
||||||
|
for i := range paint.RGB {
|
||||||
|
paint.RGB[i] = (source.paint.RGB[i]*source.paint.Alpha + backdrop.paint.RGB[i]*backdrop.paint.Alpha*(1-source.paint.Alpha)) / paint.Alpha
|
||||||
|
}
|
||||||
|
next = append(next, pdfPaintRegion{paint, overlap})
|
||||||
|
}
|
||||||
|
remaining, err = geometry.Combine(remaining, backdrop.path, GeometrySubtract)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(remaining) != 0 {
|
||||||
|
next = append(next, pdfPaintRegion{source.paint, remaining})
|
||||||
|
}
|
||||||
|
result = next
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// pdfClippedVisitor 为图元及嵌套组附加裁剪,保留未提供的访问能力
|
// pdfClippedVisitor 为图元及嵌套组附加裁剪,保留未提供的访问能力
|
||||||
// 入参: visitor 图元访问器, clip 页面坐标裁剪
|
// 入参: visitor 图元访问器, clip 页面坐标裁剪
|
||||||
// 返回: pdfgo.Visitor 裁剪访问器
|
// 返回: pdfgo.Visitor 裁剪访问器
|
||||||
@@ -216,6 +508,9 @@ func pdfClippedVisitor(visitor pdfgo.Visitor, clip pdfgo.Path) pdfgo.Visitor {
|
|||||||
// 入参: mask PDF蒙版
|
// 入参: mask PDF蒙版
|
||||||
// 返回: pdfgo.Path 页面坐标裁剪路径, error 不可精确转换或几何错误
|
// 返回: pdfgo.Path 页面坐标裁剪路径, error 不可精确转换或几何错误
|
||||||
func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
|
func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
|
||||||
|
if mask.Subtype != "Luminosity" || mask.ColorSpace == nil || mask.ColorSpace.Model != "DeviceGray" || mask.ColorSpace.Calibrated() {
|
||||||
|
return pdfgo.Path{}, &pdfgo.UnsupportedError{Feature: "nonbinary luminosity mask"}
|
||||||
|
}
|
||||||
if clip, ok := p.maskClips[mask]; ok {
|
if clip, ok := p.maskClips[mask]; ok {
|
||||||
return clip, nil
|
return clip, nil
|
||||||
}
|
}
|
||||||
@@ -230,7 +525,11 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
|
|||||||
}
|
}
|
||||||
return v == 1, nil
|
return v == 1, nil
|
||||||
}
|
}
|
||||||
background, err := opacity(mask.Background)
|
luminosity, err := mask.ColorSpace.Luminosity(mask.Backdrop, "RelativeColorimetric")
|
||||||
|
if err != nil {
|
||||||
|
return pdfgo.Path{}, err
|
||||||
|
}
|
||||||
|
background, err := opacity(luminosity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pdfgo.Path{}, err
|
return pdfgo.Path{}, err
|
||||||
}
|
}
|
||||||
@@ -249,14 +548,14 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
|
|||||||
identity := pdfImporter{matrix: pdfgo.Identity()}
|
identity := pdfImporter{matrix: pdfgo.Identity()}
|
||||||
var visitor pdfgo.Visitor
|
var visitor pdfgo.Visitor
|
||||||
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
||||||
if mark.Alpha != 1 || mark.SoftMask != nil {
|
if mark.Alpha != 1 || mark.SoftMask != nil || !pdfNormalBlend(mark.BlendMode) || mark.ColorSpace != nil && !mark.ColorSpace.Equal(mask.ColorSpace) {
|
||||||
return &pdfgo.UnsupportedError{Feature: "transparent mask group"}
|
return &pdfgo.UnsupportedError{Feature: "transparent mask group"}
|
||||||
}
|
}
|
||||||
return walk(visitor)
|
return walk(visitor)
|
||||||
}
|
}
|
||||||
visitor.Path = func(mark pdfgo.PathMark) error {
|
visitor.Path = func(mark pdfgo.PathMark) error {
|
||||||
paint := mark.Style.Fill
|
paint := mark.Style.Fill
|
||||||
if !mark.Fill || mark.Stroke || paint.Alpha != 1 || paint.CMYK != nil || paint.Axial != nil || paint.Radial != nil || paint.RGB[0] != paint.RGB[1] || paint.RGB[1] != paint.RGB[2] || mark.Style.SoftMask != nil {
|
if !mark.Fill || mark.Stroke || paint.Alpha != 1 || paint.CMYK != nil || paint.Axial != nil || paint.Radial != nil || paint.Tiling != nil || paint.Space != nil && paint.Space.Calibrated() || paint.RGB[0] != paint.RGB[1] || paint.RGB[1] != paint.RGB[2] || mark.Style.SoftMask != nil || !pdfNormalBlend(mark.Style.BlendMode) {
|
||||||
return &pdfgo.UnsupportedError{Feature: "nonbinary mask graphic"}
|
return &pdfgo.UnsupportedError{Feature: "nonbinary mask graphic"}
|
||||||
}
|
}
|
||||||
visible, err := opacity(paint.RGB[0])
|
visible, err := opacity(paint.RGB[0])
|
||||||
@@ -272,6 +571,9 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, clip := range mark.Style.Clips {
|
for _, clip := range mark.Style.Clips {
|
||||||
|
if len(clip.Text) != 0 {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "text-clipped binary mask"}
|
||||||
|
}
|
||||||
c, err := ParseGeometryPath(identity.pathData(clip, Box{}))
|
c, err := ParseGeometryPath(identity.pathData(clip, Box{}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// 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 (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rasterMaskAllowed 仅对不可矢量表达的蒙版启用显式局部合成,格式错误仍返回
|
||||||
|
// 入参: err 蒙版转换错误
|
||||||
|
// 返回: bool 是否允许局部合成
|
||||||
|
func (p *pdfImporter) rasterMaskAllowed(err error) bool {
|
||||||
|
var unsupported *pdfgo.UnsupportedError
|
||||||
|
return p.warning != nil && errors.As(err, &unsupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// applySoftMask 按原始颜色分量计算蒙版,逐块应用透明度及亮度传递
|
||||||
|
// 入参: source 源图像, box 页面区域, mask PDF软蒙版
|
||||||
|
// 返回: image.Image 合成图像, error 蒙版解释或渲染错误
|
||||||
|
func (p *pdfImporter) applySoftMask(source image.Image, box Box, mask *pdfgo.SoftMask) (image.Image, error) {
|
||||||
|
inverse, ok := p.matrix.Inverse()
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid PDF mask transform")
|
||||||
|
}
|
||||||
|
out := image.NewNRGBA(source.Bounds())
|
||||||
|
w, h := out.Rect.Dx(), out.Rect.Dy()
|
||||||
|
step := 25.4 / p.rasterDPI
|
||||||
|
cache := p.compositingCache()
|
||||||
|
for y := 0; y < h; y += 256 {
|
||||||
|
for x := 0; x < w; x += 256 {
|
||||||
|
width, height := min(256, w-x), min(256, h-y)
|
||||||
|
c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, masks: map[*pdfgo.SoftMask][]float64{}}
|
||||||
|
alpha, err := c.mask(mask, &pdfgo.ColorSpace{Model: "DeviceRGB"})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i, opacity := range alpha {
|
||||||
|
px, py := out.Rect.Min.X+x+i%width, out.Rect.Min.Y+y+i/width
|
||||||
|
pixel := color.NRGBAModel.Convert(source.At(px, py)).(color.NRGBA)
|
||||||
|
pixel.A = uint8(math.Round(float64(pixel.A) * opacity))
|
||||||
|
out.SetNRGBA(px, py, pixel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
+63
-16
@@ -33,8 +33,8 @@ import (
|
|||||||
// 入参: mark PDF图像绘制信息
|
// 入参: mark PDF图像绘制信息
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
||||||
if mark.Style.BlendMode == "Multiply" {
|
if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" {
|
||||||
return &pdfgo.UnsupportedError{Feature: "multiply image outside annotation appearance"}
|
return &pdfgo.UnsupportedError{Feature: "image blend mode " + string(mark.Style.BlendMode)}
|
||||||
}
|
}
|
||||||
if err := p.flushPath(); err != nil {
|
if err := p.flushPath(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -45,6 +45,11 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
|||||||
var err error
|
var err error
|
||||||
mark.Style, err = p.maskStyle(mark.Style)
|
mark.Style, err = p.maskStyle(mark.Style)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if p.rasterMaskAllowed(err) {
|
||||||
|
mask := mark.Style.SoftMask
|
||||||
|
mark.Style.SoftMask = nil
|
||||||
|
return p.rasterGroup(pdfgo.GroupMark{Alpha: 1, SoftMask: mask}, func(v pdfgo.Visitor) error { return v.Image(mark) })
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if mark.Style.FillOverprint {
|
if mark.Style.FillOverprint {
|
||||||
@@ -60,12 +65,10 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
filter, err := p.reader.Resolve(source.Stream.Dictionary["Filter"])
|
jpegOriginal, err := source.JPEGFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
space, _ := source.ColorSpace.(pdfgo.Name)
|
|
||||||
jpegOriginal := filter == pdfgo.Name("DCTDecode") && len(source.Decode) == 0 && !source.ImageMask && (space == "DeviceGray" || space == "DeviceRGB") && source.Mask == nil && source.SoftMask == nil
|
|
||||||
var decoded image.Image
|
var decoded image.Image
|
||||||
if len(jbig2Original) == 0 {
|
if len(jbig2Original) == 0 {
|
||||||
decoded, err = source.DecodeImage()
|
decoded, err = source.DecodeImage()
|
||||||
@@ -106,8 +109,8 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
|||||||
var encoded bytes.Buffer
|
var encoded bytes.Buffer
|
||||||
if len(jbig2Original) != 0 {
|
if len(jbig2Original) != 0 {
|
||||||
encoded.Write(jbig2Original)
|
encoded.Write(jbig2Original)
|
||||||
} else if jpegOriginal {
|
} else if len(jpegOriginal) != 0 {
|
||||||
encoded.Write(source.Stream.Data)
|
encoded.Write(jpegOriginal)
|
||||||
} else {
|
} else {
|
||||||
if err := png.Encode(&encoded, decoded); err != nil {
|
if err := png.Encode(&encoded, decoded); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -124,6 +127,13 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
|||||||
p.imageIDs[source.Stream] = id
|
p.imageIDs[source.Stream] = id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return p.appendImage(mark, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendImage 定位已注册的图像资源,保留透明度与裁剪
|
||||||
|
// 入参: mark 图像绘制信息, id 资源编号
|
||||||
|
// 返回: error 裁剪错误
|
||||||
|
func (p *pdfImporter) appendImage(mark pdfgo.ImageMark, id string) error {
|
||||||
m := p.matrix.Mul(mark.Matrix).Mul(pdfgo.Matrix{1, 0, 0, -1, 0, 1})
|
m := p.matrix.Mul(mark.Matrix).Mul(pdfgo.Matrix{1, 0, 0, -1, 0, 1})
|
||||||
box := pdfBounds([]pdfgo.Point{m.Apply(pdfgo.Point{}), m.Apply(pdfgo.Point{X: 1}), m.Apply(pdfgo.Point{Y: 1}), m.Apply(pdfgo.Point{X: 1, Y: 1})})
|
box := pdfBounds([]pdfgo.Point{m.Apply(pdfgo.Point{}), m.Apply(pdfgo.Point{X: 1}), m.Apply(pdfgo.Point{Y: 1}), m.Apply(pdfgo.Point{X: 1, Y: 1})})
|
||||||
alpha := int(math.Round(mark.Style.Fill.Alpha * 255))
|
alpha := int(math.Round(mark.Style.Fill.Alpha * 255))
|
||||||
@@ -141,6 +151,9 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
|||||||
// 入参: mark PDF文字绘制信息
|
// 入参: mark PDF文字绘制信息
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||||
|
if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "text blend mode " + string(mark.Style.BlendMode)}
|
||||||
|
}
|
||||||
if mark.Font.Subtype == pdfgo.Name("Type3") {
|
if mark.Font.Subtype == pdfgo.Name("Type3") {
|
||||||
if mark.Clip != nil {
|
if mark.Clip != nil {
|
||||||
return &pdfgo.UnsupportedError{Feature: "Type3 text clipping"}
|
return &pdfgo.UnsupportedError{Feature: "Type3 text clipping"}
|
||||||
@@ -159,21 +172,26 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
}
|
}
|
||||||
return p.flushPath()
|
return p.flushPath()
|
||||||
}
|
}
|
||||||
if mark.Style.BlendMode == "Multiply" {
|
|
||||||
return &pdfgo.UnsupportedError{Feature: "multiply text"}
|
|
||||||
}
|
|
||||||
if err := p.flushPath(); err != nil {
|
if err := p.flushPath(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
mark.Style, err = p.maskStyle(mark.Style)
|
mark.Style, err = p.maskStyle(mark.Style)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if mark.Clip == nil && p.rasterMaskAllowed(err) {
|
||||||
|
mask := mark.Style.SoftMask
|
||||||
|
mark.Style.SoftMask = nil
|
||||||
|
return p.rasterGroup(pdfgo.GroupMark{Alpha: 1, SoftMask: mask}, func(v pdfgo.Visitor) error { return v.Text(mark) })
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
paintMode := mark.Mode % 4
|
paintMode := mark.Mode % 4
|
||||||
if (paintMode == 0 || paintMode == 2) && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || (paintMode == 1 || paintMode == 2) && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
|
if (paintMode == 0 || paintMode == 2) && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || (paintMode == 1 || paintMode == 2) && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
|
||||||
return &pdfgo.UnsupportedError{Feature: "text color separation overprint"}
|
return &pdfgo.UnsupportedError{Feature: "text color separation overprint"}
|
||||||
}
|
}
|
||||||
|
if p.warning != nil && ((paintMode == 0 || paintMode == 2) && pdfGradientError(mark.Style.Fill) != nil || (paintMode == 1 || paintMode == 2) && pdfGradientError(mark.Style.Stroke) != nil) {
|
||||||
|
return p.compositeRegion(nil, pdfCompositeNode{text: &mark}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false)
|
||||||
|
}
|
||||||
font := mark.Font
|
font := mark.Font
|
||||||
embedded := len(font.Program) != 0
|
embedded := len(font.Program) != 0
|
||||||
if embedded && font.ProgramType != "FontFile" && font.ProgramType != "FontFile2" && font.ProgramType != "OpenType" && font.ProgramType != "Type1C" && font.ProgramType != "CIDFontType0C" {
|
if embedded && font.ProgramType != "FontFile" && font.ProgramType != "FontFile2" && font.ProgramType != "OpenType" && font.ProgramType != "Type1C" && font.ProgramType != "CIDFontType0C" {
|
||||||
@@ -290,7 +308,21 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
position += count
|
position += count
|
||||||
path, err := outline.GlyphOutline(gid, object.Size)
|
path, err := outline.GlyphOutline(gid, object.Size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("PDF font %s outline: %w", font.Name, err)
|
||||||
|
}
|
||||||
|
if diagnostics, ok := outline.(FontGlyphDiagnostics); ok {
|
||||||
|
if err := diagnostics.GlyphWarning(gid); err != nil {
|
||||||
|
if p.warning == nil {
|
||||||
|
return fmt.Errorf("PDF font %s glyph %d hinting: %w", font.Name, gid, err)
|
||||||
|
}
|
||||||
|
if p.fontWarnings == nil {
|
||||||
|
p.fontWarnings = make(map[string]bool)
|
||||||
|
}
|
||||||
|
if !p.fontWarnings[id] {
|
||||||
|
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF font %s has invalid hinting instructions; design outlines and original font retained", font.Name)})
|
||||||
|
p.fontWarnings[id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bounds, err := path.Bounds()
|
bounds, err := path.Bounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -309,13 +341,22 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
box := pdfBounds(points)
|
box := pdfBounds(points)
|
||||||
fill, stroke, visible := paintMode == 0 || paintMode == 2, paintMode == 1 || paintMode == 2, paintMode != 3
|
fill, stroke, visible := paintMode == 0 || paintMode == 2, paintMode == 1 || paintMode == 2, paintMode != 3
|
||||||
if stroke {
|
if stroke {
|
||||||
|
strokeMatrix := mark.StrokeMatrix
|
||||||
|
if strokeMatrix == (pdfgo.Matrix{}) {
|
||||||
|
strokeMatrix = pdfgo.Identity()
|
||||||
|
}
|
||||||
|
sx, sy := math.Hypot(strokeMatrix[0], strokeMatrix[1]), math.Hypot(strokeMatrix[2], strokeMatrix[3])
|
||||||
|
if math.Abs(sx-sy) > 1e-8*math.Max(1, sx) || math.Abs(strokeMatrix[0]*strokeMatrix[2]+strokeMatrix[1]*strokeMatrix[3]) > 1e-8*math.Max(1, sx*sy) {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "anisotropic text stroke"}
|
||||||
|
}
|
||||||
scale := math.Sqrt(math.Abs(m[0]*m[3] - m[1]*m[2]))
|
scale := math.Sqrt(math.Abs(m[0]*m[3] - m[1]*m[2]))
|
||||||
if scale == 0 || math.IsNaN(scale) || math.IsInf(scale, 0) {
|
if scale == 0 || math.IsNaN(scale) || math.IsInf(scale, 0) {
|
||||||
return &pdfgo.UnsupportedError{Feature: "degenerate text stroke matrix"}
|
return &pdfgo.UnsupportedError{Feature: "degenerate text stroke matrix"}
|
||||||
}
|
}
|
||||||
margin := mark.Style.LineWidth * unit / 2 * math.Max(1, mark.Style.MiterLimit)
|
lineWidth := mark.Style.LineWidth * math.Hypot(p.matrix[0], p.matrix[1]) * sx
|
||||||
|
margin := lineWidth / 2 * math.Max(1, mark.Style.MiterLimit)
|
||||||
box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin}
|
box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin}
|
||||||
object.LineWidth = mark.Style.LineWidth * unit / scale
|
object.LineWidth = lineWidth / scale
|
||||||
object.LineWidthSet = object.LineWidth == 0
|
object.LineWidthSet = object.LineWidth == 0
|
||||||
object.Join = []string{"Miter", "Round", "Bevel"}[mark.Style.Join]
|
object.Join = []string{"Miter", "Round", "Bevel"}[mark.Style.Join]
|
||||||
object.MiterLimit = mark.Style.MiterLimit
|
object.MiterLimit = mark.Style.MiterLimit
|
||||||
@@ -339,9 +380,12 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
object.Fill = &fill
|
object.Fill = &fill
|
||||||
object.Stroke = &stroke
|
object.Stroke = &stroke
|
||||||
object.Visible = &visible
|
object.Visible = &visible
|
||||||
object.FillColor, err = p.paintColor(mark.Style.Fill, box)
|
object.FillColor = p.color(mark.Style.Fill)
|
||||||
if err != nil {
|
if fill {
|
||||||
return err
|
object.FillColor, err = p.paintColor(mark.Style.Fill, box)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
object.Clips, err = p.clips(mark.Style.Clips, box)
|
object.Clips, err = p.clips(mark.Style.Clips, box)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -354,6 +398,9 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
*clipObject.Fill = true
|
*clipObject.Fill = true
|
||||||
*clipObject.Stroke = false
|
*clipObject.Stroke = false
|
||||||
clipObject.Visible = nil
|
clipObject.Visible = nil
|
||||||
|
if p.clipTexts == nil {
|
||||||
|
p.clipTexts = make(map[*pdfgo.TextClip]TextObject)
|
||||||
|
}
|
||||||
p.clipTexts[mark.Clip] = clipObject
|
p.clipTexts[mark.Clip] = clipObject
|
||||||
}
|
}
|
||||||
p.objects = append(p.objects, GraphicObject{Type: "TextObject", TextObject: object})
|
p.objects = append(p.objects, GraphicObject{Type: "TextObject", TextObject: object})
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
// 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 (
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pdfGradientSegments 将线性源颜色及设备四色的截断点表达为OFD分段
|
||||||
|
// 入参: stops 源颜色分段, space 源颜色空间
|
||||||
|
// 返回: []ShdSegment 等价RGB分段, error 不可线性表达的变换
|
||||||
|
func pdfGradientSegments(stops []pdfgo.GradientStop, space *pdfgo.ColorSpace) ([]ShdSegment, error) {
|
||||||
|
if len(stops) == 0 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
appendStop(a.Position+(b.Position-a.Position)*t, rgb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendStop(b.Position, b.RGB)
|
||||||
|
}
|
||||||
|
return segments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfGradientError 检查渐变能否精确映射为OFD线性分段
|
||||||
|
// 入参: paint 画刷
|
||||||
|
// 返回: error 不可等价表达的渐变
|
||||||
|
func pdfGradientError(paint pdfgo.Paint) error {
|
||||||
|
if paint.Axial != nil {
|
||||||
|
_, err := pdfGradientSegments(paint.Axial.Stops, paint.Axial.Space)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if paint.Radial != nil {
|
||||||
|
_, err := pdfGradientSegments(paint.Radial.Stops, paint.Radial.Space)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gradientPath 按原始函数合成渐变路径,保留填充描边的单一对象语义
|
||||||
|
// 入参: mark 路径绘制信息
|
||||||
|
// 返回: error 几何或着色错误
|
||||||
|
func (p *pdfImporter) gradientPath(mark pdfgo.PathMark) error {
|
||||||
|
return p.compositeRegion(nil, pdfCompositeNode{path: &mark}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfShadingPosition 计算渐变参数,未延伸区域及不存在非负圆半径的位置不着色
|
||||||
|
// 入参: paint 渐变画刷, point 页面坐标
|
||||||
|
// 返回: float64 归一化参数, bool 是否着色
|
||||||
|
func pdfShadingPosition(paint pdfgo.Paint, point pdfgo.Point) (float64, bool) {
|
||||||
|
if g := paint.Axial; g != nil {
|
||||||
|
dx, dy := g.End.X-g.Start.X, g.End.Y-g.Start.Y
|
||||||
|
t := ((point.X-g.Start.X)*dx + (point.Y-g.Start.Y)*dy) / (dx*dx + dy*dy)
|
||||||
|
return t, finite(t) && (t >= 0 || g.Extend[0]) && (t <= 1 || g.Extend[1])
|
||||||
|
}
|
||||||
|
g := paint.Radial
|
||||||
|
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
|
||||||
|
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
|
||||||
|
roots := [2]float64{math.NaN(), math.NaN()}
|
||||||
|
if a == 0 {
|
||||||
|
if b != 0 {
|
||||||
|
roots[0] = -c / b
|
||||||
|
}
|
||||||
|
} else if discriminant := b*b - 4*a*c; discriminant >= 0 {
|
||||||
|
q := -.5 * (b + math.Copysign(math.Sqrt(discriminant), b))
|
||||||
|
if q == 0 {
|
||||||
|
roots[0] = -b / (2 * a)
|
||||||
|
} else {
|
||||||
|
roots[0], roots[1] = q/a, c/q
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t := math.Inf(-1)
|
||||||
|
for _, root := range roots {
|
||||||
|
if finite(root) && g.StartRadius+root*dr >= 0 && (root >= 0 || g.Extend[0]) && (root <= 1 || g.Extend[1]) && root > t {
|
||||||
|
t = root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t, finite(t)
|
||||||
|
}
|
||||||
@@ -68,6 +68,13 @@ func (f canvasFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPat
|
|||||||
return *geometryFromCanvasPath(path), nil
|
return *geometryFromCanvasPath(path), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GlyphWarning 返回字形指令栈下溢后保留设计轮廓的提示,原始字体不变
|
||||||
|
// 入参: glyph 字形编号
|
||||||
|
// 返回: error 恢复原因,无恢复时为空
|
||||||
|
func (f canvasFontMetrics) GlyphWarning(glyph uint16) error {
|
||||||
|
return f.outlines.glyphWarning(glyph)
|
||||||
|
}
|
||||||
|
|
||||||
// GlyphOutlines 批量提取字形轮廓,重复编号只解析一次
|
// GlyphOutlines 批量提取字形轮廓,重复编号只解析一次
|
||||||
// 入参: glyphs 字形编号, size 毫米字号
|
// 入参: glyphs 字形编号, size 毫米字号
|
||||||
// 返回: []GeometryPath 同序只读轮廓, error 轮廓错误
|
// 返回: []GeometryPath 同序只读轮廓, error 轮廓错误
|
||||||
|
|||||||
Reference in new issue
Block a user