feat(界面设计): 优化图片操作布局与建页预览
build.yaml / build (push) Successful in 45s

This commit is contained in:
xiaoqidun committed 2026-10-31 14:22:43 +08:00
1 parent 22c8a95e7c
commit 171f118a46
5 files changed
+82 -21

No files matched your search

+1 -1
View File
@@ -356,7 +356,7 @@
</div> </div>
<div id="createImagesRow" class="create-images-row" hidden> <div id="createImagesRow" class="create-images-row" hidden>
<div class="create-image-actions"><button id="createImageAdd" class="button" type="button">添加图片</button><div id="createImageOrder" class="form-options" hidden><label><input type="radio" name="imageOrder" value="name-asc" checked><span>名称升序</span></label><label><input type="radio" name="imageOrder" value="name-desc"><span>名称降序</span></label><label><input type="radio" name="imageOrder" value="time-asc"><span>时间升序</span></label><label><input type="radio" name="imageOrder" value="time-desc"><span>时间降序</span></label><label><input type="radio" name="imageOrder" value="added"><span>添加顺序</span></label></div></div> <div class="create-image-actions"><button id="createImageAdd" class="button" type="button">添加图片</button><div id="createImageOrder" class="form-options" hidden><label><input type="radio" name="imageOrder" value="name-asc" checked><span>名称升序</span></label><label><input type="radio" name="imageOrder" value="name-desc"><span>名称降序</span></label><label><input type="radio" name="imageOrder" value="time-asc"><span>时间升序</span></label><label><input type="radio" name="imageOrder" value="time-desc"><span>时间降序</span></label><label><input type="radio" name="imageOrder" value="added"><span>添加顺序</span></label></div></div>
<span id="createImageCount">0</span><input id="createImages" type="file" accept=".png,.jpg,.jpeg,.bmp,.tif,.tiff,.jb2,.jbig2" multiple hidden disabled> <input id="createImages" type="file" accept=".png,.jpg,.jpeg,.bmp,.tif,.tiff,.jb2,.jbig2" multiple hidden disabled>
</div> </div>
<div id="createImageQueue" hidden> <div id="createImageQueue" hidden>
<div id="createImageList" class="create-image-list"></div> <div id="createImageList" class="create-image-list"></div>
+15 -14
View File
@@ -1666,10 +1666,6 @@ button:not(:disabled):hover {
} }
.create-images-row { .create-images-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 4px;
margin-bottom: 12px; margin-bottom: 12px;
} }
@@ -1685,12 +1681,6 @@ button:not(:disabled):hover {
} }
} }
#createImageCount {
align-self: start;
line-height: var(--control-height);
color: var(--text-muted);
}
#createImageQueue { #createImageQueue {
margin-bottom: 12px; margin-bottom: 12px;
} }
@@ -1765,11 +1755,22 @@ button:not(:disabled):hover {
font-size: 11px; font-size: 11px;
} }
.create-image-preview img { .create-image-paper {
display: block; display: grid;
max-width: 100%; place-items: center;
max-height: 100%; box-sizing: border-box;
background: #fff; background: #fff;
box-shadow: 0 0 0 1px var(--line);
}
.create-image-paper img {
display: block;
width: 100%;
height: 100%;
}
.create-image-paper[hidden] {
display: none;
} }
.create-image-details { .create-image-details {
+38 -4
View File
@@ -508,7 +508,6 @@ const el = {
createImageQueue: document.querySelector("#createImageQueue"), createImageQueue: document.querySelector("#createImageQueue"),
createImageList: document.querySelector("#createImageList"), createImageList: document.querySelector("#createImageList"),
createImageAdd: document.querySelector("#createImageAdd"), createImageAdd: document.querySelector("#createImageAdd"),
createImageCount: document.querySelector("#createImageCount"),
createImageOrder: document.querySelector("#createImageOrder"), createImageOrder: document.querySelector("#createImageOrder"),
createPortrait: document.querySelector("#createPortrait"), createPortrait: document.querySelector("#createPortrait"),
createLandscape: document.querySelector("#createLandscape"), createLandscape: document.querySelector("#createLandscape"),
@@ -1681,6 +1680,7 @@ el.createMode.addEventListener("change", () => {
}); });
el.createSize.addEventListener("change", applyCreatePaper); el.createSize.addEventListener("change", applyCreatePaper);
for (const input of [el.createWidth, el.createHeight]) input.addEventListener("input", () => updateCreateDirection(true)); for (const input of [el.createWidth, el.createHeight]) input.addEventListener("input", () => updateCreateDirection(true));
el.createMargin.addEventListener("input", updateCreateImagePreviews);
for (const radio of [el.createPortrait, el.createLandscape]) radio.addEventListener("change", () => { for (const radio of [el.createPortrait, el.createLandscape]) radio.addEventListener("change", () => {
[el.createWidth.value, el.createHeight.value] = [el.createHeight.value, el.createWidth.value]; [el.createWidth.value, el.createHeight.value] = [el.createHeight.value, el.createWidth.value];
updateCreateDirection(); updateCreateDirection();
@@ -3886,6 +3886,7 @@ function updateCreateDirection(matchPaper = false) {
el.createLandscape.checked = valid && width > height; el.createLandscape.checked = valid && width > height;
el.createPortrait.disabled = el.createLandscape.disabled = !valid || width === height || el.createWidth.disabled; el.createPortrait.disabled = el.createLandscape.disabled = !valid || width === height || el.createWidth.disabled;
if (matchPaper) el.createSize.value = Object.entries(createPaperSizes).find(([, [w, h]]) => Math.min(width, height) === w && Math.max(width, height) === h)?.[0] || "custom"; if (matchPaper) el.createSize.value = Object.entries(createPaperSizes).find(([, [w, h]]) => Math.min(width, height) === w && Math.max(width, height) === h)?.[0] || "custom";
updateCreateImagePreviews();
} }
function releaseCreateImages(tab) { function releaseCreateImages(tab) {
@@ -3963,7 +3964,6 @@ function renderCreateImages() {
const focused = el.createImageList.contains(document.activeElement); const focused = el.createImageList.contains(document.activeElement);
el.createImageList.replaceChildren(); el.createImageList.replaceChildren();
for (const input of el.createImageOrder.querySelectorAll("input")) input.checked = input.value === queue.order; for (const input of el.createImageOrder.querySelectorAll("input")) input.checked = input.value === queue.order;
el.createImageCount.textContent = String(queue.entries.length);
if (!queue.entries.some(entry => entry.id === queue.selected)) queue.selected = queue.entries[0]?.id ?? null; if (!queue.entries.some(entry => entry.id === queue.selected)) queue.selected = queue.entries[0]?.id ?? null;
queue.observer = new IntersectionObserver(items => { queue.observer = new IntersectionObserver(items => {
for (const item of items) if (item.isIntersecting && isForegroundDocument(tab)) { for (const item of items) if (item.isIntersecting && isForegroundDocument(tab)) {
@@ -4000,10 +4000,39 @@ function renderCreateImages() {
} }
function showCreateImagePreview(entry, row) { function showCreateImagePreview(entry, row) {
const image = document.createElement("img"); const paper = document.createElement("span"), image = document.createElement("img");
paper.className = "create-image-paper";
image.src = entry.url; image.src = entry.url;
image.draggable = false; image.draggable = false;
row.querySelector(".create-image-preview").replaceChildren(image); paper.append(image);
row.querySelector(".create-image-preview").replaceChildren(paper);
layoutCreateImagePreview(entry, row);
}
function updateCreateImagePreviews() {
const queue = state.createImageQueue;
if (!queue) return;
const entries = new Map(queue.entries.map(entry => [entry.id, entry]));
for (const row of el.createImageList.children) {
const entry = entries.get(Number(row.dataset.imageId));
if (entry?.url && row.querySelector(".create-image-paper")) layoutCreateImagePreview(entry, row);
}
}
function layoutCreateImagePreview(entry, row) {
const paper = row.querySelector(".create-image-paper"), image = paper.firstElementChild;
const margin = Number(el.createMargin.value), follow = el.createSize.value === "image";
const width = follow ? entry.width + 2 * margin : Number(el.createWidth.value);
const height = follow ? entry.height + 2 * margin : Number(el.createHeight.value);
const valid = Number.isFinite(width + height + margin) && margin >= 0 && width > 2 * margin && height > 2 * margin;
paper.hidden = !valid;
if (!valid) return;
const scale = 46 / Math.max(width, height);
const fit = follow ? 1 : Math.min((width - 2 * margin) / entry.width, (height - 2 * margin) / entry.height);
paper.style.width = `${width * scale}px`;
paper.style.height = `${height * scale}px`;
image.style.width = `${entry.width * fit * scale}px`;
image.style.height = `${entry.height * fit * scale}px`;
} }
async function loadCreateImagePreview(tab, queue, entry) { async function loadCreateImagePreview(tab, queue, entry) {
@@ -4014,6 +4043,11 @@ async function loadCreateImagePreview(tab, queue, entry) {
const source = entry.file instanceof Blob ? entry.file : new Blob([await entry.file.arrayBuffer()]); const source = entry.file instanceof Blob ? entry.file : new Blob([await entry.file.arrayBuffer()]);
if (tab.state.createImageQueue !== queue || !queue.entries.includes(entry)) return; if (tab.state.createImageQueue !== queue || !queue.entries.includes(entry)) return;
bitmap = await createImageBitmap(source, { resizeWidth: 64 }); bitmap = await createImageBitmap(source, { resizeWidth: 64 });
if (tab.closed || tab.state.createImageQueue !== queue || !queue.entries.includes(entry)) return;
const size = await callTabWASM(tab, "ofdgoImagePageSize", new Uint8Array(await source.arrayBuffer()));
if (tab.state.createImageQueue !== queue || !queue.entries.includes(entry)) return;
entry.width = size.width;
entry.height = size.height;
const canvas = document.createElement("canvas"), scale = Math.min(64 / bitmap.width, 64 / bitmap.height, 1); const canvas = document.createElement("canvas"), scale = Math.min(64 / bitmap.width, 64 / bitmap.height, 1);
canvas.width = Math.max(1, Math.round(bitmap.width * scale)); canvas.width = Math.max(1, Math.round(bitmap.width * scale));
canvas.height = Math.max(1, Math.round(bitmap.height * scale)); canvas.height = Math.max(1, Math.round(bitmap.height * scale));
+1 -1
View File
@@ -478,7 +478,7 @@ class BrowserEngine {
"ofdgoLoadPDF", "ofdgoDocumentInfo", "ofdgoDocumentVersions", "ofdgoDocumentNavigation", "ofdgoOpenLinks", "ofdgoOutlineLinks", "ofdgoRenderBackends", "ofdgoSVGFontData", "ofdgoLoadPDF", "ofdgoDocumentInfo", "ofdgoDocumentVersions", "ofdgoDocumentNavigation", "ofdgoOpenLinks", "ofdgoOutlineLinks", "ofdgoRenderBackends", "ofdgoSVGFontData",
"ofdgoSearchPage", "ofdgoPageTextString", "ofdgoResources", "ofdgoResourceReferences", "ofdgoResourceTargets", "ofdgoSearchPage", "ofdgoPageTextString", "ofdgoResources", "ofdgoResourceReferences", "ofdgoResourceTargets",
"ofdgoResourceLocations", "ofdgoFontPreview", "ofdgoFontFaces", "ofdgoFontFace", "ofdgoMatchFontFiles", "ofdgoResourceLocations", "ofdgoFontPreview", "ofdgoFontFaces", "ofdgoFontFace", "ofdgoMatchFontFiles",
"ofdgoImages", "ofdgoStructureTargets", "ofdgoStructureObjects", "ofdgoPackageDocuments", "ofdgoVerifySignatures", "ofdgoImages", "ofdgoImagePageSize", "ofdgoStructureTargets", "ofdgoStructureObjects", "ofdgoPackageDocuments", "ofdgoVerifySignatures",
"ofdgoAnnotationInfos", "ofdgoInvoices", "ofdgoInvoice", "ofdgoExportInvoice", "ofdgoValidateInvoice", "ofdgoAnnotationInfos", "ofdgoInvoices", "ofdgoInvoice", "ofdgoExportInvoice", "ofdgoValidateInvoice",
"ofdgoActionList", "ofdgoPageSettings", "ofdgoReadAnnotation", "ofdgoPreviewSignatureSeal", "ofdgoActionList", "ofdgoPageSettings", "ofdgoReadAnnotation", "ofdgoPreviewSignatureSeal",
"ofdgoExportFormats", "ofdgoOutputFormats", "ofdgoParsePageRange", "ofdgoExportPage", "ofdgoExportDocument", "ofdgoExportFormats", "ofdgoOutputFormats", "ofdgoParsePageRange", "ofdgoExportPage", "ofdgoExportDocument",
+27 -1
View File
@@ -36,6 +36,7 @@ import (
"time" "time"
"github.com/xiaoqidun/ofdgo" "github.com/xiaoqidun/ofdgo"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// sessionTextCacheBytes 限制会话文字索引的内存预算 // sessionTextCacheBytes 限制会话文字索引的内存预算
@@ -1622,7 +1623,7 @@ func (e *businessEngine) checkDocument(name string) error {
return nil return nil
} }
switch name { switch name {
case "ofdgoFontFaces", "ofdgoFontMenu", "ofdgoFontPreferenceOrder", "ofdgoFontFace", "ofdgoCheckTextFont": case "ofdgoFontFaces", "ofdgoFontMenu", "ofdgoFontPreferenceOrder", "ofdgoFontFace", "ofdgoCheckTextFont", "ofdgoImagePageSize":
return nil return nil
case "ofdgoDiscardObjects", "ofdgoExportObjects", "ofdgoCheckDocument", "ofdgoPDFContext", "ofdgoOpenPDF", "ofdgoOpen", "ofdgoLoadPDF", "ofdgoConfigure", "ofdgoDocumentInfo", "ofdgoSVGFontData", "ofdgoRenderBackends", "ofdgoOutputFormats", "ofdgoExportFormats", "ofdgoParsePageRange", "ofdgoMatchFontFiles", "ofdgoOutlineLinks", "ofdgoDocumentNavigation", "ofdgoInvoices", "ofdgoRenderPage", "ofdgoSearchPage", "ofdgoPageTextString", "ofdgoOpenLinks", "ofdgoDiscardPrint", "ofdgoDiscardSave", "ofdgoDetectFormat", "ofdgoConvertFile", "ofdgoPackFiles", "ofdgoLoadImport", "ofdgoCreateDocument", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF": case "ofdgoDiscardObjects", "ofdgoExportObjects", "ofdgoCheckDocument", "ofdgoPDFContext", "ofdgoOpenPDF", "ofdgoOpen", "ofdgoLoadPDF", "ofdgoConfigure", "ofdgoDocumentInfo", "ofdgoSVGFontData", "ofdgoRenderBackends", "ofdgoOutputFormats", "ofdgoExportFormats", "ofdgoParsePageRange", "ofdgoMatchFontFiles", "ofdgoOutlineLinks", "ofdgoDocumentNavigation", "ofdgoInvoices", "ofdgoRenderPage", "ofdgoSearchPage", "ofdgoPageTextString", "ofdgoOpenLinks", "ofdgoDiscardPrint", "ofdgoDiscardSave", "ofdgoDetectFormat", "ofdgoConvertFile", "ofdgoPackFiles", "ofdgoLoadImport", "ofdgoCreateDocument", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF":
return nil return nil
@@ -6260,6 +6261,30 @@ func (e *businessEngine) importImages(args []platformValue) (any, error) {
return editorPageInfo{info, position}, err return editorPageInfo{info, position}, err
} }
// imagePageSize 读取图片建页使用的物理尺寸和方向,不改变文档状态
// 入参: args 图片数据
// 返回: any 图片宽高,单位为毫米, error 错误信息
func (e *businessEngine) imagePageSize(args []platformValue) (any, error) {
data, err := e.bytesFromJS(args[0])
if err != nil {
return nil, err
}
ctx := e.operationContext()
config, format, err := baseimage.DecodeConfigDataContext(ctx, data)
if err != nil {
return nil, err
}
metadata, err := baseimage.PageMetadataContext(ctx, data, format, 96)
if err != nil {
return nil, err
}
width, height := float64(config.Width)*25.4/metadata.XDPI, float64(config.Height)*25.4/metadata.YDPI
if metadata.Orientation >= 5 {
width, height = height, width
}
return successResult(map[string]any{"width": width, "height": height}), nil
}
// createDocument 新建单页文档 // createDocument 新建单页文档
// 入参: args 标题、纸张宽高和注解设置 // 入参: args 标题、纸张宽高和注解设置
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
@@ -8111,6 +8136,7 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an
"ofdgoFontFace": e.fontFace, "ofdgoFontFace": e.fontFace,
"ofdgoCreateDocument": e.createDocument, "ofdgoCreateDocument": e.createDocument,
"ofdgoImportImages": e.importImages, "ofdgoImportImages": e.importImages,
"ofdgoImagePageSize": e.imagePageSize,
"ofdgoEditDocument": e.editDocument, "ofdgoEditDocument": e.editDocument,
"ofdgoUpdateInfo": e.updateInfo, "ofdgoUpdateInfo": e.updateInfo,
"ofdgoChangeAttachment": e.changeAttachment, "ofdgoChangeAttachment": e.changeAttachment,