feat: configure canvas pages sent for note analysis

Canvas analysis previously sent every page. Add a Magic Notes setting for
1-8 canvas pages, defaulting to 1 for new and historical settings. Select
the first N pages in current canvas order before capture and use the same
text/image range for saved entries, drafts and canvas-source todos.

Keep full 50-page saving/export, expose the same setting through config
tools, and use the shared settings select with an inline help icon.
Include pagination, persistence, production IPC and isolated Electron
regressions, including text-only input and scaled page boundaries.

Validation: focused feature tests and isolated Electron regressions passed;
the final compact UI passed 14 tests. Typecheck and lint were reported
passing by the coordinating agent. No full-suite pass is claimed because
the completed full test command exposed no summary. No live model calls
were made for this commit preparation.

Release note: 魔法笔记新增“发送画布页数”设置,可选 1~8 页,默认 1 页,按当前画布顺序发送前 N 页的文字和图片;标题旁问号可查看说明,保存和导出完整画布不受影响。
This commit is contained in:
mesalogo
2026-09-20 00:07:07 +08:00
parent ecf09f4e95
commit bf1d34e897
39 changed files with 475 additions and 102 deletions
+15
View File
@@ -1094,6 +1094,7 @@ function installRemoteProjectsSetting(enabled: boolean): {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate",
magicNoteCommentFormat: "combined",
magicNoteCanvasPageCount: 1,
};
const updateSettings = vi.fn<
NonNullable<DesktopApi["updates"]>["updateSettings"]
@@ -3092,6 +3093,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate" as const,
magicNoteCommentFormat: "combined" as const,
magicNoteCanvasPageCount: 1,
})),
updateSettings: vi.fn(),
check: vi.fn(),
@@ -3193,6 +3195,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate" as const,
magicNoteCommentFormat: "combined" as const,
magicNoteCanvasPageCount: 1,
})),
updateSettings: vi.fn(async () => ({
checkUpdatesOnStartup: true,
@@ -3206,6 +3209,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate" as const,
magicNoteCommentFormat: "combined" as const,
magicNoteCanvasPageCount: 1,
})),
check,
openReleasePage: vi.fn(async () => {}),
@@ -3305,6 +3309,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate" as const,
magicNoteCommentFormat: "combined" as const,
magicNoteCanvasPageCount: 1,
})),
updateSettings: vi.fn(async () => ({
checkUpdatesOnStartup: true,
@@ -3318,6 +3323,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate" as const,
magicNoteCommentFormat: "combined" as const,
magicNoteCanvasPageCount: 1,
})),
check,
openReleasePage: vi.fn(async () => {}),
@@ -5529,6 +5535,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate",
magicNoteCommentFormat: "combined",
magicNoteCanvasPageCount: 1,
};
let shortcutSnapshot: GlobalShortcutSettingsSnapshot = {
settings: {
@@ -13228,6 +13235,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate" as const,
magicNoteCommentFormat: "combined" as const,
magicNoteCanvasPageCount: 1,
})),
updateSettings: vi.fn(async () => ({
checkUpdatesOnStartup: false,
@@ -13241,6 +13249,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate" as const,
magicNoteCommentFormat: "combined" as const,
magicNoteCanvasPageCount: 1,
})),
check: vi.fn(),
openReleasePage: vi.fn(async () => {}),
@@ -13291,6 +13300,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate" as const,
magicNoteCommentFormat: "combined" as const,
magicNoteCanvasPageCount: 1,
})),
updateSettings: vi.fn(),
check: vi.fn(),
@@ -13348,6 +13358,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: "immediate" as const,
magicNoteCommentFormat: "combined" as const,
magicNoteCanvasPageCount: 1,
})),
updateSettings: vi.fn(),
check: vi.fn(),
@@ -13382,6 +13393,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate" as const,
magicNoteCommentFormat: "combined" as const,
magicNoteCanvasPageCount: 1,
})),
updateSettings: vi.fn(async () => ({
checkUpdatesOnStartup: false,
@@ -13395,6 +13407,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate" as const,
magicNoteCommentFormat: "combined" as const,
magicNoteCanvasPageCount: 1,
})),
check: vi.fn(),
openReleasePage: vi.fn(async () => {}),
@@ -13454,6 +13467,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate",
magicNoteCommentFormat: "combined",
magicNoteCanvasPageCount: 1,
};
const updateSettings = vi.fn<
NonNullable<DesktopApi["updates"]>["updateSettings"]
@@ -13510,6 +13524,7 @@ describe("App", () => {
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: "immediate",
magicNoteCommentFormat: "combined",
magicNoteCanvasPageCount: 1,
};
api.updates = {
getSettings: vi.fn(async () => ({ ...applicationSettings })),
@@ -279,6 +279,18 @@ describe('Application Center', () => {
)
fireEvent.click(screen.getByRole('switch', { name: '启用应用' }))
expect(onUpdate).toHaveBeenLastCalledWith({ magicNotesEnabled: false })
const pageCount = screen.getByRole('combobox', { name: '发送画布页数' })
expect(pageCount).toHaveValue('1')
expect(within(pageCount).getAllByRole('option')).toHaveLength(8)
expect(pageCount).toHaveAccessibleDescription(expect.stringContaining('不是笔记记录数量'))
expect(pageCount.parentElement).toHaveClass('field')
const help = screen.getByRole('button', { name: '发送画布页数' })
expect(help).toHaveClass('icon-button')
expect(help).toHaveAccessibleDescription(help.getAttribute('title')!)
expect(help).toHaveAttribute('title', expect.stringContaining('不是笔记记录数量'))
expect(document.getElementById('magic-note-canvas-page-count-help')).toHaveClass('sr-only')
fireEvent.change(pageCount, { target: { value: '8' } })
expect(onUpdate).toHaveBeenLastCalledWith({ magicNoteCanvasPageCount: 8 })
fireEvent.click(screen.getByRole('switch', { name: '显示未完成待办数量' }))
expect(onUpdate).toHaveBeenLastCalledWith({
magicNotesShowIncompleteTodoCount: false,
+22
View File
@@ -10,6 +10,7 @@ import {
ArrowDown,
ArrowLeft,
ArrowUp,
CircleHelp,
Cpu,
HeartPulse,
Library,
@@ -157,6 +158,27 @@ export function ApplicationSettingsView({
</div>
{id === 'magic-notes' && (
<div className="application-settings__group">
<div className="platform-feature-option application-settings__compact-option">
<div className="application-settings__label-with-help">
<label htmlFor="magic-note-canvas-page-count">{t('platformFeatures.magicNotes.canvasPageCount', { ns: 'settingsSections' })}</label>
<button type="button" className="icon-button"
aria-label={t('platformFeatures.magicNotes.canvasPageCount', { ns: 'settingsSections' })}
aria-describedby="magic-note-canvas-page-count-help"
title={t('platformFeatures.magicNotes.canvasPageCountHelp', { ns: 'settingsSections' })}>
<CircleHelp size={14} aria-hidden="true" />
</button>
</div>
<div className="field">
<select id="magic-note-canvas-page-count" disabled={pending} value={settings.magicNoteCanvasPageCount ?? 1}
aria-describedby="magic-note-canvas-page-count-help"
onChange={event => void onUpdate({ magicNoteCanvasPageCount: Number(event.target.value) })}>
{[1, 2, 3, 4, 5, 6, 7, 8].map(count => <option key={count} value={count}>{count}</option>)}
</select>
</div>
</div>
<span id="magic-note-canvas-page-count-help" className="sr-only">
{t('platformFeatures.magicNotes.canvasPageCountHelp', { ns: 'settingsSections' })}
</span>
<div className="platform-feature-option">
<span>
{t('platformFeatures.magicNotes.commentMode', {
+2 -2
View File
@@ -14,9 +14,9 @@ export type MagicCanvasContentHandle = Pick<MagicCanvasEditorHandle, 'capturePag
export const MagicCanvasContent = forwardRef<MagicCanvasContentHandle, MagicCanvasContentProps>(function MagicCanvasContent({ content, onError, onEdit }, ref) {
const { t } = useTranslation('magicNotes')
const editorRef = useRef<MagicCanvasEditorHandle>(null)
useImperativeHandle(ref, () => ({ async capturePages() {
useImperativeHandle(ref, () => ({ async capturePages(pageLimit, includeImages) {
if (!editorRef.current) throw new Error(t('canvas.notReady'))
return editorRef.current.capturePages()
return editorRef.current.capturePages(pageLimit, includeImages)
} }), [t])
const [snapshot, setSnapshot] = useState({ content, revision: 0 })
if (snapshot.content !== content) setSnapshot({ content, revision: snapshot.revision + 1 })
+3 -3
View File
@@ -11,7 +11,7 @@ export type { MagicNoteCanvasContent } from './magic-canvas/model'
export type MagicCanvasEditorHandle = {
flush(): Promise<MagicNoteCanvasContent>
focus(): void
capturePages(): Promise<{ pageId: string; dataUrl: string }[]>
capturePages(pageLimit?: number, includeImages?: boolean): Promise<{ pageId: string; dataUrl: string; text?: string }[]>
}
export type MagicCanvasEditorProps = {
@@ -41,9 +41,9 @@ export const MagicCanvasEditor = forwardRef<MagicCanvasEditorHandle, MagicCanvas
return serialize.current(await controller.current.flush())
},
focus() { controller.current?.focus() },
async capturePages() {
async capturePages(pageLimit, includeImages) {
if (!controller.current) throw new Error('画布尚未加载')
return controller.current.capturePages()
return controller.current.capturePages({ pageLimit, includeImages })
}
}), [])
@@ -77,7 +77,7 @@ const settings: ApplicationSettings = {
localToolEnvironment: defaultLocalToolEnvironmentSettings, applicationNavigation: defaultApplicationNavigation,
localInferenceEnabled: true, conversationHtmlRenderingEnabled: true, remoteProjectsEnabled: false,
magicNotesEnabled: true, magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate', magicNoteCommentFormat: 'combined'
magicNoteCommentMode: 'immediate', magicNoteCommentFormat: 'combined', magicNoteCanvasPageCount: 1
}
const list = vi.fn<DesktopApi['magicNotes']['list']>()
const get = vi.fn<DesktopApi['magicNotes']['get']>()
@@ -1256,8 +1256,8 @@ describe('MagicNotesWorkspace canvas integration', () => {
// The selected profile wins over a stale top-level capability flag.
getRuntime.mockResolvedValue({ supportsImageInput: true, defaultModelProfileId: 'text', modelProfiles: [{ id: 'vision', supportsImageInput: true }, { id: 'text', supportsImageInput: false }] })
get.mockResolvedValue({ ...detail, entries: [{ ...detail.entries[0]!, content: canvasContent }] })
canvas.capture.mockRejectedValue(new Error('capture must not run'))
canvas.viewCapture.mockRejectedValue(new Error('viewer capture must not run'))
canvas.capture.mockResolvedValue([{ pageId: 'page-1', text: 'First page', dataUrl: '' }])
canvas.viewCapture.mockResolvedValue([{ pageId: 'page-1', text: 'First page', dataUrl: '' }])
render(<MagicNotesWorkspace onNotify={onNotify} />)
if (action === 'todo') {
await openTodo()
@@ -1281,8 +1281,8 @@ describe('MagicNotesWorkspace canvas integration', () => {
await waitFor(() => expect(analyzeCall).toHaveBeenCalledOnce())
expect(analyzeCall.mock.calls[0]![1]).not.toHaveProperty('canvasImages')
expect(getRuntime).toHaveBeenCalledOnce()
expect(canvas.capture).not.toHaveBeenCalled()
expect(canvas.viewCapture).not.toHaveBeenCalled()
expect(action === 'entry' || action === 'todo' ? canvas.viewCapture : canvas.capture).toHaveBeenCalledWith(1, false)
expect(analyzeCall.mock.calls[0]![1]).toHaveProperty('canvasPageText', [{ pageId: 'page-1', text: 'First page' }])
})
it('reads fresh capability for every analysis rather than caching the previous model', async () => {
@@ -1298,7 +1298,8 @@ describe('MagicNotesWorkspace canvas integration', () => {
await waitFor(() => expect(analyzeDraft).toHaveBeenCalledTimes(2))
expect(analyzeDraft.mock.calls[0]![1]).toHaveProperty('canvasImages', canvasImages)
expect(analyzeDraft.mock.calls[1]![1]).not.toHaveProperty('canvasImages')
expect(canvas.capture).toHaveBeenCalledOnce()
expect(canvas.capture).toHaveBeenNthCalledWith(1, 1, true)
expect(canvas.capture).toHaveBeenNthCalledWith(2, 1, false)
expect(getRuntime).toHaveBeenCalledTimes(2)
})
@@ -1493,6 +1494,7 @@ describe('MagicNotesWorkspace canvas integration', () => {
await waitFor(() => expect(call).toHaveBeenCalledOnce())
expect(call.mock.calls[0]![1]).toEqual({
requestId: expect.any(String), direction: 'general', format: 'combined', canvasImages,
canvasPageText: [{ pageId: 'page-1', text: '' }],
...(action === 'todo' ? { sourceEntryRevision: 7 } : action === 'draft' ? {} : { expectedRevision: action === 'entry' ? 7 : 11 })
})
if (action !== 'todo' && action !== 'draft') expect(analyze.mock.calls[0]![0]).toBe(savedId)
+7 -5
View File
@@ -534,6 +534,7 @@ export function MagicNotesWorkspace({
const createAnalysisOptions = useCallback(
async (canvas?: MagicCanvasContentHandle | null): Promise<MagicNoteAnalysisOptions> => {
let format = commentFormat
let pageCount = applicationSettings?.magicNoteCanvasPageCount ?? 1
// Read capability for this request, never reuse a previous profile's flag.
const runtimePromise = canvas !== undefined
? window.goodbuddy.settings.getRuntime()
@@ -544,6 +545,7 @@ export function MagicNotesWorkspace({
// Keep the last loaded format if application settings cannot be refreshed.
if (settingsResult.status === 'fulfilled' && settingsResult.value) {
format = settingsResult.value.magicNoteCommentFormat
pageCount = settingsResult.value.magicNoteCanvasPageCount ?? 1
setCommentFormat(format)
}
const options: MagicNoteAnalysisOptions = {
@@ -556,14 +558,14 @@ export function MagicNotesWorkspace({
if (runtime) {
const profile = runtime.modelProfiles.find((candidate) => candidate.id === runtime.defaultModelProfileId)
const supportsImages = (profile ? profile.supportsImageInput : runtime.supportsImageInput) === true
if (supportsImages) {
if (!canvas) throw new Error(tRef.current('canvas.notReady'))
options.canvasImages = await canvas.capturePages()
}
if (!canvas) throw new Error(tRef.current('canvas.notReady'))
const pages = await canvas.capturePages(pageCount, supportsImages)
options.canvasPageText = pages.map(({ pageId, text }) => ({ pageId, text: text ?? '' }))
if (supportsImages) options.canvasImages = pages.map(({ pageId, dataUrl }) => ({ pageId, dataUrl }))
}
return options
},
[commentDirection, commentFormat]
[commentDirection, commentFormat, applicationSettings?.magicNoteCanvasPageCount]
)
const getLayoutBounds = useCallback((): {
+4 -2
View File
@@ -554,7 +554,8 @@ applicationNavigation: defaultApplicationNavigation, localInferenceEnabled: true
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
magicNoteCommentFormat: 'combined',
magicNoteCanvasPageCount: 1
}
const agentPackageInventory: AgentPackageInventory = {
checkedAt: '2026-08-24T00:00:00.000Z',
@@ -846,7 +847,8 @@ describe('SettingsPanel runtime files', () => {
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
magicNoteCommentFormat: 'combined',
magicNoteCanvasPageCount: 1
}
getAgentPackageInventory.mockReset()
getAgentPackageInventory.mockResolvedValue(
@@ -32,7 +32,8 @@ describe('UpdateSettingsSection', () => {
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate' as const,
magicNoteCommentFormat: 'combined' as const
magicNoteCommentFormat: 'combined' as const,
magicNoteCanvasPageCount: 1
}
const updateSettings = vi.fn<
NonNullable<DesktopApi['updates']>['updateSettings']
+14
View File
@@ -148,6 +148,20 @@
width: fit-content;
max-width: 100%;
}
.application-settings .application-settings__compact-option {
display: flex;
justify-content: space-between;
}
.application-settings__label-with-help {
display: inline-flex;
align-items: center;
gap: var(--space-1);
white-space: nowrap;
}
.application-settings__label-with-help label {
font-size: var(--font-body);
font-weight: 600;
}
@media (max-width: 720px) {
.application-center .settings-panel__header {
padding: var(--space-4);
@@ -694,6 +694,8 @@ export const settingsSections = {
commentModeHelp:
'Immediate mode comments on an unsaved draft 5 seconds after you press Enter and stop typing. Automatic mode comments after saving. Manual mode comments only after you select Analyze with AI.',
commentFormat: 'AI comment format',
canvasPageCount: 'Canvas pages to send',
canvasPageCountHelp: 'Send the first N pages in the current canvas order for each AI analysis. Default: 1; range: 1–8. If fewer pages exist, send them all. This counts pages in one canvas, not note entries. Text and images use the same range, including text-only models. Applies to entries, drafts and to-do source canvases. Saving and exporting the full canvas are unaffected.',
commentFormatAria: 'Magic Notes AI comment format',
formats: {
combined: 'Long-form + points',
@@ -648,6 +648,8 @@ export const settingsSections = {
commentModeHelp:
'即时模式会在按回车并停止输入 5 秒后评论未保存草稿;自动模式在保存后评论;手动模式仅在点击 AI 分析后评论。',
commentFormat: 'AI 评论形式',
canvasPageCount: '发送画布页数',
canvasPageCountHelp: '每次 AI 分析按当前画布页面顺序发送前 N 页,默认 1 页,可选 1~8 页;不足 N 页时发送全部页。这是单个画布的页数,不是笔记记录数量。文字和图片使用相同范围,不支持图像的模型也只接收这些页的文字。适用于记录、草稿和待办来源画布,不影响保存或导出全部画布。',
commentFormatAria: '魔法笔记 AI 评论形式',
formats: {
combined: '长评 + 要点',
+6 -2
View File
@@ -21,7 +21,7 @@ type MagicCanvasEditorProps = {
type MagicCanvasEditorHandle = {
flush(): Promise<MagicNoteCanvasContent>
focus(): void
capturePages(): Promise<{ pageId: string; dataUrl: string }[]>
capturePages(pageLimit?: number, includeImages?: boolean): Promise<{ pageId: string; dataUrl: string; text?: string }[]>
}
```
@@ -48,7 +48,11 @@ The core controller accepts `capturePages({ firstPageOnly: true, thumbnailWidth:
to composite only page zero, including the background, body and annotations.
Visible thumbnails are queued and cached by content identity; they do not leave
full editors mounted in the record list. The React editor's AI capture method
continues to capture every page.
passes the configured page limit (1-8, default 1) before compositing. The core
equivalent is `capturePages({ pageLimit, includeImages })`; it returns only the
current first N pages and their measured Quill flow text. `includeImages: false`
skips PNG compositing and returns empty data URLs for text-only analysis. Omitting
`pageLimit` retains the all-page API for existing export and core callers.
The same module exports `createEmptyCanvasContent()` and
`canvasHasContent(content?)`.
@@ -431,6 +431,25 @@ export function mountFlowText(layerHost, toolbarHost, initialContent, options =
await scheduleLayout();
return cloneJson(content());
},
pageText(pageIndex) {
// CSS columns determine automatic page breaks; Delta alone has no page geometry.
const scale = surface.getBoundingClientRect().width / layout.width || 1;
const span = layout.width + layout.gap;
const boundary = (target) => {
let low = 0, high = quill.getLength();
while (low < high) {
const middle = Math.floor((low + high) / 2);
const bounds = quill.getBounds(middle, 1);
// Half a logical pixel absorbs scaled DOMRect rounding at column starts.
const column = layout.pageIndex + Math.floor(((Number(bounds?.left) || 0) / scale + 0.5) / span);
if (column < target) low = middle + 1;
else high = middle;
}
return low;
};
const start = boundary(pageIndex);
return quill.getText(start, boundary(pageIndex + 1) - start);
},
async renderPage(pageIndex, pageWidth, pageHeight) {
const canvas = document.createElement('canvas');
canvas.width = pageWidth;
@@ -3,7 +3,7 @@ import type { CoreContent } from './model'
export interface CanvasController {
content(): CoreContent
flush(): Promise<CoreContent>
capturePages(options?: { firstPageOnly?: boolean; thumbnailWidth?: number }): Promise<{ pageId: string; dataUrl: string }[]>
capturePages(options?: { firstPageOnly?: boolean; thumbnailWidth?: number; pageLimit?: number; includeImages?: boolean }): Promise<{ pageId: string; dataUrl: string; text?: string }[]>
focus(): void
setDisabled(value: boolean): void
destroy(): Promise<void>
+10 -3
View File
@@ -10,6 +10,7 @@ import {
} from 'fabric';
import { jsPDF as JsPdf } from 'jspdf';
import { mountFlowText, normalizeFlowContent } from './canvas-flow.mjs';
import { selectMagicNotePages } from '../../../shared/magic-note-pages.mjs';
const VERSION = 2;
const MAX_PAGES = 50;
@@ -1608,7 +1609,7 @@ export function mountCanvasNote(host, initialContent, options = {}) {
fabricCanvas.discardActiveObject();
applyMode();
},
async capturePages({ firstPageOnly = false, thumbnailWidth } = {}) {
async capturePages({ firstPageOnly = false, thumbnailWidth, pageLimit, includeImages = true } = {}) {
await operationQueue;
flushPendingText();
captureCurrentObjects();
@@ -1619,9 +1620,15 @@ export function mountCanvasNote(host, initialContent, options = {}) {
setBusy(true);
try {
const result = [];
for (let index = 0; index < (firstPageOnly ? Math.min(1, pages.length) : pages.length); index += 1) {
const selected = pageLimit === undefined ? pages : selectMagicNotePages(pages, pageLimit);
for (let index = 0; index < (firstPageOnly ? Math.min(1, selected.length) : selected.length); index += 1) {
if (destroyed || options.signal?.aborted) throw new DOMException('Destroyed', 'AbortError');
const page = pages[index];
const text = pageLimit === undefined ? undefined : flowEditor.pageText(index);
if (!includeImages) {
result.push({ pageId: page.id, dataUrl: '', text });
continue;
}
const composite = document.createElement('canvas');
await paintPageBackground(composite, page);
const context = composite.getContext('2d');
@@ -1641,7 +1648,7 @@ export function mountCanvasNote(host, initialContent, options = {}) {
output.height = Math.round(page.height * thumbnailWidth / page.width);
output.getContext('2d').drawImage(composite, 0, 0, output.width, output.height);
}
result.push({ pageId: page.id, dataUrl: output.toDataURL('image/png') });
result.push({ pageId: page.id, dataUrl: output.toDataURL('image/png'), ...(text !== undefined ? { text } : {}) });
} finally {
await objectCanvas.dispose();
}