feat(代码优化): 修复小问题和升级jbig2
build.yaml / build (push) Successful in 36s

This commit is contained in:
xiaoqidun committed 2026-09-07 14:48:56 +08:00
1 parent 0322995771
commit 677396c71d
12 files changed
+377 -64

No files matched your search

+78 -15
View File
@@ -30,6 +30,8 @@ const STATUS = {
};
const WASM_CALLBACKS = [
"ofdgoOpen",
"ofdgoConfigure",
"ofdgoDocumentInfo",
"ofdgoRenderPage",
"ofdgoExportFormats",
"ofdgoExportPage",
@@ -66,10 +68,12 @@ const state = {
pageRenderQueue: [],
pageRenderRunning: false,
pageObserver: null,
visiblePages: new Set(),
scrollFrame: 0,
thumbnailCache: new Map(),
thumbnailInFlight: new Set(),
thumbnailObserver: null,
visibleThumbnails: new Set(),
exportFormats: [],
showPages: !COMPACT_LAYOUT.matches,
showMeta: !COMPACT_LAYOUT.matches,
@@ -725,6 +729,8 @@ async function applyFontChange(options = {}) {
pageIndex: state.pageIndex,
fitMode: state.fitMode,
skipAutoFonts: options.skipAutoFonts !== false,
reuseSession: true,
fontsChanged: true,
});
}
@@ -738,6 +744,7 @@ async function toggleAnnotations() {
pageIndex: state.pageIndex,
fitMode: state.fitMode,
skipAutoFonts: true,
reuseSession: true,
});
}
@@ -884,7 +891,10 @@ async function openDocument(options = {}) {
if (openSeq !== state.openSeq) {
return;
}
const doc = callWASM("ofdgoOpen", state.ofdBytes, resetLocalFonts ? uploadedFonts() : allFonts(), state.renderAnnotations);
const fonts = resetLocalFonts ? uploadedFonts() : allFonts();
const doc = options.reuseSession
? callWASM("ofdgoConfigure", options.fontsChanged ? fonts : null, state.renderAnnotations)
: callWASM("ofdgoOpen", state.ofdBytes, fonts, state.renderAnnotations);
if (openSeq !== state.openSeq) {
return;
}
@@ -904,6 +914,8 @@ async function openDocument(options = {}) {
resetScroll: options.resetScroll,
skipAutoFonts: true,
openSeq,
reuseSession: true,
fontsChanged: true,
});
return;
}
@@ -926,6 +938,7 @@ async function openDocument(options = {}) {
await nextFrame();
await renderPage(pageIndex, { keepBusy: true, scroll: false, openSeq });
queueNearbyPages(pageIndex, openSeq);
loadDocumentDetails(openSeq);
} catch (err) {
if (openSeq !== state.openSeq) {
return;
@@ -938,6 +951,24 @@ async function openDocument(options = {}) {
}
}
async function loadDocumentDetails(openSeq) {
await waitForPaint();
if (openSeq !== state.openSeq) {
return;
}
try {
const info = callWASM("ofdgoDocumentInfo");
if (openSeq === state.openSeq) {
Object.assign(state.doc, info, { detailsPending: false });
renderMeta();
}
} catch (err) {
if (openSeq === state.openSeq) {
showError(err, false);
}
}
}
async function renderPage(index, options = {}) {
if (!state.doc) {
return;
@@ -1100,6 +1131,7 @@ function renderPageFlow() {
function resetPageFlow() {
state.pageCache.clear();
state.visiblePages.clear();
for (const task of state.pageInFlight.values()) {
task.resolve(null);
}
@@ -1116,7 +1148,6 @@ function observeFlowPage(shell, index) {
const openSeq = state.openSeq;
if (index < 4 && index !== state.pageIndex) {
renderFlowPage(index, { openSeq, priority: 3 });
return;
}
const observer = flowPageObserver();
if (observer) {
@@ -1131,12 +1162,20 @@ function flowPageObserver() {
if (!state.pageObserver) {
const openSeq = state.openSeq;
state.pageObserver = new IntersectionObserver((entries) => {
if (openSeq !== state.openSeq) {
return;
}
for (const entry of entries) {
const index = Number.parseInt(entry.target.dataset.pageIndex, 10);
if (!entry.isIntersecting) {
state.visiblePages.delete(index);
if (entry.target.classList.contains("rendered")) {
entry.target.querySelector(".page-surface").replaceChildren();
entry.target.classList.remove("rendered");
}
continue;
}
const index = Number.parseInt(entry.target.dataset.pageIndex, 10);
state.pageObserver.unobserve(entry.target);
state.visiblePages.add(index);
renderFlowPage(index, { openSeq, priority: 4 });
}
}, {
@@ -1153,13 +1192,13 @@ async function renderFlowPage(index, options = {}) {
return null;
}
try {
const page = await loadPageData(index, { openSeq, priority: options.priority || 2 });
const page = await loadPageData(index, { openSeq, priority: options.priority ?? 2 });
if (openSeq !== state.openSeq) {
return null;
}
if (page) {
const shell = pageShell(index);
if (!shell?.classList.contains("rendered")) {
if (!shell?.classList.contains("rendered") && (!state.pageObserver || state.visiblePages.has(index) || index === state.pageIndex)) {
mountPageSVG(index, page, openSeq);
}
updateThumbnail(index, openSeq);
@@ -1185,7 +1224,7 @@ function loadPageData(index, options = {}) {
return Promise.resolve(state.pageCache.get(index));
}
const key = `${openSeq}:${index}`;
const priority = options.priority || 3;
const priority = options.priority ?? 3;
const current = state.pageInFlight.get(key);
if (current) {
current.priority = Math.min(current.priority, priority);
@@ -1526,6 +1565,7 @@ function renderPageList() {
function resetThumbnails() {
state.thumbnailCache.clear();
state.visibleThumbnails.clear();
state.thumbnailInFlight.clear();
if (state.thumbnailObserver) {
state.thumbnailObserver.disconnect();
@@ -1540,6 +1580,11 @@ function cacheThumbnail(index, svgText) {
}
function setThumbnailContent(container, svgText, index, openSeq = state.openSeq) {
const renderKey = `${openSeq}:${index}`;
if (svgText && container.dataset.renderKey === renderKey) {
return;
}
delete container.dataset.renderKey;
container.replaceChildren();
if (!svgText) {
container.classList.add("pending");
@@ -1552,6 +1597,7 @@ function setThumbnailContent(container, svgText, index, openSeq = state.openSeq)
svg.setAttribute("aria-hidden", "true");
container.classList.remove("pending", "error");
container.append(svg);
container.dataset.renderKey = renderKey;
} catch {
container.classList.add("error");
container.textContent = String(index + 1);
@@ -1559,16 +1605,15 @@ function setThumbnailContent(container, svgText, index, openSeq = state.openSeq)
}
function observeThumbnail(button, index, openSeq = state.openSeq) {
if (state.thumbnailCache.has(index)) {
return;
const observer = thumbnailObserver();
if (observer) {
observer.observe(button);
}
if (index === state.pageIndex || index < 6) {
renderThumbnail(index, openSeq);
return;
}
const observer = thumbnailObserver();
if (observer) {
observer.observe(button);
return;
}
if (index < 8) {
@@ -1583,12 +1628,21 @@ function thumbnailObserver() {
if (!state.thumbnailObserver) {
const openSeq = state.openSeq;
state.thumbnailObserver = new IntersectionObserver((entries) => {
if (openSeq !== state.openSeq) {
return;
}
for (const entry of entries) {
const index = Number.parseInt(entry.target.dataset.pageIndex, 10);
if (!entry.isIntersecting) {
state.visibleThumbnails.delete(index);
const thumb = entry.target.querySelector(".thumb-paper");
if (thumb.dataset.renderKey) {
setThumbnailContent(thumb, "", index, openSeq);
}
continue;
}
state.thumbnailObserver.unobserve(entry.target);
renderThumbnail(Number.parseInt(entry.target.dataset.pageIndex, 10), openSeq);
state.visibleThumbnails.add(index);
renderThumbnail(index, openSeq);
}
}, {
root: el.pageListPanel,
@@ -1599,7 +1653,11 @@ function thumbnailObserver() {
}
async function renderThumbnail(index, openSeq = state.openSeq) {
if (openSeq !== state.openSeq || !state.doc || state.thumbnailCache.has(index)) {
if (openSeq !== state.openSeq || !state.doc) {
return;
}
if (state.thumbnailCache.has(index)) {
updateThumbnail(index, openSeq);
return;
}
const key = `${openSeq}:${index}`;
@@ -1626,6 +1684,9 @@ async function renderThumbnail(index, openSeq = state.openSeq) {
}
function updateThumbnail(index, openSeq = state.openSeq) {
if (openSeq !== state.openSeq || (state.thumbnailObserver && !state.visibleThumbnails.has(index) && index !== state.pageIndex)) {
return;
}
const thumb = el.pageList.querySelector(`[data-page-index="${index}"] .thumb-paper`);
if (thumb) {
setThumbnailContent(thumb, state.thumbnailCache.get(index), index, openSeq);
@@ -1647,7 +1708,7 @@ function renderMeta() {
el.metaVersion.textContent = doc.version || "-";
el.metaType.textContent = doc.docType || "-";
el.metaFonts.textContent = String(doc.fontCount || 0);
el.metaSignatures.textContent = String(doc.signatureCount || 0);
el.metaSignatures.textContent = doc.detailsPending ? "正在检查" : String(doc.signatureCount || 0);
el.pageTotal.textContent = String(doc.pageCount || 0);
renderSignatures();
renderDocumentFonts();
@@ -1948,6 +2009,8 @@ function pageStatus(index, pageCount) {
function statusText(status) {
switch (status) {
case "pending":
return "待检查";
case "embedded":
return "内嵌";
case "matched":
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.26.0
require (
github.com/tdewolff/canvas v0.0.0-20260901160717-dae8cd8e19a7
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059
github.com/xiaoqidun/jbig2 v0.0.0-20260907061036-a2fe11801f67
golang.org/x/image v0.45.0
golang.org/x/text v0.41.0
)
+2 -2
View File
@@ -71,8 +71,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059 h1:/JXxWbNTePSVfshS7BVRm75Kh1/5rixvLpgTRw7MMv8=
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059/go.mod h1:mdoUQnVPIA4yS0/+ZrtxJNiDdbphI0RMu+DPfxXfVhE=
github.com/xiaoqidun/jbig2 v0.0.0-20260907061036-a2fe11801f67 h1:tUpMiGYu6SQ8W1aHxAc/NGMmd0N0IqfvcaoE14q3WeQ=
github.com/xiaoqidun/jbig2 v0.0.0-20260907061036-a2fe11801f67/go.mod h1:mdoUQnVPIA4yS0/+ZrtxJNiDdbphI0RMu+DPfxXfVhE=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+32
View File
@@ -40,6 +40,8 @@ type apiResult struct {
// RunWASM 注册浏览器WASM接口并阻塞运行
func RunWASM() {
registerCallback("ofdgoOpen", openDocument)
registerCallback("ofdgoConfigure", configureDocument)
registerCallback("ofdgoDocumentInfo", documentInfo)
registerCallback("ofdgoRenderPage", renderPage)
registerCallback("ofdgoExportFormats", exportFormats)
registerCallback("ofdgoExportPage", exportPage)
@@ -102,6 +104,36 @@ func openDocument(args []js.Value) (any, error) {
return nil, err
}
currentSession = session
return currentSession.Summary(), nil
}
// configureDocument 更新当前会话的字体和注解设置
// 入参: args 浏览器参数
// 返回: any 文档信息, error 错误信息
func configureDocument(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
if !args[0].IsNull() && !args[0].IsUndefined() {
fonts, err := fontsFromJS(args[0])
if err != nil {
return nil, err
}
if err := currentSession.SetFonts(fonts); err != nil {
return nil, err
}
}
currentSession.Renderer.RenderAnnotations = args[1].Bool()
return currentSession.Summary(), nil
}
// documentInfo 获取首屏之后加载的字体统计和验签结果
// 入参: args 浏览器参数
// 返回: any 文档信息, error 错误信息
func documentInfo(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
return currentSession.Info(), nil
}
+87 -7
View File
@@ -94,11 +94,14 @@ type SignatureStampInfo struct {
// Session WebUI文档会话
type Session struct {
Reader *ofdgo.Reader
Renderer *ofdgo.Renderer
doc *ofdgo.Document
pageCache map[int]*ofdgo.PageContent
boxCache map[int]ofdgo.Box
Reader *ofdgo.Reader
Renderer *ofdgo.Renderer
doc *ofdgo.Document
pageCache map[int]*ofdgo.PageContent
boxCache map[int]ofdgo.Box
signatures []SignatureInfo
signatureError error
signaturesRead bool
}
// DocumentInfo 文档信息
@@ -117,6 +120,7 @@ type DocumentInfo struct {
Fonts []FontInfo `json:"fonts"`
Signatures []SignatureInfo `json:"signatures"`
Pages []PageInfo `json:"pages"`
DetailsPending bool `json:"detailsPending,omitempty"`
}
// PageInfo 页面信息
@@ -222,7 +226,69 @@ func (s *Session) Close() error {
return s.Reader.Close()
}
// Info 获取文档信息
// SetFonts 更新字体配置并保留文档、页面和验签结果
// 入参: fonts 字体文件列表
// 返回: error 错误信息
func (s *Session) SetFonts(fonts []FontFile) error {
opts := []ofdgo.RendererOption{
ofdgo.WithDPI(s.Renderer.DPI),
ofdgo.WithAnnotations(s.Renderer.RenderAnnotations),
}
if len(fonts) > 0 {
fontFS := ofdgo.NewFontFS(fonts)
if fontFS.Len() == 0 {
return fmt.Errorf("invalid font file")
}
opts = append(opts, ofdgo.WithFontFS(fontFS))
}
s.Renderer = ofdgo.NewRenderer(s.Reader, opts...)
return nil
}
// Summary 获取首屏所需信息,不解析页面图元或执行验签
// 返回: DocumentInfo 文档信息
func (s *Session) Summary() DocumentInfo {
info := DocumentInfo{
Version: s.Reader.Version(),
DocType: s.Reader.DocType(),
PageCount: len(s.doc.Pages.Page),
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
DetailsPending: true,
}
if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil {
info.Title = docInfo.Title
info.Author = docInfo.Author
info.Subject = docInfo.Subject
info.CreationDate = docInfo.CreationDate
info.ModDate = docInfo.ModDate
}
for index, page := range s.doc.Pages.Page {
box, ok := s.boxCache[index]
if !ok {
if area, err := s.Reader.PageArea(page); err == nil {
box, _ = s.pageBox(index, &ofdgo.PageContent{Area: area})
}
}
info.Pages = append(info.Pages, PageInfo{Index: index, ID: page.ID, Width: box.W, Height: box.H})
}
if fonts, err := s.Reader.Fonts(); err == nil {
for _, font := range fonts {
info.Fonts = append(info.Fonts, FontInfo{
ID: font.ID,
FontName: font.FontName,
FamilyName: font.FamilyName,
Charset: font.Charset,
FontFile: font.FontFile,
Embedded: font.FontFile != "",
Status: "pending",
})
}
}
info.FontCount = len(info.Fonts)
return info
}
// Info 获取完整文档信息
// 返回: DocumentInfo 文档信息
func (s *Session) Info() DocumentInfo {
info := DocumentInfo{
@@ -339,14 +405,20 @@ func (s *Session) renderPageImage(page *ofdgo.PageContent, dpi float64) (image.I
// signatureInfos 获取签名验证信息
// 返回: []SignatureInfo 签名验证信息, error 错误信息
func (s *Session) signatureInfos() ([]SignatureInfo, error) {
if s.signaturesRead {
return s.signatures, s.signatureError
}
s.signaturesRead = true
reports, err := s.Reader.VerifySignatures()
if err != nil {
s.signatureError = err
return nil, err
}
infos := make([]SignatureInfo, 0, len(reports))
for _, report := range reports {
infos = append(infos, signatureInfo(report))
}
s.signatures = infos
return infos, nil
}
@@ -464,7 +536,15 @@ func (s *Session) ExportPDF() ([]byte, error) {
return nil, fmt.Errorf("ofd document is not opened")
}
var buf bytes.Buffer
if err := s.Renderer.RenderToMultiPagePDF(&buf); err != nil {
pages := make([]*ofdgo.PageContent, len(s.doc.Pages.Page))
for i := range pages {
_, page, err := s.pageContent(i)
if err != nil {
return nil, err
}
pages[i] = page
}
if err := s.Renderer.RenderPagesToPDF(pages, &buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
+2
View File
@@ -17,6 +17,7 @@ package ofdgo
import (
"archive/zip"
"image"
"io"
"io/fs"
@@ -79,6 +80,7 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
fontDirCandidates: make(map[string][]fontFileCandidate),
textGlyphPathCache: make(map[textGlyphPathCacheKey]textGlyphPathCacheValue),
templatePageCache: make(map[string]*PageContent),
imageCache: make(map[string]image.Image),
}
for _, opt := range opts {
opt(r)
+31
View File
@@ -248,6 +248,37 @@ func (r *Reader) PageContent(page Page) (*PageContent, error) {
return &content, nil
}
// PageArea 读取页面区域,不解析页面图元
// 入参: page 页面对象
// 返回: PageArea 页面区域, error 错误信息
func (r *Reader) PageArea(page Page) (PageArea, error) {
f, err := r.openFile(r.ResPath(page.BaseLoc))
if err != nil {
return PageArea{}, err
}
defer f.Close()
d := xml.NewDecoder(f)
for {
token, err := d.Token()
if err == io.EOF {
return PageArea{}, nil
}
if err != nil {
return PageArea{}, err
}
if start, ok := token.(xml.StartElement); ok {
switch start.Name.Local {
case "Area":
var area PageArea
err := d.DecodeElement(&area, &start)
return area, err
case "Content":
return PageArea{}, nil
}
}
}
}
// ResPath 获取资源的完整路径
// 入参: resLink 资源链接
// 返回: string 完整路径
+2
View File
@@ -16,6 +16,7 @@ package ofdgo
import (
"fmt"
"image"
"io/fs"
"github.com/tdewolff/canvas"
@@ -39,6 +40,7 @@ type Renderer struct {
fontDirCandidates map[string][]fontFileCandidate
textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue
templatePageCache map[string]*PageContent
imageCache map[string]image.Image
fontDirs []string
fontFS []fs.FS
decodeImages bool
+26 -7
View File
@@ -188,7 +188,32 @@ func imageWithClip(img image.Image, clipPath *canvas.Path, m canvas.Matrix) imag
// 入参: resPath 图片资源路径
// 返回: image.Image 图片对象, error 错误信息
func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
rc, err := r.Reader.openFile(r.Reader.ResPath(resPath))
resPath = cleanPackagePath(r.Reader.ResPath(resPath))
img, ok := r.imageCache[resPath]
if !ok {
var err error
img, err = r.readImageResource(resPath)
if err != nil {
return nil, err
}
if r.imageCache == nil {
r.imageCache = make(map[string]image.Image)
}
r.imageCache[resPath] = img
}
if r.decodeImages {
if source, ok := img.(*canvasimage.Image); ok {
return source.Image()
}
}
return img, nil
}
// readImageResource 读取图片资源并保留可直接嵌入的原始编码
// 入参: resPath 图片资源路径
// 返回: image.Image 图片对象, error 错误信息
func (r *Renderer) readImageResource(resPath string) (image.Image, error) {
rc, err := r.Reader.openFile(resPath)
if err != nil {
return nil, err
}
@@ -200,9 +225,6 @@ func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
if err != nil {
return nil, err
}
if r.decodeImages {
return img.Image()
}
return img, nil
}
if isPNGData(header) {
@@ -210,9 +232,6 @@ func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
if err != nil {
return nil, err
}
if r.decodeImages {
return img.Image()
}
return img, nil
}
data, err := io.ReadAll(reader)
+13 -5
View File
@@ -95,12 +95,20 @@ func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu Composite
return
}
ctx.Push()
currentCTM := NewMatrix(cgu.CTM)
if parentCTM != nil {
currentCTM = parentCTM.Multiply(currentCTM)
}
box, _ := ParseBox(cgu.Boundary)
clipPath := intersectClipPath(parentClip, r.buildClipPath(cgu.Clips, pageH, box.X, box.Y, currentCTM))
boundaryCTM := TranslationMatrix(box.X, box.Y)
if parentCTM != nil {
boundaryCTM = parentCTM.Multiply(boundaryCTM)
}
currentCTM := boundaryCTM.Multiply(NewMatrix(cgu.CTM))
clips, clipCTM := cgu.Clips, currentCTM
if clips != nil && clips.TransFlag != nil && !*clips.TransFlag {
// 不参与对象CTM的裁剪仍位于对象边界及父级坐标系中
clipCopy := *clips
clipCopy.TransFlag = nil
clips, clipCTM = &clipCopy, boundaryCTM
}
clipPath := intersectClipPath(parentClip, r.buildClipPath(clips, pageH, 0, 0, clipCTM))
if cgu.ResourceID != "" {
if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok {
refCopy := *ref
+44 -25
View File
@@ -67,26 +67,12 @@ func replacePDFProducer(data []byte) []byte {
// 入参: page 页面内容, writer 输出流
// 返回: error 错误信息
func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error {
c, err := r.renderPage(page)
if err != nil {
return err
}
box, err := r.GetPageBox(page)
if err != nil {
return err
}
pages := []pdfPage{{Content: page, Box: box}}
navigation := newPDFNavigation(r, r.Reader.doc, pages)
var buf bytes.Buffer
p := pdf.New(&buf, c.W, c.H, nil)
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
navigation.apply(p, 0)
c.RenderTo(p)
if err := p.Close(); err != nil {
return err
}
_, err = writer.Write(replacePDFProducer(buf.Bytes()))
return err
return r.renderPDFPages(pages, writer)
}
// RenderToEPS 渲染为EPS
@@ -123,24 +109,57 @@ func (r *Renderer) RenderToMultiPagePDF(writer io.Writer) error {
}
pages[i] = pdfPage{Content: page, Box: box}
}
navigation := newPDFNavigation(r, doc, pages)
var buf bytes.Buffer
p := pdf.New(&buf, pages[0].Box.W, pages[0].Box.H, nil)
return r.renderPDFPages(pages, writer)
}
// RenderPagesToPDF 按指定顺序导出已解析的页面,复用调用方的页面缓存
// 入参: contents 页面内容列表, writer 输出流
// 返回: error 错误信息
func (r *Renderer) RenderPagesToPDF(contents []*PageContent, writer io.Writer) error {
if len(contents) == 0 {
return fmt.Errorf("no pages found")
}
pages := make([]pdfPage, len(contents))
for i, page := range contents {
box, err := r.GetPageBox(page)
if err != nil {
return fmt.Errorf("failed to read page %d area: %w", i+1, err)
}
pages[i] = pdfPage{Content: page, Box: box}
}
return r.renderPDFPages(pages, writer)
}
// renderPDFPages 渲染页面并复用调用方的字节缓冲
// 入参: pages 页面列表, writer 输出流
// 返回: error 错误信息
func (r *Renderer) renderPDFPages(pages []pdfPage, writer io.Writer) error {
navigation := newPDFNavigation(r, r.Reader.doc, pages)
buf, direct := writer.(*bytes.Buffer)
if !direct {
buf = &bytes.Buffer{}
}
start := buf.Len()
p := pdf.New(buf, pages[0].Box.W, pages[0].Box.H, nil)
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
for i, page := range pages {
c, err := r.renderPage(page.Content)
if err != nil {
return fmt.Errorf("failed to render page %d: %w", i+1, err)
}
if i > 0 {
p.NewPage(c.W, c.H)
p.NewPage(page.Box.W, page.Box.H)
}
navigation.apply(p, i)
c.RenderTo(p)
if err := r.renderPageToContext(canvas.NewContext(p), page.Content, true); err != nil {
buf.Truncate(start)
return fmt.Errorf("failed to render page %d: %w", i+1, err)
}
}
if err := p.Close(); err != nil {
buf.Truncate(start)
return err
}
_, err = writer.Write(replacePDFProducer(buf.Bytes()))
data := replacePDFProducer(buf.Bytes()[start:])
if direct {
return nil
}
_, err := writer.Write(data)
return err
}
+59 -2
View File
@@ -29,6 +29,11 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if obj.Visible != nil && !*obj.Visible {
return
}
shouldFill := obj.Fill == nil || *obj.Fill
shouldStroke := obj.Stroke != nil && *obj.Stroke
if !shouldFill && !shouldStroke {
return
}
ctx.Push()
bx, by := 0.0, 0.0
if obj.Boundary != "" {
@@ -84,6 +89,22 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if fillPaint == nil {
fillPaint = fillColor
}
strokeStyle := newPathStyle(nil, defaultStroke, 0, obj.Alpha)
if shouldStroke {
if dp != nil {
strokeStyle.applyDrawParam(dp, bx, by, pageH, obj.Alpha)
}
if obj.StrokeColor != nil {
strokeStyle.applyStrokeColor(obj.StrokeColor, bx, by, pageH, obj.Alpha)
}
if obj.LineWidth > 0 {
strokeStyle.lineWidth = obj.LineWidth
}
if strokeStyle.strokePaint == nil {
strokeStyle.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha)
}
strokeStyle.scale(ctm)
}
fontStyle := canvas.FontRegular
weight := obj.Weight
if weight == 0 && dp != nil && dp.Weight > 0 {
@@ -136,8 +157,9 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
}
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY)
xs, ys := parseFloats(tc.X), parseFloats(tc.Y)
drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys) || clipPath != nil
drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys) || clipPath != nil || shouldStroke
cx, cy := 0.0, 0.0
previousWidth := 0.0
if len(xs) > 0 {
cx = xs[0]
}
@@ -160,9 +182,10 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if dx, ok := textDelta(dxs, i-1); ok {
cx += dx
} else if len(dys) == 0 {
cx += glyphWidth * hScale
cx += previousWidth * hScale
}
}
previousWidth = glyphWidth
if i < len(ys) {
cy = ys[i]
} else if i > 0 {
@@ -185,6 +208,40 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY)
}
advanceLimit := textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx)
if shouldStroke {
scaleX := hScale
if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
scaleX = advanceLimit / glyphWidth
}
transform := canvas.Identity.Translate(canvasX, canvasY)
if useTextMatrix {
transform = transform.Mul(textMatrix(ctm))
}
path := glyphPath.Copy().Transform(transform.Scale(scaleX, 1))
if shouldFill && glyphFillPaint != nil {
ctx.SetFill(glyphFillPaint)
ctx.SetStrokeColor(canvas.Transparent)
ctx.DrawPath(0, 0, applyClipPath(path.Copy(), clipPath))
}
if len(strokeStyle.dashPattern) > 0 {
path = path.Dash(strokeStyle.dashOffset, strokeStyle.dashPattern...)
}
path = path.Stroke(strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin, canvas.Tolerance)
ctx.SetFill(strokeStyle.strokePaint)
ctx.SetStrokeColor(canvas.Transparent)
ctx.DrawPath(0, 0, applyClipPath(path, clipPath))
if hasUnderline {
underline := &canvas.Path{}
underline.MoveTo(0, -sizeMM*0.1)
underline.LineTo(glyphWidth*scaleX, -sizeMM*0.1)
underline = underline.Stroke(sizeMM*0.05, canvas.ButtCap, canvas.MiterJoin, canvas.Tolerance)
if shouldFill {
ctx.SetFillColor(fillColor)
}
ctx.DrawPath(0, 0, applyClipPath(underline.Transform(transform), clipPath))
}
continue
}
if glyphFillPaint != nil {
ctx.SetFill(glyphFillPaint)
if clipPath != nil {