@embedpdf/plugin-page-edit 3.0.0-next.15 → 3.0.0-next.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{contract-dl4LfrRX.d.ts → contract-IN8rk5LG.d.cts} +41 -30
- package/dist/{contract-dl4LfrRX.d.cts → contract-IN8rk5LG.d.ts} +41 -30
- package/dist/contract.d.cts +2 -2
- package/dist/contract.d.ts +2 -2
- package/dist/index.cjs +218 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -6
- package/dist/index.d.ts +20 -6
- package/dist/index.js +219 -90
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["PluginError","DocumentsToken","PageEditToken"],"sources":["../src/controller.ts","../src/page-edit.plugin.ts"],"sourcesContent":["/**\n * The page-edit controller. Stateless: it turns the engine handle's page\n * service into page edits that take refs or indexes. Turning a page relative\n * to its own rotation, and fixing a placement's page, live here once instead\n * of in every app's click handler.\n *\n * A verb refuses before anything starts (permission, pages, placement), then\n * queues its edit: edits run one at a time, in the order they were called.\n */\nimport { DocumentsToken, PluginError, toPluginError } from '@embedpdf/core';\nimport type {\n DocCapability,\n PageInfo,\n PagePosition,\n PageRef,\n PdfRotation,\n PdfSize,\n PluginContext,\n} from '@embedpdf/core';\n\nimport type { PageEditCapability, PageEditInsertResult, PagePlacement } from './contract';\n\n/** Inserting, rotating and deleting pages (PDF permission bit 4 or 11). The engine enforces it too. */\nconst ASSEMBLE: DocCapability = 'doc.pages.assemble';\n/** Copying pages out of a document is a partial download. The engine enforces it too. */\nconst DOWNLOAD: DocCapability = 'doc.download';\n\n/** The size of a blank page when the document has no page to match: Letter, in points. */\nconst LETTER_SIZE: PdfSize = { width: 612, height: 792 };\n\nexport function createPageEditController(ctx: PluginContext<void>) {\n const enqueue = ctx.serialQueue('edits');\n // A download waits for the page edits on their way.\n ctx.onSettle(() => enqueue.idle());\n\n /** Engine calls outside the guarded `ctx.doc` (another document, a scratch document) map their errors here. */\n const mapErrors = async <T>(work: () => Promise<T>): Promise<T> => {\n try {\n return await work();\n } catch (error) {\n throw toPluginError('page-edit', error);\n }\n };\n const registry = (): readonly PageInfo[] => ctx.document()?.pages ?? [];\n const noPages = () => new PluginError('invalid-input', 'page-edit', 'no pages given');\n\n /**\n * A verb's pages as refs, checked when it is called: an index names the\n * page at that position now, which is the page the user clicked.\n */\n const refsOf = (pages: readonly (PageRef | number)[]): PageRef[] => {\n if (pages.length === 0) throw noPages();\n return pages.map((page) => ctx.pageOf(page).ref);\n };\n\n /**\n * A placement as the engine's position, its page fixed when the verb is\n * called, like the verb's own pages. Default `'end'`.\n */\n const positionOf = (placement: PagePlacement | undefined): PagePosition => {\n if (!placement || placement === 'start' || placement === 'end') return placement ?? 'end';\n if ('after' in placement) return { after: ctx.pageOf(placement.after).ref };\n return { before: ctx.pageOf(placement.before).ref };\n };\n /**\n * Default blank-page size: the page the placement names, else the page the\n * new ones go next to (the first or the last), else Letter.\n */\n const sizeAt = (position: PagePosition): PdfSize => {\n if (typeof position === 'object') {\n return ctx.pageOf('after' in position ? position.after : position.before).size;\n }\n const pages = registry();\n if (pages.length === 0) return LETTER_SIZE;\n return (position === 'start' ? pages[0] : pages[pages.length - 1]).size;\n };\n\n /** Insert a PDF's pages; resolves the new pages. */\n const insertBytes = async (\n bytes: Uint8Array | ArrayBuffer,\n position: PagePosition,\n signal: AbortSignal | undefined,\n ): Promise<PageEditInsertResult> => {\n const result = await ctx.cancellable(signal, ctx.doc.pages.insert(bytes, position));\n return { pages: result.insertedPages };\n };\n\n const api: PageEditCapability = {\n canEdit: () => ctx.allows(ASSEMBLE),\n canExtract: () => ctx.allows(DOWNLOAD),\n\n rotateBy: async (pages, delta, options) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.rotateBy');\n const refs = refsOf(pages);\n const signal = options?.signal;\n await enqueue(async () => {\n // The engine sets one rotation per call, so pages are grouped by where\n // they end up. Each page's rotation is read when the edit runs, after\n // the edits queued before it; the double modulo keeps -90 from 0 at 270.\n const groups = new Map<PdfRotation, PageRef[]>();\n for (const ref of refs) {\n const turned = (ctx.pageOf(ref).rotation + delta) % 360;\n const rotation = ((turned + 360) % 360) as PdfRotation;\n groups.set(rotation, [...(groups.get(rotation) ?? []), ref]);\n }\n for (const [rotation, group] of groups) {\n await ctx.cancellable(signal, ctx.doc.pages.rotate(group, rotation));\n }\n }, options);\n },\n\n setRotation: async (pages, rotation, options) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.setRotation');\n const refs = refsOf(pages);\n await enqueue(\n () => ctx.cancellable(options?.signal, ctx.doc.pages.rotate(refs, rotation)),\n options,\n );\n },\n\n reorder: async (pages, placement, options) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.reorder');\n const refs = refsOf(pages);\n const position = positionOf(placement);\n await enqueue(\n () => ctx.cancellable(options?.signal, ctx.doc.pages.reorder(refs, position)),\n options,\n );\n },\n\n delete: async (pages, options) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.delete');\n const refs = refsOf(pages);\n const deleting = new Set(refs.map((ref) => ref.objectNumber));\n if (registry().every((page) => deleting.has(page.ref.objectNumber))) {\n throw new PluginError('invalid-input', 'page-edit', 'a document keeps at least one page');\n }\n await enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.delete(refs)), options);\n },\n\n insertBlank: async (options = {}) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.insertBlank');\n const position = positionOf(options.placement);\n return enqueue(async () => {\n const size = options.size ?? sizeAt(position);\n const result = await ctx.cancellable(\n options.signal,\n ctx.doc.pages.insertBlank({ size, count: options.count }, position),\n );\n return { pages: result.insertedPages };\n }, options);\n },\n\n insertFromBytes: async (bytes, options = {}) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.insertFromBytes');\n const position = positionOf(options.placement);\n const { pageIndexes, signal } = options;\n if (pageIndexes?.length === 0) throw noPages();\n return enqueue(async () => {\n if (!pageIndexes) return insertBytes(bytes, position, signal);\n // Some of the pages: open the bytes beside the document, extract them, insert those.\n const source = await mapErrors(() =>\n ctx.engine.open(\n {\n kind: 'bytes',\n id: `page-edit-source-${Date.now().toString(36)}`,\n bytes: new Uint8Array(bytes),\n },\n { scope: ['*'] },\n ),\n );\n try {\n const layout = await mapErrors(() => ctx.cancellable(signal, source.pages.list()));\n const refs = pageIndexes.map((index) => {\n const page = layout.pages[index];\n if (!page) {\n throw new PluginError('not-found', 'page-edit', `the PDF has no page ${index}`);\n }\n return page.ref;\n });\n const subset = await mapErrors(() => ctx.cancellable(signal, source.pages.extract(refs)));\n return await insertBytes(subset, position, signal);\n } finally {\n await source.close();\n }\n }, options);\n },\n\n insertFromDocument: async (documentId, pages, options = {}) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.insertFromDocument');\n if (pages.length === 0) throw noPages();\n const other = ctx.documentHandle(documentId);\n if (!other) {\n throw new PluginError('not-found', 'page-edit', `document '${documentId}' is not open`);\n }\n // The pages are the other document's, so its page list resolves them.\n const otherPages = ctx.get(DocumentsToken).listPages(documentId);\n const refs = pages.map((page) => {\n const found =\n typeof page === 'number'\n ? otherPages[page]\n : otherPages.find((candidate) => candidate.ref.objectNumber === page.objectNumber);\n if (!found) {\n throw new PluginError(\n 'not-found',\n 'page-edit',\n `document '${documentId}' has no such page`,\n );\n }\n return found.ref;\n });\n const position = positionOf(options.placement);\n return enqueue(async () => {\n // The other document's session decides whether its pages may be copied\n // out: the engine refuses `doc.download` there with the permission named.\n const bytes = await mapErrors(() =>\n ctx.cancellable(options.signal, other.pages.extract(refs)),\n );\n return insertBytes(bytes, position, options.signal);\n }, options);\n },\n\n duplicate: async (pages, options = {}) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.duplicate');\n ctx.assertAllowed(DOWNLOAD, 'pageEdit.duplicate');\n const refs = refsOf(pages);\n const position = options.placement\n ? positionOf(options.placement)\n : { after: refs[refs.length - 1]! };\n return enqueue(async () => {\n const bytes = await ctx.cancellable(options.signal, ctx.doc.pages.extract(refs));\n return insertBytes(bytes, position, options.signal);\n }, options);\n },\n\n extract: async (pages, options) => {\n ctx.assertAllowed(DOWNLOAD, 'pageEdit.extract');\n const refs = refsOf(pages);\n return enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.extract(refs)), options);\n },\n };\n return { api };\n}\n","import { definePlugin } from '@embedpdf/core';\n\nimport { PageEditToken } from './contract';\nimport { createPageEditController } from './controller';\n\n/**\n * Document-scoped and stateless: the engine handle's page service as page\n * edits that take refs or indexes. Turning a page relative to its own\n * rotation lives in the controller, so no app re-derives it.\n */\nexport const pageEditPlugin = () =>\n definePlugin({\n id: 'page-edit',\n scope: 'document',\n token: PageEditToken,\n create: createPageEditController,\n });\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["PluginError","DocumentsToken","PageEditToken","PageEditToken"],"sources":["../src/model.ts","../src/controller.ts","../src/page-edit.plugin.ts","../src/state.ts"],"sourcesContent":["/**\n * The page-edit session state: which pages are selected. A plain list of\n * refs, changed only by the transitions here; the controller reads it over the\n * document's pages, so a page that went leaves the selection.\n */\nimport type { PageRef } from '@embedpdf/core';\n\nexport interface PageEditState {\n /** The selected pages, in the order they were selected. */\n readonly selected: readonly PageRef[];\n}\n\nexport const initialPageEditState = (): PageEditState => ({ selected: [] });\n\n/** The selection becomes `refs`, each page once. */\nexport function selected(state: PageEditState, refs: readonly PageRef[]): PageEditState {\n const numbers = refs.map((ref) => ref.objectNumber);\n const unique = refs.filter((_ref, i) => numbers.indexOf(numbers[i]!) === i);\n const same =\n unique.length === state.selected.length &&\n unique.every((ref, i) => ref.objectNumber === state.selected[i]!.objectNumber);\n return same ? state : { ...state, selected: unique };\n}\n","/**\n * The page-edit controller: page edits that take refs or indexes, and the\n * page selection. Turning a page relative to its own rotation, and fixing a\n * position's page, live here once instead of in every app's click handler.\n *\n * A verb refuses before anything starts (permission, pages, position), then\n * stages its edit as one change on the document's queue (`ctx.changes`): one\n * step of the history, sent after everything staged before it. The page list\n * shows it once the engine answers; nothing predicts pages yet.\n */\nimport { memo, PluginError, toPluginError, DocumentsToken } from '@embedpdf/core';\nimport type {\n ChangeLabel,\n DocCapability,\n OperationOptions,\n PageLayout,\n PagePosition,\n PageRef,\n PdfRotation,\n PdfSize,\n PendingChange,\n PluginContext,\n PredictedOp,\n} from '@embedpdf/core';\n\nimport type { PageEditCapability, PageEditInsertResult } from './contract';\nimport { selected, type PageEditState } from './model';\n\n/** Inserting, rotating and deleting pages (PDF permission bit 4 or 11). The engine enforces it too. */\nconst ASSEMBLE: DocCapability = 'doc.pages.assemble';\n/** Copying pages out of a document is a partial download. The engine enforces it too. */\nconst DOWNLOAD: DocCapability = 'doc.download';\n\n/** The size of a blank page when the document has no page to match: Letter, in points. */\nconst LETTER_SIZE: PdfSize = { width: 612, height: 792 };\n\nconst NO_PAGES: readonly PageRef[] = Object.freeze([]);\nconst NO_LAYOUTS: readonly PageLayout[] = Object.freeze([]);\n\n/** A page op, as a verb stages it. */\ntype PageOp = Extract<PredictedOp, { type: `pages.${string}` }>;\n\n/** What a change is called in history and refusals. */\ntype PageVerb = 'rotate' | 'reorder' | 'delete' | 'insertBlank' | 'insert' | 'duplicate';\n\nexport function createPageEditController(ctx: PluginContext<PageEditState>) {\n /** Engine calls outside the guarded `ctx.doc` (another document, a scratch document) map their errors here. */\n const mapErrors = async <T>(work: () => Promise<T>): Promise<T> => {\n try {\n return await work();\n } catch (error) {\n throw toPluginError('page-edit', error);\n }\n };\n const registry = (): readonly PageLayout[] => ctx.document()?.pages ?? NO_LAYOUTS;\n const noPages = () => new PluginError('invalid-input', 'page-edit', 'no pages given');\n\n /**\n * A verb's pages as refs, checked when it is called: an index names the\n * page at that position now, which is the page the user clicked.\n */\n const refsOf = (pages: readonly (PageRef | number)[]): PageRef[] => {\n if (pages.length === 0) throw noPages();\n return pages.map((page) => ctx.pageOf(page).ref);\n };\n\n /** A position, its page checked when the verb is called, like the verb's own pages. Default `'end'`. */\n const positionOf = (position: PagePosition | undefined): PagePosition => {\n if (!position || position === 'start' || position === 'end') return position ?? 'end';\n if ('after' in position) return { after: ctx.pageOf(position.after).ref };\n return { before: ctx.pageOf(position.before).ref };\n };\n /**\n * Default blank-page size: the page the position names, else the page the\n * new ones go next to (the first or the last), else Letter.\n */\n const sizeAt = (position: PagePosition): PdfSize => {\n if (typeof position === 'object') {\n return ctx.pageOf('after' in position ? position.after : position.before).size;\n }\n const pages = registry();\n if (pages.length === 0) return LETTER_SIZE;\n return (position === 'start' ? pages[0] : pages[pages.length - 1]).size;\n };\n\n /* ── the selection ─────────────────────────────────────────────────────── */\n\n const getSelected = memo(\n () => [registry(), ctx.state.get().selected] as const,\n (pages, refs) => {\n if (refs.length === 0) return NO_PAGES;\n const present = new Set(pages.map((page) => page.ref.objectNumber));\n return refs.filter((ref) => present.has(ref.objectNumber));\n },\n );\n const select = (pages: readonly (PageRef | number)[]): void => {\n const refs = pages.flatMap((page) => {\n const found = ctx.getPage(page);\n return found ? [found.ref] : [];\n });\n ctx.state.update(selected, refs);\n };\n\n /* ── writes ────────────────────────────────────────────────────────────── */\n\n /** Refuse before anything starts: without the right, or once the caller cancelled. */\n const refuse = (\n permission: DocCapability,\n operation: string,\n options: OperationOptions | undefined,\n ): void => {\n ctx.assertAllowed(permission, operation);\n if (options?.signal?.aborted) {\n throw new PluginError('operation-cancelled', 'page-edit', `${operation} was cancelled`);\n }\n };\n\n /**\n * Stage one user action: one change, one step of the history. Its label\n * counts the pages it acts on, when the verb knows how many.\n */\n const stageChange = (\n verb: PageVerb,\n count: number | null,\n ops: readonly PageOp[],\n undo?: readonly PageOp[],\n ): PendingChange => {\n const label: ChangeLabel =\n count === null ? { key: `pages.${verb}` } : { key: `pages.${verb}`, count };\n return ctx.changes.stage({ label, ops, ...(undo ? { undo } : {}) });\n };\n\n /** The engine's answer to a staged change: its first item. */\n const answer = async (change: PendingChange, options: OperationOptions | undefined) => {\n try {\n const result = await ctx.cancellable(options?.signal, change.result);\n return result.items[0]!;\n } catch (error) {\n throw toPluginError('page-edit', error);\n }\n };\n\n /** What an insert's answer names: the new pages. */\n const inserted = async (\n change: PendingChange,\n options: OperationOptions | undefined,\n ): Promise<PageEditInsertResult> => {\n const item = await answer(change, options);\n return { pages: 'insertedPages' in item ? item.insertedPages : NO_PAGES };\n };\n\n /**\n * A page's rotation once this session's changes on their way apply: the\n * last rotation one of them gives it, else the page's own. What a turn\n * starts from, so two quick quarter turns are a half turn.\n */\n const rotationOf = (ref: PageRef): PdfRotation => {\n let rotation = ctx.pageOf(ref).rotation;\n for (const change of ctx.changes.pending()) {\n for (const op of change.shows) {\n if (op.type !== 'pages.rotate') continue;\n if (op.pages.some((page) => page.objectNumber === ref.objectNumber)) {\n rotation = op.rotation;\n }\n }\n }\n return rotation;\n };\n\n /**\n * Rotate pages as one change: one op per rotation they end up with. What\n * undoing it looks like is each page turned back, so a turn right after an\n * undo starts from where the undo leaves the page.\n */\n const rotate = (refs: readonly PageRef[], rotationAfter: (ref: PageRef) => PdfRotation) => {\n const after = new Map<PdfRotation, PageRef[]>();\n const before = new Map<PdfRotation, PageRef[]>();\n for (const ref of refs) {\n const add = (groups: Map<PdfRotation, PageRef[]>, rotation: PdfRotation) =>\n groups.set(rotation, [...(groups.get(rotation) ?? []), ref]);\n add(before, rotationOf(ref));\n add(after, rotationAfter(ref));\n }\n const opsOf = (groups: Map<PdfRotation, PageRef[]>): PageOp[] =>\n [...groups].map(([rotation, pages]) => ({ type: 'pages.rotate', pages, rotation }));\n return stageChange('rotate', refs.length, opsOf(after), opsOf(before));\n };\n\n /**\n * The pages as they show: every change on its way sent and answered first,\n * so a copy holds the edits made before it.\n */\n const settled = async (options: OperationOptions | undefined): Promise<void> => {\n ctx.changes.sendHolds();\n await ctx.cancellable(options?.signal, ctx.changes.whenSettled());\n };\n\n /** Copy pages of this document out, as they show. */\n const extractShown = async (\n refs: readonly PageRef[],\n options: OperationOptions | undefined,\n ): Promise<Uint8Array> => {\n await settled(options);\n return ctx.cancellable(options?.signal, ctx.doc.pages.extract([...refs]));\n };\n\n /** Stage the insert of a PDF's pages; resolves the new pages. */\n const insertBytes = (\n verb: 'insert' | 'duplicate',\n count: number | null,\n bytes: Uint8Array | ArrayBuffer,\n position: PagePosition,\n options: OperationOptions | undefined,\n ): Promise<PageEditInsertResult> => {\n const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n const change = stageChange(verb, count, [{ type: 'pages.insert', source, position }]);\n return inserted(change, options);\n };\n\n /** Some pages of a PDF: open the bytes beside the document and extract them. */\n const pagesOfBytes = async (\n bytes: Uint8Array | ArrayBuffer,\n pageIndexes: readonly number[],\n signal: AbortSignal | undefined,\n ): Promise<Uint8Array> => {\n const source = await mapErrors(() =>\n ctx.engine.open(\n {\n kind: 'bytes',\n id: `page-edit-source-${Date.now().toString(36)}`,\n bytes: new Uint8Array(bytes),\n },\n { scope: ['*'] },\n ),\n );\n try {\n const layout = await mapErrors(() => ctx.cancellable(signal, source.pages.list()));\n const refs = pageIndexes.map((index) => {\n const page = layout.pages[index];\n if (!page) {\n throw new PluginError('not-found', 'page-edit', `the PDF has no page ${index}`);\n }\n return page.ref;\n });\n return await mapErrors(() => ctx.cancellable(signal, source.pages.extract(refs)));\n } finally {\n await source.close();\n }\n };\n\n const api: PageEditCapability = {\n canEdit: () => ctx.allows(ASSEMBLE),\n canExtract: () => ctx.allows(DOWNLOAD),\n\n getSelected,\n select,\n\n rotateBy: async (pages, delta, options) => {\n refuse(ASSEMBLE, 'pageEdit.rotateBy', options);\n const refs = refsOf(pages);\n // The double modulo keeps -90 from 0 at 270.\n const change = rotate(\n refs,\n (ref) => ((((rotationOf(ref) + delta) % 360) + 360) % 360) as PdfRotation,\n );\n await answer(change, options);\n },\n\n setRotation: async (pages, rotation, options) => {\n refuse(ASSEMBLE, 'pageEdit.setRotation', options);\n const change = rotate(refsOf(pages), () => rotation);\n await answer(change, options);\n },\n\n reorder: async (pages, position, options) => {\n refuse(ASSEMBLE, 'pageEdit.reorder', options);\n const refs = refsOf(pages);\n const change = stageChange('reorder', refs.length, [\n { type: 'pages.reorder', pages: refs, position: positionOf(position) },\n ]);\n await answer(change, options);\n },\n\n delete: async (pages, options) => {\n refuse(ASSEMBLE, 'pageEdit.delete', options);\n const refs = refsOf(pages);\n const deleting = new Set(refs.map((ref) => ref.objectNumber));\n if (registry().every((page) => deleting.has(page.ref.objectNumber))) {\n throw new PluginError('invalid-input', 'page-edit', 'a document keeps at least one page');\n }\n const change = stageChange('delete', refs.length, [{ type: 'pages.delete', pages: refs }]);\n await answer(change, options);\n },\n\n insertBlank: async (options = {}) => {\n refuse(ASSEMBLE, 'pageEdit.insertBlank', options);\n const position = positionOf(options.position);\n const count = options.count ?? 1;\n const size = options.size ?? sizeAt(position);\n const change = stageChange('insertBlank', count, [\n { type: 'pages.insertBlank', spec: { size, count }, position },\n ]);\n return inserted(change, options);\n },\n\n insertFromBytes: async (bytes, options = {}) => {\n refuse(ASSEMBLE, 'pageEdit.insertFromBytes', options);\n const position = positionOf(options.position);\n const { pageIndexes, signal } = options;\n if (pageIndexes?.length === 0) throw noPages();\n const source = pageIndexes ? await pagesOfBytes(bytes, pageIndexes, signal) : bytes;\n return insertBytes('insert', pageIndexes?.length ?? null, source, position, options);\n },\n\n insertFromDocument: async (documentId, pages, options = {}) => {\n refuse(ASSEMBLE, 'pageEdit.insertFromDocument', options);\n if (pages.length === 0) throw noPages();\n const other = ctx.documentHandle(documentId);\n if (!other) {\n throw new PluginError('not-found', 'page-edit', `document '${documentId}' is not open`);\n }\n // The pages are the other document's, so its page list resolves them.\n const otherPages = ctx.get(DocumentsToken).listPages(documentId);\n const refs = pages.map((page) => {\n const found =\n typeof page === 'number'\n ? otherPages[page]\n : otherPages.find((candidate) => candidate.ref.objectNumber === page.objectNumber);\n if (!found) {\n throw new PluginError(\n 'not-found',\n 'page-edit',\n `document '${documentId}' has no such page`,\n );\n }\n return found.ref;\n });\n const position = positionOf(options.position);\n // The other document's session decides whether its pages may be copied\n // out: the engine refuses `doc.download` there with the permission named.\n const bytes = await mapErrors(() =>\n ctx.cancellable(options.signal, other.pages.extract(refs)),\n );\n return insertBytes('insert', refs.length, bytes, position, options);\n },\n\n duplicate: async (pages, options = {}) => {\n refuse(ASSEMBLE, 'pageEdit.duplicate', options);\n ctx.assertAllowed(DOWNLOAD, 'pageEdit.duplicate');\n const refs = refsOf(pages);\n const position = options.position\n ? positionOf(options.position)\n : { after: refs[refs.length - 1]! };\n const bytes = await extractShown(refs, options);\n return insertBytes('duplicate', refs.length, bytes, position, options);\n },\n\n extract: async (pages, options) => {\n refuse(DOWNLOAD, 'pageEdit.extract', options);\n return extractShown(refsOf(pages), options);\n },\n };\n return { api };\n}\n","import { definePlugin } from '@embedpdf/core';\n\nimport { PageEditToken } from './contract';\nimport { createPageEditController } from './controller';\nimport { initialPageEditState } from './model';\n\n/**\n * Document-scoped: the document's page edits, each one change of its\n * history, and which of its pages are selected. Turning a page relative to\n * its own rotation lives in the controller, so no app re-derives it.\n */\nexport const pageEditPlugin = () =>\n definePlugin({\n id: 'page-edit',\n scope: 'document',\n token: PageEditToken,\n state: initialPageEditState,\n create: createPageEditController,\n });\n","/**\n * The pages page's State table as code: what `usePageEditState()` returns,\n * and the same fields in every other framework.\n */\nimport { defineState } from '@embedpdf/core';\n\nimport { PageEditToken } from './contract';\n\nexport const pageEditState = defineState(PageEditToken, {\n read: (pageEdit) => ({\n selected: pageEdit.getSelected(),\n canEdit: pageEdit.canEdit(),\n canExtract: pageEdit.canExtract(),\n }),\n empty: {\n selected: [],\n canEdit: false,\n canExtract: false,\n },\n});\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as PagePosition, c as PdfSize, i as PageInsertFromBytesOptions, l as PageEditToken, n as PageEditInsertResult, o as PagePositionOptions, r as PageInsertBlankOptions, s as PdfRotation, t as PageEditCapability } from "./contract-IN8rk5LG.cjs";
|
|
2
|
+
import { PageRef } from "@embedpdf/core";
|
|
3
|
+
//#region src/model.d.ts
|
|
4
|
+
interface PageEditState {
|
|
5
|
+
/** The selected pages, in the order they were selected. */
|
|
6
|
+
readonly selected: readonly PageRef[];
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
2
9
|
//#region src/page-edit.plugin.d.ts
|
|
3
10
|
/**
|
|
4
|
-
* Document-scoped
|
|
5
|
-
*
|
|
6
|
-
* rotation lives in the controller, so no app re-derives it.
|
|
11
|
+
* Document-scoped: the document's page edits, each one change of its
|
|
12
|
+
* history, and which of its pages are selected. Turning a page relative to
|
|
13
|
+
* its own rotation lives in the controller, so no app re-derives it.
|
|
7
14
|
*/
|
|
8
|
-
declare const pageEditPlugin: () => import("@embedpdf/core").PluginDef<
|
|
15
|
+
declare const pageEditPlugin: () => import("@embedpdf/core").PluginDef<PageEditState, PageEditCapability, import("@embedpdf/core").NoSettings>;
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/state.d.ts
|
|
18
|
+
declare const pageEditState: import("@embedpdf/core").StateDeclaration<PageEditCapability, {
|
|
19
|
+
selected: readonly import("@embedpdf/core").PageRef[];
|
|
20
|
+
canEdit: boolean;
|
|
21
|
+
canExtract: boolean;
|
|
22
|
+
}>;
|
|
9
23
|
//#endregion
|
|
10
|
-
export { PageEditCapability, PageEditInsertResult, PageEditToken, PageInsertBlankOptions, PageInsertFromBytesOptions,
|
|
24
|
+
export { PageEditCapability, PageEditInsertResult, PageEditToken, PageInsertBlankOptions, PageInsertFromBytesOptions, type PagePosition, PagePositionOptions, type PdfRotation, type PdfSize, pageEditPlugin, pageEditState };
|
|
11
25
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as PagePosition, c as PdfSize, i as PageInsertFromBytesOptions, l as PageEditToken, n as PageEditInsertResult, o as PagePositionOptions, r as PageInsertBlankOptions, s as PdfRotation, t as PageEditCapability } from "./contract-IN8rk5LG.js";
|
|
2
|
+
import { PageRef } from "@embedpdf/core";
|
|
3
|
+
//#region src/model.d.ts
|
|
4
|
+
interface PageEditState {
|
|
5
|
+
/** The selected pages, in the order they were selected. */
|
|
6
|
+
readonly selected: readonly PageRef[];
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
2
9
|
//#region src/page-edit.plugin.d.ts
|
|
3
10
|
/**
|
|
4
|
-
* Document-scoped
|
|
5
|
-
*
|
|
6
|
-
* rotation lives in the controller, so no app re-derives it.
|
|
11
|
+
* Document-scoped: the document's page edits, each one change of its
|
|
12
|
+
* history, and which of its pages are selected. Turning a page relative to
|
|
13
|
+
* its own rotation lives in the controller, so no app re-derives it.
|
|
7
14
|
*/
|
|
8
|
-
declare const pageEditPlugin: () => import("@embedpdf/core").PluginDef<
|
|
15
|
+
declare const pageEditPlugin: () => import("@embedpdf/core").PluginDef<PageEditState, PageEditCapability, import("@embedpdf/core").NoSettings>;
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/state.d.ts
|
|
18
|
+
declare const pageEditState: import("@embedpdf/core").StateDeclaration<PageEditCapability, {
|
|
19
|
+
selected: readonly import("@embedpdf/core").PageRef[];
|
|
20
|
+
canEdit: boolean;
|
|
21
|
+
canExtract: boolean;
|
|
22
|
+
}>;
|
|
9
23
|
//#endregion
|
|
10
|
-
export { PageEditCapability, PageEditInsertResult, PageEditToken, PageInsertBlankOptions, PageInsertFromBytesOptions,
|
|
24
|
+
export { PageEditCapability, PageEditInsertResult, PageEditToken, PageInsertBlankOptions, PageInsertFromBytesOptions, type PagePosition, PagePositionOptions, type PdfRotation, type PdfSize, pageEditPlugin, pageEditState };
|
|
11
25
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { t as PageEditToken } from "./contract-CYWYR5Pd.js";
|
|
2
|
-
import { DocumentsToken, PluginError, definePlugin, toPluginError } from "@embedpdf/core";
|
|
2
|
+
import { DocumentsToken, PluginError, definePlugin, defineState, memo, toPluginError } from "@embedpdf/core";
|
|
3
|
+
//#region src/model.ts
|
|
4
|
+
const initialPageEditState = () => ({ selected: [] });
|
|
5
|
+
/** The selection becomes `refs`, each page once. */
|
|
6
|
+
function selected(state, refs) {
|
|
7
|
+
const numbers = refs.map((ref) => ref.objectNumber);
|
|
8
|
+
const unique = refs.filter((_ref, i) => numbers.indexOf(numbers[i]) === i);
|
|
9
|
+
return unique.length === state.selected.length && unique.every((ref, i) => ref.objectNumber === state.selected[i].objectNumber) ? state : {
|
|
10
|
+
...state,
|
|
11
|
+
selected: unique
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
3
15
|
//#region src/controller.ts
|
|
4
16
|
/**
|
|
5
|
-
* The page-edit controller
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* of in every app's click handler.
|
|
17
|
+
* The page-edit controller: page edits that take refs or indexes, and the
|
|
18
|
+
* page selection. Turning a page relative to its own rotation, and fixing a
|
|
19
|
+
* position's page, live here once instead of in every app's click handler.
|
|
9
20
|
*
|
|
10
|
-
* A verb refuses before anything starts (permission, pages,
|
|
11
|
-
*
|
|
21
|
+
* A verb refuses before anything starts (permission, pages, position), then
|
|
22
|
+
* stages its edit as one change on the document's queue (`ctx.changes`): one
|
|
23
|
+
* step of the history, sent after everything staged before it. The page list
|
|
24
|
+
* shows it once the engine answers; nothing predicts pages yet.
|
|
12
25
|
*/
|
|
13
26
|
/** Inserting, rotating and deleting pages (PDF permission bit 4 or 11). The engine enforces it too. */
|
|
14
27
|
const ASSEMBLE = "doc.pages.assemble";
|
|
@@ -19,9 +32,9 @@ const LETTER_SIZE = {
|
|
|
19
32
|
width: 612,
|
|
20
33
|
height: 792
|
|
21
34
|
};
|
|
35
|
+
const NO_PAGES = Object.freeze([]);
|
|
36
|
+
const NO_LAYOUTS = Object.freeze([]);
|
|
22
37
|
function createPageEditController(ctx) {
|
|
23
|
-
const enqueue = ctx.serialQueue("edits");
|
|
24
|
-
ctx.onSettle(() => enqueue.idle());
|
|
25
38
|
/** Engine calls outside the guarded `ctx.doc` (another document, a scratch document) map their errors here. */
|
|
26
39
|
const mapErrors = async (work) => {
|
|
27
40
|
try {
|
|
@@ -30,7 +43,7 @@ function createPageEditController(ctx) {
|
|
|
30
43
|
throw toPluginError("page-edit", error);
|
|
31
44
|
}
|
|
32
45
|
};
|
|
33
|
-
const registry = () => ctx.document()?.pages ??
|
|
46
|
+
const registry = () => ctx.document()?.pages ?? NO_LAYOUTS;
|
|
34
47
|
const noPages = () => new PluginError("invalid-input", "page-edit", "no pages given");
|
|
35
48
|
/**
|
|
36
49
|
* A verb's pages as refs, checked when it is called: an index names the
|
|
@@ -40,17 +53,14 @@ function createPageEditController(ctx) {
|
|
|
40
53
|
if (pages.length === 0) throw noPages();
|
|
41
54
|
return pages.map((page) => ctx.pageOf(page).ref);
|
|
42
55
|
};
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!placement || placement === "start" || placement === "end") return placement ?? "end";
|
|
49
|
-
if ("after" in placement) return { after: ctx.pageOf(placement.after).ref };
|
|
50
|
-
return { before: ctx.pageOf(placement.before).ref };
|
|
56
|
+
/** A position, its page checked when the verb is called, like the verb's own pages. Default `'end'`. */
|
|
57
|
+
const positionOf = (position) => {
|
|
58
|
+
if (!position || position === "start" || position === "end") return position ?? "end";
|
|
59
|
+
if ("after" in position) return { after: ctx.pageOf(position.after).ref };
|
|
60
|
+
return { before: ctx.pageOf(position.before).ref };
|
|
51
61
|
};
|
|
52
62
|
/**
|
|
53
|
-
* Default blank-page size: the page the
|
|
63
|
+
* Default blank-page size: the page the position names, else the page the
|
|
54
64
|
* new ones go next to (the first or the last), else Letter.
|
|
55
65
|
*/
|
|
56
66
|
const sizeAt = (position) => {
|
|
@@ -59,83 +69,188 @@ function createPageEditController(ctx) {
|
|
|
59
69
|
if (pages.length === 0) return LETTER_SIZE;
|
|
60
70
|
return (position === "start" ? pages[0] : pages[pages.length - 1]).size;
|
|
61
71
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
const getSelected = memo(() => [registry(), ctx.state.get().selected], (pages, refs) => {
|
|
73
|
+
if (refs.length === 0) return NO_PAGES;
|
|
74
|
+
const present = new Set(pages.map((page) => page.ref.objectNumber));
|
|
75
|
+
return refs.filter((ref) => present.has(ref.objectNumber));
|
|
76
|
+
});
|
|
77
|
+
const select = (pages) => {
|
|
78
|
+
const refs = pages.flatMap((page) => {
|
|
79
|
+
const found = ctx.getPage(page);
|
|
80
|
+
return found ? [found.ref] : [];
|
|
81
|
+
});
|
|
82
|
+
ctx.state.update(selected, refs);
|
|
83
|
+
};
|
|
84
|
+
/** Refuse before anything starts: without the right, or once the caller cancelled. */
|
|
85
|
+
const refuse = (permission, operation, options) => {
|
|
86
|
+
ctx.assertAllowed(permission, operation);
|
|
87
|
+
if (options?.signal?.aborted) throw new PluginError("operation-cancelled", "page-edit", `${operation} was cancelled`);
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Stage one user action: one change, one step of the history. Its label
|
|
91
|
+
* counts the pages it acts on, when the verb knows how many.
|
|
92
|
+
*/
|
|
93
|
+
const stageChange = (verb, count, ops, undo) => {
|
|
94
|
+
const label = count === null ? { key: `pages.${verb}` } : {
|
|
95
|
+
key: `pages.${verb}`,
|
|
96
|
+
count
|
|
97
|
+
};
|
|
98
|
+
return ctx.changes.stage({
|
|
99
|
+
label,
|
|
100
|
+
ops,
|
|
101
|
+
...undo ? { undo } : {}
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
/** The engine's answer to a staged change: its first item. */
|
|
105
|
+
const answer = async (change, options) => {
|
|
106
|
+
try {
|
|
107
|
+
return (await ctx.cancellable(options?.signal, change.result)).items[0];
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw toPluginError("page-edit", error);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
/** What an insert's answer names: the new pages. */
|
|
113
|
+
const inserted = async (change, options) => {
|
|
114
|
+
const item = await answer(change, options);
|
|
115
|
+
return { pages: "insertedPages" in item ? item.insertedPages : NO_PAGES };
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* A page's rotation once this session's changes on their way apply: the
|
|
119
|
+
* last rotation one of them gives it, else the page's own. What a turn
|
|
120
|
+
* starts from, so two quick quarter turns are a half turn.
|
|
121
|
+
*/
|
|
122
|
+
const rotationOf = (ref) => {
|
|
123
|
+
let rotation = ctx.pageOf(ref).rotation;
|
|
124
|
+
for (const change of ctx.changes.pending()) for (const op of change.shows) {
|
|
125
|
+
if (op.type !== "pages.rotate") continue;
|
|
126
|
+
if (op.pages.some((page) => page.objectNumber === ref.objectNumber)) rotation = op.rotation;
|
|
127
|
+
}
|
|
128
|
+
return rotation;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Rotate pages as one change: one op per rotation they end up with. What
|
|
132
|
+
* undoing it looks like is each page turned back, so a turn right after an
|
|
133
|
+
* undo starts from where the undo leaves the page.
|
|
134
|
+
*/
|
|
135
|
+
const rotate = (refs, rotationAfter) => {
|
|
136
|
+
const after = /* @__PURE__ */ new Map();
|
|
137
|
+
const before = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const ref of refs) {
|
|
139
|
+
const add = (groups, rotation) => groups.set(rotation, [...groups.get(rotation) ?? [], ref]);
|
|
140
|
+
add(before, rotationOf(ref));
|
|
141
|
+
add(after, rotationAfter(ref));
|
|
142
|
+
}
|
|
143
|
+
const opsOf = (groups) => [...groups].map(([rotation, pages]) => ({
|
|
144
|
+
type: "pages.rotate",
|
|
145
|
+
pages,
|
|
146
|
+
rotation
|
|
147
|
+
}));
|
|
148
|
+
return stageChange("rotate", refs.length, opsOf(after), opsOf(before));
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* The pages as they show: every change on its way sent and answered first,
|
|
152
|
+
* so a copy holds the edits made before it.
|
|
153
|
+
*/
|
|
154
|
+
const settled = async (options) => {
|
|
155
|
+
ctx.changes.sendHolds();
|
|
156
|
+
await ctx.cancellable(options?.signal, ctx.changes.whenSettled());
|
|
157
|
+
};
|
|
158
|
+
/** Copy pages of this document out, as they show. */
|
|
159
|
+
const extractShown = async (refs, options) => {
|
|
160
|
+
await settled(options);
|
|
161
|
+
return ctx.cancellable(options?.signal, ctx.doc.pages.extract([...refs]));
|
|
162
|
+
};
|
|
163
|
+
/** Stage the insert of a PDF's pages; resolves the new pages. */
|
|
164
|
+
const insertBytes = (verb, count, bytes, position, options) => {
|
|
165
|
+
const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
166
|
+
const change = stageChange(verb, count, [{
|
|
167
|
+
type: "pages.insert",
|
|
168
|
+
source,
|
|
169
|
+
position
|
|
170
|
+
}]);
|
|
171
|
+
return inserted(change, options);
|
|
172
|
+
};
|
|
173
|
+
/** Some pages of a PDF: open the bytes beside the document and extract them. */
|
|
174
|
+
const pagesOfBytes = async (bytes, pageIndexes, signal) => {
|
|
175
|
+
const source = await mapErrors(() => ctx.engine.open({
|
|
176
|
+
kind: "bytes",
|
|
177
|
+
id: `page-edit-source-${Date.now().toString(36)}`,
|
|
178
|
+
bytes: new Uint8Array(bytes)
|
|
179
|
+
}, { scope: ["*"] }));
|
|
180
|
+
try {
|
|
181
|
+
const layout = await mapErrors(() => ctx.cancellable(signal, source.pages.list()));
|
|
182
|
+
const refs = pageIndexes.map((index) => {
|
|
183
|
+
const page = layout.pages[index];
|
|
184
|
+
if (!page) throw new PluginError("not-found", "page-edit", `the PDF has no page ${index}`);
|
|
185
|
+
return page.ref;
|
|
186
|
+
});
|
|
187
|
+
return await mapErrors(() => ctx.cancellable(signal, source.pages.extract(refs)));
|
|
188
|
+
} finally {
|
|
189
|
+
await source.close();
|
|
190
|
+
}
|
|
65
191
|
};
|
|
66
192
|
return { api: {
|
|
67
193
|
canEdit: () => ctx.allows(ASSEMBLE),
|
|
68
194
|
canExtract: () => ctx.allows(DOWNLOAD),
|
|
195
|
+
getSelected,
|
|
196
|
+
select,
|
|
69
197
|
rotateBy: async (pages, delta, options) => {
|
|
70
|
-
|
|
198
|
+
refuse(ASSEMBLE, "pageEdit.rotateBy", options);
|
|
71
199
|
const refs = refsOf(pages);
|
|
72
|
-
const
|
|
73
|
-
await
|
|
74
|
-
const groups = /* @__PURE__ */ new Map();
|
|
75
|
-
for (const ref of refs) {
|
|
76
|
-
const rotation = ((ctx.pageOf(ref).rotation + delta) % 360 + 360) % 360;
|
|
77
|
-
groups.set(rotation, [...groups.get(rotation) ?? [], ref]);
|
|
78
|
-
}
|
|
79
|
-
for (const [rotation, group] of groups) await ctx.cancellable(signal, ctx.doc.pages.rotate(group, rotation));
|
|
80
|
-
}, options);
|
|
200
|
+
const change = rotate(refs, (ref) => ((rotationOf(ref) + delta) % 360 + 360) % 360);
|
|
201
|
+
await answer(change, options);
|
|
81
202
|
},
|
|
82
203
|
setRotation: async (pages, rotation, options) => {
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
await
|
|
204
|
+
refuse(ASSEMBLE, "pageEdit.setRotation", options);
|
|
205
|
+
const change = rotate(refsOf(pages), () => rotation);
|
|
206
|
+
await answer(change, options);
|
|
86
207
|
},
|
|
87
|
-
reorder: async (pages,
|
|
88
|
-
|
|
208
|
+
reorder: async (pages, position, options) => {
|
|
209
|
+
refuse(ASSEMBLE, "pageEdit.reorder", options);
|
|
89
210
|
const refs = refsOf(pages);
|
|
90
|
-
const
|
|
91
|
-
|
|
211
|
+
const change = stageChange("reorder", refs.length, [{
|
|
212
|
+
type: "pages.reorder",
|
|
213
|
+
pages: refs,
|
|
214
|
+
position: positionOf(position)
|
|
215
|
+
}]);
|
|
216
|
+
await answer(change, options);
|
|
92
217
|
},
|
|
93
218
|
delete: async (pages, options) => {
|
|
94
|
-
|
|
219
|
+
refuse(ASSEMBLE, "pageEdit.delete", options);
|
|
95
220
|
const refs = refsOf(pages);
|
|
96
221
|
const deleting = new Set(refs.map((ref) => ref.objectNumber));
|
|
97
222
|
if (registry().every((page) => deleting.has(page.ref.objectNumber))) throw new PluginError("invalid-input", "page-edit", "a document keeps at least one page");
|
|
98
|
-
|
|
223
|
+
const change = stageChange("delete", refs.length, [{
|
|
224
|
+
type: "pages.delete",
|
|
225
|
+
pages: refs
|
|
226
|
+
}]);
|
|
227
|
+
await answer(change, options);
|
|
99
228
|
},
|
|
100
229
|
insertBlank: async (options = {}) => {
|
|
101
|
-
|
|
102
|
-
const position = positionOf(options.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
230
|
+
refuse(ASSEMBLE, "pageEdit.insertBlank", options);
|
|
231
|
+
const position = positionOf(options.position);
|
|
232
|
+
const count = options.count ?? 1;
|
|
233
|
+
const size = options.size ?? sizeAt(position);
|
|
234
|
+
const change = stageChange("insertBlank", count, [{
|
|
235
|
+
type: "pages.insertBlank",
|
|
236
|
+
spec: {
|
|
106
237
|
size,
|
|
107
|
-
count
|
|
108
|
-
},
|
|
109
|
-
|
|
238
|
+
count
|
|
239
|
+
},
|
|
240
|
+
position
|
|
241
|
+
}]);
|
|
242
|
+
return inserted(change, options);
|
|
110
243
|
},
|
|
111
244
|
insertFromBytes: async (bytes, options = {}) => {
|
|
112
|
-
|
|
113
|
-
const position = positionOf(options.
|
|
245
|
+
refuse(ASSEMBLE, "pageEdit.insertFromBytes", options);
|
|
246
|
+
const position = positionOf(options.position);
|
|
114
247
|
const { pageIndexes, signal } = options;
|
|
115
248
|
if (pageIndexes?.length === 0) throw noPages();
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const source = await mapErrors(() => ctx.engine.open({
|
|
119
|
-
kind: "bytes",
|
|
120
|
-
id: `page-edit-source-${Date.now().toString(36)}`,
|
|
121
|
-
bytes: new Uint8Array(bytes)
|
|
122
|
-
}, { scope: ["*"] }));
|
|
123
|
-
try {
|
|
124
|
-
const layout = await mapErrors(() => ctx.cancellable(signal, source.pages.list()));
|
|
125
|
-
const refs = pageIndexes.map((index) => {
|
|
126
|
-
const page = layout.pages[index];
|
|
127
|
-
if (!page) throw new PluginError("not-found", "page-edit", `the PDF has no page ${index}`);
|
|
128
|
-
return page.ref;
|
|
129
|
-
});
|
|
130
|
-
const subset = await mapErrors(() => ctx.cancellable(signal, source.pages.extract(refs)));
|
|
131
|
-
return await insertBytes(subset, position, signal);
|
|
132
|
-
} finally {
|
|
133
|
-
await source.close();
|
|
134
|
-
}
|
|
135
|
-
}, options);
|
|
249
|
+
const source = pageIndexes ? await pagesOfBytes(bytes, pageIndexes, signal) : bytes;
|
|
250
|
+
return insertBytes("insert", pageIndexes?.length ?? null, source, position, options);
|
|
136
251
|
},
|
|
137
252
|
insertFromDocument: async (documentId, pages, options = {}) => {
|
|
138
|
-
|
|
253
|
+
refuse(ASSEMBLE, "pageEdit.insertFromDocument", options);
|
|
139
254
|
if (pages.length === 0) throw noPages();
|
|
140
255
|
const other = ctx.documentHandle(documentId);
|
|
141
256
|
if (!other) throw new PluginError("not-found", "page-edit", `document '${documentId}' is not open`);
|
|
@@ -145,43 +260,57 @@ function createPageEditController(ctx) {
|
|
|
145
260
|
if (!found) throw new PluginError("not-found", "page-edit", `document '${documentId}' has no such page`);
|
|
146
261
|
return found.ref;
|
|
147
262
|
});
|
|
148
|
-
const position = positionOf(options.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return insertBytes(bytes, position, options.signal);
|
|
152
|
-
}, options);
|
|
263
|
+
const position = positionOf(options.position);
|
|
264
|
+
const bytes = await mapErrors(() => ctx.cancellable(options.signal, other.pages.extract(refs)));
|
|
265
|
+
return insertBytes("insert", refs.length, bytes, position, options);
|
|
153
266
|
},
|
|
154
267
|
duplicate: async (pages, options = {}) => {
|
|
155
|
-
|
|
268
|
+
refuse(ASSEMBLE, "pageEdit.duplicate", options);
|
|
156
269
|
ctx.assertAllowed(DOWNLOAD, "pageEdit.duplicate");
|
|
157
270
|
const refs = refsOf(pages);
|
|
158
|
-
const position = options.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return insertBytes(bytes, position, options.signal);
|
|
162
|
-
}, options);
|
|
271
|
+
const position = options.position ? positionOf(options.position) : { after: refs[refs.length - 1] };
|
|
272
|
+
const bytes = await extractShown(refs, options);
|
|
273
|
+
return insertBytes("duplicate", refs.length, bytes, position, options);
|
|
163
274
|
},
|
|
164
275
|
extract: async (pages, options) => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.extract(refs)), options);
|
|
276
|
+
refuse(DOWNLOAD, "pageEdit.extract", options);
|
|
277
|
+
return extractShown(refsOf(pages), options);
|
|
168
278
|
}
|
|
169
279
|
} };
|
|
170
280
|
}
|
|
171
281
|
//#endregion
|
|
172
282
|
//#region src/page-edit.plugin.ts
|
|
173
283
|
/**
|
|
174
|
-
* Document-scoped
|
|
175
|
-
*
|
|
176
|
-
* rotation lives in the controller, so no app re-derives it.
|
|
284
|
+
* Document-scoped: the document's page edits, each one change of its
|
|
285
|
+
* history, and which of its pages are selected. Turning a page relative to
|
|
286
|
+
* its own rotation lives in the controller, so no app re-derives it.
|
|
177
287
|
*/
|
|
178
288
|
const pageEditPlugin = () => definePlugin({
|
|
179
289
|
id: "page-edit",
|
|
180
290
|
scope: "document",
|
|
181
291
|
token: PageEditToken,
|
|
292
|
+
state: initialPageEditState,
|
|
182
293
|
create: createPageEditController
|
|
183
294
|
});
|
|
184
295
|
//#endregion
|
|
185
|
-
|
|
296
|
+
//#region src/state.ts
|
|
297
|
+
/**
|
|
298
|
+
* The pages page's State table as code: what `usePageEditState()` returns,
|
|
299
|
+
* and the same fields in every other framework.
|
|
300
|
+
*/
|
|
301
|
+
const pageEditState = defineState(PageEditToken, {
|
|
302
|
+
read: (pageEdit) => ({
|
|
303
|
+
selected: pageEdit.getSelected(),
|
|
304
|
+
canEdit: pageEdit.canEdit(),
|
|
305
|
+
canExtract: pageEdit.canExtract()
|
|
306
|
+
}),
|
|
307
|
+
empty: {
|
|
308
|
+
selected: [],
|
|
309
|
+
canEdit: false,
|
|
310
|
+
canExtract: false
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
//#endregion
|
|
314
|
+
export { PageEditToken, pageEditPlugin, pageEditState };
|
|
186
315
|
|
|
187
316
|
//# sourceMappingURL=index.js.map
|