mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
0e423c9713
commit
7bdb03f7bd
12 files changed
+300
-49
No files matched your search
+10
-6
@@ -752,8 +752,8 @@ resourceElements.Locate.addEventListener("click", () => {
|
||||
resourceElements.Add.addEventListener("click", () => {
|
||||
if (!state.editing) return;
|
||||
const kind = resourceElements.Kind.value;
|
||||
if (kind === "Font") { resourceElements.Panel.close(); openFontFile(el.fontInput); }
|
||||
else if (kind === "Image") { resourceElements.Panel.close(); openInsertPanel(); }
|
||||
if (kind === "Font") openFontFile(el.fontInput);
|
||||
else if (kind === "Image") openInsertPanel();
|
||||
else if (kind === "Attachment") openAttachmentDialog("add");
|
||||
else chooseResourceFile("add");
|
||||
});
|
||||
@@ -3144,6 +3144,7 @@ async function insertObject(event) {
|
||||
return;
|
||||
}
|
||||
el.insertPanel.close();
|
||||
if (!item && resourceElements.Panel.open) resourceElements.Panel.close();
|
||||
if (item && doc.revision === state.editorInfo?.revision) {
|
||||
canvasEditor.focus();
|
||||
return;
|
||||
@@ -4334,6 +4335,7 @@ async function openSelectedFonts(event) {
|
||||
const { saved, changed } = await fontManager.add(fonts);
|
||||
if (changed) {
|
||||
await applyFontChange();
|
||||
if (resourceElements.Panel.open) await resourceTask(loadManagedResources);
|
||||
}
|
||||
setStatus(saved ? "字体已保存" : "字体未保存,仅本次可用");
|
||||
} catch (err) {
|
||||
@@ -5098,6 +5100,8 @@ function resourceError(message) {
|
||||
if (/still referenced/.test(message)) return "资源仍被引用";
|
||||
if (/unsupported content/.test(message)) return "引用无法确认";
|
||||
if (/empty media|nonempty Audio/.test(message)) return "文件内容为空";
|
||||
if (/image mask .*dimensions differ from source/.test(message)) return "蒙版尺寸不符";
|
||||
if (/image mask contains non-binary pixel/.test(message)) return "蒙版像素不符";
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -5158,7 +5162,7 @@ function selectManagedResource(item) {
|
||||
function updateResourceControls() {
|
||||
const item = resourceManager.selected, editing = state.editing;
|
||||
const disabled = resourceManager.busy || !state.doc || document.body.hasAttribute("aria-busy");
|
||||
const media = item && ["Audio", "Video"].includes(item.kind);
|
||||
const actionable = item && ["Audio", "Video", "Attachment"].includes(item.kind);
|
||||
resourceElements.Add.hidden = resourceElements.Replace.hidden = resourceElements.Delete.hidden = !editing;
|
||||
resourceElements.Rename.hidden = !editing || item?.kind !== "Attachment";
|
||||
resourceElements.Rename.disabled = disabled || item?.kind !== "Attachment";
|
||||
@@ -5171,8 +5175,8 @@ function updateResourceControls() {
|
||||
resourceElements.Delete.disabled = disabled || !item || item.kind !== "Attachment" && (!item.complete || Boolean(item.references?.length));
|
||||
resourceElements.Delete.title = item?.kind === "Attachment" ? "删除附件" : item?.references?.length ? "资源仍被引用" : "删除资源";
|
||||
resourceElements.Locate.disabled = disabled || !(item?.references?.[Number(resourceElements.Reference.value)]?.page >= 0);
|
||||
resourceElements.Binding.hidden = !editing || !media;
|
||||
resourceElements.Bind.disabled = resourceElements.Unbind.disabled = disabled || !media || !resourceElements.Object.value;
|
||||
resourceElements.Binding.hidden = !editing || !actionable;
|
||||
resourceElements.Bind.disabled = resourceElements.Unbind.disabled = disabled || !actionable || !resourceElements.Object.value;
|
||||
}
|
||||
|
||||
async function loadResourceTargets() {
|
||||
@@ -5198,7 +5202,7 @@ async function changeManagedResource(action, item, file) {
|
||||
if (!item || !state.editing) return;
|
||||
releaseResourcePreview();
|
||||
const format = file?.name.includes(".") ? file.name.split(".").at(-1).toUpperCase() : "";
|
||||
const changed = item.kind === "Attachment"
|
||||
const changed = item.kind === "Attachment" && action !== "bind" && action !== "unbind"
|
||||
? await changeDocument("ofdgoChangeAttachment", null, action, item.id, file ? [file.name] : [], file ? [file] : [])
|
||||
: await changeDocument("ofdgoChangeResource", null, action, item.kind, item.id, format, file || null, Number(resourceElements.Page.value), resourceElements.Object.value);
|
||||
if (changed) {
|
||||
|
||||
@@ -842,6 +842,12 @@ func changeResource(args []js.Value) (any, error) {
|
||||
case "delete":
|
||||
return currentEditor.DeleteResource(ctx, kind, id)
|
||||
case "bind", "unbind":
|
||||
if kind == "Attachment" {
|
||||
return currentEditor.BindAttachment(ctx, id, args[5].Int(), args[6].String(), action == "bind")
|
||||
}
|
||||
if kind != "Audio" && kind != "Video" {
|
||||
return fmt.Errorf("resource does not support actions")
|
||||
}
|
||||
return currentEditor.BindMedia(ctx, id, args[5].Int(), args[6].String(), action == "bind")
|
||||
case "add", "replace":
|
||||
data, err := bytesFromJS(args[4])
|
||||
|
||||
+7
-4
@@ -665,6 +665,7 @@ func (e *Editor) TransformObject(page int, id string, dx, dy, scale float64) err
|
||||
|
||||
// ReplaceImage 替换图片资源,保留对象标识和绘制顺序,一次撤销恢复资源及布局
|
||||
// 更换主图资源时清除旧替换图引用,保留蒙版及其他绘制属性
|
||||
// 完整校验图片像素,失败时不修改文档或注册新资源
|
||||
// 入参: page 页面索引, id 图片对象标识, data AddImage支持的图片数据, fit 为contain、cover或空,空值保留原变换和裁剪
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) error {
|
||||
@@ -675,16 +676,17 @@ func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) erro
|
||||
if object.Type != "ImageObject" {
|
||||
return fmt.Errorf("object %q is not an image", id)
|
||||
}
|
||||
if object.ImageObject.ImageMask != "" {
|
||||
config, _, err := decodeImageConfigData(data)
|
||||
decoded, _, err := decodeImagePixelsContext(context.Background(), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.validateImageMask(object.ImageObject.ImageMask, image.Pt(config.Width, config.Height)); err != nil {
|
||||
if object.ImageObject.ImageMask != "" {
|
||||
if err := e.validateImageMask(object.ImageObject.ImageMask, decoded.Bounds().Size()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
resource, err := e.AddImage(data)
|
||||
return e.Transaction(func(e *Editor) error {
|
||||
resource, err := e.addDecodedImage(data, decoded)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -698,6 +700,7 @@ func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) erro
|
||||
}
|
||||
}
|
||||
return e.UpdateObject(page, id, object)
|
||||
})
|
||||
}
|
||||
|
||||
// AlignObject 将对象对齐页面,文字采用实际字形范围,路径和图片采用对象边界
|
||||
|
||||
@@ -16,6 +16,7 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"maps"
|
||||
@@ -163,11 +164,33 @@ func (e *Editor) RenameAttachment(id, name string) error {
|
||||
}
|
||||
|
||||
// ReplaceAttachment 替换附件内容,使用独立资源避免影响共享引用,保留名称和格式,不新增日期
|
||||
// 内容相同时不修改文件及撤销记录
|
||||
// 入参: id 附件标识, data 文件数据,调用后可复用
|
||||
// 返回: error 附件结构或提交错误
|
||||
func (e *Editor) ReplaceAttachment(id string, data []byte) error {
|
||||
return e.replaceAttachment(context.Background(), id, data)
|
||||
}
|
||||
|
||||
// replaceAttachment 比较附件内容并提交替换,传递内容读取过程中的取消状态
|
||||
// 入参: ctx 取消上下文, id 附件标识, data 新文件数据
|
||||
// 返回: error 读取、取消或提交错误
|
||||
func (e *Editor) replaceAttachment(ctx context.Context, id string, data []byte) error {
|
||||
var maximum int
|
||||
changed := false
|
||||
err := e.editAttachment(id, func(fragment []byte, node *editorXML, parts map[string][]byte) ([]byte, error) {
|
||||
if child := node.child("FileLoc"); child != nil {
|
||||
location := strings.TrimSpace(editorImportText(fragment, child))
|
||||
if location != "" {
|
||||
name := e.source.reader.documentRoot
|
||||
if external := strings.TrimSpace(e.source.document.Attachments.Path); external != "" {
|
||||
name = e.source.reader.ResPath(external)
|
||||
}
|
||||
equal, err := packageDataEqual(ctx, e.source.reader, resolveResourcePath(name, "", location), data)
|
||||
if err != nil || equal {
|
||||
return fragment, err
|
||||
}
|
||||
}
|
||||
}
|
||||
var err error
|
||||
maximum, err = e.sourceMaxID(nil)
|
||||
if err != nil {
|
||||
@@ -194,12 +217,15 @@ func (e *Editor) ReplaceAttachment(id string, data []byte) error {
|
||||
return nil, err
|
||||
}
|
||||
parts[file] = bytes.Clone(data)
|
||||
changed = true
|
||||
return fragment, nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if changed {
|
||||
e.maxID, e.source.idsReady = maximum+1, true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"maps"
|
||||
"slices"
|
||||
)
|
||||
@@ -122,20 +121,20 @@ func (e *Editor) ReplaceCompositeImage(page int, path ObjectPath, index int, dat
|
||||
return fmt.Errorf("composite member is not an image")
|
||||
}
|
||||
object := node.object
|
||||
if object.ImageObject.ImageMask != "" {
|
||||
config, _, err := decodeImageConfigData(data)
|
||||
decoded, _, err := decodeImagePixelsContext(context.Background(), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object.ImageObject.ImageMask != "" {
|
||||
mask, err := renderer.ImageResource(object.ImageObject.ImageMask)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateImageMask(context.Background(), mask, image.Pt(config.Width, config.Height)); err != nil {
|
||||
if err := validateImageMask(context.Background(), mask, decoded.Bounds().Size()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
id, err := e.AddImage(data)
|
||||
id, err := e.addDecodedImage(data, decoded)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+1
-10
@@ -15,12 +15,10 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
)
|
||||
|
||||
@@ -127,14 +125,7 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag
|
||||
var decoded image.Image
|
||||
checksum := sha256.Sum256(data)
|
||||
if !verified[checksum] {
|
||||
if format == "bmp" || format == "tiff" {
|
||||
decoded, _, err = decodeImageDataContext(ctx, data)
|
||||
} else {
|
||||
decoded, _, err = image.Decode(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
||||
}
|
||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||
return 0, cancelErr
|
||||
}
|
||||
decoded, _, err = decodeImagePixelsContext(ctx, data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -768,6 +768,9 @@ func (e *Editor) sourceReader(ctx context.Context, progress editorProgress) (*Re
|
||||
reader.files = make(map[string][]byte)
|
||||
}
|
||||
maps.Copy(reader.files, parts)
|
||||
if len(parts) != 0 {
|
||||
reader.resourceInfoCache = nil
|
||||
}
|
||||
reader.encryption = e.encryption
|
||||
for name := range reader.pageHeaderCache {
|
||||
if _, changed := parts[name]; changed {
|
||||
|
||||
+135
-15
@@ -17,9 +17,9 @@ package ofdgo
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"image"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -106,31 +106,34 @@ func (e *Editor) ImportMedia(ctx context.Context, kind, format string, data []by
|
||||
|
||||
// ReplaceResource 替换当前文档图片、音视频或附件的文件,保留标识和全部使用位置
|
||||
// 图片同时清除受影响对象的旧替换图、底纹缩略图及复合预览;字体须使用EmbedFont或ExternalizeFont
|
||||
// 图片替换须满足关联蒙版与主图尺寸一致、蒙版像素为不透明黑白二值的要求
|
||||
// 入参: ctx 取消上下文, kind 资源类型, id 资源标识, format 媒体格式,图片按实际数据确定, data 完整文件
|
||||
// 返回: error 校验或提交错误,失败时文档不变
|
||||
func (e *Editor) ReplaceResource(ctx context.Context, kind, id, format string, data []byte) error {
|
||||
if kind == "Font" {
|
||||
return fmt.Errorf("use font binding operations for fonts")
|
||||
}
|
||||
return e.changeResource(ctx, kind, id, func(reader *Reader, item ResourceInfo, target editorFontText, changes map[string][]byte) error {
|
||||
return e.changeResource(ctx, kind, id, func(catalog *resourceCatalog, item ResourceInfo, changes map[string][]byte) error {
|
||||
reader := catalog.scan.reader
|
||||
target := catalog.nodes[item.Kind+"/"+item.ID]
|
||||
if len(data) == 0 {
|
||||
return fmt.Errorf("empty media data")
|
||||
}
|
||||
if kind == "Image" {
|
||||
_, actual, err := decodeImageDataContext(ctx, data)
|
||||
img, actual, err := decodeImagePixelsContext(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateResourceImageMask(ctx, catalog, item.ID, img); err != nil {
|
||||
return err
|
||||
}
|
||||
format = actual
|
||||
}
|
||||
if item.Size == uint64(len(data)) && strings.EqualFold(item.Format, strings.TrimSpace(format)) {
|
||||
digest, err := outputResourceDigest(ctx, reader, nil, item.File, make([]byte, 32<<10))
|
||||
if err != nil {
|
||||
equal, err := packageDataEqual(ctx, reader, item.File, data)
|
||||
if err != nil || equal {
|
||||
return err
|
||||
}
|
||||
if digest == sha256.Sum256(data) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
name := packageAvailableName(reader, changes, path.Join(e.packageDirectory(), "Res", "Media", "Media_"+item.ID))
|
||||
changes[name] = bytes.Clone(data)
|
||||
@@ -167,7 +170,7 @@ func (e *Editor) DeleteResource(ctx context.Context, kind, id string) error {
|
||||
// changeResource 校验引用后在独立快照中修改声明,借用既有版本隔离和历史提交路径
|
||||
// 入参: ctx 取消上下文, kind 资源类型, id 标识, replace 替换操作,nil表示删除, data 附件替换数据
|
||||
// 返回: error 引用、结构或提交错误
|
||||
func (e *Editor) changeResource(ctx context.Context, kind, id string, replace func(*Reader, ResourceInfo, editorFontText, map[string][]byte) error, data []byte) error {
|
||||
func (e *Editor) changeResource(ctx context.Context, kind, id string, replace func(*resourceCatalog, ResourceInfo, map[string][]byte) error, data []byte) error {
|
||||
reader, err := e.ReaderContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -176,7 +179,7 @@ func (e *Editor) changeResource(ctx context.Context, kind, id string, replace fu
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item, err := catalog.find(kind, id)
|
||||
item, err := findResource(catalog.items, kind, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -191,14 +194,18 @@ func (e *Editor) changeResource(ctx context.Context, kind, id string, replace fu
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
revision := working.Revision()
|
||||
if replace == nil {
|
||||
err = working.DeleteAttachment(item.ID)
|
||||
} else {
|
||||
err = working.ReplaceAttachment(item.ID, data)
|
||||
err = working.replaceAttachment(ctx, item.ID, data)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if working.Revision() == revision {
|
||||
return ctx.Err()
|
||||
}
|
||||
reader, err = working.ReaderContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -213,7 +220,7 @@ func (e *Editor) changeResource(ctx context.Context, kind, id string, replace fu
|
||||
node = node.parent
|
||||
}
|
||||
changes[target.file.name] = editorPatchXML(target.file.data, []editorXMLPatch{{start: node.start, end: node.end}})
|
||||
} else if err := replace(reader, item, target, changes); err != nil {
|
||||
} else if err := replace(catalog, item, changes); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
@@ -233,6 +240,65 @@ func (e *Editor) changeResource(ctx context.Context, kind, id string, replace fu
|
||||
return e.commitResourceSnapshot(ctx, reader)
|
||||
}
|
||||
|
||||
// validateResourceImageMask 校验图片替换涉及的全部蒙版引用,复用尺寸和二值像素校验结果
|
||||
// 入参: ctx 取消上下文, catalog 当前文档资源, id 替换图片标识, replacement 新图片
|
||||
// 返回: error 资源、尺寸、像素或取消错误
|
||||
func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id string, replacement image.Image) error {
|
||||
sizes := map[string]image.Point{id: replacement.Bounds().Size()}
|
||||
validated := make(map[string]image.Point)
|
||||
read := func(id string) ([]byte, error) {
|
||||
item, err := findResource(catalog.items, "Image", id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return catalog.scan.reader.readFileViewContext(ctx, item.File)
|
||||
}
|
||||
for _, entry := range catalog.images {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
source, mask := editorResourceID(entry.node.attr("ResourceID")), editorResourceID(entry.node.attr("ImageMask"))
|
||||
if mask == "" || source != id && mask != id {
|
||||
continue
|
||||
}
|
||||
size, ok := sizes[source]
|
||||
if !ok {
|
||||
data, err := read(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config, _, err := decodeImageConfigDataContext(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
size = image.Pt(config.Width, config.Height)
|
||||
sizes[source] = size
|
||||
}
|
||||
if previous, ok := validated[mask]; ok {
|
||||
if previous != size {
|
||||
return fmt.Errorf("image mask %s dimensions differ from source %s", mask, source)
|
||||
}
|
||||
continue
|
||||
}
|
||||
img := replacement
|
||||
if mask != id {
|
||||
data, err := read(mask)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
img, _, err = decodeImageDataContext(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := validateImageMask(ctx, img, size); err != nil {
|
||||
return fmt.Errorf("image object %s in %s: %w", entry.node.attr("ID"), entry.file.name, err)
|
||||
}
|
||||
validated[mask] = size
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// invalidateResourcePreviews 沿绘制参数和复合引用移除图片变化后失效的替换图及底纹预览
|
||||
// 入参: ctx 取消上下文, scan 当前文档结构, id 图片标识, changes 待提交文件
|
||||
// 返回: error 解析或取消错误
|
||||
@@ -349,6 +415,46 @@ func (e *Editor) BindMedia(ctx context.Context, id string, page int, object stri
|
||||
if kind == "" {
|
||||
return fmt.Errorf("audio or video resource not found")
|
||||
}
|
||||
return e.bindResourceAction(ctx, kind, id, page, object, bind)
|
||||
}
|
||||
|
||||
// BindAttachment 追加或移除指定图元的附件点击动作,附件标识须属于当前文档及所选版本
|
||||
// 保留其他动作及已有附件动作的区域和窗口设置,重复操作不产生修改
|
||||
// 入参: ctx 取消上下文, id 附件标识, page 零基页码, object 图元标识, bind 为false时移除该附件的点击动作
|
||||
// 返回: error 附件、对象或提交错误
|
||||
func (e *Editor) BindAttachment(ctx context.Context, id string, page int, object string, bind bool) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
attachments, err := e.Attachments()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
found := false
|
||||
for _, attachment := range attachments {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if attachment.ID == id {
|
||||
if found {
|
||||
return fmt.Errorf("duplicate attachment ID: %s", id)
|
||||
}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("attachment not found: %s", id)
|
||||
}
|
||||
return e.bindResourceAction(ctx, "Attachment", id, page, object, bind)
|
||||
}
|
||||
|
||||
// bindResourceAction 修改单个资源的点击动作,沿用图元编辑的版本隔离及撤销机制
|
||||
// 入参: ctx 取消上下文, kind 资源类型, id 资源标识, page 零基页码, object 图元标识, bind 是否绑定
|
||||
// 返回: error 对象或提交错误
|
||||
func (e *Editor) bindResourceAction(ctx context.Context, kind, id string, page int, object string, bind bool) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
target, err := e.Object(page, object)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -368,15 +474,29 @@ func (e *Editor) BindMedia(ctx context.Context, id string, page int, object stri
|
||||
}
|
||||
found := false
|
||||
actions = slices.DeleteFunc(slices.Clone(actions), func(action Action) bool {
|
||||
match := action.Event == "CLICK" && (action.Sound != nil && sameResourceID(action.Sound.ResourceID, id) || action.Movie != nil && sameResourceID(action.Movie.ResourceID, id) && (action.Movie.Operator == "" || action.Movie.Operator == "Play"))
|
||||
if action.Event != "CLICK" {
|
||||
return false
|
||||
}
|
||||
match := false
|
||||
switch kind {
|
||||
case "Attachment":
|
||||
match = action.GotoA != nil && action.GotoA.AttachID == id
|
||||
case "Audio":
|
||||
match = action.Sound != nil && sameResourceID(action.Sound.ResourceID, id)
|
||||
case "Video":
|
||||
match = action.Movie != nil && sameResourceID(action.Movie.ResourceID, id) && (action.Movie.Operator == "" || action.Movie.Operator == "Play")
|
||||
}
|
||||
found = found || match
|
||||
return match && !bind
|
||||
})
|
||||
if bind && !found {
|
||||
action := Action{Event: "CLICK"}
|
||||
if kind == "Audio" {
|
||||
switch kind {
|
||||
case "Attachment":
|
||||
action.GotoA = &GotoA{AttachID: id}
|
||||
case "Audio":
|
||||
action.Sound = &Sound{ResourceID: id}
|
||||
} else {
|
||||
case "Video":
|
||||
action.Movie = &Movie{ResourceID: id, Operator: "Play"}
|
||||
}
|
||||
actions = append(actions, action)
|
||||
|
||||
@@ -183,6 +183,18 @@ func decodeImageDataContext(ctx context.Context, data []byte) (img image.Image,
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// decodeImagePixelsContext 完整解码图片,确保像素可读取并传递取消状态
|
||||
// 入参: ctx 取消上下文, data 图片编码数据
|
||||
// 返回: image.Image 解码像素, string 图片格式, error 解码或取消错误
|
||||
func decodeImagePixelsContext(ctx context.Context, data []byte) (image.Image, string, error) {
|
||||
img, format, err := decodeImageDataContext(ctx, data)
|
||||
if err != nil {
|
||||
return nil, format, err
|
||||
}
|
||||
pixels, err := imagePixelDataContext(ctx, img)
|
||||
return pixels, format, err
|
||||
}
|
||||
|
||||
// isJPEGData 判断是否为JPEG图片数据
|
||||
// 入参: data 图片数据
|
||||
// 返回: bool 是否为JPEG图片数据
|
||||
|
||||
+53
-1
@@ -17,6 +17,7 @@ package ofdgo
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -40,6 +41,7 @@ func packagePagePath(directory, id string) string {
|
||||
}
|
||||
|
||||
// packageName 为新增条目分配文档内路径,不覆盖原文件或本次资源
|
||||
// 无暂存资源冲突时直接返回路径,避免重复构建资源映射
|
||||
// 入参: relative 文档相对路径
|
||||
// 返回: string 未占用路径
|
||||
func (e *Editor) packageName(relative string) string {
|
||||
@@ -47,11 +49,16 @@ func (e *Editor) packageName(relative string) string {
|
||||
if e.source != nil {
|
||||
reader = e.source.reader
|
||||
}
|
||||
name := path.Join(e.packageDirectory(), relative)
|
||||
candidate := packageAvailableName(reader, nil, name)
|
||||
if !slices.ContainsFunc(e.resources, func(resource editorResource) bool { return resource.name == candidate }) {
|
||||
return candidate
|
||||
}
|
||||
files := make(map[string][]byte, len(e.resources))
|
||||
for _, resource := range e.resources {
|
||||
files[resource.name] = nil
|
||||
}
|
||||
return packageAvailableName(reader, files, path.Join(e.packageDirectory(), relative))
|
||||
return packageAvailableName(reader, files, name)
|
||||
}
|
||||
|
||||
// packageAvailableName 仅在包内同名条目冲突时添加序号,路径区分大小写
|
||||
@@ -78,6 +85,51 @@ func packageAvailableName(reader *Reader, files map[string][]byte, name string)
|
||||
}
|
||||
}
|
||||
|
||||
// packageDataEqual 流式比较包内文件与给定数据,文件缺失时返回false
|
||||
// 入参: ctx 取消上下文, reader 原包, name 包内路径, data 待比较内容
|
||||
// 返回: bool 内容是否相同, error 读取或取消错误
|
||||
func packageDataEqual(ctx context.Context, reader *Reader, name string, data []byte) (bool, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
name = cleanPackagePath(name)
|
||||
if current, ok := reader.files[name]; ok {
|
||||
return bytes.Equal(current, data), ctx.Err()
|
||||
}
|
||||
if _, ok := reader.packageFile(name); !ok {
|
||||
return false, nil
|
||||
}
|
||||
stream, err := reader.openFile(name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer stream.Close()
|
||||
input := imageInput{ReadCloser: stream, context: ctx}
|
||||
var buffer [32 << 10]byte
|
||||
for len(data) != 0 {
|
||||
block := buffer[:min(len(buffer), len(data))]
|
||||
_, err := io.ReadFull(input, block)
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !bytes.Equal(block, data[:len(block)]) {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
data = data[len(block):]
|
||||
}
|
||||
_, err = io.ReadFull(input, buffer[:1])
|
||||
if err == io.EOF {
|
||||
return true, ctx.Err()
|
||||
}
|
||||
if err == nil {
|
||||
err = ctx.Err()
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// mergePackageResources 合并新增资源定义,保留原索引、路径基准与未知字段
|
||||
// 入参: ctx 取消上下文, reader 原包, parts 待写入条目, doc 文档, directory 文档目录, added 新增资源XML,文件引用使用绝对包路径
|
||||
// 返回: string 资源索引路径, bool 是否需要新增文档引用, error 读取、解析或取消错误
|
||||
|
||||
@@ -39,6 +39,7 @@ type Reader struct {
|
||||
resourcesRead map[string]bool
|
||||
resourceFiles map[string]string
|
||||
imageCatalog []ImageInfo
|
||||
resourceInfoCache []ResourceInfo
|
||||
mediaCache map[string]MultiMedia
|
||||
fontCache map[string]*Font
|
||||
fontResourcesRead bool
|
||||
@@ -658,6 +659,7 @@ func (r *Reader) initRootContext(ctx context.Context) error {
|
||||
r.documentRoot, r.RootDir = "", ""
|
||||
r.relatedPending, r.fontResourcesRead = false, false
|
||||
r.imageCatalog = nil
|
||||
r.resourceInfoCache = nil
|
||||
r.Stamps, r.Annots, r.annotationFiles = nil, nil, nil
|
||||
r.ResMap = make(map[string]string)
|
||||
r.resourcesRead = make(map[string]bool)
|
||||
|
||||
+41
-8
@@ -54,28 +54,40 @@ type resourceCatalog struct {
|
||||
scan *editorFontDocument
|
||||
items []ResourceInfo
|
||||
nodes map[string]editorFontText
|
||||
images []editorFontText
|
||||
}
|
||||
|
||||
// Resources 列出当前文档及所选版本的资源,标记缺失文件,不读取二进制内容或执行媒体动作
|
||||
// 重复查询复用已完成的扫描,返回值及引用位置与阅读器相互独立
|
||||
// 入参: ctx 取消上下文
|
||||
// 返回: []ResourceInfo 资源及引用位置, error 结构或读取错误
|
||||
func (r *Reader) Resources(ctx context.Context) ([]ResourceInfo, error) {
|
||||
catalog, err := r.resourceCatalog(ctx)
|
||||
items, err := r.resourceList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return catalog.items, nil
|
||||
result := slices.Clone(items)
|
||||
for i := range result {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[i].References = slices.Clone(result[i].References)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// OpenResource 打开当前资源的原始文件,外部字体无可导出的内嵌文件
|
||||
// 入参: ctx 取消上下文, kind 资源类型, id 资源标识
|
||||
// 返回: io.ReadCloser 文件流,由调用方关闭, error 资源或文件错误
|
||||
func (r *Reader) OpenResource(ctx context.Context, kind, id string) (io.ReadCloser, error) {
|
||||
catalog, err := r.resourceCatalog(ctx)
|
||||
items, err := r.resourceList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item, err := catalog.find(kind, id)
|
||||
item, err := findResource(items, kind, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -89,6 +101,24 @@ func (r *Reader) OpenResource(ctx context.Context, kind, id string) (io.ReadClos
|
||||
return imageInput{ReadCloser: stream, context: ctx}, nil
|
||||
}
|
||||
|
||||
// resourceList 缓存当前阅读视图的资源摘要,不保留扫描使用的XML树
|
||||
// 入参: ctx 取消上下文
|
||||
// 返回: []ResourceInfo 内部只读摘要, error 扫描或取消错误
|
||||
func (r *Reader) resourceList(ctx context.Context) ([]ResourceInfo, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.resourceInfoCache != nil {
|
||||
return r.resourceInfoCache, nil
|
||||
}
|
||||
catalog, err := r.resourceCatalog(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.resourceInfoCache = catalog.items
|
||||
return r.resourceInfoCache, nil
|
||||
}
|
||||
|
||||
// resourceCatalog 按标准内容路径收集资源声明及引用,不把任意数字属性作为资源引用
|
||||
// 入参: ctx 取消上下文
|
||||
// 返回: *resourceCatalog 资源索引, error 结构或读取错误
|
||||
@@ -158,6 +188,9 @@ func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error)
|
||||
if slices.Contains([]string{"TextObject", "PathObject", "ImageObject", "CompositeObject"}, node.name.Local) {
|
||||
object = node.attr("ID")
|
||||
}
|
||||
if node.matchesOFD("ImageObject") {
|
||||
catalog.images = append(catalog.images, editorFontText{file: file, node: node})
|
||||
}
|
||||
add := func(role, id string) {
|
||||
key := editorResourceID(id)
|
||||
if role == "AttachID" {
|
||||
@@ -238,11 +271,11 @@ func (c *resourceCatalog) add(item ResourceInfo, node editorFontText) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// find 查找当前视图内的资源,不接受调用方提供的任意包路径
|
||||
// 入参: kind 资源类型, id 资源标识
|
||||
// findResource 查找当前视图内的资源,不接受调用方提供的任意包路径
|
||||
// 入参: items 资源列表, kind 资源类型, id 资源标识
|
||||
// 返回: ResourceInfo 资源信息, error 未找到资源
|
||||
func (c *resourceCatalog) find(kind, id string) (ResourceInfo, error) {
|
||||
for _, item := range c.items {
|
||||
func findResource(items []ResourceInfo, kind, id string) (ResourceInfo, error) {
|
||||
for _, item := range items {
|
||||
if item.Kind == kind && (item.ID == id || kind != "Attachment" && sameResourceID(item.ID, id)) {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user