Skip to content

Commit d624231

Browse files
authored
feat(document-api): initial image commands (#2290)
* feat(document-api): initial image commands * chore: fix review comments, ooxml spec
1 parent 689f6b5 commit d624231

59 files changed

Lines changed: 8111 additions & 67 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/cli/scripts/export-sdk-contract.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,20 @@ const INTENT_NAMES = {
194194
'doc.history.get': 'get_history',
195195
'doc.history.undo': 'undo',
196196
'doc.history.redo': 'redo',
197+
'doc.create.image': 'create_image',
198+
'doc.images.list': 'list_images',
199+
'doc.images.get': 'get_image',
200+
'doc.images.delete': 'delete_image',
201+
'doc.images.move': 'move_image',
202+
'doc.images.convertToInline': 'convert_image_to_inline',
203+
'doc.images.convertToFloating': 'convert_image_to_floating',
204+
'doc.images.setSize': 'set_image_size',
205+
'doc.images.setWrapType': 'set_image_wrap_type',
206+
'doc.images.setWrapSide': 'set_image_wrap_side',
207+
'doc.images.setWrapDistances': 'set_image_wrap_distances',
208+
'doc.images.setPosition': 'set_image_position',
209+
'doc.images.setAnchorOptions': 'set_image_anchor_options',
210+
'doc.images.setZOrder': 'set_image_z_order',
197211
} as const satisfies Record<DocBackedCliOpId, string>;
198212

199213
// ---------------------------------------------------------------------------

apps/cli/src/__tests__/conformance/scenarios.ts

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,112 @@ async function createDocWithMarkedTocEntry(
559559
return { docPath: markedDoc, entryAddress };
560560
}
561561

562+
const CONFORMANCE_IMAGE_DATA_URI =
563+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=';
564+
565+
type ImagePlacement = 'inline' | 'floating';
566+
type ImageFixture = {
567+
docPath: string;
568+
imageId: string;
569+
};
570+
571+
function pickImageId(
572+
items: Record<string, unknown>[],
573+
context: string,
574+
placement?: ImagePlacement,
575+
): { imageId: string; item: Record<string, unknown> } {
576+
const match =
577+
placement === undefined
578+
? items[0]
579+
: (items.find((item) => {
580+
const address = item.address;
581+
if (!address || typeof address !== 'object') return false;
582+
return (address as Record<string, unknown>).placement === placement;
583+
}) ?? items[0]);
584+
585+
if (!match) {
586+
throw new Error(`[${context}] No images available.`);
587+
}
588+
589+
const imageId = match.sdImageId;
590+
if (typeof imageId !== 'string' || imageId.length === 0) {
591+
throw new Error(`[${context}] Unable to resolve image id from list output.`);
592+
}
593+
594+
return { imageId, item: match };
595+
}
596+
597+
async function resolveImageFixture(
598+
harness: ConformanceHarness,
599+
stateDir: string,
600+
docPath: string,
601+
context: string,
602+
placement?: ImagePlacement,
603+
): Promise<ImageFixture> {
604+
const listed = await harness.runCli([...commandTokens('doc.images.list'), docPath, '--limit', '20'], stateDir);
605+
if (listed.result.code !== 0 || listed.envelope.ok !== true) {
606+
throw new Error(`[${context}] Failed to list images.`);
607+
}
608+
609+
const items = extractDiscoveryItems(listed.envelope.data);
610+
const { imageId } = pickImageId(items, context, placement);
611+
return { docPath, imageId };
612+
}
613+
614+
async function createInlineImageFixture(
615+
harness: ConformanceHarness,
616+
stateDir: string,
617+
label: string,
618+
): Promise<ImageFixture> {
619+
const sourceDoc = await harness.copyFixtureDoc(`${label}-source`);
620+
const outputDoc = harness.createOutputPath(`${label}-with-image`);
621+
const created = await harness.runCli(
622+
[
623+
...commandTokens('doc.create.image'),
624+
sourceDoc,
625+
'--src',
626+
CONFORMANCE_IMAGE_DATA_URI,
627+
'--alt',
628+
'Conformance image',
629+
'--at-json',
630+
JSON.stringify({ kind: 'documentEnd' }),
631+
'--out',
632+
outputDoc,
633+
],
634+
stateDir,
635+
);
636+
if (created.result.code !== 0 || created.envelope.ok !== true) {
637+
throw new Error(`[${label}] Failed to create image fixture.`);
638+
}
639+
640+
return resolveImageFixture(harness, stateDir, outputDoc, `${label}:inline`, 'inline');
641+
}
642+
643+
async function createFloatingImageFixture(
644+
harness: ConformanceHarness,
645+
stateDir: string,
646+
label: string,
647+
): Promise<ImageFixture> {
648+
const inlineFixture = await createInlineImageFixture(harness, stateDir, `${label}-seed-inline`);
649+
const floatingDoc = harness.createOutputPath(`${label}-floating`);
650+
const converted = await harness.runCli(
651+
[
652+
...commandTokens('doc.images.convertToFloating'),
653+
inlineFixture.docPath,
654+
'--image-id',
655+
inlineFixture.imageId,
656+
'--out',
657+
floatingDoc,
658+
],
659+
stateDir,
660+
);
661+
if (converted.result.code !== 0 || converted.envelope.ok !== true) {
662+
throw new Error(`[${label}] Failed to convert fixture image to floating.`);
663+
}
664+
665+
return resolveImageFixture(harness, stateDir, floatingDoc, `${label}:floating`, 'floating');
666+
}
667+
562668
export const SUCCESS_SCENARIOS = {
563669
'doc.open': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
564670
const stateDir = await harness.createStateDir('doc-open-success');
@@ -1642,6 +1748,227 @@ export const SUCCESS_SCENARIOS = {
16421748
],
16431749
};
16441750
},
1751+
1752+
// ---------------------------------------------------------------------------
1753+
// Image operations
1754+
// ---------------------------------------------------------------------------
1755+
1756+
'doc.create.image': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1757+
const stateDir = await harness.createStateDir('doc-create-image-success');
1758+
const docPath = await harness.copyFixtureDoc('doc-create-image');
1759+
return {
1760+
stateDir,
1761+
args: [
1762+
...commandTokens('doc.create.image'),
1763+
docPath,
1764+
'--src',
1765+
CONFORMANCE_IMAGE_DATA_URI,
1766+
'--alt',
1767+
'Conformance image',
1768+
'--at-json',
1769+
JSON.stringify({ kind: 'documentEnd' }),
1770+
'--out',
1771+
harness.createOutputPath('doc-create-image-output'),
1772+
],
1773+
};
1774+
},
1775+
'doc.images.list': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1776+
const stateDir = await harness.createStateDir('doc-images-list-success');
1777+
const fixture = await createInlineImageFixture(harness, stateDir, 'doc-images-list');
1778+
return {
1779+
stateDir,
1780+
args: [...commandTokens('doc.images.list'), fixture.docPath, '--limit', '20'],
1781+
};
1782+
},
1783+
'doc.images.get': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1784+
const stateDir = await harness.createStateDir('doc-images-get-success');
1785+
const fixture = await createInlineImageFixture(harness, stateDir, 'doc-images-get');
1786+
return {
1787+
stateDir,
1788+
args: [...commandTokens('doc.images.get'), fixture.docPath, '--image-id', fixture.imageId],
1789+
};
1790+
},
1791+
'doc.images.delete': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1792+
const stateDir = await harness.createStateDir('doc-images-delete-success');
1793+
const fixture = await createInlineImageFixture(harness, stateDir, 'doc-images-delete');
1794+
return {
1795+
stateDir,
1796+
args: [
1797+
...commandTokens('doc.images.delete'),
1798+
fixture.docPath,
1799+
'--image-id',
1800+
fixture.imageId,
1801+
'--out',
1802+
harness.createOutputPath('doc-images-delete-output'),
1803+
],
1804+
};
1805+
},
1806+
'doc.images.move': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1807+
const stateDir = await harness.createStateDir('doc-images-move-success');
1808+
const fixture = await createInlineImageFixture(harness, stateDir, 'doc-images-move');
1809+
return {
1810+
stateDir,
1811+
args: [
1812+
...commandTokens('doc.images.move'),
1813+
fixture.docPath,
1814+
'--image-id',
1815+
fixture.imageId,
1816+
'--to-json',
1817+
JSON.stringify({ kind: 'documentStart' }),
1818+
'--out',
1819+
harness.createOutputPath('doc-images-move-output'),
1820+
],
1821+
};
1822+
},
1823+
'doc.images.convertToInline': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1824+
const stateDir = await harness.createStateDir('doc-images-convert-to-inline-success');
1825+
const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-convert-to-inline');
1826+
return {
1827+
stateDir,
1828+
args: [
1829+
...commandTokens('doc.images.convertToInline'),
1830+
fixture.docPath,
1831+
'--image-id',
1832+
fixture.imageId,
1833+
'--out',
1834+
harness.createOutputPath('doc-images-convert-to-inline-output'),
1835+
],
1836+
};
1837+
},
1838+
'doc.images.convertToFloating': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1839+
const stateDir = await harness.createStateDir('doc-images-convert-to-floating-success');
1840+
const fixture = await createInlineImageFixture(harness, stateDir, 'doc-images-convert-to-floating');
1841+
return {
1842+
stateDir,
1843+
args: [
1844+
...commandTokens('doc.images.convertToFloating'),
1845+
fixture.docPath,
1846+
'--image-id',
1847+
fixture.imageId,
1848+
'--out',
1849+
harness.createOutputPath('doc-images-convert-to-floating-output'),
1850+
],
1851+
};
1852+
},
1853+
'doc.images.setSize': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1854+
const stateDir = await harness.createStateDir('doc-images-set-size-success');
1855+
const fixture = await createInlineImageFixture(harness, stateDir, 'doc-images-set-size');
1856+
return {
1857+
stateDir,
1858+
args: [
1859+
...commandTokens('doc.images.setSize'),
1860+
fixture.docPath,
1861+
'--image-id',
1862+
fixture.imageId,
1863+
'--size-json',
1864+
JSON.stringify({ width: 240, height: 120 }),
1865+
'--out',
1866+
harness.createOutputPath('doc-images-set-size-output'),
1867+
],
1868+
};
1869+
},
1870+
'doc.images.setWrapType': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1871+
const stateDir = await harness.createStateDir('doc-images-set-wrap-type-success');
1872+
const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-set-wrap-type');
1873+
return {
1874+
stateDir,
1875+
args: [
1876+
...commandTokens('doc.images.setWrapType'),
1877+
fixture.docPath,
1878+
'--image-id',
1879+
fixture.imageId,
1880+
'--type',
1881+
'Tight',
1882+
'--out',
1883+
harness.createOutputPath('doc-images-set-wrap-type-output'),
1884+
],
1885+
};
1886+
},
1887+
'doc.images.setWrapSide': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1888+
const stateDir = await harness.createStateDir('doc-images-set-wrap-side-success');
1889+
const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-set-wrap-side');
1890+
return {
1891+
stateDir,
1892+
args: [
1893+
...commandTokens('doc.images.setWrapSide'),
1894+
fixture.docPath,
1895+
'--image-id',
1896+
fixture.imageId,
1897+
'--side',
1898+
'left',
1899+
'--out',
1900+
harness.createOutputPath('doc-images-set-wrap-side-output'),
1901+
],
1902+
};
1903+
},
1904+
'doc.images.setWrapDistances': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1905+
const stateDir = await harness.createStateDir('doc-images-set-wrap-distances-success');
1906+
const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-set-wrap-distances');
1907+
return {
1908+
stateDir,
1909+
args: [
1910+
...commandTokens('doc.images.setWrapDistances'),
1911+
fixture.docPath,
1912+
'--image-id',
1913+
fixture.imageId,
1914+
'--distances-json',
1915+
JSON.stringify({ distTop: 100, distBottom: 100 }),
1916+
'--out',
1917+
harness.createOutputPath('doc-images-set-wrap-distances-output'),
1918+
],
1919+
};
1920+
},
1921+
'doc.images.setPosition': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1922+
const stateDir = await harness.createStateDir('doc-images-set-position-success');
1923+
const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-set-position');
1924+
return {
1925+
stateDir,
1926+
args: [
1927+
...commandTokens('doc.images.setPosition'),
1928+
fixture.docPath,
1929+
'--image-id',
1930+
fixture.imageId,
1931+
'--position-json',
1932+
JSON.stringify({ hRelativeFrom: 'column', alignH: 'center' }),
1933+
'--out',
1934+
harness.createOutputPath('doc-images-set-position-output'),
1935+
],
1936+
};
1937+
},
1938+
'doc.images.setAnchorOptions': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1939+
const stateDir = await harness.createStateDir('doc-images-set-anchor-options-success');
1940+
const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-set-anchor-options');
1941+
return {
1942+
stateDir,
1943+
args: [
1944+
...commandTokens('doc.images.setAnchorOptions'),
1945+
fixture.docPath,
1946+
'--image-id',
1947+
fixture.imageId,
1948+
'--options-json',
1949+
JSON.stringify({ behindDoc: true, allowOverlap: false }),
1950+
'--out',
1951+
harness.createOutputPath('doc-images-set-anchor-options-output'),
1952+
],
1953+
};
1954+
},
1955+
'doc.images.setZOrder': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
1956+
const stateDir = await harness.createStateDir('doc-images-set-z-order-success');
1957+
const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-set-z-order');
1958+
return {
1959+
stateDir,
1960+
args: [
1961+
...commandTokens('doc.images.setZOrder'),
1962+
fixture.docPath,
1963+
'--image-id',
1964+
fixture.imageId,
1965+
'--z-order-json',
1966+
JSON.stringify({ relativeHeight: 500 }),
1967+
'--out',
1968+
harness.createOutputPath('doc-images-set-z-order-output'),
1969+
],
1970+
};
1971+
},
16451972
'doc.toc.list': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
16461973
const stateDir = await harness.createStateDir('doc-toc-list-success');
16471974
const docPath = await harness.copyTocFixtureDoc('doc-toc-list', stateDir);

0 commit comments

Comments
 (0)