diff --git a/assets/webui/index.html b/assets/webui/index.html index 9ce698c..0bee732 100644 --- a/assets/webui/index.html +++ b/assets/webui/index.html @@ -51,13 +51,13 @@
- - +
diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 0b4aeb1..936913d 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -2806,7 +2806,7 @@ async function changeDocument(name, item, ...args) { const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages"; state.imageImporting = name === "ofdgoImportImages"; if (importing) state.importing = true; - setBusy(true, importing ? "正在导入" : "", null); + setBusy(true, importing ? "正在导入页面" : "", null); try { args = await Promise.all(args); if (openSeq !== state.openSeq) { @@ -3352,11 +3352,11 @@ async function loadWASM() { setProgress(data.text, data.percent); } else if (data.type === "conversion") { if (wasmRequests.get(data.id)?.openSeq === state.openSeq && !el.cancelExportButton.disabled) { - if (data.phase === "open") setProgress("正在读取 PDF", null); - else if (data.phase === "pages") setProgress(data.completed ? `正在整理 ${data.completed} 页` : "正在整理页面", null); - else if (data.phase === "convert") setProgress(`正在转换 ${data.completed} / ${data.total} 页`, data.total ? 28 + data.completed / data.total * 22 : null); + if (data.phase === "open") setProgress("正在读取文档", null); + else if (data.phase === "pages") setProgress(data.completed ? `正在整理页面 · ${data.completed} 页` : "正在整理页面", null); + else if (data.phase === "convert") setProgress(`正在转换页面 · ${data.completed} / ${data.total} 页`, data.total ? 28 + data.completed / data.total * 22 : null); else if (data.phase === "write.fonts") setProgress("正在处理字体", null); - else setProgress("正在生成 OFD", null); + else setProgress("正在生成文档", null); } } else if (data.type === "export") { if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) { @@ -3377,15 +3377,15 @@ async function loadWASM() { } } else if (data.type === "import") { if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.imageImporting) { - const text = data.phase === "commit" ? "正在完成" : `正在导入 ${data.completed} / ${data.total} 张`; + const text = data.phase === "commit" ? "正在完成导入" : `正在导入图片 · ${data.completed} / ${data.total} 张`; el.createStatus.textContent = text; setProgress(text, data.total ? data.completed / data.total * 100 : null); if (data.phase === "commit") el.createCancel.disabled = el.cancelExportButton.disabled = true; return; } if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.importing && !el.importCancel.disabled) { - const label = { ids: "正在检查", pages: "正在导入", resources: "正在读取", commit: "正在完成" }[data.phase]; - el.importStatus.textContent = data.total ? `${label} ${data.completed} / ${data.total} 页` : label; + const label = { ids: "正在检查标识", pages: "正在导入页面", resources: "正在读取资源", commit: "正在完成导入" }[data.phase]; + el.importStatus.textContent = data.total ? `${label} · ${data.completed} / ${data.total} 页` : label; setProgress(label, data.total ? data.completed / data.total * 100 : null); if (data.phase === "commit") el.importCancel.disabled = el.cancelExportButton.disabled = true; } @@ -3508,7 +3508,7 @@ async function downloadRemoteDocument(options) { if (done) break; chunks.push(value); received += value.byteLength; - setProgress(`正在下载 ${(received / 1048576).toFixed(1)} MB`, total > 0 && received <= total ? received / total * 20 : null); + setProgress(`正在下载文档 · ${(received / 1048576).toFixed(1)} MB`, total > 0 && received <= total ? received / total * 20 : null); } } finally { reader.releaseLock(); } controller.signal.throwIfAborted(); @@ -3562,7 +3562,7 @@ async function openOFD(file, remote = null) { return; } if (downloaded ? downloaded.pdf : /\.pdf$/i.test(file.name || "")) { - setProgress("正在转换 PDF", 25); + setProgress("正在转换文档", 25); await ensureWASM(); if (openSeq !== state.openSeq) return; let converted; @@ -3646,7 +3646,7 @@ async function openSelectedFonts(event) { try { const fonts = []; for (let i = 0; i < files.length; i += 1) { - setProgress(`正在读取字体 ${i + 1} / ${files.length}`, 10 + Math.round(i / files.length * 60)); + setProgress(`正在读取字体 · ${i + 1} / ${files.length}`, 10 + Math.round(i / files.length * 60)); const file = files[i]; fonts.push(fontManager.record(file.name, new Uint8Array(await file.arrayBuffer()), "upload")); } @@ -3755,7 +3755,7 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) { const emptyStatus = available.length === 0 ? "暂无系统字体" : "暂无匹配字体"; const fonts = []; for (let i = 0; i < selected.length; i += 1) { - setProgress(`正在读取字体 ${i + 1} / ${selected.length}`, 20 + Math.round(i / selected.length * 60)); + setProgress(`正在读取字体 · ${i + 1} / ${selected.length}`, 20 + Math.round(i / selected.length * 60)); const item = selected[i]; const data = await fontManager.read(item); if (openSeq !== state.openSeq) { @@ -4103,7 +4103,7 @@ async function openWithCredentials(fonts, openSeq, data = state.ofdBytes, import canceled.name = "AbortError"; throw canceled; } - setProgress(pdf ? "正在读取 PDF" : "正在解析文档", null); + setProgress(pdf ? "正在读取文档" : "正在解析文档", null); el.progressPanel.hidden = false; } } @@ -4469,7 +4469,7 @@ function renderSecurity() { function cancelSigning() { if (el.signCancel.disabled) return; state.signCanceled = true; - el.signStatus.textContent = "正在取消"; + el.signStatus.textContent = "正在取消操作"; cancelExport(); } @@ -4681,7 +4681,7 @@ async function verifyDocument(event) { event.preventDefault(); if (!state.doc || document.body.hasAttribute("aria-busy")) return; const openSeq = state.openSeq; - setBusy(true, "正在验签", null, "正在验签"); + setBusy(true, "正在验证签名", null, "正在验证签名"); el.verifyForm.inert = true; try { const options = { @@ -4733,7 +4733,7 @@ async function updateExportRange() { el.exportRangeRow.hidden = !specified; el.exportRange.removeAttribute("aria-invalid"); el.exportSubmit.disabled = specified; - el.exportRangeStatus.textContent = specified && value ? "正在校验" : ""; + el.exportRangeStatus.textContent = specified && value ? "正在校验页码" : ""; if (!specified || !value) { return; } @@ -4952,7 +4952,7 @@ function startBatchWorker() { function cancelBatch() { batch.canceled = true; batchElements.Cancel.disabled = true; - batchElements.Status.textContent = "正在取消"; + batchElements.Status.textContent = "正在取消操作"; if (batch.activeID) batch.worker.postMessage({ type: "cancel", id: batch.activeID }); } @@ -5201,12 +5201,12 @@ function cancelExport() { el.cancelExportButton.disabled = true; if (state.imageImporting) { el.createCancel.disabled = true; - el.createStatus.textContent = "正在取消"; + el.createStatus.textContent = "正在取消操作"; } else if (state.importing) { el.importCancel.disabled = true; - el.importStatus.textContent = "正在取消"; + el.importStatus.textContent = "正在取消操作"; } - setProgress("正在取消", null); + setProgress("正在取消操作", null); wasmWorker.postMessage({ type: "cancel", id: state.exportRequestID }); } @@ -5872,7 +5872,7 @@ async function selectDocumentText() { const count = state.doc.pageCount; try { for (let index = 0; index < count; index += 1) { - setStatus(`正在全选 ${index + 1} / ${count} 页`); + setStatus(`正在全选文字 · ${index + 1} / ${count} 页`); const text = await callWASM("ofdgoPageTextString", index); if (!active()) { return; @@ -6415,7 +6415,7 @@ async function searchDocument() { if (seq !== state.searchSeq || openSeq !== state.openSeq) { return; } - el.searchStatus.textContent = `正在搜索 ${page + 1} / ${pageCount} 页`; + el.searchStatus.textContent = `正在搜索文字 · ${page + 1} / ${pageCount} 页`; if (!matches?.length) { continue; } @@ -7158,7 +7158,7 @@ function renderMeta(keepDetails = false) { el.metaFonts.textContent = String(doc.fontCount || 0); el.pageTotal.textContent = String(doc.pageCount || 0); if (keepDetails && doc.detailsPending) return; - el.metaSignatures.textContent = doc.detailsPending ? "正在检查" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0); + el.metaSignatures.textContent = doc.detailsPending ? "正在检查签名" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0); renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError], renderAttachments); renderMetaContent(el.signatureList, [doc.signatures, doc.signatureError], renderSignatures); renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts); @@ -8228,7 +8228,7 @@ function exportFormatUsesDPI(value) { function updateDPIControl() { el.imageDPI.disabled = state.exporting || !state.doc || document.body.hasAttribute("aria-busy") || (displayMode() !== "raster" && !exportFormatUsesDPI(el.exportFormat.value)); - el.imageDPI.title = `DPI:${currentImageDPI()}`; + el.imageDPI.title = `输出精度:${currentImageDPI()} DPI`; el.dpiValue.textContent = String(currentImageDPI()); const format = exportFormatInfo(el.exportFormat.value); el.formatValue.textContent = format?.label || ""; diff --git a/go.mod b/go.mod index 927f393..1409ae1 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/xiaoqidun/jbig2 v1.1.1 - github.com/xiaoqidun/pdfgo v0.0.0-20260929133432-1988ce19768e + github.com/xiaoqidun/pdfgo v0.0.0-20260929145256-b04a96874771 golang.org/x/crypto v0.57.0 golang.org/x/image v0.46.0 golang.org/x/text v0.42.0 diff --git a/go.sum b/go.sum index 2464a63..3f5148e 100644 --- a/go.sum +++ b/go.sum @@ -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/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= -github.com/xiaoqidun/pdfgo v0.0.0-20260929133432-1988ce19768e h1:mO1e6Z6RgCrTdPOO7zH1w1oUmSLFRBMijkOCQAlqit4= -github.com/xiaoqidun/pdfgo v0.0.0-20260929133432-1988ce19768e/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20260929145256-b04a96874771 h1:qddSlHkgZhRMyawpOS3TlW5nu8+IurZFjxS4Kwg8YLA= +github.com/xiaoqidun/pdfgo v0.0.0-20260929145256-b04a96874771/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= 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.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/ofdgo_compression.go b/ofdgo_compression.go index 438e9ca..75f6514 100644 --- a/ofdgo_compression.go +++ b/ofdgo_compression.go @@ -63,6 +63,7 @@ type outputOptimization struct { protected bool completed int deflater *flate.Writer + optimized map[string]bool } // WriteToWithOptions 按压缩策略写出OFD,签名文档不额外改写资源内容 @@ -275,7 +276,7 @@ func (e *Editor) writeOutputEntry(archive *zip.Writer, header zip.FileHeader, da if err := editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0); err != nil { return err } - if lossy, ok := e.output.images[strings.ToLower(cleanPackagePath(header.Name))]; ok { + if lossy, ok := e.output.images[strings.ToLower(cleanPackagePath(header.Name))]; ok && !e.output.optimized[strings.ToLower(cleanPackagePath(header.Name))] { options := e.output.options if !lossy { options.Mode = CompressionLossless diff --git a/ofdgo_compression_resources.go b/ofdgo_compression_resources.go new file mode 100644 index 0000000..e8fb5a1 --- /dev/null +++ b/ofdgo_compression_resources.go @@ -0,0 +1,263 @@ +// 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 ( + "bytes" + "crypto/sha256" + "encoding/xml" + "fmt" + "io" + "maps" + "path" + "slices" + "strings" + + "github.com/xiaoqidun/pdfgo" +) + +// compressResourceParts 优化输出图片并合并相同资源文件,保留资源标识及各自属性 +// 入参: parts 输出改动, reader 原包,可为空, removed 已移除条目 +// 返回: error 读取或取消错误 +func (e *Editor) compressResourceParts(parts map[string][]byte, reader *Reader, removed map[string]bool) error { + if e.output == nil || e.output.options.Mode == CompressionUnchanged || e.output.protected { + return nil + } + names := make(map[string]bool) + if reader != nil { + for name := range reader.fileIndex { + names[name] = true + } + for name := range reader.files { + names[name] = true + } + } + for name := range parts { + names[name] = true + } + read := func(name string) ([]byte, error) { + if data, ok := parts[name]; ok { + return data, nil + } + return reader.readFile(name) + } + refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)} + var resources []string + actual := make(map[string]string) + for _, name := range slices.Sorted(maps.Keys(names)) { + if err := e.output.ctx.Err(); err != nil { + return err + } + key := strings.ToLower(cleanPackagePath(name)) + if removed[key] || strings.HasSuffix(name, "/") { + continue + } + if prior, ok := actual[key]; ok && prior != name { + return nil + } + actual[key] = name + var input io.ReadCloser + if data, ok := parts[name]; ok { + input = io.NopCloser(bytes.NewReader(data)) + } else { + var err error + input, err = reader.openFile(name) + if err != nil { + return err + } + } + resource, safe := refs.scan(input, name) + input.Close() + if !safe { + return nil + } + if resource { + resources = append(resources, name) + } + } + type resourceFile struct{ key, role string } + files := make(map[resourceFile]bool) + for _, name := range resources { + data, err := read(name) + if err != nil { + return err + } + root, err := parseEditorXML(data) + if err != nil { + return err + } + for _, group := range root.children { + for _, node := range group.children { + field := "" + if group.name.Local == "Fonts" && node.name.Local == "Font" { + field = "FontFile" + } + if group.name.Local == "MultiMedias" && node.name.Local == "MultiMedia" { + field = "MediaFile" + } + if field == "" { + continue + } + file := node.child(field) + if file == nil { + continue + } + var value string + if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil { + return err + } + key := editorResourceLocation(name, root.attr("BaseLoc"), value) + if field == "MediaFile" && node.attr("Type") != "Image" { + refs.files[key] = true + continue + } + if key != strings.ToLower(cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))) { + return nil + } + if actual[key] != "" && !refs.files[key] { + files[resourceFile{key, field}] = true + } + } + } + } + aliases := make(map[string]string) + locations := maps.Clone(actual) + reserved := make(map[string]bool, len(actual)) + for key := range actual { + reserved[key] = true + } + formats := make(map[string]string) + seen := make(map[string]map[[32]byte]string) + e.output.optimized = make(map[string]bool) + for _, key := range slices.Sorted(maps.Keys(actual)) { + if refs.files[key] || files[resourceFile{key, "FontFile"}] && files[resourceFile{key, "MediaFile"}] { + continue + } + for _, role := range []string{"FontFile", "MediaFile"} { + if !files[resourceFile{key, role}] { + continue + } + if err := editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0); err != nil { + return err + } + data, err := read(actual[key]) + if err != nil { + return err + } + if role == "MediaFile" { + options := e.output.options + if !e.output.images[key] { + options.Mode = CompressionLossless + } + candidate, err := pdfgo.OptimizeImageResource(e.output.ctx, data, options, e.output.sizes[key]) + if err != nil { + if e.output.ctx.Err() != nil { + return e.output.ctx.Err() + } + } else if len(candidate) < len(data) { + data = candidate + parts[actual[key]] = data + } + if bytes.HasPrefix(data, []byte{255, 216}) { + formats[key] = "JPEG" + ext := path.Ext(actual[key]) + if !strings.EqualFold(ext, ".jpg") && !strings.EqualFold(ext, ".jpeg") { + stem := strings.TrimSuffix(actual[key], ext) + name := stem + ".jpg" + for n := 1; reserved[strings.ToLower(name)]; n++ { + name = fmt.Sprintf("%s-%d.jpg", stem, n) + } + reserved[strings.ToLower(name)] = true + locations[key] = name + parts[name] = data + delete(parts, actual[key]) + removed[key] = true + e.output.optimized[strings.ToLower(name)] = true + } + } + e.output.optimized[key] = true + } + if seen[role] == nil { + seen[role] = make(map[[32]byte]string) + } + hash := sha256.Sum256(data) + if first, ok := seen[role][hash]; ok { + aliases[key] = first + removed[key] = true + if locations[key] != actual[key] { + delete(parts, locations[key]) + } + } else { + seen[role][hash] = key + } + } + } + for _, name := range resources { + data, err := read(name) + if err != nil { + return err + } + root, err := parseEditorXML(data) + if err != nil { + return err + } + var patches []editorXMLPatch + for _, group := range root.children { + for _, node := range group.children { + for _, field := range []string{"FontFile", "MediaFile"} { + file := node.child(field) + if file == nil { + continue + } + var value string + if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil { + return err + } + key := editorResourceLocation(name, root.attr("BaseLoc"), value) + fragment := data[node.start:node.end] + location := locations[key] + if first, ok := aliases[key]; ok { + location = locations[first] + } + if location != "" && location != actual[key] { + var escaped bytes.Buffer + xml.EscapeText(&escaped, []byte("/"+location)) + patch := editorXMLContent(data, file, escaped.Bytes()) + patch.start -= node.start + patch.end -= node.start + fragment = editorPatchXML(fragment, []editorXMLPatch{patch}) + } + if field == "MediaFile" && formats[key] != "" && node.attr("Format") != formats[key] { + parsed, err := parseEditorXML(fragment) + if err != nil { + return err + } + fragment, err = editorXMLAttribute(fragment, parsed, "Format", formats[key]) + if err != nil { + return err + } + } + if !bytes.Equal(fragment, data[node.start:node.end]) { + patches = append(patches, editorXMLPatch{start: node.start, end: node.end, data: fragment}) + } + } + } + } + if len(patches) > 0 { + parts[name] = editorPatchXML(data, patches) + } + } + return e.output.ctx.Err() +} diff --git a/ofdgo_editor_package.go b/ofdgo_editor_package.go index 3bd89c3..76164ca 100644 --- a/ofdgo_editor_package.go +++ b/ofdgo_editor_package.go @@ -450,6 +450,12 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress return 0, err } reader := e.source.reader + if removed == nil { + removed = make(map[string]bool) + } + if err := e.compressResourceParts(parts, reader, removed); err != nil { + return 0, err + } if len(parts) != 0 || len(removed) != 0 || e.output != nil && e.output.options.Mode != CompressionUnchanged && !e.output.protected { name := "OFD.xml" if file, ok := reader.packageFile(name); ok { diff --git a/ofdgo_editor_resources.go b/ofdgo_editor_resources.go index 62cb766..75d5c5b 100644 --- a/ofdgo_editor_resources.go +++ b/ofdgo_editor_resources.go @@ -40,7 +40,10 @@ type editorResourceRefs struct { // 入参: parts 已修改和新增的包内条目, progress 保存进度回调 // 返回: map[string]bool 可移除的二进制条目, error 读取错误 func (e *Editor) compactSourceResources(parts map[string][]byte, progress editorProgress) (map[string]bool, error) { - if len(parts) == 0 && e.revision == 0 { + if e.output != nil && e.output.protected { + return nil, nil + } + if len(parts) == 0 && e.revision == 0 && (e.output == nil || e.output.options.Mode == CompressionUnchanged) { return nil, nil } reader := e.source.reader diff --git a/ofdgo_renderer_svg_canvas.go b/ofdgo_renderer_svg_canvas.go index acea07a..9eccd56 100644 --- a/ofdgo_renderer_svg_canvas.go +++ b/ofdgo_renderer_svg_canvas.go @@ -89,13 +89,16 @@ func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) { resource.Data = buffer.Bytes() } if s.renderer != nil && s.renderer.Compression.Mode != CompressionUnchanged { - data, err := pdfgo.OptimizeImageSize(s.renderer.outputContext(), resource.Data, s.renderer.Compression, target) + data, err := pdfgo.OptimizeImageResource(s.renderer.outputContext(), resource.Data, s.renderer.Compression, target) if err != nil { if s.err = s.renderer.outputContext().Err(); s.err != nil { return } } else { resource.Data = data + if bytes.HasPrefix(data, []byte{255, 216}) { + resource.MIME = "image/jpeg" + } } } if s.imageNames != nil { diff --git a/ofdgo_sign_write.go b/ofdgo_sign_write.go index 68dbfe5..54acc81 100644 --- a/ofdgo_sign_write.go +++ b/ofdgo_sign_write.go @@ -28,8 +28,6 @@ import ( "strconv" "strings" "time" - - "github.com/xiaoqidun/pdfgo" ) const ( @@ -256,25 +254,31 @@ func signatureWritePackage(parts map[string][]byte, options SignatureWriteOption if err != nil { return nil, err } - seen := make(map[string]bool) - for i, img := range images { - if err := options.progress.report("compress", i, len(images)); err != nil { - return nil, err - } - if seen[img.Location] { - continue - } - seen[img.Location] = true - if data, ok := parts[img.Location]; ok { - compression := options.Compression - if masks[strings.ToLower(cleanPackagePath(img.Location))] { - compression.Mode = CompressionLossless - } - if optimized, err := pdfgo.OptimizeImageSize(context.Background(), data, compression, sizes[strings.ToLower(cleanPackagePath(img.Location))]); err == nil { - parts[img.Location] = optimized - } + optimizer := &Editor{output: &outputOptimization{ctx: context.Background(), options: options.Compression, sizes: sizes, images: make(map[string]bool)}} + optimizer.OnWriteProgress = func(stage string, completed, total int) error { + return options.progress.report(stage, completed, total) + } + for _, img := range images { + key := strings.ToLower(cleanPackagePath(img.Location)) + optimizer.output.images[key] = !masks[key] + } + removed := make(map[string]bool) + if err := optimizer.compressResourceParts(parts, nil, removed); err != nil { + return nil, err + } + for name := range parts { + if removed[strings.ToLower(cleanPackagePath(name))] { + delete(parts, name) } } + r = &Reader{files: parts} + if err := r.initRoot(); err != nil { + return nil, err + } + doc, err = r.Doc() + if err != nil { + return nil, err + } } if options.Time.IsZero() { options.Time = time.Now() diff --git a/ofdgo_writer.go b/ofdgo_writer.go index 66ac58c..a8b7516 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -169,6 +169,33 @@ func (e *Editor) writePlaintext(writer io.Writer) (int64, error) { } output := &ofdCountingWriter{writer: writer} archive := zip.NewWriter(output) + if e.output != nil && e.output.options.Mode != CompressionUnchanged { + parts := make(map[string][]byte) + err := e.writeParts(func(name string, data []byte, _ bool) error { + if subset, ok := fonts[name]; ok { + data = subset + } + parts[name] = data + return nil + }, progress) + if err != nil { + return 0, err + } + removed := make(map[string]bool) + if err := e.compressResourceParts(parts, nil, removed); err != nil { + return 0, err + } + for _, name := range slices.Sorted(maps.Keys(parts)) { + if removed[strings.ToLower(cleanPackagePath(name))] { + continue + } + if err := e.writeOutputEntry(archive, zip.FileHeader{Name: name, Method: zip.Deflate}, parts[name]); err != nil { + return output.count, err + } + } + err = archive.Close() + return output.count, err + } err = e.writeParts(func(name string, data []byte, compressed bool) error { if subset, ok := fonts[name]; ok { data = subset