@json-to-office/core-docx 1.0.0 → 1.1.0

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.
@@ -1977,6 +1977,12 @@ function pointsToEighthPoints(points) {
1977
1977
  function inchesToTwips(inches) {
1978
1978
  return Math.round(inches * TWIPS_PER_INCH);
1979
1979
  }
1980
+ function inchesToEmu(inches) {
1981
+ return Math.round(inches * EMU_PER_INCH);
1982
+ }
1983
+ function pointsToEmu(points) {
1984
+ return Math.round(points * EMU_PER_INCH / 72);
1985
+ }
1980
1986
  function twipsToEmu(twips) {
1981
1987
  return Math.round(twips * EMU_PER_TWIP);
1982
1988
  }
@@ -2179,6 +2185,10 @@ function inlineChildren(children, resources = /* @__PURE__ */ new Map()) {
2179
2185
  case "shape":
2180
2186
  out.push(emitShape(child, resources));
2181
2187
  break;
2188
+ case "drawingGroup":
2189
+ throw new Error(
2190
+ "the docxjs renderer has no emitter for a drawing group; this document should have been refused by the capability check"
2191
+ );
2182
2192
  case "noteReference":
2183
2193
  out.push(
2184
2194
  child.noteKind === "endnote" ? new EndnoteReferenceRun(child.id) : new FootnoteReferenceRun(child.id)
@@ -2280,7 +2290,7 @@ function paragraphOptions(formatting) {
2280
2290
  const options = {};
2281
2291
  if (!formatting) return options;
2282
2292
  if (formatting.alignment) {
2283
- options.alignment = ALIGNMENT[formatting.alignment];
2293
+ options.alignment = ALIGNMENT2[formatting.alignment];
2284
2294
  }
2285
2295
  if (formatting.spacing) {
2286
2296
  const spacing = {};
@@ -2514,11 +2524,11 @@ function emitBorder(border3) {
2514
2524
  color: border3.color?.hex ?? "000000"
2515
2525
  };
2516
2526
  }
2517
- var ALIGNMENT, PAGE_FIELD, WRAP_TYPE, VERTICAL_ALIGN, TABLE_ANCHOR, HORIZONTAL_POSITION, VERTICAL_POSITION, BORDER_STYLE;
2527
+ var ALIGNMENT2, PAGE_FIELD, WRAP_TYPE, VERTICAL_ALIGN, TABLE_ANCHOR, HORIZONTAL_POSITION, VERTICAL_POSITION, BORDER_STYLE;
2518
2528
  var init_emit = __esm({
2519
2529
  "src/renderers/docxjs/emit.ts"() {
2520
2530
  "use strict";
2521
- ALIGNMENT = {
2531
+ ALIGNMENT2 = {
2522
2532
  left: AlignmentType.LEFT,
2523
2533
  center: AlignmentType.CENTER,
2524
2534
  right: AlignmentType.RIGHT,
@@ -2641,7 +2651,7 @@ function paragraphProperties(formatting) {
2641
2651
  out.spacing = spacing;
2642
2652
  }
2643
2653
  if (formatting.alignment !== void 0) {
2644
- out.alignment = ALIGNMENT[formatting.alignment] ?? formatting.alignment;
2654
+ out.alignment = ALIGNMENT2[formatting.alignment] ?? formatting.alignment;
2645
2655
  }
2646
2656
  if (formatting.keepNext !== void 0) out.keepNext = formatting.keepNext;
2647
2657
  if (formatting.keepLines !== void 0) out.keepLines = formatting.keepLines;
@@ -2770,6 +2780,15 @@ var init_features = __esm({
2770
2780
  "text-frames",
2771
2781
  /** Native shape text boxes. */
2772
2782
  "text-boxes",
2783
+ /**
2784
+ * A DrawingML group: shapes, text boxes and pictures sharing one child
2785
+ * coordinate space, drawn as a single anchored or inline object.
2786
+ *
2787
+ * Separate from `text-boxes` because a lone `wps:wsp` run is a far smaller
2788
+ * ask than `wpg:wgp` with child transforms, preset geometry and grouped
2789
+ * pictures — a backend can plausibly have the first and not the second.
2790
+ */
2791
+ "drawing-groups",
2773
2792
  /** A table-of-contents field. */
2774
2793
  "toc",
2775
2794
  /** TOC entries baked in so an unrefreshed reader still shows them. */
@@ -3016,7 +3035,7 @@ function numberingConfig(numbering) {
3016
3035
  level: level.level,
3017
3036
  format: level.format,
3018
3037
  text: level.text,
3019
- alignment: level.alignment ? ALIGNMENT[level.alignment] : void 0,
3038
+ alignment: level.alignment ? ALIGNMENT2[level.alignment] : void 0,
3020
3039
  ...level.suffix ? { suffix: level.suffix } : {},
3021
3040
  style: {
3022
3041
  ...level.indent ? {
@@ -3234,7 +3253,12 @@ var init_docxjs = __esm({
3234
3253
  "cached-fields",
3235
3254
  "shading",
3236
3255
  "borders",
3237
- "rtl"
3256
+ "rtl",
3257
+ // `drawing-groups` is the one exclusion that is a real backend gap rather
3258
+ // than a slice boundary: docx.js has no `wpg:wgp`. Leaving it out is what
3259
+ // turns a native `visual` sent to this backend into a named capability
3260
+ // error instead of a document with the graphic missing.
3261
+ "drawing-groups"
3238
3262
  ]);
3239
3263
  DOCXJS_CAPABILITIES = new Set(
3240
3264
  [...ALL_DOCX_FEATURES].filter((feature) => !NOT_YET_EMITTED.has(feature))
@@ -3345,15 +3369,15 @@ function inlineChildren2(children, ctx = emptyContext()) {
3345
3369
  out.push({ bookmarkEnd: { id: child.id } });
3346
3370
  break;
3347
3371
  case "image": {
3348
- const build = ctx.pictures.get(child.resourceId);
3349
- if (!build) {
3350
- throw new Error(
3351
- `no image was prepared for resource "${child.resourceId}"`
3352
- );
3353
- }
3354
3372
  const pending = breakOption();
3355
3373
  if (pending.break) out.push(pending);
3356
- out.push({ picture: build(child, ctx.nextDrawingId()) });
3374
+ out.push({ picture: pictureOptions(child, ctx) });
3375
+ break;
3376
+ }
3377
+ case "drawingGroup": {
3378
+ const pending = breakOption();
3379
+ if (pending.break) out.push(pending);
3380
+ out.push({ wpgGroup: drawingGroupOptions(child, ctx) });
3357
3381
  break;
3358
3382
  }
3359
3383
  case "hyperlink":
@@ -3448,6 +3472,166 @@ function floatingOptions2(floating) {
3448
3472
  zIndex: floating.zIndex
3449
3473
  };
3450
3474
  }
3475
+ function imageMedia(ctx, resourceId, placement) {
3476
+ const build = ctx.pictures.get(resourceId);
3477
+ if (!build) {
3478
+ throw new Error(`no image was prepared for resource "${resourceId}"`);
3479
+ }
3480
+ return build(placement);
3481
+ }
3482
+ function pictureOptions(image, ctx) {
3483
+ const media = imageMedia(ctx, image.resourceId, image);
3484
+ return {
3485
+ type: media.type,
3486
+ data: media.data,
3487
+ // No `fileName`: at run level the backend allocates one, and stating our
3488
+ // own would only fight it.
3489
+ ...media.fallback ? { fallback: media.fallback } : {},
3490
+ // Raw numbers are EMUs in @office-open/docx. Passing pixels here makes a
3491
+ // normal image only a few hundred EMUs wide, effectively a dot.
3492
+ transformation: { width: image.widthEmu, height: image.heightEmu },
3493
+ // The id is stated rather than left to the backend's process-global
3494
+ // counter. `name` stays empty, which is what it was before and what the
3495
+ // backend falls back to.
3496
+ altText: { id: String(ctx.nextDrawingId()) },
3497
+ ...image.floating ? { floating: floatingOptions2(image.floating) } : {}
3498
+ // No `description` or `title`: no DOCX this pipeline has produced carries
3499
+ // `wp:docPr` alt text, and the compiler warns so the gap is visible rather
3500
+ // than silent.
3501
+ };
3502
+ }
3503
+ function drawingGroupOptions(group, ctx) {
3504
+ const id = ctx.nextDrawingId();
3505
+ return {
3506
+ altText: {
3507
+ id: String(id),
3508
+ ...group.altText ? { description: group.altText } : {}
3509
+ },
3510
+ transformation: { width: group.widthEmu, height: group.heightEmu },
3511
+ childOffset: { x: 0, y: 0 },
3512
+ childExtent: { cx: group.canvasWidthEmu, cy: group.canvasHeightEmu },
3513
+ children: group.children.map((child) => groupChild(child, ctx)),
3514
+ ...group.floating ? { floating: floatingOptions2(group.floating) } : {}
3515
+ };
3516
+ }
3517
+ function groupChild(child, ctx) {
3518
+ return child.kind === "shape" ? groupShape(child, ctx) : groupPicture(child, ctx);
3519
+ }
3520
+ function groupShape(shape, ctx) {
3521
+ const id = ctx.nextDrawingId();
3522
+ return {
3523
+ type: "wps",
3524
+ transformation: childTransformation(shape.frame),
3525
+ data: {
3526
+ nonVisualProperties: {
3527
+ id,
3528
+ ...shape.name ? { name: shape.name } : {},
3529
+ // `txBox="1"` is how Word tells a text box from a shape that happens
3530
+ // to hold text, and it changes how the object behaves on selection.
3531
+ ...shape.isTextBox ? { textBox: "1" } : {}
3532
+ },
3533
+ presetGeometry: { preset: shape.geometry },
3534
+ ...shape.fill ? { fill: drawingFill(shape.fill) } : {},
3535
+ ...shape.outline ? { outline: drawingOutline(shape.outline) } : {},
3536
+ children: (shape.text?.paragraphs ?? []).map(
3537
+ (child) => paragraph(child, ctx)
3538
+ ),
3539
+ ...shape.text ? { bodyProperties: bodyProperties(shape.text) } : {}
3540
+ }
3541
+ };
3542
+ }
3543
+ function groupPicture(picture2, ctx) {
3544
+ const id = ctx.nextDrawingId();
3545
+ const media = imageMedia(ctx, picture2.resourceId, {
3546
+ widthEmu: picture2.frame.widthEmu,
3547
+ heightEmu: picture2.frame.heightEmu
3548
+ });
3549
+ return {
3550
+ type: media.type,
3551
+ data: media.data,
3552
+ fileName: media.fileName,
3553
+ ...media.fallback ? {
3554
+ fallback: {
3555
+ ...media.fallback,
3556
+ fileName: media.fallbackFileName
3557
+ }
3558
+ } : {},
3559
+ transformation: childTransformation(picture2.frame),
3560
+ nonVisualProperties: {
3561
+ id,
3562
+ ...picture2.name ? { name: picture2.name } : {},
3563
+ ...picture2.altText ? { description: picture2.altText } : {}
3564
+ },
3565
+ ...picture2.crop ? { sourceRectangle: sourceRectangle(picture2.crop) } : {}
3566
+ };
3567
+ }
3568
+ function childTransformation(frame) {
3569
+ const flip = {
3570
+ ...frame.flipHorizontal ? { horizontal: true } : {},
3571
+ ...frame.flipVertical ? { vertical: true } : {}
3572
+ };
3573
+ return {
3574
+ offset: {
3575
+ emus: { x: frame.xEmu, y: frame.yEmu },
3576
+ pixels: {
3577
+ x: Math.round(emuToPixels(frame.xEmu)),
3578
+ y: Math.round(emuToPixels(frame.yEmu))
3579
+ }
3580
+ },
3581
+ emus: { x: frame.widthEmu, y: frame.heightEmu },
3582
+ pixels: {
3583
+ x: Math.round(emuToPixels(frame.widthEmu)),
3584
+ y: Math.round(emuToPixels(frame.heightEmu))
3585
+ },
3586
+ // Degrees: the backend multiplies by 60000 on the way into `@rot`.
3587
+ ...frame.rotationDegrees !== void 0 ? { rotation: frame.rotationDegrees } : {},
3588
+ ...Object.keys(flip).length > 0 ? { flip } : {}
3589
+ };
3590
+ }
3591
+ function drawingFill(fill) {
3592
+ if (fill.kind === "none") return { type: "none" };
3593
+ return {
3594
+ type: "solid",
3595
+ color: {
3596
+ type: "rgb",
3597
+ value: fill.color.hex,
3598
+ // Transparency becomes an alpha transform on the colour. DrawingML
3599
+ // states *opacity*, so the value is the complement of what the author
3600
+ // wrote; the backend scales the percentage into `a:alpha`'s thousandths
3601
+ // itself, so it must be handed a plain percentage here.
3602
+ ...fill.transparencyPercent !== void 0 ? { transforms: { alpha: 100 - fill.transparencyPercent } } : {}
3603
+ }
3604
+ };
3605
+ }
3606
+ function drawingOutline(outline) {
3607
+ return {
3608
+ ...outline.widthEmu !== void 0 ? { width: outline.widthEmu } : {},
3609
+ ...outline.dash ? { dash: outline.dash } : {},
3610
+ ...outline.color ? { type: "solidFill", color: { value: outline.color.hex } } : {}
3611
+ };
3612
+ }
3613
+ function bodyProperties(text) {
3614
+ const insets = text.insetsEmu;
3615
+ return {
3616
+ ...text.anchor ? { anchor: BODY_ANCHOR[text.anchor] } : {},
3617
+ // `lIns`/`tIns`/`rIns`/`bIns` is the vocabulary `a:bodyPr` actually has;
3618
+ // the backend also accepts a `margins` object and folds it into the same
3619
+ // attributes.
3620
+ ...insets?.left !== void 0 ? { lIns: insets.left } : {},
3621
+ ...insets?.top !== void 0 ? { tIns: insets.top } : {},
3622
+ ...insets?.right !== void 0 ? { rIns: insets.right } : {},
3623
+ ...insets?.bottom !== void 0 ? { bIns: insets.bottom } : {}
3624
+ };
3625
+ }
3626
+ function sourceRectangle(crop) {
3627
+ const thousandths = (fraction) => Math.round(fraction * 1e5);
3628
+ return {
3629
+ ...crop.left !== void 0 ? { left: thousandths(crop.left) } : {},
3630
+ ...crop.top !== void 0 ? { top: thousandths(crop.top) } : {},
3631
+ ...crop.right !== void 0 ? { right: thousandths(crop.right) } : {},
3632
+ ...crop.bottom !== void 0 ? { bottom: thousandths(crop.bottom) } : {}
3633
+ };
3634
+ }
3451
3635
  function shapeOptions(shape, ctx) {
3452
3636
  const id = String(ctx.nextDrawingId());
3453
3637
  return {
@@ -3465,23 +3649,8 @@ function shapeOptions(shape, ctx) {
3465
3649
  color: { type: "rgb", value: shape.fill.hex }
3466
3650
  }
3467
3651
  } : {},
3468
- ...shape.outline ? {
3469
- outline: {
3470
- ...shape.outline.widthEmu !== void 0 ? { width: shape.outline.widthEmu } : {},
3471
- fill: {
3472
- type: "solid",
3473
- color: { type: "rgb", value: shape.outline.color.hex }
3474
- }
3475
- }
3476
- } : {},
3477
- ...shape.insetsEmu ? {
3478
- bodyProperties: {
3479
- ...shape.insetsEmu.top !== void 0 ? { topInset: shape.insetsEmu.top } : {},
3480
- ...shape.insetsEmu.bottom !== void 0 ? { bottomInset: shape.insetsEmu.bottom } : {},
3481
- ...shape.insetsEmu.left !== void 0 ? { leftInset: shape.insetsEmu.left } : {},
3482
- ...shape.insetsEmu.right !== void 0 ? { rightInset: shape.insetsEmu.right } : {}
3483
- }
3484
- } : {},
3652
+ ...shape.outline ? { outline: drawingOutline(shape.outline) } : {},
3653
+ ...shape.insetsEmu ? { bodyProperties: bodyProperties({ insetsEmu: shape.insetsEmu }) } : {},
3485
3654
  ...shape.floating ? { floating: floatingOptions2(shape.floating) } : {}
3486
3655
  };
3487
3656
  }
@@ -3838,7 +4007,7 @@ function numberingConfig2(numbering) {
3838
4007
  }))
3839
4008
  };
3840
4009
  }
3841
- var WRAP_TYPE2, TOC_PAGE_TAB_TWIPS;
4010
+ var WRAP_TYPE2, BODY_ANCHOR, TOC_PAGE_TAB_TWIPS;
3842
4011
  var init_emit2 = __esm({
3843
4012
  "src/renderers/office-open/emit.ts"() {
3844
4013
  "use strict";
@@ -3849,6 +4018,11 @@ var init_emit2 = __esm({
3849
4018
  tight: 2,
3850
4019
  topAndBottom: 3
3851
4020
  };
4021
+ BODY_ANCHOR = {
4022
+ top: "t",
4023
+ middle: "ctr",
4024
+ bottom: "b"
4025
+ };
3852
4026
  TOC_PAGE_TAB_TWIPS = 9025;
3853
4027
  }
3854
4028
  });
@@ -4026,18 +4200,9 @@ async function prepareImages2(ir) {
4026
4200
  index === 0 ? data : distinguishImageBytes(data, type, size)
4027
4201
  );
4028
4202
  });
4029
- resources.set(resource.id, (image, drawingId) => {
4030
- const rasterSize = {
4031
- width: emuToPixels(image.widthEmu),
4032
- height: emuToPixels(image.heightEmu)
4033
- };
4034
- const sizeKey = `${rasterSize.width}x${rasterSize.height}`;
4035
- const transformation = {
4036
- // Raw numbers are EMUs in @office-open/docx. Passing pixels here makes
4037
- // a normal image only a few hundred EMUs wide, effectively a dot.
4038
- width: image.widthEmu,
4039
- height: image.heightEmu
4040
- };
4203
+ resources.set(resource.id, (placement) => {
4204
+ const sizeKey = placementKey(placement);
4205
+ const stem = `${resource.id}-${sizeKey}`;
4041
4206
  return {
4042
4207
  type,
4043
4208
  // @office-open/docx stores a drawing transformation on its deduplicated
@@ -4045,29 +4210,28 @@ async function prepareImages2(ir) {
4045
4210
  // reuse the first size, so each distinct placement size gets equivalent
4046
4211
  // image bytes carrying a harmless format-native marker.
4047
4212
  data: placementData.get(sizeKey) ?? data,
4048
- transformation,
4049
- // The id is stated rather than left to the backend's process-global
4050
- // counter. `name` stays empty, which is what it was before and what
4051
- // the backend falls back to.
4052
- altText: { id: String(drawingId) },
4213
+ // Named after the resource and the size it is drawn at, so two
4214
+ // placements of one image at one size share a single part and a third
4215
+ // at another size gets its own.
4216
+ fileName: `${stem}.${type}`,
4053
4217
  ...type === "svg" ? {
4054
4218
  // Word before 2016 draws the fallback rather than the vector. A
4055
4219
  // raster that could not be produced falls back to the SVG bytes,
4056
4220
  // which is what this pipeline has always shipped.
4057
4221
  fallback: {
4058
4222
  type: "png",
4059
- data: rasters.get(`${image.resourceId}:${sizeKey}`) ?? data
4060
- }
4061
- } : {},
4062
- ...image.floating ? { floating: floatingOptions2(image.floating) } : {}
4063
- // No `description` or `title`: no DOCX this pipeline has produced
4064
- // carries `wp:docPr` alt text, and the compiler warns so the gap is
4065
- // visible rather than silent.
4223
+ data: rasters.get(`${resource.id}:${sizeKey}`) ?? data
4224
+ },
4225
+ fallbackFileName: `${stem}-fallback.png`
4226
+ } : {}
4066
4227
  };
4067
4228
  });
4068
4229
  }
4069
4230
  return resources;
4070
4231
  }
4232
+ function placementKey(placement) {
4233
+ return `${emuToPixels(placement.widthEmu)}x${emuToPixels(placement.heightEmu)}`;
4234
+ }
4071
4235
  function distinguishImageBytes(data, mediaType, placement) {
4072
4236
  const marker = Buffer.from(`json-to-office:${placement}`, "utf8");
4073
4237
  switch (mediaType) {
@@ -4166,12 +4330,27 @@ function crc32(data) {
4166
4330
  }
4167
4331
  function collectImagePlacements2(ir) {
4168
4332
  const placements = /* @__PURE__ */ new Map();
4333
+ const record = (resourceId, placement) => {
4334
+ const sizes = placements.get(resourceId) ?? /* @__PURE__ */ new Set();
4335
+ sizes.add(placementKey(placement));
4336
+ placements.set(resourceId, sizes);
4337
+ };
4169
4338
  const visitInline = (inline) => {
4170
4339
  if (inline.kind === "image") {
4171
- const key = `${emuToPixels(inline.widthEmu)}x${emuToPixels(inline.heightEmu)}`;
4172
- const sizes = placements.get(inline.resourceId) ?? /* @__PURE__ */ new Set();
4173
- sizes.add(key);
4174
- placements.set(inline.resourceId, sizes);
4340
+ record(inline.resourceId, inline);
4341
+ return;
4342
+ }
4343
+ if (inline.kind === "drawingGroup") {
4344
+ for (const child of inline.children) {
4345
+ if (child.kind === "picture") {
4346
+ record(child.resourceId, {
4347
+ widthEmu: child.frame.widthEmu,
4348
+ heightEmu: child.frame.heightEmu
4349
+ });
4350
+ continue;
4351
+ }
4352
+ child.text?.paragraphs.forEach(visitBlock);
4353
+ }
4175
4354
  return;
4176
4355
  }
4177
4356
  if (inline.kind === "hyperlink" || inline.kind === "revision") {
@@ -4556,6 +4735,9 @@ import {
4556
4735
  DEFAULT_VISUAL_DPI,
4557
4736
  MAX_RASTERIZE_BATCH_SLIDES
4558
4737
  } from "@json-to-office/shared";
4738
+ import {
4739
+ isNativeVisualProps
4740
+ } from "@json-to-office/shared-docx";
4559
4741
 
4560
4742
  // src/components/visual.ts
4561
4743
  import { createHash } from "crypto";
@@ -4753,7 +4935,9 @@ function collectVisualProps(root) {
4753
4935
  const obj = node;
4754
4936
  if (typeof obj.name === "string" && obj.enabled === false) return;
4755
4937
  if (obj.name === "visual" && obj.props && typeof obj.props === "object") {
4756
- found.push(obj.props);
4938
+ if (!isNativeVisualProps(obj.props)) {
4939
+ found.push(obj.props);
4940
+ }
4757
4941
  return;
4758
4942
  }
4759
4943
  for (const value of Object.values(obj)) visit(value);
@@ -5033,6 +5217,9 @@ import {
5033
5217
  clampVisualDpi as clampVisualDpi2,
5034
5218
  DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI2
5035
5219
  } from "@json-to-office/shared";
5220
+ import {
5221
+ isNativeVisualProps as isNativeVisualProps2
5222
+ } from "@json-to-office/shared-docx";
5036
5223
 
5037
5224
  // src/components/highcharts.ts
5038
5225
  init_colorUtils();
@@ -5194,6 +5381,7 @@ async function desugarExternals(document, options) {
5194
5381
  return transformComponents(document, async (node) => {
5195
5382
  if (node.enabled === false) return void 0;
5196
5383
  if (node.name === "visual") {
5384
+ if (isNativeVisualProps2(node.props)) return void 0;
5197
5385
  return withNodeIdentity(node, {
5198
5386
  name: "image",
5199
5387
  props: await visualImageProps(
@@ -5246,6 +5434,9 @@ async function visualImageProps(props, prerastered, options) {
5246
5434
 
5247
5435
  // src/core/imageResources.ts
5248
5436
  init_imageUtils();
5437
+ import {
5438
+ isNativeVisualProps as isNativeVisualProps3
5439
+ } from "@json-to-office/shared-docx";
5249
5440
  async function loadImageResources(components) {
5250
5441
  const sources = /* @__PURE__ */ new Set();
5251
5442
  for (const component of components) collectSources(component, sources);
@@ -5284,6 +5475,9 @@ function collectSources(component, out) {
5284
5475
  const children = component.children;
5285
5476
  for (const child of children ?? []) collectSources(child, out);
5286
5477
  const props = component.props ?? {};
5478
+ if (component.name === "visual" && isNativeVisualProps3(props)) {
5479
+ collectNativeVisualSources(props, out);
5480
+ }
5287
5481
  for (const key of ["header", "footer"]) {
5288
5482
  const part = props[key];
5289
5483
  if (Array.isArray(part)) {
@@ -5305,6 +5499,23 @@ function collectSources(component, out) {
5305
5499
  }
5306
5500
  }
5307
5501
  }
5502
+ function collectNativeVisualSources(props, out) {
5503
+ const canvas = props.canvas;
5504
+ const background = canvas?.background?.image;
5505
+ if (background) {
5506
+ const source = resolveImageSource(background);
5507
+ if (source) out.add(source);
5508
+ }
5509
+ const elements = props.elements;
5510
+ if (!Array.isArray(elements)) return;
5511
+ for (const element of elements) {
5512
+ if (!element || element.name !== "image") continue;
5513
+ const source = resolveImageSource(
5514
+ element.props ?? {}
5515
+ );
5516
+ if (source) out.add(source);
5517
+ }
5518
+ }
5308
5519
  function collectCellSource(content, out) {
5309
5520
  if (content && typeof content === "object") {
5310
5521
  collectSources(content, out);
@@ -5317,6 +5528,516 @@ import {
5317
5528
  FeatureRequirementCollector
5318
5529
  } from "@json-to-office/shared/rendering";
5319
5530
  import { synthesizeFamilyName } from "@json-to-office/shared";
5531
+ import {
5532
+ isNativeVisualProps as isNativeVisualProps4
5533
+ } from "@json-to-office/shared-docx";
5534
+
5535
+ // src/ir/nativeVisual.ts
5536
+ init_units();
5537
+ init_imageUtils();
5538
+ var PIXELS_PER_INCH2 = 96;
5539
+ var DEFAULT_WIDTH_FRACTION = 0.75;
5540
+ var ASSUMED_FONT_SIZE_POINTS = 18;
5541
+ var MIN_DERIVED_TEXT_HEIGHT_INCHES = 0.5;
5542
+ var DERIVED_LINE_HEIGHT_RATIO = 1.6;
5543
+ var GEOMETRY_ALIASES = {
5544
+ arrow: "rightArrow",
5545
+ lightning: "lightningBolt"
5546
+ };
5547
+ var DASH_VALUES = /* @__PURE__ */ new Set([
5548
+ "solid",
5549
+ "dash",
5550
+ "dot",
5551
+ "dashDot"
5552
+ ]);
5553
+ var ANCHOR = {
5554
+ top: "top",
5555
+ middle: "middle",
5556
+ bottom: "bottom"
5557
+ };
5558
+ var ALIGNMENT = {
5559
+ left: "left",
5560
+ center: "center",
5561
+ right: "right",
5562
+ justify: "justified"
5563
+ };
5564
+ function compileNativeVisualGroup(props, outer) {
5565
+ const canvas = {
5566
+ widthEmu: inchesToEmu(props.canvas.width),
5567
+ heightEmu: inchesToEmu(props.canvas.height)
5568
+ };
5569
+ const children = [];
5570
+ let refused = false;
5571
+ const deps = {
5572
+ ...outer,
5573
+ reject: (detail) => {
5574
+ refused = true;
5575
+ outer.reject(detail);
5576
+ }
5577
+ };
5578
+ const background = props.canvas.background;
5579
+ if (background?.color) {
5580
+ const color = deps.color(background.color);
5581
+ if (!color) {
5582
+ deps.reject(`canvas.background.color "${background.color}"`);
5583
+ } else {
5584
+ children.push({
5585
+ kind: "shape",
5586
+ frame: fullBleed(canvas),
5587
+ geometry: "rect",
5588
+ fill: { kind: "solid", color },
5589
+ name: "Canvas background"
5590
+ });
5591
+ }
5592
+ }
5593
+ if (background?.image) {
5594
+ const source = resolveImageSource(background.image);
5595
+ const resource = source ? deps.picture(source) : void 0;
5596
+ if (!resource) {
5597
+ deps.reject("canvas.background.image could not be loaded");
5598
+ } else {
5599
+ children.push({
5600
+ kind: "picture",
5601
+ frame: fullBleed(canvas),
5602
+ resourceId: resource.resourceId,
5603
+ name: "Canvas background"
5604
+ });
5605
+ }
5606
+ }
5607
+ for (const [index, element] of (props.elements ?? []).entries()) {
5608
+ if (element.enabled === false) continue;
5609
+ const child = compileElement(element, index, canvas, deps);
5610
+ if (child) children.push(child);
5611
+ }
5612
+ return refused ? void 0 : { canvas, children };
5613
+ }
5614
+ function compileElement(element, index, canvas, deps) {
5615
+ const at = `elements[${index}]`;
5616
+ switch (element.name) {
5617
+ case "text":
5618
+ return compileText(element.props, at, canvas, deps);
5619
+ case "shape":
5620
+ return compileShape(element.props, at, canvas, deps);
5621
+ case "image":
5622
+ return compileImage(element.props, at, canvas, deps);
5623
+ default:
5624
+ deps.reject(`${at} is a "${element.name}"`);
5625
+ return void 0;
5626
+ }
5627
+ }
5628
+ function compileText(props, at, canvas, deps) {
5629
+ const runs = textRuns(props);
5630
+ if (!runs) {
5631
+ deps.warn(
5632
+ `[core-docx] native visual ${at} has neither "text" nor "runs"; nothing was drawn for it.`
5633
+ );
5634
+ return void 0;
5635
+ }
5636
+ const colors = resolveColors(
5637
+ [
5638
+ ["color", props.color],
5639
+ ["fill.color", props.fill?.color],
5640
+ ...runs.map((run, i) => [`runs[${i}].color`, run.color]),
5641
+ ...runs.map(
5642
+ (run, i) => [
5643
+ `runs[${i}].underline.color`,
5644
+ underlineColor(run.underline)
5645
+ ]
5646
+ ),
5647
+ ["underline.color", underlineColor(props.underline)]
5648
+ ],
5649
+ at,
5650
+ deps
5651
+ );
5652
+ if (!colors) return void 0;
5653
+ const frame = resolveFrame(props, canvas, {
5654
+ defaultHeightInches: derivedTextHeightInches(props, runs)
5655
+ });
5656
+ const paragraphs = textParagraphs(runs, props, colors, at);
5657
+ return {
5658
+ kind: "shape",
5659
+ frame,
5660
+ geometry: "rect",
5661
+ isTextBox: true,
5662
+ // A text box with no fill is transparent, which is what the rasterized
5663
+ // path draws; saying so explicitly stops Word applying its own default.
5664
+ fill: props.fill?.color ? solidFill(colors.get("fill.color"), props.fill.transparency) : { kind: "none" },
5665
+ text: {
5666
+ paragraphs,
5667
+ // pptx anchors a text box at the top unless told otherwise, and a
5668
+ // drawing that changes anchor between render modes moves its text.
5669
+ anchor: ANCHOR[props.valign ?? "top"] ?? "top",
5670
+ // A text box's insets default to nothing, again matching pptx. A shape
5671
+ // deliberately leaves them unstated so OOXML's own defaults apply.
5672
+ insetsEmu: marginInsets(props.margin) ?? ZERO_INSETS
5673
+ },
5674
+ name: `Text ${at}`
5675
+ };
5676
+ }
5677
+ var ZERO_INSETS = { top: 0, bottom: 0, left: 0, right: 0 };
5678
+ function textRuns(props) {
5679
+ if (props.runs?.length) return props.runs;
5680
+ if (props.text === void 0) return void 0;
5681
+ return [{ text: props.text }];
5682
+ }
5683
+ function textParagraphs(runs, props, colors, at) {
5684
+ const paragraphs = [];
5685
+ let current = [];
5686
+ const flush = () => {
5687
+ paragraphs.push({
5688
+ kind: "paragraph",
5689
+ id: `${at}.p${paragraphs.length}`,
5690
+ path: `${at}.paragraphs[${paragraphs.length}]`,
5691
+ children: current,
5692
+ formatting: {
5693
+ ...props.align ? { alignment: ALIGNMENT[props.align] } : {},
5694
+ // A drawing's text is set flush against the shape; the document's
5695
+ // paragraph spacing would push it away from the top edge, which is not
5696
+ // what an absolutely-placed box means.
5697
+ spacing: { beforeTwips: 0, afterTwips: 0 }
5698
+ }
5699
+ });
5700
+ current = [];
5701
+ };
5702
+ runs.forEach((run, index) => {
5703
+ const formatting = runFormatting(run, props, colors, index);
5704
+ const lines = run.text.split("\n");
5705
+ lines.forEach((line, lineIndex) => {
5706
+ if (lineIndex > 0) current.push({ kind: "lineBreak" });
5707
+ if (line) {
5708
+ current.push({ kind: "text", text: line, ...formatting });
5709
+ }
5710
+ });
5711
+ if (run.breakLine && index < runs.length - 1) flush();
5712
+ });
5713
+ flush();
5714
+ return paragraphs;
5715
+ }
5716
+ function runFormatting(run, props, colors, index) {
5717
+ const fontFamily = run.fontFace ?? props.fontFace;
5718
+ const sizePoints = run.fontSize ?? props.fontSize;
5719
+ const color = colors.get(`runs[${index}].color`) ?? (run.color === void 0 ? colors.get("color") : void 0);
5720
+ const bold = run.bold ?? props.bold;
5721
+ const italic = run.italic ?? props.italic;
5722
+ const strike = run.strike ?? props.strike;
5723
+ const underline = compileUnderline(
5724
+ run.underline ?? props.underline,
5725
+ colors.get(
5726
+ run.underline !== void 0 ? `runs[${index}].underline.color` : "underline.color"
5727
+ )
5728
+ );
5729
+ const formatting = {
5730
+ ...fontFamily ? { fontFamily } : {},
5731
+ ...sizePoints !== void 0 ? { sizeHalfPoints: pointsToHalfPoints(sizePoints) } : {},
5732
+ ...color ? { color } : {},
5733
+ ...bold !== void 0 ? { bold } : {},
5734
+ ...italic !== void 0 ? { italic } : {},
5735
+ ...strike !== void 0 ? { strike } : {},
5736
+ ...underline ? { underline } : {}
5737
+ };
5738
+ return Object.keys(formatting).length > 0 ? { formatting } : {};
5739
+ }
5740
+ var UNDERLINE_TYPES = {
5741
+ sng: "single",
5742
+ dbl: "double",
5743
+ dash: "dash",
5744
+ dotted: "dotted"
5745
+ };
5746
+ function compileUnderline(value, color) {
5747
+ if (value === void 0 || value === false) return void 0;
5748
+ const style = value === true ? "sng" : value.style ?? "sng";
5749
+ return {
5750
+ type: UNDERLINE_TYPES[style] ?? "single",
5751
+ ...color ? { color } : {}
5752
+ };
5753
+ }
5754
+ function underlineColor(value) {
5755
+ return typeof value === "object" ? value.color : void 0;
5756
+ }
5757
+ function derivedTextHeightInches(props, runs) {
5758
+ const fontSize = props.fontSize ?? ASSUMED_FONT_SIZE_POINTS;
5759
+ const lineCount = runs.reduce(
5760
+ (total, run) => total + (run.breakLine ? 1 : 0) + (run.text.match(/\n/g)?.length ?? 0),
5761
+ 1
5762
+ );
5763
+ return Math.max(
5764
+ MIN_DERIVED_TEXT_HEIGHT_INCHES,
5765
+ fontSize / 72 * DERIVED_LINE_HEIGHT_RATIO * lineCount
5766
+ );
5767
+ }
5768
+ function compileShape(props, at, canvas, deps) {
5769
+ const segments = shapeSegments(props);
5770
+ const colors = resolveColors(
5771
+ [
5772
+ ["fill.color", props.fill?.color],
5773
+ ["line.color", props.line?.color],
5774
+ ["fontColor", props.fontColor],
5775
+ ...segments.map(
5776
+ (segment, i) => [`text[${i}].color`, segment.color]
5777
+ )
5778
+ ],
5779
+ at,
5780
+ deps
5781
+ );
5782
+ if (!colors) return void 0;
5783
+ const frame = resolveFrame(props, canvas, { defaultHeightInches: 0 });
5784
+ if (props.flipH) frame.flipHorizontal = true;
5785
+ if (props.flipV) frame.flipVertical = true;
5786
+ const dash = props.line?.dashType;
5787
+ if (dash !== void 0 && !DASH_VALUES.has(dash)) {
5788
+ deps.reject(`${at}.line.dashType "${dash}"`);
5789
+ return void 0;
5790
+ }
5791
+ const outline = props.line ? {
5792
+ ...colors.get("line.color") ? { color: colors.get("line.color") } : {},
5793
+ ...props.line.width !== void 0 ? { widthEmu: pointsToEmu(props.line.width) } : {},
5794
+ ...dash && dash !== "solid" ? { dash } : {}
5795
+ } : void 0;
5796
+ const text = segments.length ? {
5797
+ paragraphs: shapeParagraphs(segments, props, colors, at),
5798
+ anchor: ANCHOR[props.valign ?? "middle"] ?? "middle",
5799
+ // Unstated insets are left unstated so OOXML's own shape defaults
5800
+ // apply, which is what the raster path draws.
5801
+ ...marginInsets(props.margin) ? { insetsEmu: marginInsets(props.margin) } : {}
5802
+ } : void 0;
5803
+ return {
5804
+ kind: "shape",
5805
+ frame,
5806
+ geometry: GEOMETRY_ALIASES[props.type] ?? props.type,
5807
+ ...props.fill ? {
5808
+ fill: props.fill.color ? solidFill(colors.get("fill.color"), props.fill.transparency) : { kind: "none" }
5809
+ } : {},
5810
+ ...outline && Object.keys(outline).length > 0 ? { outline } : {},
5811
+ ...text ? { text } : {},
5812
+ name: `Shape ${at}`
5813
+ };
5814
+ }
5815
+ function shapeSegments(props) {
5816
+ if (props.text === void 0) return [];
5817
+ if (typeof props.text === "string") {
5818
+ return props.text ? [{ text: props.text }] : [];
5819
+ }
5820
+ return props.text;
5821
+ }
5822
+ function shapeParagraphs(segments, props, colors, at) {
5823
+ const paragraphs = [];
5824
+ let current = [];
5825
+ const flush = () => {
5826
+ paragraphs.push({
5827
+ kind: "paragraph",
5828
+ id: `${at}.p${paragraphs.length}`,
5829
+ path: `${at}.paragraphs[${paragraphs.length}]`,
5830
+ children: current,
5831
+ formatting: {
5832
+ // A shape centres its text unless told otherwise, which is what the
5833
+ // rasterized shape draws.
5834
+ alignment: props.align ? ALIGNMENT[props.align] : "center",
5835
+ spacing: { beforeTwips: 0, afterTwips: 0 }
5836
+ }
5837
+ });
5838
+ current = [];
5839
+ };
5840
+ segments.forEach((segment, index) => {
5841
+ const fontFamily = segment.fontFace ?? props.fontFace;
5842
+ const sizePoints = segment.fontSize ?? props.fontSize;
5843
+ const color = colors.get(`text[${index}].color`) ?? (segment.color === void 0 ? colors.get("fontColor") : void 0);
5844
+ const bold = segment.bold ?? props.bold;
5845
+ const italic = segment.italic ?? props.italic;
5846
+ const formatting = {
5847
+ ...fontFamily ? { fontFamily } : {},
5848
+ ...sizePoints !== void 0 ? { sizeHalfPoints: pointsToHalfPoints(sizePoints) } : {},
5849
+ ...color ? { color } : {},
5850
+ ...bold !== void 0 ? { bold } : {},
5851
+ ...italic !== void 0 ? { italic } : {}
5852
+ };
5853
+ const wrapped = Object.keys(formatting).length > 0 ? { formatting } : void 0;
5854
+ segment.text.split("\n").forEach((line, lineIndex) => {
5855
+ if (lineIndex > 0) current.push({ kind: "lineBreak" });
5856
+ if (line) current.push({ kind: "text", text: line, ...wrapped });
5857
+ });
5858
+ if (segment.breakLine && index < segments.length - 1) flush();
5859
+ });
5860
+ flush();
5861
+ return paragraphs;
5862
+ }
5863
+ function compileImage(props, at, canvas, deps) {
5864
+ const source = resolveImageSource(props);
5865
+ if (!source) {
5866
+ deps.reject(`${at} has none of "path", "base64" or "svg"`);
5867
+ return void 0;
5868
+ }
5869
+ const resource = deps.picture(source);
5870
+ if (!resource) {
5871
+ deps.reject(`${at} image could not be loaded`);
5872
+ return void 0;
5873
+ }
5874
+ const aspect = resource.intrinsic ? resource.intrinsic.width / resource.intrinsic.height : void 0;
5875
+ const intrinsicInches = resource.intrinsic ? {
5876
+ width: resource.intrinsic.width / PIXELS_PER_INCH2,
5877
+ height: resource.intrinsic.height / PIXELS_PER_INCH2
5878
+ } : void 0;
5879
+ const box = resolveImageBox(props, canvas, aspect, intrinsicInches);
5880
+ const sizing = props.sizing;
5881
+ if (!sizing || !aspect) {
5882
+ if (sizing && !aspect) {
5883
+ deps.warn(
5884
+ `[core-docx] native visual ${at} asks for "${sizing.type}" sizing, but the image's stored size could not be read; it was stretched to its box instead.`
5885
+ );
5886
+ }
5887
+ return picture(box, props, resource, at);
5888
+ }
5889
+ const boxAspect = box.widthEmu / box.heightEmu;
5890
+ if (sizing.type === "contain") {
5891
+ const fitted = boxAspect > aspect ? {
5892
+ widthEmu: Math.round(box.heightEmu * aspect),
5893
+ heightEmu: box.heightEmu
5894
+ } : {
5895
+ widthEmu: box.widthEmu,
5896
+ heightEmu: Math.round(box.widthEmu / aspect)
5897
+ };
5898
+ return picture(
5899
+ {
5900
+ ...box,
5901
+ xEmu: box.xEmu + Math.round((box.widthEmu - fitted.widthEmu) / 2),
5902
+ yEmu: box.yEmu + Math.round((box.heightEmu - fitted.heightEmu) / 2),
5903
+ ...fitted
5904
+ },
5905
+ props,
5906
+ resource,
5907
+ at
5908
+ );
5909
+ }
5910
+ const visible = boxAspect > aspect ? { widthFraction: 1, heightFraction: aspect / boxAspect } : { widthFraction: boxAspect / aspect, heightFraction: 1 };
5911
+ const crop = sizing.type === "cover" ? {
5912
+ ...visible.widthFraction < 1 ? {
5913
+ left: (1 - visible.widthFraction) / 2,
5914
+ right: (1 - visible.widthFraction) / 2
5915
+ } : {},
5916
+ ...visible.heightFraction < 1 ? {
5917
+ top: (1 - visible.heightFraction) / 2,
5918
+ bottom: (1 - visible.heightFraction) / 2
5919
+ } : {}
5920
+ } : {
5921
+ ...visible.widthFraction < 1 ? { right: 1 - visible.widthFraction } : {},
5922
+ ...visible.heightFraction < 1 ? { bottom: 1 - visible.heightFraction } : {}
5923
+ };
5924
+ return picture(box, props, resource, at, crop);
5925
+ }
5926
+ function picture(frame, props, resource, at, crop) {
5927
+ return {
5928
+ kind: "picture",
5929
+ frame,
5930
+ resourceId: resource.resourceId,
5931
+ ...props.alt ? { altText: props.alt } : {},
5932
+ ...crop && Object.keys(crop).length > 0 ? { crop } : {},
5933
+ name: `Image ${at}`
5934
+ };
5935
+ }
5936
+ function resolveImageBox(props, canvas, aspect, intrinsicInches) {
5937
+ const width = props.sizing?.w ?? props.w;
5938
+ const height = props.sizing?.h ?? props.h;
5939
+ const widthEmu = width !== void 0 ? resolveExtent(width, canvas.widthEmu) : void 0;
5940
+ const heightEmu = height !== void 0 ? resolveExtent(height, canvas.heightEmu) : void 0;
5941
+ const resolved = (() => {
5942
+ if (widthEmu !== void 0 && heightEmu !== void 0) {
5943
+ return { widthEmu, heightEmu };
5944
+ }
5945
+ if (widthEmu !== void 0) {
5946
+ return {
5947
+ widthEmu,
5948
+ heightEmu: aspect ? Math.round(widthEmu / aspect) : Math.round(widthEmu * (3 / 4))
5949
+ };
5950
+ }
5951
+ if (heightEmu !== void 0) {
5952
+ return {
5953
+ widthEmu: aspect ? Math.round(heightEmu * aspect) : Math.round(heightEmu * (4 / 3)),
5954
+ heightEmu
5955
+ };
5956
+ }
5957
+ if (intrinsicInches) {
5958
+ return {
5959
+ widthEmu: inchesToEmu(intrinsicInches.width),
5960
+ heightEmu: inchesToEmu(intrinsicInches.height)
5961
+ };
5962
+ }
5963
+ const fallbackWidth = Math.round(canvas.widthEmu * DEFAULT_WIDTH_FRACTION);
5964
+ return {
5965
+ widthEmu: fallbackWidth,
5966
+ heightEmu: Math.round(fallbackWidth * (3 / 4))
5967
+ };
5968
+ })();
5969
+ return {
5970
+ xEmu: props.x !== void 0 ? resolveOffset(props.x, canvas.widthEmu) : 0,
5971
+ yEmu: props.y !== void 0 ? resolveOffset(props.y, canvas.heightEmu) : 0,
5972
+ ...resolved,
5973
+ ...rotation(props.rotate)
5974
+ };
5975
+ }
5976
+ function fullBleed(canvas) {
5977
+ return {
5978
+ xEmu: 0,
5979
+ yEmu: 0,
5980
+ widthEmu: canvas.widthEmu,
5981
+ heightEmu: canvas.heightEmu
5982
+ };
5983
+ }
5984
+ function resolveFrame(props, canvas, options) {
5985
+ return {
5986
+ xEmu: props.x !== void 0 ? resolveOffset(props.x, canvas.widthEmu) : 0,
5987
+ yEmu: props.y !== void 0 ? resolveOffset(props.y, canvas.heightEmu) : 0,
5988
+ widthEmu: props.w !== void 0 ? resolveExtent(props.w, canvas.widthEmu) : Math.round(canvas.widthEmu * DEFAULT_WIDTH_FRACTION),
5989
+ heightEmu: props.h !== void 0 ? resolveExtent(props.h, canvas.heightEmu) : inchesToEmu(options.defaultHeightInches),
5990
+ ...rotation(props.rotate)
5991
+ };
5992
+ }
5993
+ function rotation(degrees) {
5994
+ if (degrees === void 0 || degrees % 360 === 0) return {};
5995
+ return { rotationDegrees: degrees };
5996
+ }
5997
+ function resolveOffset(value, axisEmu) {
5998
+ return resolveLength(value, axisEmu);
5999
+ }
6000
+ function resolveExtent(value, axisEmu) {
6001
+ return Math.max(0, resolveLength(value, axisEmu));
6002
+ }
6003
+ function resolveLength(value, axisEmu) {
6004
+ if (typeof value === "number") return inchesToEmu(value);
6005
+ const percent = Number.parseFloat(value);
6006
+ if (!Number.isFinite(percent)) return 0;
6007
+ return Math.round(percent / 100 * axisEmu);
6008
+ }
6009
+ function marginInsets(margin) {
6010
+ if (margin === void 0) return void 0;
6011
+ const [top, right, bottom, left] = Array.isArray(margin) ? margin : [margin, margin, margin, margin];
6012
+ return {
6013
+ top: pointsToEmu(top ?? 0),
6014
+ right: pointsToEmu(right ?? 0),
6015
+ bottom: pointsToEmu(bottom ?? 0),
6016
+ left: pointsToEmu(left ?? 0)
6017
+ };
6018
+ }
6019
+ function solidFill(color, transparency) {
6020
+ return {
6021
+ kind: "solid",
6022
+ color,
6023
+ ...transparency !== void 0 && transparency > 0 ? { transparencyPercent: transparency } : {}
6024
+ };
6025
+ }
6026
+ function resolveColors(entries, at, deps) {
6027
+ const resolved = /* @__PURE__ */ new Map();
6028
+ let failed = false;
6029
+ for (const [key, value] of entries) {
6030
+ if (value === void 0) continue;
6031
+ const color = deps.color(value);
6032
+ if (!color) {
6033
+ deps.reject(`${at}.${key} "${value}"`);
6034
+ failed = true;
6035
+ continue;
6036
+ }
6037
+ resolved.set(key, color);
6038
+ }
6039
+ return failed ? void 0 : resolved;
6040
+ }
5320
6041
 
5321
6042
  // src/styles/themeToStyles.ts
5322
6043
  init_themes();
@@ -7159,7 +7880,7 @@ function decoratedStyle(match, base) {
7159
7880
  function pushSegment(out, text, options, override = {}) {
7160
7881
  const bold = override.bold ?? options.base.bold;
7161
7882
  const italic = override.italic ?? options.base.italic;
7162
- const formatting = runFormatting(options, { bold, italic });
7883
+ const formatting = runFormatting2(options, { bold, italic });
7163
7884
  const lines = text.split("\n");
7164
7885
  for (const [lineIndex, line] of lines.entries()) {
7165
7886
  if (!line && lineIndex === 0) continue;
@@ -7237,7 +7958,7 @@ function pushNoProofWords(out, text, formatting, options) {
7237
7958
  out.push({ kind: "text", text: text.slice(lastIndex), formatting });
7238
7959
  }
7239
7960
  }
7240
- function runFormatting(options, state) {
7961
+ function runFormatting2(options, state) {
7241
7962
  const formatting = { ...options.base };
7242
7963
  if (state.bold !== void 0) formatting.bold = state.bold;
7243
7964
  if (state.italic !== void 0) formatting.italic = state.italic;
@@ -7609,7 +8330,9 @@ function compileComponent(component, scope) {
7609
8330
  case "columns":
7610
8331
  return compileColumns(component, scope);
7611
8332
  case "image":
7612
- return compileImage(component, scope);
8333
+ return compileImage2(component, scope);
8334
+ case "visual":
8335
+ return compileVisual(component, scope);
7613
8336
  case "table":
7614
8337
  return compileTable(component, scope);
7615
8338
  default:
@@ -7853,7 +8576,7 @@ function compileTextBox(component, scope) {
7853
8576
  const style = props.style;
7854
8577
  const padding = style?.padding;
7855
8578
  if (props.renderAs === "shape") {
7856
- const shape = compileShape(props, children, scope);
8579
+ const shape = compileShape2(props, children, scope);
7857
8580
  if (shape) return shape;
7858
8581
  }
7859
8582
  ctx.features.require("tables", path3);
@@ -7907,7 +8630,7 @@ function compileTextBox(component, scope) {
7907
8630
  ];
7908
8631
  }
7909
8632
  var EMU_PER_PIXEL = 9525;
7910
- function compileShape(props, children, scope) {
8633
+ function compileShape2(props, children, scope) {
7911
8634
  const { ctx, path: path3 } = scope;
7912
8635
  const width = shapeSize(props.width, "width", ctx);
7913
8636
  const height = shapeSize(props.height, "height", ctx);
@@ -8401,7 +9124,7 @@ function reportUnlowered(component, props, text, scope) {
8401
9124
  }
8402
9125
  function compileRuns(props, text, ctx, path3, mode = "inline") {
8403
9126
  const font = props.font ?? {};
8404
- const base = runFormatting2(font, props, ctx);
9127
+ const base = runFormatting3(font, props, ctx);
8405
9128
  if (base.language) ctx.features.require("proofing-language", path3);
8406
9129
  if (base.noProof) ctx.features.require("proofing-language", path3);
8407
9130
  const words = mergeNoProofWords(noProofWords(ctx.theme), props.noProofWords);
@@ -8671,7 +9394,7 @@ function isoDate(date) {
8671
9394
  function isoDateTime(date) {
8672
9395
  return date.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
8673
9396
  }
8674
- function runFormatting2(font, props, ctx) {
9397
+ function runFormatting3(font, props, ctx) {
8675
9398
  const hasWeightRequest = font.fontWeight != null || font.bold === true;
8676
9399
  const effectiveFamily = font.family ?? (hasWeightRequest ? resolveBodyFamily(ctx.theme) : void 0);
8677
9400
  const weighted = applyFontWeightAlias({
@@ -8834,8 +9557,107 @@ function headingLevel(level) {
8834
9557
  const value = typeof level === "number" ? level : 1;
8835
9558
  return value >= 1 && value <= 6 ? value : 1;
8836
9559
  }
9560
+ function compileVisual(component, scope) {
9561
+ const { ctx, path: path3 } = scope;
9562
+ const props = component.props ?? {};
9563
+ if (!isNativeVisualProps4(props)) {
9564
+ ctx.unsupported.push({
9565
+ name: "visual",
9566
+ path: path3,
9567
+ detail: "a raster visual reached the compiler; it should have been rasterized during desugaring"
9568
+ });
9569
+ return [];
9570
+ }
9571
+ const group = compileNativeVisualGroup(
9572
+ props,
9573
+ nativeVisualDeps(ctx, path3)
9574
+ );
9575
+ if (!group) return [];
9576
+ const size = visualPlacementPixels(props, group.canvas, {
9577
+ widthPx: Math.round(twipsToPixels(getAvailableWidthTwips(ctx.theme))),
9578
+ heightPx: Math.round(twipsToPixels(getAvailableHeightTwips(ctx.theme)))
9579
+ });
9580
+ const drawing = {
9581
+ kind: "drawingGroup",
9582
+ widthEmu: pixelsToEmu(size.width),
9583
+ heightEmu: pixelsToEmu(size.height),
9584
+ canvasWidthEmu: group.canvas.widthEmu,
9585
+ canvasHeightEmu: group.canvas.heightEmu,
9586
+ children: group.children,
9587
+ ...typeof props.alt === "string" && props.alt ? { altText: props.alt } : {},
9588
+ ...props.floating ? { floating: compileFloating(props.floating, ctx, path3) } : {}
9589
+ };
9590
+ ctx.features.require("drawing-groups", path3);
9591
+ if (props.floating) ctx.features.require("floating-images", path3);
9592
+ const spacing = {};
9593
+ if (props.spacing?.before !== void 0) {
9594
+ spacing.beforeTwips = pointsToTwips2(props.spacing.before);
9595
+ }
9596
+ if (props.spacing?.after !== void 0) {
9597
+ spacing.afterTwips = pointsToTwips2(props.spacing.after);
9598
+ }
9599
+ const blocks = [
9600
+ {
9601
+ kind: "paragraph",
9602
+ id: scope.id,
9603
+ path: path3,
9604
+ children: [drawing],
9605
+ formatting: {
9606
+ // A floating drawing is anchored, so aligning its paragraph would move
9607
+ // the anchor rather than the graphic — the same rule an image follows.
9608
+ ...props.floating ? {} : { alignment: compileAlignment(props.alignment) ?? "center" },
9609
+ ...Object.keys(spacing).length > 0 ? { spacing } : {},
9610
+ ...props.keepNext !== void 0 ? { keepNext: props.keepNext } : {},
9611
+ ...props.keepLines !== void 0 ? { keepLines: props.keepLines } : {}
9612
+ }
9613
+ }
9614
+ ];
9615
+ const caption = captionBlock(props.caption, scope, "visual");
9616
+ if (caption === void 0) return [];
9617
+ if (caption) blocks.push(caption);
9618
+ return blocks;
9619
+ }
9620
+ function nativeVisualDeps(ctx, path3) {
9621
+ return {
9622
+ color: (value) => {
9623
+ try {
9624
+ return irColor(resolveColor(value, ctx.theme));
9625
+ } catch {
9626
+ return void 0;
9627
+ }
9628
+ },
9629
+ picture: (source) => {
9630
+ const loaded = ctx.images.get(source);
9631
+ if (!loaded) return void 0;
9632
+ const mediaType = detectImageType(source, loaded.contentType);
9633
+ if (mediaType === "svg") ctx.features.require("svg-images", path3);
9634
+ return {
9635
+ resourceId: declareResource(loaded, mediaType, ctx),
9636
+ mediaType,
9637
+ ...loaded.intrinsic ? { intrinsic: loaded.intrinsic } : {}
9638
+ };
9639
+ },
9640
+ reject: (detail) => ctx.unsupported.push({ name: "visual", path: path3, detail }),
9641
+ warn: (message) => warnOnce(ctx, `visual:${message}`, message)
9642
+ };
9643
+ }
9644
+ function visualPlacementPixels(props, canvas, reference) {
9645
+ const canvasWidthPx = Math.round(emuToPixels(canvas.widthEmu));
9646
+ const canvasHeightPx = Math.round(emuToPixels(canvas.heightEmu));
9647
+ const targetWidth = props.width !== void 0 ? parseWidthValue(props.width, reference.widthPx) : void 0;
9648
+ const targetHeight = props.height !== void 0 ? parseDimensionValue(props.height, reference.heightPx) : void 0;
9649
+ if (targetWidth === void 0 && targetHeight === void 0) {
9650
+ return { width: canvasWidthPx, height: canvasHeightPx };
9651
+ }
9652
+ return calculateMissingDimension(
9653
+ canvasWidthPx,
9654
+ canvasHeightPx,
9655
+ targetWidth,
9656
+ targetHeight
9657
+ );
9658
+ }
8837
9659
  var UNLOWERED_IMAGE_PROPS = ["comment"];
8838
- function compileImage(component, scope) {
9660
+ function compileImage2(component, scope) {
8839
9661
  const { ctx, path: path3 } = scope;
8840
9662
  const props = component.props ?? {};
8841
9663
  for (const prop of UNLOWERED_IMAGE_PROPS) {
@@ -8896,32 +9718,37 @@ function compileImage(component, scope) {
8896
9718
  }
8897
9719
  }
8898
9720
  ];
8899
- if (props.caption) {
8900
- const caption = String(props.caption);
8901
- const syntax = containsUnsupportedSyntax(caption);
8902
- if (syntax) {
8903
- ctx.unsupported.push({ name: "image", path: path3, detail: syntax });
8904
- return [];
8905
- }
8906
- blocks.push({
8907
- kind: "paragraph",
8908
- id: `${scope.id}:caption`,
8909
- path: `${path3}.caption`,
8910
- styleId: "Normal",
8911
- // Captions default to left alignment, whatever the figure did.
8912
- formatting: { alignment: "left" },
8913
- // A caption reaches the parser only when it carries a decorator, which
8914
- // is why a link in an otherwise plain caption stays literal.
8915
- children: DECORATED.test(caption) ? parseInline(caption, {
8916
- base: {},
8917
- hyperlinks: true,
8918
- resolvePlaceholder: placeholderResolver(ctx),
8919
- resolveCrossReference: crossReferenceResolver(ctx, path3)
8920
- }) : parseLiteral(caption, { base: {} })
8921
- });
8922
- }
9721
+ const caption = captionBlock(props.caption, scope, "image");
9722
+ if (caption === void 0) return [];
9723
+ if (caption) blocks.push(caption);
8923
9724
  return blocks;
8924
9725
  }
9726
+ function captionBlock(value, scope, componentName) {
9727
+ if (!value) return null;
9728
+ const { ctx, path: path3 } = scope;
9729
+ const caption = String(value);
9730
+ const syntax = containsUnsupportedSyntax(caption);
9731
+ if (syntax) {
9732
+ ctx.unsupported.push({ name: componentName, path: path3, detail: syntax });
9733
+ return void 0;
9734
+ }
9735
+ return {
9736
+ kind: "paragraph",
9737
+ id: `${scope.id}:caption`,
9738
+ path: `${path3}.caption`,
9739
+ styleId: "Normal",
9740
+ // Captions default to left alignment, whatever the figure did.
9741
+ formatting: { alignment: "left" },
9742
+ // A caption reaches the parser only when it carries a decorator, which
9743
+ // is why a link in an otherwise plain caption stays literal.
9744
+ children: DECORATED.test(caption) ? parseInline(caption, {
9745
+ base: {},
9746
+ hyperlinks: true,
9747
+ resolvePlaceholder: placeholderResolver(ctx),
9748
+ resolveCrossReference: crossReferenceResolver(ctx, path3)
9749
+ }) : parseLiteral(caption, { base: {} })
9750
+ };
9751
+ }
8925
9752
  function relativeFrom(value, axis) {
8926
9753
  const allowed = axis === "horizontal" ? ["character", "column", "margin", "page"] : ["margin", "page", "paragraph", "line"];
8927
9754
  return typeof value === "string" && allowed.includes(value) ? value : void 0;
@@ -9345,6 +10172,9 @@ function cellChildren(cell, baseStyle, scope, rowRevision) {
9345
10172
  if (content.name === "image") {
9346
10173
  return wrap(cellImage(content, base, ctx));
9347
10174
  }
10175
+ if (content.name === "visual") {
10176
+ return wrap(cellVisual(content, scope));
10177
+ }
9348
10178
  if (content.name !== "paragraph") {
9349
10179
  return wrap([
9350
10180
  {
@@ -9384,6 +10214,40 @@ function cellChildren(cell, baseStyle, scope, rowRevision) {
9384
10214
  })
9385
10215
  );
9386
10216
  }
10217
+ function cellVisual(component, scope) {
10218
+ const { ctx, path: path3 } = scope;
10219
+ const props = component.props ?? {};
10220
+ if (!isNativeVisualProps4(props)) {
10221
+ ctx.unsupported.push({
10222
+ name: "visual",
10223
+ path: path3,
10224
+ detail: "a raster visual reached the compiler; it should have been rasterized during desugaring"
10225
+ });
10226
+ return [];
10227
+ }
10228
+ const group = compileNativeVisualGroup(
10229
+ props,
10230
+ nativeVisualDeps(ctx, path3)
10231
+ );
10232
+ if (!group) return [];
10233
+ const size = visualPlacementPixels(props, group.canvas, {
10234
+ widthPx: CELL_DRAWING_REFERENCE.width,
10235
+ heightPx: CELL_DRAWING_REFERENCE.height
10236
+ });
10237
+ ctx.features.require("drawing-groups", path3);
10238
+ return [
10239
+ {
10240
+ kind: "drawingGroup",
10241
+ widthEmu: pixelsToEmu(size.width),
10242
+ heightEmu: pixelsToEmu(size.height),
10243
+ canvasWidthEmu: group.canvas.widthEmu,
10244
+ canvasHeightEmu: group.canvas.heightEmu,
10245
+ children: group.children,
10246
+ ...typeof props.alt === "string" && props.alt ? { altText: props.alt } : {}
10247
+ }
10248
+ ];
10249
+ }
10250
+ var CELL_DRAWING_REFERENCE = { width: 300, height: 200 };
9387
10251
  function cellImage(component, base, ctx) {
9388
10252
  const props = component.props ?? {};
9389
10253
  const source = resolveImageSource(props);